diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 8006d8a..0000000 --- a/.eslintignore +++ /dev/null @@ -1,4 +0,0 @@ -.nyc/ -node_modules/ -dist/ -*.ignore diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index e28b6a8..0000000 --- a/.eslintrc +++ /dev/null @@ -1,51 +0,0 @@ -{ - "root": true, - "parser": "@typescript-eslint/parser", - "parserOptions": { - "sourceType": "module", - "ecmaFeatures": { - "jsx": true - } - }, - "plugins": ["@typescript-eslint", "react"], - "extends": [ - "eslint:recommended", - "plugin:react/recommended", - "plugin:react/jsx-runtime", - "plugin:react-hooks/recommended", - "plugin:@typescript-eslint/eslint-recommended", - "plugin:@typescript-eslint/recommended" - ], - "env": { - "browser": true, - "es6": true - }, - "settings": { - "react": { "version": "detect" } - }, - "rules": { - "linebreak-style": ["error", "unix"], - "lines-around-comment": 0, - "no-confusing-arrow": 0, - "no-alert": "error", - "no-console": "error", - "no-debugger": "error", - "no-shadow": "warn", - - "react/jsx-indent": 0, - "react/jsx-indent-props": 0, - "react/prop-types": 0, - - "@typescript-eslint/consistent-type-imports": "error", - "@typescript-eslint/no-unused-vars": [ - "warn", - { - "args": "all", - // Although this repo uses React 18 the library supports React 16+ so we still need to import React. - "argsIgnorePattern": "^(_|React)", - "varsIgnorePattern": "^(_|React)" - } - ], - "@typescript-eslint/no-use-before-define": "warn" - } -} diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 82f4b6f..33478d4 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,4 +1,7 @@ -name: ci +name: CI + +env: + ANALYZE: true on: push: @@ -11,47 +14,47 @@ on: jobs: build: - name: Build, lint, and test on Node ${{ matrix.node }} and ${{ matrix.os }} - runs-on: ${{ matrix.os }} - strategy: - matrix: - node: ['20.x'] - os: [ubuntu-latest] + name: Build, lint and test + runs-on: ubuntu-latest steps: - - name: Checkout repo - uses: actions/checkout@v2 + - name: 🛒 Checkout Repo + uses: actions/checkout@v4 - - name: Cache pnpm modules - uses: actions/cache@v2 - with: - path: ~/.pnpm-store - key: ${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}- + - uses: pnpm/action-setup@v4 - - name: Use Node ${{ matrix.node }} - uses: actions/setup-node@v2 + - name: ⚒️ Use Node.js + uses: actions/setup-node@v4 with: - node-version: ${{ matrix.node }} + node-version-file: '.nvmrc' + cache: 'pnpm' - - name: Install dependencies + - name: Install Dependencies run: npm run bootstrap + - name: Build Library + run: pnpm --filter ./lib build + - name: Lint - run: pnpm lint + run: pnpm lint --reporter=github - - name: Test:Execute - run: pnpm run test + - name: Run Tests + run: | + pnpm run test + cat ${{github.workspace}}/example/tests/coverage-reports/coverage-summary.md >> $GITHUB_STEP_SUMMARY - - name: Test:Upload - uses: paambaati/codeclimate-action@v5.0.0 + - name: Check Package + run: pnpm --filter ./lib run check + + - name: Upload Coverage + uses: paambaati/codeclimate-action@v9.0.0 env: CC_TEST_REPORTER_ID: '${{ secrets.CC_TEST_REPORTER_ID }}' with: - coverageLocations: ${{github.workspace}}/coverage/*.info:lcov + debug: true + workingDirectory: ${{github.workspace}}/lib + coverageLocations: ${{github.workspace}}/example/tests/coverage-reports/*.info:lcov - # Disabled until https://github.com/pnpm/pnpm/issues/6424 is resolved. - # - uses: preactjs/compressed-size-action@v2 - # with: - # repo-token: '${{ secrets.GITHUB_TOKEN }}' - # build-script: 'build' + - name: Analyze esbuild bundle size + uses: exoego/esbuild-bundle-analyzer@v1 + with: + metafiles: "lib/dist/metafile-esm.json" diff --git a/.github/workflows/publish-preview.yml b/.github/workflows/publish-preview.yml new file mode 100644 index 0000000..322c56b --- /dev/null +++ b/.github/workflows/publish-preview.yml @@ -0,0 +1,29 @@ +name: Publish Preview Release +on: + pull_request: + types: [synchronize] +jobs: + approved: + if: contains(join(github.event.pull_request.labels.*.name, ','), 'publish-preview') + runs-on: ubuntu-latest + + steps: + - name: 🛒 Checkout repo + uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + + - name: ⚒️ Use Node.js + uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + cache: 'pnpm' + + - name: 📦 Install Dependencies + run: npm run bootstrap + + - name: 🔨 Build + run: pnpm run --filter react-image-turntable build + + - name: 🚀 Publish Preview Release + run: pnpx pkg-pr-new publish './lib' --template='./example' diff --git a/.gitignore b/.gitignore index 27956b6..898c047 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,5 @@ storybook-static .nyc_output test-results/ playwright-report/ +lib/README.md +lib/LICENSE diff --git a/.husky/post-merge b/.husky/post-merge index 9585e5a..071c26a 100755 --- a/.husky/post-merge +++ b/.husky/post-merge @@ -1,4 +1 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" - pnpm i --frozen-lockfile diff --git a/.husky/pre-commit b/.husky/pre-commit index 8b5c435..61dcdbd 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1 @@ -#!/usr/bin/env sh -. "$(dirname -- "$0")/_/husky.sh" - -pnpm lint-staged && pnpm run test:no-coverage +pnpm biome check --staged --files-ignore-unknown=true --write --no-errors-on-unmatched diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index 892f909..0000000 --- a/.prettierignore +++ /dev/null @@ -1,7 +0,0 @@ -node_modules/ -dist/ -coverage/ -.nyc_output/ -*.ignore -pnpm-lock.yaml -.pnpm-store diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 1d7ac85..699ed73 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,3 +1,3 @@ { - "recommendations": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"] + "recommendations": ["biomejs.biome"] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 96dac68..ec42139 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,25 +1,8 @@ { - "editor.codeActionsOnSave": { - "source.fixAll.eslint": true - }, - "editor.rulers": [100], + "editor.defaultFormatter": "biomejs.biome", "editor.formatOnSave": true, - "eslint.packageManager": "pnpm", - "[javascript]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, - "[javascriptreact]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, - "[jsonc]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, - "[typescript]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, - "[typescriptreact]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" + "editor.codeActionsOnSave": { + "source.fixAll.biome": "always" }, - "javascript.suggest.autoImports": false, - "typescript.suggest.autoImports": false + "editor.rulers": [100] } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dc003cf..850279f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,37 +2,44 @@ Thanks for contributing! +## About + +The repo is split up into [`lib`](./lib) for the main library code and [`example`](./example) for +demos and testing. + +If you're on Windows, it's recommended that you use WSL. + +## Standards + +- Commits use the [Conventional Commits](https://conventionalcommits.org/) standard +- pnpm to manage dependencies +- [fnm](https://github.com/Schniz/fnm) or [nvm](https://github.com/nvm-sh/nvm) to manage Node.js versions +- EditorConfig and Biome for formatting +- Biome for linting +- Husky for Git hooks + ## Getting Started Ensure you're using the Node version specified in [.nvmrc](./.nvmrc) and run the following to -bootstrap the project: +set up the project: ```sh -npm run dx +npm run bootstrap ``` -Then run the following commands in separate terminals: +Then run the following: ```sh # Run both the library dev build and the `example` repo dev server -pnpm run dev +pnpm -r run start # Or run them separately ## Start the library dev build -pnpm run start +pnpm run --filter ./lib start ## Start the `example` codebase dev build -pnpm run --prefix example start +pnpm run --filter ./example start ``` -## Standards - -- Commits use the [Conventional Commits](https://conventionalcommits.org/) standard -- pnpm to manage dependencies -- nvm to manage Node.js versions -- Prettier & EditorConfig for code style -- ESLint for quality -- Husky for Git hooks - ## VS Code If you're using VS Code please make sure you install the [recommended extensions](./.vscode/extensions.json). diff --git a/README.md b/README.md index 4087c2e..6f35d43 100644 --- a/README.md +++ b/README.md @@ -6,15 +6,9 @@ Display a set of images as a draggable 360 degree turntable. [![React Image Turntable with rotating car](https://raw.githubusercontent.com/nerdyman/stuff/main/libs/react-image-turntable/capture.gif)](https://codesandbox.io/s/github/nerdyman/react-image-turntable/tree/main/example?file=/src/App.tsx:5537-5598) - +Open in StackBlitz -### [👉   Try the demo  👈](https://codesandbox.io/s/github/nerdyman/react-image-turntable/tree/main/example?file=/src/App.tsx:5537-5598) - - - -
- -NPM package +NPM package License MIT GitHub CI status @@ -23,10 +17,11 @@ Display a set of images as a draggable 360 degree turntable. ## Features -- Accessible -- Responsive & fluid with intrinsic sizing -- Supports keyboard navigation -- Teeny Tiny (less than 1kb gzipped) +- Accessible & screen-reader friendly +- Responsive & intrinsically sized +- Built-in keyboard navigation +- Programmatically controllable +- Teeny Tiny - Zero dependencies - Type safe @@ -42,19 +37,62 @@ pnpm i react-image-turntable ## Usage -### Props +### Example -| Props | Type | Required | Default Value | Description | -| --------------------- | :--------- | :------: | :------------ | :--------------------------------------------------------------------------- | -| `images` | `string[]` | ✓ | `undefined` | List of image `src` attributes. | -| `initialImageIndex` | `number` | | `0` | Index of image to show first. | -| `movementSensitivity` | `number` | | `20` | The amount a "drag" has to move before an image changes to next or previous. | +```tsx +import { ReactImageTurntable, useReactImageTurntable } from 'react-image-turntable'; -### Example +const images = [ + 'https://via.placeholder.com/1200x800?text=1', + 'https://via.placeholder.com/1200x800?text=2', + 'https://via.placeholder.com/1200x800?text=3', +]; + +export const App = () => { + const turntableProps = useReactImageTurntable({ images }); + + return ; +}; +``` + +See the [example code](./example) for full demo. + +### `useReactImageTurntable` Props + +#### Input Props + +The `useReactImageTurntable` hook accepts the following properties. + +| Props | Type | Required | Default Value | Description | +| --------------------- | :------------------------ | :------: | :------------ | :--------------------------------------------------------------------------- | +| `autoRotate.enabled` | `boolean` | | `false` | Whether to automatically rotate the turntable. | +| `autoRotate.interval` | `number` | | `200` | The interval between autorotations in ms. | +| `images` | `string[]` | ✓ | `undefined` | List of image `src` attributes. | +| `initialImageIndex` | `number` | | `0` | Index of image to show first. | +| `movementSensitivity` | `number` | | `20` | The amount a "drag" has to move before an image changes to next or previous. | +| `onIndexChange` | `(index: number) => void` | | `undefined` | Callback to trigger whenever the active index changes. | + +#### Output Props + +The `useReactImageTurntable` hook returns the following properties. + +| Props | Type | Description | +| --------------------- | :-------------------------- | --------------------------------------------- | +| `activeImageIndex` | `number` | The index of the image currently being shown. | +| `setActiveImageIndex` | `(index: number) => void` | Function to set the active index. | +| `images` | `string[]` | The images passed into the hook. | +| `ref` | `RefObject` | The ref of the root turntable element. | + +Note that there is no need for a `setImages` function. `images` is not stored in state. If you want +to change the images simply change the `images` prop passed into the hook. + +##### Example Usage + +
+View example -```ts -import React from 'react'; -import { ReactImageTurntable } from 'react-image-turntable'; +```tsx +import { ReactImageTurntable, useReactImageTurntable } from 'react-image-turntable'; const images = [ 'https://via.placeholder.com/1200x800?text=1', @@ -62,21 +100,42 @@ const images = [ 'https://via.placeholder.com/1200x800?text=3', ]; -export const Turntable = () => ; +export const App = () => { + const turntableProps = useReactImageTurntable({ + autoRotate: { disabled: true, interval: 75 }, + images, + initialImageIndex: 1, // Start on the second image. + movementSensitivity: 50, // Increase the amount of drag needed to change images. + onIndexChange: (index) => console.log(`Active image index changed to ${index}`), + }); + + const handleSelectFirstImage = () => { + turntableProps.setActiveIndex(0); + }; + + return ( + <> + + + + ); +}; ``` -Also see the [example code](./example) in the repo. +
### Custom Styling -The library uses the first image passed to intrinsically size the component, it also exports following -`className`s to apply custom styles when needed. +The library uses the first image in `images[]` to intrinsically size the component, it also exports +the following `className`s allowing you to apply custom styles. -| `className` | Purpose | -| :------------------------- | :------------------------------------------------------------------- | -| `CLASS_NAME_IMG` | Base class for images. | -| `CLASS_NAME_IMG_PRIMARY` | Class of first rendered image (sets the size of the main component). | -| `CLASS_NAME_IMG_SECONDARY` | Class of subsequent images. | +| `className` | Purpose | +| :------------------------- | :------------------------------------------------------------------------ | +| `CLASS_NAME_IMG` | Base class for all images. | +| `CLASS_NAME_IMG_PRIMARY` | Class of first image in `images[]` (sets the size of the main component). | +| `CLASS_NAME_IMG_SECONDARY` | Class of subsequent images. | --- @@ -93,5 +152,4 @@ The library is built for `ES2021`. ## Notes - It's recommended you use a minimum of 24-36 for a smooth experience -- Legacy [v2.5.4 Demo](https://codesandbox.io/s/react-image-turntable-riy93) -- Original version by [@andrewmcoupe](https://github.com/andrewmcoupe) +- Legacy version by [@andrewmcoupe](https://github.com/andrewmcoupe) diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..2eda095 --- /dev/null +++ b/biome.json @@ -0,0 +1,47 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "vcs": { + "enabled": false, + "clientKind": "git", + "useIgnoreFile": false + }, + "files": { + "ignoreUnknown": false, + "ignore": ["*.ignore", "dist", "tests/coverage-reports/*", "tests/results/*"] + }, + "formatter": { + "enabled": true, + "lineWidth": 100, + "useEditorconfig": true + }, + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + }, + "javascript": { + "formatter": { + "enabled": true, + "jsxQuoteStyle": "double", + "quoteStyle": "single", + "semicolons": "always", + "trailingCommas": "all" + } + }, + "json": { + "formatter": { + "enabled": true, + "trailingCommas": "none" + } + }, + "css": { + "formatter": { + "enabled": true, + "quoteStyle": "single" + } + } +} diff --git a/example/.gitignore b/example/.gitignore index c103aa1..35ab0e0 100644 --- a/example/.gitignore +++ b/example/.gitignore @@ -23,3 +23,7 @@ dist-ssr *.sln *.sw? *.ignore + +# Test output +tests/results/ +coverage-reports/ diff --git a/example/README.md b/example/README.md index 6d72d91..deb646d 100644 --- a/example/README.md +++ b/example/README.md @@ -1,6 +1,12 @@ +
+ # React Image Turntable Example -Basic example built with Vite. +Built with Vite. + +Open in StackBlitz + +
## Scripts diff --git a/example/index.html b/example/index.html index 933149c..2283b8e 100644 --- a/example/index.html +++ b/example/index.html @@ -7,7 +7,7 @@ React Image Turntable – Example -
+
diff --git a/example/package.json b/example/package.json index 47363c9..722a148 100644 --- a/example/package.json +++ b/example/package.json @@ -1,23 +1,31 @@ { - "name": "react-image-turntable-example", + "name": "example", "private": true, "version": "0.0.0", + "packageManager": "pnpm@9.12.2", "scripts": { + "setup": "pnpm exec playwright install", "start": "vite", "build": "vite build", - "preview": "vite preview --port 3000" + "preview": "vite preview --port 3000", + "test": "playwright test" }, "dependencies": { - "pepjs": "^0.5.3", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-image-turntable": "latest" + "modern-normalize": "^3.0.1", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-image-turntable": "*" }, "devDependencies": { - "@types/react": "^18.2.37", - "@types/react-dom": "^18.2.15", - "@vitejs/plugin-react": "^4.2.0", - "typescript": "^5.2.2", - "vite": "^5.0.0" + "@axe-core/playwright": "^4.10.0", + "@playwright/test": "^1.48.1", + "@types/react": "^18.3.11", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.2", + "monocart-coverage-reports": "^2.11.1", + "playwright-core": "^1.48.1", + "react-router-dom": "^6.27.0", + "typescript": "^5.6.3", + "vite": "^5.4.9" } } diff --git a/playwright.config.ts b/example/playwright.config.mts similarity index 52% rename from playwright.config.ts rename to example/playwright.config.mts index 9e9922a..b807ee3 100644 --- a/playwright.config.ts +++ b/example/playwright.config.mts @@ -1,27 +1,27 @@ /* eslint no-console: 0 */ -import { devices } from '@playwright/test'; -import type { PlaywrightTestConfig } from '@playwright/test'; +import { type PlaywrightTestConfig, devices } from '@playwright/test'; const IS_CI = !!process.env.CI; -const PORT = !isNaN(Number(process.env.PORT)) ? Number(process.env.PORT) : 3000; +const PORT = !Number.isNaN(Number(process.env.PORT)) ? Number(process.env.PORT) : 3000; process.env.PORT = String(PORT); console.info('[playwright.config]', { IS_CI, PORT }); const config: PlaywrightTestConfig = { + globalSetup: './tests/global.setup.ts', + globalTeardown: './tests/global.teardown.ts', forbidOnly: IS_CI, retries: IS_CI ? 2 : 0, - testDir: './test', - outputDir: './test/results', + testDir: './', + outputDir: './tests/results', reporter: 'list', webServer: { - command: 'pnpm run build && pnpm --filter "react-image-turntable-example" run start', + command: 'pnpm run start', reuseExistingServer: !IS_CI, url: `http://localhost:${PORT}`, env: { NODE_ENV: 'test', - USE_BABEL_PLUGIN_ISTANBUL: '1', }, }, use: { @@ -35,15 +35,7 @@ const config: PlaywrightTestConfig = { name: 'chromium', use: { ...devices['Desktop Chrome'] }, }, - // { - // name: 'firefox', - // use: { ...devices['Desktop Firefox'] }, - // }, - // { - // name: 'webkit', - // use: { ...devices['Desktop Safari'] }, - // }, - ], + ].filter(Boolean) as PlaywrightTestConfig['projects'], }; export default config; diff --git a/example/src/App.tsx b/example/src/App.tsx index 2315334..91c6e94 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -1,68 +1,77 @@ -import { useState } from 'react'; -import { ReactImageTurntable } from 'react-image-turntable'; -import type { ReactImageTurntableProps } from 'react-image-turntable'; +import type { FC } from 'react'; +import { + Link, + type LinkProps, + RouterProvider, + createBrowserRouter, + useLocation, +} from 'react-router-dom'; -export const images = [ - 'https://static2.zerolight.com/web/df3a45687480167bb4451d79c02f67bd/img/2d-explorer/bmw/P0C1G__S022B__FX3A9/P0C1G__S022B__FX3A9__Spin_01.jpg?v=1', - 'https://static2.zerolight.com/web/df3a45687480167bb4451d79c02f67bd/img/2d-explorer/bmw/P0C1G__S022B__FX3A9/P0C1G__S022B__FX3A9__Spin_02.jpg?v=1', - 'https://static2.zerolight.com/web/df3a45687480167bb4451d79c02f67bd/img/2d-explorer/bmw/P0C1G__S022B__FX3A9/P0C1G__S022B__FX3A9__Spin_03.jpg?v=1', - 'https://static2.zerolight.com/web/df3a45687480167bb4451d79c02f67bd/img/2d-explorer/bmw/P0C1G__S022B__FX3A9/P0C1G__S022B__FX3A9__Spin_04.jpg?v=1', - 'https://static2.zerolight.com/web/df3a45687480167bb4451d79c02f67bd/img/2d-explorer/bmw/P0C1G__S022B__FX3A9/P0C1G__S022B__FX3A9__Spin_05.jpg?v=1', - 'https://static2.zerolight.com/web/df3a45687480167bb4451d79c02f67bd/img/2d-explorer/bmw/P0C1G__S022B__FX3A9/P0C1G__S022B__FX3A9__Spin_06.jpg?v=1', - 'https://static2.zerolight.com/web/df3a45687480167bb4451d79c02f67bd/img/2d-explorer/bmw/P0C1G__S022B__FX3A9/P0C1G__S022B__FX3A9__Spin_07.jpg?v=1', - 'https://static2.zerolight.com/web/df3a45687480167bb4451d79c02f67bd/img/2d-explorer/bmw/P0C1G__S022B__FX3A9/P0C1G__S022B__FX3A9__Spin_08.jpg?v=1', - 'https://static2.zerolight.com/web/df3a45687480167bb4451d79c02f67bd/img/2d-explorer/bmw/P0C1G__S022B__FX3A9/P0C1G__S022B__FX3A9__Spin_09.jpg?v=1', - 'https://static2.zerolight.com/web/df3a45687480167bb4451d79c02f67bd/img/2d-explorer/bmw/P0C1G__S022B__FX3A9/P0C1G__S022B__FX3A9__Spin_10.jpg?v=1', - 'https://static2.zerolight.com/web/df3a45687480167bb4451d79c02f67bd/img/2d-explorer/bmw/P0C1G__S022B__FX3A9/P0C1G__S022B__FX3A9__Spin_11.jpg?v=1', - 'https://static2.zerolight.com/web/df3a45687480167bb4451d79c02f67bd/img/2d-explorer/bmw/P0C1G__S022B__FX3A9/P0C1G__S022B__FX3A9__Spin_12.jpg?v=1', - 'https://static2.zerolight.com/web/df3a45687480167bb4451d79c02f67bd/img/2d-explorer/bmw/P0C1G__S022B__FX3A9/P0C1G__S022B__FX3A9__Spin_13.jpg?v=1', - 'https://static2.zerolight.com/web/df3a45687480167bb4451d79c02f67bd/img/2d-explorer/bmw/P0C1G__S022B__FX3A9/P0C1G__S022B__FX3A9__Spin_14.jpg?v=1', - 'https://static2.zerolight.com/web/df3a45687480167bb4451d79c02f67bd/img/2d-explorer/bmw/P0C1G__S022B__FX3A9/P0C1G__S022B__FX3A9__Spin_15.jpg?v=1', - 'https://static2.zerolight.com/web/df3a45687480167bb4451d79c02f67bd/img/2d-explorer/bmw/P0C1G__S022B__FX3A9/P0C1G__S022B__FX3A9__Spin_16.jpg?v=1', - 'https://static2.zerolight.com/web/df3a45687480167bb4451d79c02f67bd/img/2d-explorer/bmw/P0C1G__S022B__FX3A9/P0C1G__S022B__FX3A9__Spin_17.jpg?v=1', - 'https://static2.zerolight.com/web/df3a45687480167bb4451d79c02f67bd/img/2d-explorer/bmw/P0C1G__S022B__FX3A9/P0C1G__S022B__FX3A9__Spin_18.jpg?v=1', - 'https://static2.zerolight.com/web/df3a45687480167bb4451d79c02f67bd/img/2d-explorer/bmw/P0C1G__S022B__FX3A9/P0C1G__S022B__FX3A9__Spin_19.jpg?v=1', - 'https://static2.zerolight.com/web/df3a45687480167bb4451d79c02f67bd/img/2d-explorer/bmw/P0C1G__S022B__FX3A9/P0C1G__S022B__FX3A9__Spin_20.jpg?v=1', - 'https://static2.zerolight.com/web/df3a45687480167bb4451d79c02f67bd/img/2d-explorer/bmw/P0C1G__S022B__FX3A9/P0C1G__S022B__FX3A9__Spin_21.jpg?v=1', - 'https://static2.zerolight.com/web/df3a45687480167bb4451d79c02f67bd/img/2d-explorer/bmw/P0C1G__S022B__FX3A9/P0C1G__S022B__FX3A9__Spin_22.jpg?v=1', - 'https://static2.zerolight.com/web/df3a45687480167bb4451d79c02f67bd/img/2d-explorer/bmw/P0C1G__S022B__FX3A9/P0C1G__S022B__FX3A9__Spin_23.jpg?v=1', - 'https://static2.zerolight.com/web/df3a45687480167bb4451d79c02f67bd/img/2d-explorer/bmw/P0C1G__S022B__FX3A9/P0C1G__S022B__FX3A9__Spin_24.jpg?v=1', - 'https://static2.zerolight.com/web/df3a45687480167bb4451d79c02f67bd/img/2d-explorer/bmw/P0C1G__S022B__FX3A9/P0C1G__S022B__FX3A9__Spin_25.jpg?v=1', - 'https://static2.zerolight.com/web/df3a45687480167bb4451d79c02f67bd/img/2d-explorer/bmw/P0C1G__S022B__FX3A9/P0C1G__S022B__FX3A9__Spin_26.jpg?v=1', - 'https://static2.zerolight.com/web/df3a45687480167bb4451d79c02f67bd/img/2d-explorer/bmw/P0C1G__S022B__FX3A9/P0C1G__S022B__FX3A9__Spin_27.jpg?v=1', - 'https://static2.zerolight.com/web/df3a45687480167bb4451d79c02f67bd/img/2d-explorer/bmw/P0C1G__S022B__FX3A9/P0C1G__S022B__FX3A9__Spin_28.jpg?v=1', - 'https://static2.zerolight.com/web/df3a45687480167bb4451d79c02f67bd/img/2d-explorer/bmw/P0C1G__S022B__FX3A9/P0C1G__S022B__FX3A9__Spin_29.jpg?v=1', - 'https://static2.zerolight.com/web/df3a45687480167bb4451d79c02f67bd/img/2d-explorer/bmw/P0C1G__S022B__FX3A9/P0C1G__S022B__FX3A9__Spin_30.jpg?v=1', - 'https://static2.zerolight.com/web/df3a45687480167bb4451d79c02f67bd/img/2d-explorer/bmw/P0C1G__S022B__FX3A9/P0C1G__S022B__FX3A9__Spin_31.jpg?v=1', - 'https://static2.zerolight.com/web/df3a45687480167bb4451d79c02f67bd/img/2d-explorer/bmw/P0C1G__S022B__FX3A9/P0C1G__S022B__FX3A9__Spin_32.jpg?v=1', - 'https://static2.zerolight.com/web/df3a45687480167bb4451d79c02f67bd/img/2d-explorer/bmw/P0C1G__S022B__FX3A9/P0C1G__S022B__FX3A9__Spin_33.jpg?v=1', - 'https://static2.zerolight.com/web/df3a45687480167bb4451d79c02f67bd/img/2d-explorer/bmw/P0C1G__S022B__FX3A9/P0C1G__S022B__FX3A9__Spin_34.jpg?v=1', - 'https://static2.zerolight.com/web/df3a45687480167bb4451d79c02f67bd/img/2d-explorer/bmw/P0C1G__S022B__FX3A9/P0C1G__S022B__FX3A9__Spin_35.jpg?v=1', - 'https://static2.zerolight.com/web/df3a45687480167bb4451d79c02f67bd/img/2d-explorer/bmw/P0C1G__S022B__FX3A9/P0C1G__S022B__FX3A9__Spin_36.jpg?v=1', +import ErrorPage from './ErrorPage'; +import { AdvancedDemo } from './demos/Advanced'; +import { BasicDemo } from './demos/Basic'; + +const links: LinkProps[] = [ + { children: 'Advanced', to: '/' }, + { children: 'Basic', to: '/basic' }, ]; -function App(props: Partial) { - const [rotationDisabled, setRotationDisabled] = useState(false); +const Nav: FC = () => { + const location = useLocation(); + + if (new URLSearchParams(location.search).get('noNav') === 'true') { + return false; + } - const handleKeyDown = (ev: React.KeyboardEvent) => { - if (rotationDisabled) return; + return ( + + ); +}; - if (ev.key === 'ArrowLeft' || ev.key === 'ArrowRight') { - setRotationDisabled(true); - } - }; +const router = createBrowserRouter([ + { + path: '/', + element: ( + <> +