commit df11a86181963a2854e909ac8adccc5adbedeff8 Author: Sam Becker Date: Tue Sep 5 09:00:57 2023 -0500 Init diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 00000000..a85f46a4 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,38 @@ +{ + "extends": "next/core-web-vitals", + "plugins": ["@typescript-eslint"], + "rules": { + "@next/next/no-img-element": "off", + "@typescript-eslint/no-unused-expressions": ["warn"], + "@typescript-eslint/no-unused-vars": [ + "warn", { + "argsIgnorePattern": "^_", + "varsIgnorePattern": "^_" + } + ], + "comma-dangle": [ + "warn", + "always-multiline" + ], + "indent": [ + "warn", + 2 + ], + "linebreak-style": [ + "warn", + "unix" + ], + "quotes": [ + "warn", + "single" + ], + "semi": [ + "warn", + "always" + ], + "max-len": [ + "warn", + { "code": 80 } + ] + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..da90ec50 --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +# temp files +temp.jpg diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..6fda0a0a --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,21 @@ +{ + "cSpell.words": [ + "ARROWLEFT", + "ARROWRIGHT", + "camelcase", + "exif", + "hgetall", + "hset", + "nextjs", + "qaub", + "skippable", + "thephotoblog", + "trpc", + "WRHGZC", + "zadd", + "zrange" + ], + "files.associations": { + "*.css": "tailwindcss" + } +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 00000000..91e63f65 --- /dev/null +++ b/README.md @@ -0,0 +1,40 @@ +# 📸 Photo Blog `(BETA)` + +_This template is in `BETA`. Optimizations still being made around auth and cache behavior. Database schema changes to be expected._ + +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?demo-title=Photo+Blog&demo-description=Store+photos+with+original+camera+data&demo-url=https%3A%2F%2Fphotos.sambecker.com&demo-image=https%3A%2F%2Fphotos.sambecker.com%2Fdeploy-image&project-name=Photo+Blog&repository-name=photo-blog&repository-url=https%3A%2F%2Fgithub.com%2Fsambecker%2Fphoto-blog&from=templates&skippable-integrations=1&env-description=Configure+your+photo+blog+meta&env-link=BLANK&env=NEXT_PUBLIC_SITE_TITLE%2CNEXT_PUBLIC_SITE_DOMAIN&teamCreateStatus=hidden&stores=%5B%7B%22type%22%3A%22postgres%22%7D%2C%7B%22type%22%3A%22blob%22%7D%5D) + +### 1. Deploy to Vercel + +1. Click Deploy +2. Add required storage +3. Add environment variables +- `NEXT_PUBLIC_SITE_TITLE` (e.g., My Photos) +- `NEXT_PUBLIC_SITE_DOMAIN` (e.g., photos.domain.com) +- `NEXT_PUBLIC_SITE_DESCRIPTION` (optional—mainly used for og meta) + +### 2. Setup Vercel Postgres + +1. Visit the `Storage` tab on your project +2. Click "Create Database" +3. Select Postgres + +### 3. Setup Vercel Blob + +1. Visit the `Storage` tab on your project +2. Click "Create Database" +3. Select Blob + +### 4. Setup Auth + +1. Create a Clerk account +2. Add Clerk environment variables to your project +3. Create an admin user +4. Add your admin user id to your environment variables as +- `CLERK_ADMIN_USER_ID` + +### 5. Develop locally + +1. Clone code +2. Install dependencies `pnpm i` +3. Run `vc dev` to utilize Vercel-stored environment variables diff --git a/next.config.js b/next.config.js new file mode 100644 index 00000000..a9dbb530 --- /dev/null +++ b/next.config.js @@ -0,0 +1,22 @@ +/** @type {import('next').NextConfig} */ + +const STORE_ID = process.env.BLOB_READ_WRITE_TOKEN?.match( + /^vercel_blob_rw_([a-z0-9]+)_[a-z0-9]+$/i, +)?.[1].toLowerCase(); + +const nextConfig = { + images: { + imageSizes: [400, 1050, 1200], + remotePatterns: [{ + protocol: 'https', + hostname: `${STORE_ID}.public.blob.vercel-storage.com`, + port: '', + pathname: '/**', + }], + }, + experimental: { + serverActions: true, + }, +}; + +module.exports = nextConfig; diff --git a/package.json b/package.json new file mode 100644 index 00000000..7913d564 --- /dev/null +++ b/package.json @@ -0,0 +1,40 @@ +{ + "name": "photo-blog", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "@clerk/nextjs": "^4.23.4", + "@tailwindcss/forms": "^0.5.6", + "@types/node": "^20.5.9", + "@types/react": "18.2.21", + "@types/react-dom": "18.2.7", + "@typescript-eslint/eslint-plugin": "^6.6.0", + "@typescript-eslint/parser": "^6.6.0", + "@vercel/analytics": "^1.0.2", + "@vercel/blob": "^0.10.0", + "@vercel/og": "^0.5.13", + "@vercel/postgres": "^0.4.1", + "autoprefixer": "10.4.15", + "camelcase-keys": "^9.0.0", + "date-fns": "^2.30.0", + "eslint": "8.48.0", + "eslint-config-next": "13.4.19", + "framer-motion": "^10.16.3", + "next": "^13.4.19", + "next-themes": "^0.2.1", + "postcss": "8.4.29", + "react": "18.2.0", + "react-dom": "18.2.0", + "react-icons": "^4.10.1", + "react-spinners": "^0.13.8", + "short-uuid": "^4.2.2", + "tailwindcss": "3.3.3", + "ts-exif-parser": "^0.2.2", + "typescript": "5.2.2" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 00000000..77f425f3 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,4041 @@ +lockfileVersion: 5.4 + +specifiers: + '@clerk/nextjs': ^4.23.4 + '@tailwindcss/forms': ^0.5.6 + '@types/node': ^20.5.9 + '@types/react': 18.2.21 + '@types/react-dom': 18.2.7 + '@typescript-eslint/eslint-plugin': ^6.6.0 + '@typescript-eslint/parser': ^6.6.0 + '@vercel/analytics': ^1.0.2 + '@vercel/blob': ^0.10.0 + '@vercel/og': ^0.5.13 + '@vercel/postgres': ^0.4.1 + autoprefixer: 10.4.15 + camelcase-keys: ^9.0.0 + date-fns: ^2.30.0 + eslint: 8.48.0 + eslint-config-next: 13.4.19 + framer-motion: ^10.16.3 + next: ^13.4.19 + next-themes: ^0.2.1 + postcss: 8.4.29 + react: 18.2.0 + react-dom: 18.2.0 + react-icons: ^4.10.1 + react-spinners: ^0.13.8 + short-uuid: ^4.2.2 + tailwindcss: 3.3.3 + ts-exif-parser: ^0.2.2 + typescript: 5.2.2 + +dependencies: + '@clerk/nextjs': 4.23.4_m2l4knchnqa6bzxbbtfxlzoxdy + '@tailwindcss/forms': 0.5.6_tailwindcss@3.3.3 + '@types/node': 20.5.9 + '@types/react': 18.2.21 + '@types/react-dom': 18.2.7 + '@typescript-eslint/eslint-plugin': 6.6.0_vt4saesn3ecaumaytn5wzbmrxe + '@typescript-eslint/parser': 6.6.0_w2g2uv42be7wag4oipwkfv43p4 + '@vercel/analytics': 1.0.2 + '@vercel/blob': 0.10.0 + '@vercel/og': 0.5.13 + '@vercel/postgres': 0.4.1 + autoprefixer: 10.4.15_postcss@8.4.29 + camelcase-keys: 9.0.0 + date-fns: 2.30.0 + eslint: 8.48.0 + eslint-config-next: 13.4.19_w2g2uv42be7wag4oipwkfv43p4 + framer-motion: 10.16.3_biqbaboplfbrettd7655fr4n2y + next: 13.4.19_biqbaboplfbrettd7655fr4n2y + next-themes: 0.2.1_m2l4knchnqa6bzxbbtfxlzoxdy + postcss: 8.4.29 + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 + react-icons: 4.10.1_react@18.2.0 + react-spinners: 0.13.8_biqbaboplfbrettd7655fr4n2y + short-uuid: 4.2.2 + tailwindcss: 3.3.3 + ts-exif-parser: 0.2.2 + typescript: 5.2.2 + +packages: + + /@aashutoshrathi/word-wrap/1.2.6: + resolution: {integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==} + engines: {node: '>=0.10.0'} + dev: false + + /@alloc/quick-lru/5.2.0: + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + dev: false + + /@babel/code-frame/7.22.10: + resolution: {integrity: sha512-/KKIMG4UEL35WmI9OlvMhurwtytjvXoFcGNrOvyG9zIzA8YmPjVtIZUf7b05+TPO7G7/GEmLHDaoCgACHl9hhA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/highlight': 7.22.10 + chalk: 2.4.2 + dev: false + + /@babel/helper-validator-identifier/7.22.5: + resolution: {integrity: sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==} + engines: {node: '>=6.9.0'} + dev: false + + /@babel/highlight/7.22.10: + resolution: {integrity: sha512-78aUtVcT7MUscr0K5mIEnkwxPE0MaxkR5RxRwuHaQ+JuU5AmTPhY+do2mdzVTnIJJpyBglql2pehuBIWHug+WQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-validator-identifier': 7.22.5 + chalk: 2.4.2 + js-tokens: 4.0.0 + dev: false + + /@babel/runtime/7.22.10: + resolution: {integrity: sha512-21t/fkKLMZI4pqP2wlmsQAWnYW1PDyKyyUV4vCi+B25ydmdaYTKXPwCj0BzSUnZf4seIiYvSA3jcZ3gdsMFkLQ==} + engines: {node: '>=6.9.0'} + dependencies: + regenerator-runtime: 0.14.0 + dev: false + + /@clerk/backend/0.28.1: + resolution: {integrity: sha512-L8YT9wCy42yA1q5sxovgmVhmGwUw1/lUY35Yy1e0sDC8FsbdW7763RgXukFvCCFyYrjXTTcfHH6VG/IndbHsTw==} + engines: {node: '>=14'} + dependencies: + '@clerk/types': 3.50.0 + '@peculiar/webcrypto': 1.4.1 + '@types/node': 16.18.6 + cookie: 0.5.0 + deepmerge: 4.2.2 + node-fetch-native: 1.0.1 + snakecase-keys: 5.4.4 + tslib: 2.4.1 + dev: false + + /@clerk/clerk-react/4.24.1_react@18.2.0: + resolution: {integrity: sha512-Xo1Y+jxGs01/v/vtfK0650Ie4ACIseAKnTX02TnXCvJAfiByGVZ6to5DyifGUcvplc7PVTy/NHRXUWSvwV+32A==} + engines: {node: '>=14'} + peerDependencies: + react: '>=16' + dependencies: + '@clerk/shared': 0.22.0_react@18.2.0 + '@clerk/types': 3.50.0 + react: 18.2.0 + tslib: 2.4.1 + dev: false + + /@clerk/clerk-sdk-node/4.12.4: + resolution: {integrity: sha512-ISXdeI+uMnydbzmjV0ONdsKNACUklpo68kz2j8EnVuxYMnzSuAevpyF3/SQlsNpiG5e4ZTT8EmKMaandUSKKDw==} + engines: {node: '>=14'} + dependencies: + '@clerk/backend': 0.28.1 + '@clerk/types': 3.50.0 + '@types/cookies': 0.7.7 + '@types/express': 4.17.14 + '@types/node-fetch': 2.6.2 + camelcase-keys: 6.2.2 + snakecase-keys: 3.2.1 + tslib: 2.4.1 + dev: false + + /@clerk/nextjs/4.23.4_m2l4knchnqa6bzxbbtfxlzoxdy: + resolution: {integrity: sha512-Alee+R1QiXKbIrVySGTZiAefoS4CE6Dmvj0LvG7n2UP3UhuEoowoz5N1rTTW3qeAffrqJepJOW8UB6jn0ac7Mg==} + engines: {node: '>=14'} + peerDependencies: + next: '>=10' + react: ^17.0.2 || ^18.0.0-0 + react-dom: ^17.0.2 || ^18.0.0-0 + dependencies: + '@clerk/backend': 0.28.1 + '@clerk/clerk-react': 4.24.1_react@18.2.0 + '@clerk/clerk-sdk-node': 4.12.4 + '@clerk/types': 3.50.0 + next: 13.4.19_biqbaboplfbrettd7655fr4n2y + path-to-regexp: 6.2.1 + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 + tslib: 2.4.1 + dev: false + + /@clerk/shared/0.22.0_react@18.2.0: + resolution: {integrity: sha512-AHPypu9gZ3v44PRqiMA56c+YNLc2IzLaPUyiYFYU+xeH/R+wqzGp7OxZoZr/kmzgA8taiVl/bjixWgpuZwzI3A==} + peerDependencies: + react: '>=16' + dependencies: + glob-to-regexp: 0.4.1 + js-cookie: 3.0.1 + react: 18.2.0 + swr: 2.2.0_react@18.2.0 + dev: false + + /@clerk/types/3.50.0: + resolution: {integrity: sha512-3TWalDWPTFToXC/W07QUIBN96TA+4dR3YLBvwr9U2Z4RVG9in9HW4CTC6aHTnJ2kVtTcgDkXjDKeVKS1GjPCcA==} + engines: {node: '>=14'} + dependencies: + csstype: 3.1.1 + dev: false + + /@emotion/is-prop-valid/0.8.8: + resolution: {integrity: sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==} + requiresBuild: true + dependencies: + '@emotion/memoize': 0.7.4 + dev: false + optional: true + + /@emotion/memoize/0.7.4: + resolution: {integrity: sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==} + dev: false + optional: true + + /@eslint-community/eslint-utils/4.4.0_eslint@8.48.0: + resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + dependencies: + eslint: 8.48.0 + eslint-visitor-keys: 3.4.3 + dev: false + + /@eslint-community/regexpp/4.6.2: + resolution: {integrity: sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + dev: false + + /@eslint/eslintrc/2.1.2: + resolution: {integrity: sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + ajv: 6.12.6 + debug: 4.3.4 + espree: 9.6.1 + globals: 13.21.0 + ignore: 5.2.4 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + dev: false + + /@eslint/js/8.48.0: + resolution: {integrity: sha512-ZSjtmelB7IJfWD2Fvb7+Z+ChTIKWq6kjda95fLcQKNS5aheVHn4IkfgRQE3sIIzTcSLwLcLZUD9UBt+V7+h+Pw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: false + + /@humanwhocodes/config-array/0.11.10: + resolution: {integrity: sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==} + engines: {node: '>=10.10.0'} + dependencies: + '@humanwhocodes/object-schema': 1.2.1 + debug: 4.3.4 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + dev: false + + /@humanwhocodes/module-importer/1.0.1: + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + dev: false + + /@humanwhocodes/object-schema/1.2.1: + resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==} + dev: false + + /@jest/environment/29.6.2: + resolution: {integrity: sha512-AEcW43C7huGd/vogTddNNTDRpO6vQ2zaQNrttvWV18ArBx9Z56h7BIsXkNFJVOO4/kblWEQz30ckw0+L3izc+Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/fake-timers': 29.6.2 + '@jest/types': 29.6.1 + '@types/node': 20.5.9 + jest-mock: 29.6.2 + dev: false + + /@jest/fake-timers/29.6.2: + resolution: {integrity: sha512-euZDmIlWjm1Z0lJ1D0f7a0/y5Kh/koLFMUBE5SUYWrmy8oNhJpbTBDAP6CxKnadcMLDoDf4waRYCe35cH6G6PA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.1 + '@sinonjs/fake-timers': 10.3.0 + '@types/node': 20.5.9 + jest-message-util: 29.6.2 + jest-mock: 29.6.2 + jest-util: 29.6.2 + dev: false + + /@jest/schemas/29.6.0: + resolution: {integrity: sha512-rxLjXyJBTL4LQeJW3aKo0M/+GkCOXsO+8i9Iu7eDb6KwtP65ayoDsitrdPBtujxQ88k4wI2FNYfa6TOGwSn6cQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@sinclair/typebox': 0.27.8 + dev: false + + /@jest/types/29.6.1: + resolution: {integrity: sha512-tPKQNMPuXgvdOn2/Lg9HNfUvjYVGolt04Hp03f5hAk878uwOLikN+JzeLY0HcVgKgFl9Hs3EIqpu3WX27XNhnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/schemas': 29.6.0 + '@types/istanbul-lib-coverage': 2.0.4 + '@types/istanbul-reports': 3.0.1 + '@types/node': 20.5.9 + '@types/yargs': 17.0.24 + chalk: 4.1.2 + dev: false + + /@jridgewell/gen-mapping/0.3.3: + resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/set-array': 1.1.2 + '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/trace-mapping': 0.3.19 + dev: false + + /@jridgewell/resolve-uri/3.1.1: + resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==} + engines: {node: '>=6.0.0'} + dev: false + + /@jridgewell/set-array/1.1.2: + resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} + engines: {node: '>=6.0.0'} + dev: false + + /@jridgewell/sourcemap-codec/1.4.15: + resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + dev: false + + /@jridgewell/trace-mapping/0.3.19: + resolution: {integrity: sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==} + dependencies: + '@jridgewell/resolve-uri': 3.1.1 + '@jridgewell/sourcemap-codec': 1.4.15 + dev: false + + /@neondatabase/serverless/0.5.6: + resolution: {integrity: sha512-Ru0lG6W/nQtHRkDFVQFF+1PJYx8wd3jereln0Ep0YkiHey50hjTLVUycQoE4X977605pXMuFWORweuktzph+Xg==} + dependencies: + '@types/pg': 8.6.6 + dev: false + + /@next/env/13.4.19: + resolution: {integrity: sha512-FsAT5x0jF2kkhNkKkukhsyYOrRqtSxrEhfliniIq0bwWbuXLgyt3Gv0Ml+b91XwjwArmuP7NxCiGd++GGKdNMQ==} + dev: false + + /@next/eslint-plugin-next/13.4.19: + resolution: {integrity: sha512-N/O+zGb6wZQdwu6atMZHbR7T9Np5SUFUjZqCbj0sXm+MwQO35M8TazVB4otm87GkXYs2l6OPwARd3/PUWhZBVQ==} + dependencies: + glob: 7.1.7 + dev: false + + /@next/swc-darwin-arm64/13.4.19: + resolution: {integrity: sha512-vv1qrjXeGbuF2mOkhkdxMDtv9np7W4mcBtaDnHU+yJG+bBwa6rYsYSCI/9Xm5+TuF5SbZbrWO6G1NfTh1TMjvQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /@next/swc-darwin-x64/13.4.19: + resolution: {integrity: sha512-jyzO6wwYhx6F+7gD8ddZfuqO4TtpJdw3wyOduR4fxTUCm3aLw7YmHGYNjS0xRSYGAkLpBkH1E0RcelyId6lNsw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /@next/swc-linux-arm64-gnu/13.4.19: + resolution: {integrity: sha512-vdlnIlaAEh6H+G6HrKZB9c2zJKnpPVKnA6LBwjwT2BTjxI7e0Hx30+FoWCgi50e+YO49p6oPOtesP9mXDRiiUg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@next/swc-linux-arm64-musl/13.4.19: + resolution: {integrity: sha512-aU0HkH2XPgxqrbNRBFb3si9Ahu/CpaR5RPmN2s9GiM9qJCiBBlZtRTiEca+DC+xRPyCThTtWYgxjWHgU7ZkyvA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@next/swc-linux-x64-gnu/13.4.19: + resolution: {integrity: sha512-htwOEagMa/CXNykFFeAHHvMJeqZfNQEoQvHfsA4wgg5QqGNqD5soeCer4oGlCol6NGUxknrQO6VEustcv+Md+g==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@next/swc-linux-x64-musl/13.4.19: + resolution: {integrity: sha512-4Gj4vvtbK1JH8ApWTT214b3GwUh9EKKQjY41hH/t+u55Knxi/0wesMzwQRhppK6Ddalhu0TEttbiJ+wRcoEj5Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@next/swc-win32-arm64-msvc/13.4.19: + resolution: {integrity: sha512-bUfDevQK4NsIAHXs3/JNgnvEY+LRyneDN788W2NYiRIIzmILjba7LaQTfihuFawZDhRtkYCv3JDC3B4TwnmRJw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /@next/swc-win32-ia32-msvc/13.4.19: + resolution: {integrity: sha512-Y5kikILFAr81LYIFaw6j/NrOtmiM4Sf3GtOc0pn50ez2GCkr+oejYuKGcwAwq3jiTKuzF6OF4iT2INPoxRycEA==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /@next/swc-win32-x64-msvc/13.4.19: + resolution: {integrity: sha512-YzA78jBDXMYiINdPdJJwGgPNT3YqBNNGhsthsDoWHL9p24tEJn9ViQf/ZqTbwSpX/RrkPupLfuuTH2sf73JBAw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /@nodelib/fs.scandir/2.1.5: + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + dev: false + + /@nodelib/fs.stat/2.0.5: + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + dev: false + + /@nodelib/fs.walk/1.2.8: + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.15.0 + dev: false + + /@peculiar/asn1-schema/2.3.6: + resolution: {integrity: sha512-izNRxPoaeJeg/AyH8hER6s+H7p4itk+03QCa4sbxI3lNdseQYCuxzgsuNK8bTXChtLTjpJz6NmXKA73qLa3rCA==} + dependencies: + asn1js: 3.0.5 + pvtsutils: 1.3.3 + tslib: 2.6.1 + dev: false + + /@peculiar/json-schema/1.1.12: + resolution: {integrity: sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w==} + engines: {node: '>=8.0.0'} + dependencies: + tslib: 2.6.1 + dev: false + + /@peculiar/webcrypto/1.4.1: + resolution: {integrity: sha512-eK4C6WTNYxoI7JOabMoZICiyqRRtJB220bh0Mbj5RwRycleZf9BPyZoxsTvpP0FpmVS2aS13NKOuh5/tN3sIRw==} + engines: {node: '>=10.12.0'} + dependencies: + '@peculiar/asn1-schema': 2.3.6 + '@peculiar/json-schema': 1.1.12 + pvtsutils: 1.3.3 + tslib: 2.6.1 + webcrypto-core: 1.7.7 + dev: false + + /@resvg/resvg-wasm/2.4.1: + resolution: {integrity: sha512-yi6R0HyHtsoWTRA06Col4WoDs7SvlXU3DLMNP2bdAgs7HK18dTEVl1weXgxRzi8gwLteGUbIg29zulxIB3GSdg==} + engines: {node: '>= 10'} + dev: false + + /@rushstack/eslint-patch/1.3.3: + resolution: {integrity: sha512-0xd7qez0AQ+MbHatZTlI1gu5vkG8r7MYRUJAHPAHJBmGLs16zpkrpAVLvjQKQOqaXPDUBwOiJzNc00znHSCVBw==} + dev: false + + /@shuding/opentype.js/1.4.0-beta.0: + resolution: {integrity: sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA==} + engines: {node: '>= 8.0.0'} + hasBin: true + dependencies: + fflate: 0.7.4 + string.prototype.codepointat: 0.2.1 + dev: false + + /@sinclair/typebox/0.27.8: + resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + dev: false + + /@sinonjs/commons/3.0.0: + resolution: {integrity: sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==} + dependencies: + type-detect: 4.0.8 + dev: false + + /@sinonjs/fake-timers/10.3.0: + resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} + dependencies: + '@sinonjs/commons': 3.0.0 + dev: false + + /@swc/helpers/0.5.1: + resolution: {integrity: sha512-sJ902EfIzn1Fa+qYmjdQqh8tPsoxyBz+8yBKC2HKUxyezKJFwPGOn7pv4WY6QuQW//ySQi5lJjA/ZT9sNWWNTg==} + dependencies: + tslib: 2.6.1 + dev: false + + /@tailwindcss/forms/0.5.6_tailwindcss@3.3.3: + resolution: {integrity: sha512-Fw+2BJ0tmAwK/w01tEFL5TiaJBX1NLT1/YbWgvm7ws3Qcn11kiXxzNTEQDMs5V3mQemhB56l3u0i9dwdzSQldA==} + peerDependencies: + tailwindcss: '>=3.0.0 || >= 3.0.0-alpha.1' + dependencies: + mini-svg-data-uri: 1.4.4 + tailwindcss: 3.3.3 + dev: false + + /@tootallnate/once/2.0.0: + resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} + engines: {node: '>= 10'} + dev: false + + /@types/body-parser/1.19.2: + resolution: {integrity: sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==} + dependencies: + '@types/connect': 3.4.35 + '@types/node': 20.5.9 + dev: false + + /@types/connect/3.4.35: + resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==} + dependencies: + '@types/node': 20.5.9 + dev: false + + /@types/cookies/0.7.7: + resolution: {integrity: sha512-h7BcvPUogWbKCzBR2lY4oqaZbO3jXZksexYJVFvkrFeLgbZjQkU4x8pRq6eg2MHXQhY0McQdqmmsxRWlVAHooA==} + dependencies: + '@types/connect': 3.4.35 + '@types/express': 4.17.14 + '@types/keygrip': 1.0.2 + '@types/node': 20.5.9 + dev: false + + /@types/express-serve-static-core/4.17.35: + resolution: {integrity: sha512-wALWQwrgiB2AWTT91CB62b6Yt0sNHpznUXeZEcnPU3DRdlDIz74x8Qg1UUYKSVFi+va5vKOLYRBI1bRKiLLKIg==} + dependencies: + '@types/node': 20.5.9 + '@types/qs': 6.9.7 + '@types/range-parser': 1.2.4 + '@types/send': 0.17.1 + dev: false + + /@types/express/4.17.14: + resolution: {integrity: sha512-TEbt+vaPFQ+xpxFLFssxUDXj5cWCxZJjIcB7Yg0k0GMHGtgtQgpvx/MUQUeAkNbA9AAGrwkAsoeItdTgS7FMyg==} + dependencies: + '@types/body-parser': 1.19.2 + '@types/express-serve-static-core': 4.17.35 + '@types/qs': 6.9.7 + '@types/serve-static': 1.15.2 + dev: false + + /@types/http-errors/2.0.1: + resolution: {integrity: sha512-/K3ds8TRAfBvi5vfjuz8y6+GiAYBZ0x4tXv1Av6CWBWn0IlADc+ZX9pMq7oU0fNQPnBwIZl3rmeLp6SBApbxSQ==} + dev: false + + /@types/istanbul-lib-coverage/2.0.4: + resolution: {integrity: sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==} + dev: false + + /@types/istanbul-lib-report/3.0.0: + resolution: {integrity: sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==} + dependencies: + '@types/istanbul-lib-coverage': 2.0.4 + dev: false + + /@types/istanbul-reports/3.0.1: + resolution: {integrity: sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==} + dependencies: + '@types/istanbul-lib-report': 3.0.0 + dev: false + + /@types/jsdom/20.0.1: + resolution: {integrity: sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==} + dependencies: + '@types/node': 20.5.9 + '@types/tough-cookie': 4.0.2 + parse5: 7.1.2 + dev: false + + /@types/json-schema/7.0.12: + resolution: {integrity: sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==} + dev: false + + /@types/json5/0.0.29: + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + dev: false + + /@types/keygrip/1.0.2: + resolution: {integrity: sha512-GJhpTepz2udxGexqos8wgaBx4I/zWIDPh/KOGEwAqtuGDkOUJu5eFvwmdBX4AmB8Odsr+9pHCQqiAqDL/yKMKw==} + dev: false + + /@types/mime/1.3.2: + resolution: {integrity: sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==} + dev: false + + /@types/mime/3.0.1: + resolution: {integrity: sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==} + dev: false + + /@types/node-fetch/2.6.2: + resolution: {integrity: sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A==} + dependencies: + '@types/node': 20.5.9 + form-data: 3.0.1 + dev: false + + /@types/node/16.18.6: + resolution: {integrity: sha512-vmYJF0REqDyyU0gviezF/KHq/fYaUbFhkcNbQCuPGFQj6VTbXuHZoxs/Y7mutWe73C8AC6l9fFu8mSYiBAqkGA==} + dev: false + + /@types/node/20.5.9: + resolution: {integrity: sha512-PcGNd//40kHAS3sTlzKB9C9XL4K0sTup8nbG5lC14kzEteTNuAFh9u5nA0o5TWnSG2r/JNPRXFVcHJIIeRlmqQ==} + dev: false + + /@types/pg/8.6.6: + resolution: {integrity: sha512-O2xNmXebtwVekJDD+02udOncjVcMZQuTEQEMpKJ0ZRf5E7/9JJX3izhKUcUifBkyKpljyUM6BTgy2trmviKlpw==} + dependencies: + '@types/node': 20.5.9 + pg-protocol: 1.6.0 + pg-types: 2.2.0 + dev: false + + /@types/prop-types/15.7.5: + resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==} + dev: false + + /@types/qs/6.9.7: + resolution: {integrity: sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==} + dev: false + + /@types/range-parser/1.2.4: + resolution: {integrity: sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==} + dev: false + + /@types/react-dom/18.2.7: + resolution: {integrity: sha512-GRaAEriuT4zp9N4p1i8BDBYmEyfo+xQ3yHjJU4eiK5NDa1RmUZG+unZABUTK4/Ox/M+GaHwb6Ow8rUITrtjszA==} + dependencies: + '@types/react': 18.2.21 + dev: false + + /@types/react/18.2.21: + resolution: {integrity: sha512-neFKG/sBAwGxHgXiIxnbm3/AAVQ/cMRS93hvBpg8xYRbeQSPVABp9U2bRnPf0iI4+Ucdv3plSxKK+3CW2ENJxA==} + dependencies: + '@types/prop-types': 15.7.5 + '@types/scheduler': 0.16.3 + csstype: 3.1.2 + dev: false + + /@types/scheduler/0.16.3: + resolution: {integrity: sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==} + dev: false + + /@types/semver/7.5.0: + resolution: {integrity: sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==} + dev: false + + /@types/send/0.17.1: + resolution: {integrity: sha512-Cwo8LE/0rnvX7kIIa3QHCkcuF21c05Ayb0ZfxPiv0W8VRiZiNW/WuRupHKpqqGVGf7SUA44QSOUKaEd9lIrd/Q==} + dependencies: + '@types/mime': 1.3.2 + '@types/node': 20.5.9 + dev: false + + /@types/serve-static/1.15.2: + resolution: {integrity: sha512-J2LqtvFYCzaj8pVYKw8klQXrLLk7TBZmQ4ShlcdkELFKGwGMfevMLneMMRkMgZxotOD9wg497LpC7O8PcvAmfw==} + dependencies: + '@types/http-errors': 2.0.1 + '@types/mime': 3.0.1 + '@types/node': 20.5.9 + dev: false + + /@types/stack-utils/2.0.1: + resolution: {integrity: sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==} + dev: false + + /@types/tough-cookie/4.0.2: + resolution: {integrity: sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==} + dev: false + + /@types/yargs-parser/21.0.0: + resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==} + dev: false + + /@types/yargs/17.0.24: + resolution: {integrity: sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==} + dependencies: + '@types/yargs-parser': 21.0.0 + dev: false + + /@typescript-eslint/eslint-plugin/6.6.0_vt4saesn3ecaumaytn5wzbmrxe: + resolution: {integrity: sha512-CW9YDGTQnNYMIo5lMeuiIG08p4E0cXrXTbcZ2saT/ETE7dWUrNxlijsQeU04qAAKkILiLzdQz+cGFxCJjaZUmA==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@eslint-community/regexpp': 4.6.2 + '@typescript-eslint/parser': 6.6.0_w2g2uv42be7wag4oipwkfv43p4 + '@typescript-eslint/scope-manager': 6.6.0 + '@typescript-eslint/type-utils': 6.6.0_w2g2uv42be7wag4oipwkfv43p4 + '@typescript-eslint/utils': 6.6.0_w2g2uv42be7wag4oipwkfv43p4 + '@typescript-eslint/visitor-keys': 6.6.0 + debug: 4.3.4 + eslint: 8.48.0 + graphemer: 1.4.0 + ignore: 5.2.4 + natural-compare: 1.4.0 + semver: 7.5.4 + ts-api-utils: 1.0.1_typescript@5.2.2 + typescript: 5.2.2 + transitivePeerDependencies: + - supports-color + dev: false + + /@typescript-eslint/parser/6.6.0_w2g2uv42be7wag4oipwkfv43p4: + resolution: {integrity: sha512-setq5aJgUwtzGrhW177/i+DMLqBaJbdwGj2CPIVFFLE0NCliy5ujIdLHd2D1ysmlmsjdL2GWW+hR85neEfc12w==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/scope-manager': 6.6.0 + '@typescript-eslint/types': 6.6.0 + '@typescript-eslint/typescript-estree': 6.6.0_typescript@5.2.2 + '@typescript-eslint/visitor-keys': 6.6.0 + debug: 4.3.4 + eslint: 8.48.0 + typescript: 5.2.2 + transitivePeerDependencies: + - supports-color + dev: false + + /@typescript-eslint/scope-manager/6.6.0: + resolution: {integrity: sha512-pT08u5W/GT4KjPUmEtc2kSYvrH8x89cVzkA0Sy2aaOUIw6YxOIjA8ilwLr/1fLjOedX1QAuBpG9XggWqIIfERw==} + engines: {node: ^16.0.0 || >=18.0.0} + dependencies: + '@typescript-eslint/types': 6.6.0 + '@typescript-eslint/visitor-keys': 6.6.0 + dev: false + + /@typescript-eslint/type-utils/6.6.0_w2g2uv42be7wag4oipwkfv43p4: + resolution: {integrity: sha512-8m16fwAcEnQc69IpeDyokNO+D5spo0w1jepWWY2Q6y5ZKNuj5EhVQXjtVAeDDqvW6Yg7dhclbsz6rTtOvcwpHg==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/typescript-estree': 6.6.0_typescript@5.2.2 + '@typescript-eslint/utils': 6.6.0_w2g2uv42be7wag4oipwkfv43p4 + debug: 4.3.4 + eslint: 8.48.0 + ts-api-utils: 1.0.1_typescript@5.2.2 + typescript: 5.2.2 + transitivePeerDependencies: + - supports-color + dev: false + + /@typescript-eslint/types/6.6.0: + resolution: {integrity: sha512-CB6QpJQ6BAHlJXdwUmiaXDBmTqIE2bzGTDLADgvqtHWuhfNP3rAOK7kAgRMAET5rDRr9Utt+qAzRBdu3AhR3sg==} + engines: {node: ^16.0.0 || >=18.0.0} + dev: false + + /@typescript-eslint/typescript-estree/6.6.0_typescript@5.2.2: + resolution: {integrity: sha512-hMcTQ6Al8MP2E6JKBAaSxSVw5bDhdmbCEhGW/V8QXkb9oNsFkA4SBuOMYVPxD3jbtQ4R/vSODBsr76R6fP3tbA==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/types': 6.6.0 + '@typescript-eslint/visitor-keys': 6.6.0 + debug: 4.3.4 + globby: 11.1.0 + is-glob: 4.0.3 + semver: 7.5.4 + ts-api-utils: 1.0.1_typescript@5.2.2 + typescript: 5.2.2 + transitivePeerDependencies: + - supports-color + dev: false + + /@typescript-eslint/utils/6.6.0_w2g2uv42be7wag4oipwkfv43p4: + resolution: {integrity: sha512-mPHFoNa2bPIWWglWYdR0QfY9GN0CfvvXX1Sv6DlSTive3jlMTUy+an67//Gysc+0Me9pjitrq0LJp0nGtLgftw==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + dependencies: + '@eslint-community/eslint-utils': 4.4.0_eslint@8.48.0 + '@types/json-schema': 7.0.12 + '@types/semver': 7.5.0 + '@typescript-eslint/scope-manager': 6.6.0 + '@typescript-eslint/types': 6.6.0 + '@typescript-eslint/typescript-estree': 6.6.0_typescript@5.2.2 + eslint: 8.48.0 + semver: 7.5.4 + transitivePeerDependencies: + - supports-color + - typescript + dev: false + + /@typescript-eslint/visitor-keys/6.6.0: + resolution: {integrity: sha512-L61uJT26cMOfFQ+lMZKoJNbAEckLe539VhTxiGHrWl5XSKQgA0RTBZJW2HFPy5T0ZvPVSD93QsrTKDkfNwJGyQ==} + engines: {node: ^16.0.0 || >=18.0.0} + dependencies: + '@typescript-eslint/types': 6.6.0 + eslint-visitor-keys: 3.4.3 + dev: false + + /@vercel/analytics/1.0.2: + resolution: {integrity: sha512-BZFxVrv24VbNNl5xMxqUojQIegEeXMI6rX3rg1uVLYUEXsuKNBSAEQf4BWEcjQDp/8aYJOj6m8V4PUA3x/cxgg==} + dev: false + + /@vercel/blob/0.10.0: + resolution: {integrity: sha512-ypRO1Q0mil2FjvNCZDGmOBrdARuuR+FpV4MKkQvFvHiyc2K6EB16XmrFOdmHojPiof172NL4b1Y6hpfbUtq5IA==} + engines: {node: '>=16.14'} + dependencies: + jest-environment-jsdom: 29.6.1 + undici: 5.22.1 + transitivePeerDependencies: + - bufferutil + - canvas + - supports-color + - utf-8-validate + dev: false + + /@vercel/og/0.5.13: + resolution: {integrity: sha512-okWQ2Jt+155Hn518Y8Tt4Iqb7QdNFoPvH/n4sMpC4K4bcijjo2RAEwXwlhTYHAAkoiJ0AEEeWeknZj1aBgOOqw==} + engines: {node: '>=16'} + dependencies: + '@resvg/resvg-wasm': 2.4.1 + satori: 0.10.4 + yoga-wasm-web: 0.3.3 + dev: false + + /@vercel/postgres/0.4.1: + resolution: {integrity: sha512-rYlNnaXrr2/NWK/OodhAUyed0bomaizKKC8XXjNYv8I1K3m75oocP4IGTcBpZe76VCrHuaKW5d6jLQnuRRoNKg==} + engines: {node: '>=14.6'} + dependencies: + '@neondatabase/serverless': 0.5.6 + bufferutil: 4.0.7 + utf-8-validate: 6.0.3 + ws: 8.13.0_2adebc2xdjqbcvbjxkodkhadp4 + dev: false + + /abab/2.0.6: + resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==} + dev: false + + /acorn-globals/7.0.1: + resolution: {integrity: sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==} + dependencies: + acorn: 8.10.0 + acorn-walk: 8.2.0 + dev: false + + /acorn-jsx/5.3.2_acorn@8.10.0: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + acorn: 8.10.0 + dev: false + + /acorn-walk/8.2.0: + resolution: {integrity: sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==} + engines: {node: '>=0.4.0'} + dev: false + + /acorn/8.10.0: + resolution: {integrity: sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==} + engines: {node: '>=0.4.0'} + hasBin: true + dev: false + + /agent-base/6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + dependencies: + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: false + + /ajv/6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + dev: false + + /ansi-regex/5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + dev: false + + /ansi-styles/3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + dependencies: + color-convert: 1.9.3 + dev: false + + /ansi-styles/4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + dependencies: + color-convert: 2.0.1 + dev: false + + /ansi-styles/5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + dev: false + + /any-base/1.1.0: + resolution: {integrity: sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg==} + dev: false + + /any-promise/1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + dev: false + + /anymatch/3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + dev: false + + /arg/5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + dev: false + + /argparse/2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + dev: false + + /aria-query/5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + dependencies: + dequal: 2.0.3 + dev: false + + /array-buffer-byte-length/1.0.0: + resolution: {integrity: sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==} + dependencies: + call-bind: 1.0.2 + is-array-buffer: 3.0.2 + dev: false + + /array-includes/3.1.6: + resolution: {integrity: sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.22.1 + get-intrinsic: 1.2.1 + is-string: 1.0.7 + dev: false + + /array-union/2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + dev: false + + /array.prototype.findlastindex/1.2.2: + resolution: {integrity: sha512-tb5thFFlUcp7NdNF6/MpDk/1r/4awWG1FIz3YqDf+/zJSTezBb+/5WViH41obXULHVpDzoiCLpJ/ZO9YbJMsdw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.22.1 + es-shim-unscopables: 1.0.0 + get-intrinsic: 1.2.1 + dev: false + + /array.prototype.flat/1.3.1: + resolution: {integrity: sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.22.1 + es-shim-unscopables: 1.0.0 + dev: false + + /array.prototype.flatmap/1.3.1: + resolution: {integrity: sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.22.1 + es-shim-unscopables: 1.0.0 + dev: false + + /array.prototype.tosorted/1.1.1: + resolution: {integrity: sha512-pZYPXPRl2PqWcsUs6LOMn+1f1532nEoPTYowBtqLwAW+W8vSVhkIGnmOX1t/UQjD6YGI0vcD2B1U7ZFGQH9jnQ==} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.22.1 + es-shim-unscopables: 1.0.0 + get-intrinsic: 1.2.1 + dev: false + + /arraybuffer.prototype.slice/1.0.1: + resolution: {integrity: sha512-09x0ZWFEjj4WD8PDbykUwo3t9arLn8NIzmmYEJFpYekOAQjpkGSyrQhNoRTcwwcFRu+ycWF78QZ63oWTqSjBcw==} + engines: {node: '>= 0.4'} + dependencies: + array-buffer-byte-length: 1.0.0 + call-bind: 1.0.2 + define-properties: 1.2.0 + get-intrinsic: 1.2.1 + is-array-buffer: 3.0.2 + is-shared-array-buffer: 1.0.2 + dev: false + + /asn1js/3.0.5: + resolution: {integrity: sha512-FVnvrKJwpt9LP2lAMl8qZswRNm3T4q9CON+bxldk2iwk3FFpuwhx2FfinyitizWHsVYyaY+y5JzDR0rCMV5yTQ==} + engines: {node: '>=12.0.0'} + dependencies: + pvtsutils: 1.3.3 + pvutils: 1.1.3 + tslib: 2.6.1 + dev: false + + /ast-types-flow/0.0.7: + resolution: {integrity: sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag==} + dev: false + + /asynciterator.prototype/1.0.0: + resolution: {integrity: sha512-wwHYEIS0Q80f5mosx3L/dfG5t5rjEa9Ft51GTaNt862EnpyGHpgz2RkZvLPp1oF5TnAiTohkEKVEu8pQPJI7Vg==} + dependencies: + has-symbols: 1.0.3 + dev: false + + /asynckit/0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + dev: false + + /autoprefixer/10.4.15_postcss@8.4.29: + resolution: {integrity: sha512-KCuPB8ZCIqFdA4HwKXsvz7j6gvSDNhDP7WnUjBleRkKjPdvCmHFuQ77ocavI8FT6NdvlBnE2UFr2H4Mycn8Vew==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + dependencies: + browserslist: 4.21.10 + caniuse-lite: 1.0.30001521 + fraction.js: 4.2.0 + normalize-range: 0.1.2 + picocolors: 1.0.0 + postcss: 8.4.29 + postcss-value-parser: 4.2.0 + dev: false + + /available-typed-arrays/1.0.5: + resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} + engines: {node: '>= 0.4'} + dev: false + + /axe-core/4.7.2: + resolution: {integrity: sha512-zIURGIS1E1Q4pcrMjp+nnEh+16G56eG/MUllJH8yEvw7asDo7Ac9uhC9KIH5jzpITueEZolfYglnCGIuSBz39g==} + engines: {node: '>=4'} + dev: false + + /axobject-query/3.2.1: + resolution: {integrity: sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==} + dependencies: + dequal: 2.0.3 + dev: false + + /balanced-match/1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + dev: false + + /base64-js/0.0.8: + resolution: {integrity: sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==} + engines: {node: '>= 0.4'} + dev: false + + /binary-extensions/2.2.0: + resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} + engines: {node: '>=8'} + dev: false + + /brace-expansion/1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + dev: false + + /braces/3.0.2: + resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} + engines: {node: '>=8'} + dependencies: + fill-range: 7.0.1 + dev: false + + /browserslist/4.21.10: + resolution: {integrity: sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + dependencies: + caniuse-lite: 1.0.30001521 + electron-to-chromium: 1.4.492 + node-releases: 2.0.13 + update-browserslist-db: 1.0.11_browserslist@4.21.10 + dev: false + + /bufferutil/4.0.7: + resolution: {integrity: sha512-kukuqc39WOHtdxtw4UScxF/WVnMFVSQVKhtx3AjZJzhd0RGZZldcrfSEbVsWWe6KNH253574cq5F+wpv0G9pJw==} + engines: {node: '>=6.14.2'} + requiresBuild: true + dependencies: + node-gyp-build: 4.6.0 + dev: false + + /busboy/1.6.0: + resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} + engines: {node: '>=10.16.0'} + dependencies: + streamsearch: 1.1.0 + dev: false + + /call-bind/1.0.2: + resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} + dependencies: + function-bind: 1.1.1 + get-intrinsic: 1.2.1 + dev: false + + /callsites/3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + dev: false + + /camelcase-css/2.0.1: + resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} + engines: {node: '>= 6'} + dev: false + + /camelcase-keys/6.2.2: + resolution: {integrity: sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==} + engines: {node: '>=8'} + dependencies: + camelcase: 5.3.1 + map-obj: 4.3.0 + quick-lru: 4.0.1 + dev: false + + /camelcase-keys/9.0.0: + resolution: {integrity: sha512-GdZ92DNXdcfFB/5Kq4O82EL6UW5neiRBhfNP5M3mGw7CX2sPDbVA04ZPLsqbp7oMi2l3m2I0AZ/kFP5Nk5kopA==} + engines: {node: '>=16'} + dependencies: + camelcase: 8.0.0 + map-obj: 5.0.0 + quick-lru: 6.1.1 + type-fest: 4.2.0 + dev: false + + /camelcase/5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + dev: false + + /camelcase/8.0.0: + resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==} + engines: {node: '>=16'} + dev: false + + /camelize/1.0.1: + resolution: {integrity: sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==} + dev: false + + /caniuse-lite/1.0.30001521: + resolution: {integrity: sha512-fnx1grfpEOvDGH+V17eccmNjucGUnCbP6KL+l5KqBIerp26WK/+RQ7CIDE37KGJjaPyqWXXlFUyKiWmvdNNKmQ==} + dev: false + + /chalk/2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + dev: false + + /chalk/4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + dev: false + + /chokidar/3.5.3: + resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} + engines: {node: '>= 8.10.0'} + dependencies: + anymatch: 3.1.3 + braces: 3.0.2 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.2 + dev: false + + /ci-info/3.8.0: + resolution: {integrity: sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==} + engines: {node: '>=8'} + dev: false + + /client-only/0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + dev: false + + /color-convert/1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + dependencies: + color-name: 1.1.3 + dev: false + + /color-convert/2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + dependencies: + color-name: 1.1.4 + dev: false + + /color-name/1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + dev: false + + /color-name/1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + dev: false + + /combined-stream/1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + dependencies: + delayed-stream: 1.0.0 + dev: false + + /commander/4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + dev: false + + /concat-map/0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + dev: false + + /cookie/0.5.0: + resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} + engines: {node: '>= 0.6'} + dev: false + + /cross-spawn/7.0.3: + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + engines: {node: '>= 8'} + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + dev: false + + /css-background-parser/0.1.0: + resolution: {integrity: sha512-2EZLisiZQ+7m4wwur/qiYJRniHX4K5Tc9w93MT3AS0WS1u5kaZ4FKXlOTBhOjc+CgEgPiGY+fX1yWD8UwpEqUA==} + dev: false + + /css-box-shadow/1.0.0-3: + resolution: {integrity: sha512-9jaqR6e7Ohds+aWwmhe6wILJ99xYQbfmK9QQB9CcMjDbTxPZjwEmUQpU91OG05Xgm8BahT5fW+svbsQGjS/zPg==} + dev: false + + /css-color-keywords/1.0.0: + resolution: {integrity: sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==} + engines: {node: '>=4'} + dev: false + + /css-to-react-native/3.2.0: + resolution: {integrity: sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==} + dependencies: + camelize: 1.0.1 + css-color-keywords: 1.0.0 + postcss-value-parser: 4.2.0 + dev: false + + /cssesc/3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + dev: false + + /cssom/0.3.8: + resolution: {integrity: sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==} + dev: false + + /cssom/0.5.0: + resolution: {integrity: sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==} + dev: false + + /cssstyle/2.3.0: + resolution: {integrity: sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==} + engines: {node: '>=8'} + dependencies: + cssom: 0.3.8 + dev: false + + /csstype/3.1.1: + resolution: {integrity: sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==} + dev: false + + /csstype/3.1.2: + resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==} + dev: false + + /damerau-levenshtein/1.0.8: + resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} + dev: false + + /data-urls/3.0.2: + resolution: {integrity: sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==} + engines: {node: '>=12'} + dependencies: + abab: 2.0.6 + whatwg-mimetype: 3.0.0 + whatwg-url: 11.0.0 + dev: false + + /date-fns/2.30.0: + resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} + engines: {node: '>=0.11'} + dependencies: + '@babel/runtime': 7.22.10 + dev: false + + /debug/3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.3 + dev: false + + /debug/4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + dev: false + + /decimal.js/10.4.3: + resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} + dev: false + + /deep-is/0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + dev: false + + /deepmerge/4.2.2: + resolution: {integrity: sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==} + engines: {node: '>=0.10.0'} + dev: false + + /define-properties/1.2.0: + resolution: {integrity: sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==} + engines: {node: '>= 0.4'} + dependencies: + has-property-descriptors: 1.0.0 + object-keys: 1.1.1 + dev: false + + /delayed-stream/1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + dev: false + + /dequal/2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + dev: false + + /didyoumean/1.2.2: + resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + dev: false + + /dir-glob/3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + dependencies: + path-type: 4.0.0 + dev: false + + /dlv/1.1.3: + resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + dev: false + + /doctrine/2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + dependencies: + esutils: 2.0.3 + dev: false + + /doctrine/3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + dependencies: + esutils: 2.0.3 + dev: false + + /domexception/4.0.0: + resolution: {integrity: sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==} + engines: {node: '>=12'} + dependencies: + webidl-conversions: 7.0.0 + dev: false + + /dot-case/3.0.4: + resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} + dependencies: + no-case: 3.0.4 + tslib: 2.6.1 + dev: false + + /electron-to-chromium/1.4.492: + resolution: {integrity: sha512-36K9b/6skMVwAIEsC7GiQ8I8N3soCALVSHqWHzNDtGemAcI9Xu8hP02cywWM0A794rTHm0b0zHPeLJHtgFVamQ==} + dev: false + + /emoji-regex/10.2.1: + resolution: {integrity: sha512-97g6QgOk8zlDRdgq1WxwgTMgEWGVAQvB5Fdpgc1MkNy56la5SKP9GsMXKDOdqwn90/41a8yPwIGk1Y6WVbeMQA==} + dev: false + + /emoji-regex/9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + dev: false + + /enhanced-resolve/5.15.0: + resolution: {integrity: sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==} + engines: {node: '>=10.13.0'} + dependencies: + graceful-fs: 4.2.11 + tapable: 2.2.1 + dev: false + + /entities/4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + dev: false + + /es-abstract/1.22.1: + resolution: {integrity: sha512-ioRRcXMO6OFyRpyzV3kE1IIBd4WG5/kltnzdxSCqoP8CMGs/Li+M1uF5o7lOkZVFjDs+NLesthnF66Pg/0q0Lw==} + engines: {node: '>= 0.4'} + dependencies: + array-buffer-byte-length: 1.0.0 + arraybuffer.prototype.slice: 1.0.1 + available-typed-arrays: 1.0.5 + call-bind: 1.0.2 + es-set-tostringtag: 2.0.1 + es-to-primitive: 1.2.1 + function.prototype.name: 1.1.5 + get-intrinsic: 1.2.1 + get-symbol-description: 1.0.0 + globalthis: 1.0.3 + gopd: 1.0.1 + has: 1.0.3 + has-property-descriptors: 1.0.0 + has-proto: 1.0.1 + has-symbols: 1.0.3 + internal-slot: 1.0.5 + is-array-buffer: 3.0.2 + is-callable: 1.2.7 + is-negative-zero: 2.0.2 + is-regex: 1.1.4 + is-shared-array-buffer: 1.0.2 + is-string: 1.0.7 + is-typed-array: 1.1.12 + is-weakref: 1.0.2 + object-inspect: 1.12.3 + object-keys: 1.1.1 + object.assign: 4.1.4 + regexp.prototype.flags: 1.5.0 + safe-array-concat: 1.0.0 + safe-regex-test: 1.0.0 + string.prototype.trim: 1.2.7 + string.prototype.trimend: 1.0.6 + string.prototype.trimstart: 1.0.6 + typed-array-buffer: 1.0.0 + typed-array-byte-length: 1.0.0 + typed-array-byte-offset: 1.0.0 + typed-array-length: 1.0.4 + unbox-primitive: 1.0.2 + which-typed-array: 1.1.11 + dev: false + + /es-iterator-helpers/1.0.13: + resolution: {integrity: sha512-LK3VGwzvaPWobO8xzXXGRUOGw8Dcjyfk62CsY/wfHN75CwsJPbuypOYJxK6g5RyEL8YDjIWcl6jgd8foO6mmrA==} + dependencies: + asynciterator.prototype: 1.0.0 + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.22.1 + es-set-tostringtag: 2.0.1 + function-bind: 1.1.1 + get-intrinsic: 1.2.1 + globalthis: 1.0.3 + has-property-descriptors: 1.0.0 + has-proto: 1.0.1 + has-symbols: 1.0.3 + internal-slot: 1.0.5 + iterator.prototype: 1.1.0 + safe-array-concat: 1.0.0 + dev: false + + /es-set-tostringtag/2.0.1: + resolution: {integrity: sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.1 + has: 1.0.3 + has-tostringtag: 1.0.0 + dev: false + + /es-shim-unscopables/1.0.0: + resolution: {integrity: sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==} + dependencies: + has: 1.0.3 + dev: false + + /es-to-primitive/1.2.1: + resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} + engines: {node: '>= 0.4'} + dependencies: + is-callable: 1.2.7 + is-date-object: 1.0.5 + is-symbol: 1.0.4 + dev: false + + /escalade/3.1.1: + resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} + engines: {node: '>=6'} + dev: false + + /escape-html/1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + dev: false + + /escape-string-regexp/1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + dev: false + + /escape-string-regexp/2.0.0: + resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} + engines: {node: '>=8'} + dev: false + + /escape-string-regexp/4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + dev: false + + /escodegen/2.1.0: + resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} + engines: {node: '>=6.0'} + hasBin: true + dependencies: + esprima: 4.0.1 + estraverse: 5.3.0 + esutils: 2.0.3 + optionalDependencies: + source-map: 0.6.1 + dev: false + + /eslint-config-next/13.4.19_w2g2uv42be7wag4oipwkfv43p4: + resolution: {integrity: sha512-WE8367sqMnjhWHvR5OivmfwENRQ1ixfNE9hZwQqNCsd+iM3KnuMc1V8Pt6ytgjxjf23D+xbesADv9x3xaKfT3g==} + peerDependencies: + eslint: ^7.23.0 || ^8.0.0 + typescript: '>=3.3.1' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@next/eslint-plugin-next': 13.4.19 + '@rushstack/eslint-patch': 1.3.3 + '@typescript-eslint/parser': 6.6.0_w2g2uv42be7wag4oipwkfv43p4 + eslint: 8.48.0 + eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 3.6.0_rh2fv5plymv6fyfib4csxejkqm + eslint-plugin-import: 2.28.0_xmc235rimipjezckpfvevprzbu + eslint-plugin-jsx-a11y: 6.7.1_eslint@8.48.0 + eslint-plugin-react: 7.33.2_eslint@8.48.0 + eslint-plugin-react-hooks: 5.0.0-canary-7118f5dd7-20230705_eslint@8.48.0 + typescript: 5.2.2 + transitivePeerDependencies: + - eslint-import-resolver-webpack + - supports-color + dev: false + + /eslint-import-resolver-node/0.3.9: + resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} + dependencies: + debug: 3.2.7 + is-core-module: 2.13.0 + resolve: 1.22.4 + transitivePeerDependencies: + - supports-color + dev: false + + /eslint-import-resolver-typescript/3.6.0_rh2fv5plymv6fyfib4csxejkqm: + resolution: {integrity: sha512-QTHR9ddNnn35RTxlaEnx2gCxqFlF2SEN0SE2d17SqwyM7YOSI2GHWRYp5BiRkObTUNYPupC/3Fq2a0PpT+EKpg==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + eslint: '*' + eslint-plugin-import: '*' + dependencies: + debug: 4.3.4 + enhanced-resolve: 5.15.0 + eslint: 8.48.0 + eslint-module-utils: 2.8.0_ieroxneheyeel7zibq7gkwr2f4 + eslint-plugin-import: 2.28.0_xmc235rimipjezckpfvevprzbu + fast-glob: 3.3.1 + get-tsconfig: 4.7.0 + is-core-module: 2.13.0 + is-glob: 4.0.3 + transitivePeerDependencies: + - '@typescript-eslint/parser' + - eslint-import-resolver-node + - eslint-import-resolver-webpack + - supports-color + dev: false + + /eslint-module-utils/2.8.0_ieroxneheyeel7zibq7gkwr2f4: + resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + dependencies: + '@typescript-eslint/parser': 6.6.0_w2g2uv42be7wag4oipwkfv43p4 + debug: 3.2.7 + eslint: 8.48.0 + eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 3.6.0_rh2fv5plymv6fyfib4csxejkqm + transitivePeerDependencies: + - supports-color + dev: false + + /eslint-plugin-import/2.28.0_xmc235rimipjezckpfvevprzbu: + resolution: {integrity: sha512-B8s/n+ZluN7sxj9eUf7/pRFERX0r5bnFA2dCaLHy2ZeaQEAz0k+ZZkFWRFHJAqxfxQDx6KLv9LeIki7cFdwW+Q==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + dependencies: + '@typescript-eslint/parser': 6.6.0_w2g2uv42be7wag4oipwkfv43p4 + array-includes: 3.1.6 + array.prototype.findlastindex: 1.2.2 + array.prototype.flat: 1.3.1 + array.prototype.flatmap: 1.3.1 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 8.48.0 + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.8.0_ieroxneheyeel7zibq7gkwr2f4 + has: 1.0.3 + is-core-module: 2.13.0 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.fromentries: 2.0.6 + object.groupby: 1.0.0 + object.values: 1.1.6 + resolve: 1.22.4 + semver: 6.3.1 + tsconfig-paths: 3.14.2 + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + dev: false + + /eslint-plugin-jsx-a11y/6.7.1_eslint@8.48.0: + resolution: {integrity: sha512-63Bog4iIethyo8smBklORknVjB0T2dwB8Mr/hIC+fBS0uyHdYYpzM/Ed+YC8VxTjlXHEWFOdmgwcDn1U2L9VCA==} + engines: {node: '>=4.0'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 + dependencies: + '@babel/runtime': 7.22.10 + aria-query: 5.3.0 + array-includes: 3.1.6 + array.prototype.flatmap: 1.3.1 + ast-types-flow: 0.0.7 + axe-core: 4.7.2 + axobject-query: 3.2.1 + damerau-levenshtein: 1.0.8 + emoji-regex: 9.2.2 + eslint: 8.48.0 + has: 1.0.3 + jsx-ast-utils: 3.3.5 + language-tags: 1.0.5 + minimatch: 3.1.2 + object.entries: 1.1.6 + object.fromentries: 2.0.6 + semver: 6.3.1 + dev: false + + /eslint-plugin-react-hooks/5.0.0-canary-7118f5dd7-20230705_eslint@8.48.0: + resolution: {integrity: sha512-AZYbMo/NW9chdL7vk6HQzQhT+PvTAEVqWk9ziruUoW2kAOcN5qNyelv70e0F1VNQAbvutOC9oc+xfWycI9FxDw==} + engines: {node: '>=10'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 + dependencies: + eslint: 8.48.0 + dev: false + + /eslint-plugin-react/7.33.2_eslint@8.48.0: + resolution: {integrity: sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw==} + engines: {node: '>=4'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 + dependencies: + array-includes: 3.1.6 + array.prototype.flatmap: 1.3.1 + array.prototype.tosorted: 1.1.1 + doctrine: 2.1.0 + es-iterator-helpers: 1.0.13 + eslint: 8.48.0 + estraverse: 5.3.0 + jsx-ast-utils: 3.3.5 + minimatch: 3.1.2 + object.entries: 1.1.6 + object.fromentries: 2.0.6 + object.hasown: 1.1.2 + object.values: 1.1.6 + prop-types: 15.8.1 + resolve: 2.0.0-next.4 + semver: 6.3.1 + string.prototype.matchall: 4.0.8 + dev: false + + /eslint-scope/7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + dev: false + + /eslint-visitor-keys/3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: false + + /eslint/8.48.0: + resolution: {integrity: sha512-sb6DLeIuRXxeM1YljSe1KEx9/YYeZFQWcV8Rq9HfigmdDEugjLEVEa1ozDjL6YDjBpQHPJxJzze+alxi4T3OLg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + hasBin: true + dependencies: + '@eslint-community/eslint-utils': 4.4.0_eslint@8.48.0 + '@eslint-community/regexpp': 4.6.2 + '@eslint/eslintrc': 2.1.2 + '@eslint/js': 8.48.0 + '@humanwhocodes/config-array': 0.11.10 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.3 + debug: 4.3.4 + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.5.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.21.0 + graphemer: 1.4.0 + ignore: 5.2.4 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-yaml: 4.1.0 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.3 + strip-ansi: 6.0.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + dev: false + + /espree/9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + acorn: 8.10.0 + acorn-jsx: 5.3.2_acorn@8.10.0 + eslint-visitor-keys: 3.4.3 + dev: false + + /esprima/4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + dev: false + + /esquery/1.5.0: + resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} + engines: {node: '>=0.10'} + dependencies: + estraverse: 5.3.0 + dev: false + + /esrecurse/4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + dependencies: + estraverse: 5.3.0 + dev: false + + /estraverse/5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + dev: false + + /esutils/2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + dev: false + + /fast-deep-equal/3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + dev: false + + /fast-glob/3.3.1: + resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} + engines: {node: '>=8.6.0'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.5 + dev: false + + /fast-json-stable-stringify/2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + dev: false + + /fast-levenshtein/2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + dev: false + + /fastq/1.15.0: + resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==} + dependencies: + reusify: 1.0.4 + dev: false + + /fflate/0.7.4: + resolution: {integrity: sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==} + dev: false + + /file-entry-cache/6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + dependencies: + flat-cache: 3.0.4 + dev: false + + /fill-range/7.0.1: + resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} + engines: {node: '>=8'} + dependencies: + to-regex-range: 5.0.1 + dev: false + + /find-up/5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + dev: false + + /flat-cache/3.0.4: + resolution: {integrity: sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==} + engines: {node: ^10.12.0 || >=12.0.0} + dependencies: + flatted: 3.2.7 + rimraf: 3.0.2 + dev: false + + /flatted/3.2.7: + resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==} + dev: false + + /for-each/0.3.3: + resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} + dependencies: + is-callable: 1.2.7 + dev: false + + /form-data/3.0.1: + resolution: {integrity: sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==} + engines: {node: '>= 6'} + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + dev: false + + /form-data/4.0.0: + resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} + engines: {node: '>= 6'} + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + dev: false + + /fraction.js/4.2.0: + resolution: {integrity: sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==} + dev: false + + /framer-motion/10.16.3_biqbaboplfbrettd7655fr4n2y: + resolution: {integrity: sha512-1OMs6wY964hX8YjiCeYQlgrZDbkKvZztnynTUgUZjdzq2au6PZUsodmUY6GAudUgImrWqTrtpSwMbi1ETmIx4A==} + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + dependencies: + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 + tslib: 2.6.1 + optionalDependencies: + '@emotion/is-prop-valid': 0.8.8 + dev: false + + /fs.realpath/1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + dev: false + + /fsevents/2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /function-bind/1.1.1: + resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} + dev: false + + /function.prototype.name/1.1.5: + resolution: {integrity: sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.22.1 + functions-have-names: 1.2.3 + dev: false + + /functions-have-names/1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + dev: false + + /get-intrinsic/1.2.1: + resolution: {integrity: sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==} + dependencies: + function-bind: 1.1.1 + has: 1.0.3 + has-proto: 1.0.1 + has-symbols: 1.0.3 + dev: false + + /get-symbol-description/1.0.0: + resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + dev: false + + /get-tsconfig/4.7.0: + resolution: {integrity: sha512-pmjiZ7xtB8URYm74PlGJozDNyhvsVLUcpBa8DZBG3bWHwaHa9bPiRpiSfovw+fjhwONSCWKRyk+JQHEGZmMrzw==} + dependencies: + resolve-pkg-maps: 1.0.0 + dev: false + + /glob-parent/5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + dependencies: + is-glob: 4.0.3 + dev: false + + /glob-parent/6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + dependencies: + is-glob: 4.0.3 + dev: false + + /glob-to-regexp/0.4.1: + resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} + dev: false + + /glob/7.1.6: + resolution: {integrity: sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==} + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + dev: false + + /glob/7.1.7: + resolution: {integrity: sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==} + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + dev: false + + /glob/7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + dev: false + + /globals/13.21.0: + resolution: {integrity: sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==} + engines: {node: '>=8'} + dependencies: + type-fest: 0.20.2 + dev: false + + /globalthis/1.0.3: + resolution: {integrity: sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==} + engines: {node: '>= 0.4'} + dependencies: + define-properties: 1.2.0 + dev: false + + /globby/11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.1 + ignore: 5.2.4 + merge2: 1.4.1 + slash: 3.0.0 + dev: false + + /gopd/1.0.1: + resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + dependencies: + get-intrinsic: 1.2.1 + dev: false + + /graceful-fs/4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + dev: false + + /graphemer/1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + dev: false + + /has-bigints/1.0.2: + resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} + dev: false + + /has-flag/3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + dev: false + + /has-flag/4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + dev: false + + /has-property-descriptors/1.0.0: + resolution: {integrity: sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==} + dependencies: + get-intrinsic: 1.2.1 + dev: false + + /has-proto/1.0.1: + resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} + engines: {node: '>= 0.4'} + dev: false + + /has-symbols/1.0.3: + resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} + engines: {node: '>= 0.4'} + dev: false + + /has-tostringtag/1.0.0: + resolution: {integrity: sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + dev: false + + /has/1.0.3: + resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} + engines: {node: '>= 0.4.0'} + dependencies: + function-bind: 1.1.1 + dev: false + + /hex-rgb/4.3.0: + resolution: {integrity: sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==} + engines: {node: '>=6'} + dev: false + + /html-encoding-sniffer/3.0.0: + resolution: {integrity: sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==} + engines: {node: '>=12'} + dependencies: + whatwg-encoding: 2.0.0 + dev: false + + /http-proxy-agent/5.0.0: + resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} + engines: {node: '>= 6'} + dependencies: + '@tootallnate/once': 2.0.0 + agent-base: 6.0.2 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: false + + /https-proxy-agent/5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + dependencies: + agent-base: 6.0.2 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: false + + /iconv-lite/0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + dependencies: + safer-buffer: 2.1.2 + dev: false + + /ignore/5.2.4: + resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} + engines: {node: '>= 4'} + dev: false + + /import-fresh/3.3.0: + resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + engines: {node: '>=6'} + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + dev: false + + /imurmurhash/0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + dev: false + + /inflight/1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + dev: false + + /inherits/2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + dev: false + + /internal-slot/1.0.5: + resolution: {integrity: sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.1 + has: 1.0.3 + side-channel: 1.0.4 + dev: false + + /is-array-buffer/3.0.2: + resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + is-typed-array: 1.1.12 + dev: false + + /is-async-function/2.0.0: + resolution: {integrity: sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: false + + /is-bigint/1.0.4: + resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} + dependencies: + has-bigints: 1.0.2 + dev: false + + /is-binary-path/2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + dependencies: + binary-extensions: 2.2.0 + dev: false + + /is-boolean-object/1.1.2: + resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + has-tostringtag: 1.0.0 + dev: false + + /is-callable/1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + dev: false + + /is-core-module/2.13.0: + resolution: {integrity: sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==} + dependencies: + has: 1.0.3 + dev: false + + /is-date-object/1.0.5: + resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: false + + /is-extglob/2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + dev: false + + /is-finalizationregistry/1.0.2: + resolution: {integrity: sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==} + dependencies: + call-bind: 1.0.2 + dev: false + + /is-generator-function/1.0.10: + resolution: {integrity: sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: false + + /is-glob/4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + dependencies: + is-extglob: 2.1.1 + dev: false + + /is-map/2.0.2: + resolution: {integrity: sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==} + dev: false + + /is-negative-zero/2.0.2: + resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==} + engines: {node: '>= 0.4'} + dev: false + + /is-number-object/1.0.7: + resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: false + + /is-number/7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + dev: false + + /is-path-inside/3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + dev: false + + /is-potential-custom-element-name/1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + dev: false + + /is-regex/1.1.4: + resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + has-tostringtag: 1.0.0 + dev: false + + /is-set/2.0.2: + resolution: {integrity: sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==} + dev: false + + /is-shared-array-buffer/1.0.2: + resolution: {integrity: sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==} + dependencies: + call-bind: 1.0.2 + dev: false + + /is-string/1.0.7: + resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: false + + /is-symbol/1.0.4: + resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + dev: false + + /is-typed-array/1.1.12: + resolution: {integrity: sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==} + engines: {node: '>= 0.4'} + dependencies: + which-typed-array: 1.1.11 + dev: false + + /is-weakmap/2.0.1: + resolution: {integrity: sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==} + dev: false + + /is-weakref/1.0.2: + resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} + dependencies: + call-bind: 1.0.2 + dev: false + + /is-weakset/2.0.2: + resolution: {integrity: sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + dev: false + + /isarray/2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + dev: false + + /isexe/2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + dev: false + + /iterator.prototype/1.1.0: + resolution: {integrity: sha512-rjuhAk1AJ1fssphHD0IFV6TWL40CwRZ53FrztKx43yk2v6rguBYsY4Bj1VU4HmoMmKwZUlx7mfnhDf9cOp4YTw==} + dependencies: + define-properties: 1.2.0 + get-intrinsic: 1.2.1 + has-symbols: 1.0.3 + has-tostringtag: 1.0.0 + reflect.getprototypeof: 1.0.3 + dev: false + + /jest-environment-jsdom/29.6.1: + resolution: {integrity: sha512-PoY+yLaHzVRhVEjcVKSfJ7wXmJW4UqPYNhR05h7u/TK0ouf6DmRNZFBL/Z00zgQMyWGMBXn69/FmOvhEJu8cIw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + canvas: ^2.5.0 + peerDependenciesMeta: + canvas: + optional: true + dependencies: + '@jest/environment': 29.6.2 + '@jest/fake-timers': 29.6.2 + '@jest/types': 29.6.1 + '@types/jsdom': 20.0.1 + '@types/node': 20.5.9 + jest-mock: 29.6.2 + jest-util: 29.6.2 + jsdom: 20.0.3 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: false + + /jest-message-util/29.6.2: + resolution: {integrity: sha512-vnIGYEjoPSuRqV8W9t+Wow95SDp6KPX2Uf7EoeG9G99J2OVh7OSwpS4B6J0NfpEIpfkBNHlBZpA2rblEuEFhZQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/code-frame': 7.22.10 + '@jest/types': 29.6.1 + '@types/stack-utils': 2.0.1 + chalk: 4.1.2 + graceful-fs: 4.2.11 + micromatch: 4.0.5 + pretty-format: 29.6.2 + slash: 3.0.0 + stack-utils: 2.0.6 + dev: false + + /jest-mock/29.6.2: + resolution: {integrity: sha512-hoSv3lb3byzdKfwqCuT6uTscan471GUECqgNYykg6ob0yiAw3zYc7OrPnI9Qv8Wwoa4lC7AZ9hyS4AiIx5U2zg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.1 + '@types/node': 20.5.9 + jest-util: 29.6.2 + dev: false + + /jest-util/29.6.2: + resolution: {integrity: sha512-3eX1qb6L88lJNCFlEADKOkjpXJQyZRiavX1INZ4tRnrBVr2COd3RgcTLyUiEXMNBlDU/cgYq6taUS0fExrWW4w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.1 + '@types/node': 20.5.9 + chalk: 4.1.2 + ci-info: 3.8.0 + graceful-fs: 4.2.11 + picomatch: 2.3.1 + dev: false + + /jiti/1.19.1: + resolution: {integrity: sha512-oVhqoRDaBXf7sjkll95LHVS6Myyyb1zaunVwk4Z0+WPSW4gjS0pl01zYKHScTuyEhQsFxV5L4DR5r+YqSyqyyg==} + hasBin: true + dev: false + + /js-cookie/3.0.1: + resolution: {integrity: sha512-+0rgsUXZu4ncpPxRL+lNEptWMOWl9etvPHc/koSRp6MPwpRYAhmk0dUG00J4bxVV3r9uUzfo24wW0knS07SKSw==} + engines: {node: '>=12'} + dev: false + + /js-tokens/4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + dev: false + + /js-yaml/4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + dependencies: + argparse: 2.0.1 + dev: false + + /jsdom/20.0.3: + resolution: {integrity: sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==} + engines: {node: '>=14'} + peerDependencies: + canvas: ^2.5.0 + peerDependenciesMeta: + canvas: + optional: true + dependencies: + abab: 2.0.6 + acorn: 8.10.0 + acorn-globals: 7.0.1 + cssom: 0.5.0 + cssstyle: 2.3.0 + data-urls: 3.0.2 + decimal.js: 10.4.3 + domexception: 4.0.0 + escodegen: 2.1.0 + form-data: 4.0.0 + html-encoding-sniffer: 3.0.0 + http-proxy-agent: 5.0.0 + https-proxy-agent: 5.0.1 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.7 + parse5: 7.1.2 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 4.1.3 + w3c-xmlserializer: 4.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 2.0.0 + whatwg-mimetype: 3.0.0 + whatwg-url: 11.0.0 + ws: 8.13.0 + xml-name-validator: 4.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: false + + /json-schema-traverse/0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + dev: false + + /json-stable-stringify-without-jsonify/1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + dev: false + + /json5/1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + dependencies: + minimist: 1.2.8 + dev: false + + /jsx-ast-utils/3.3.5: + resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} + engines: {node: '>=4.0'} + dependencies: + array-includes: 3.1.6 + array.prototype.flat: 1.3.1 + object.assign: 4.1.4 + object.values: 1.1.6 + dev: false + + /language-subtag-registry/0.3.22: + resolution: {integrity: sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==} + dev: false + + /language-tags/1.0.5: + resolution: {integrity: sha512-qJhlO9cGXi6hBGKoxEG/sKZDAHD5Hnu9Hs4WbOY3pCWXDhw0N8x1NenNzm2EnNLkLkk7J2SdxAkDSbb6ftT+UQ==} + dependencies: + language-subtag-registry: 0.3.22 + dev: false + + /levn/0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + dev: false + + /lilconfig/2.1.0: + resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} + engines: {node: '>=10'} + dev: false + + /linebreak/1.1.0: + resolution: {integrity: sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==} + dependencies: + base64-js: 0.0.8 + unicode-trie: 2.0.0 + dev: false + + /lines-and-columns/1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + dev: false + + /locate-path/6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + dependencies: + p-locate: 5.0.0 + dev: false + + /lodash.merge/4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + dev: false + + /loose-envify/1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + dependencies: + js-tokens: 4.0.0 + dev: false + + /lower-case/2.0.2: + resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} + dependencies: + tslib: 2.6.1 + dev: false + + /lru-cache/6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + dependencies: + yallist: 4.0.0 + dev: false + + /map-obj/4.3.0: + resolution: {integrity: sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==} + engines: {node: '>=8'} + dev: false + + /map-obj/5.0.0: + resolution: {integrity: sha512-2L3MIgJynYrZ3TYMriLDLWocz15okFakV6J12HXvMXDHui2x/zgChzg1u9mFFGbbGWE+GsLpQByt4POb9Or+uA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: false + + /merge2/1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + dev: false + + /micromatch/4.0.5: + resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} + engines: {node: '>=8.6'} + dependencies: + braces: 3.0.2 + picomatch: 2.3.1 + dev: false + + /mime-db/1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + dev: false + + /mime-types/2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + dependencies: + mime-db: 1.52.0 + dev: false + + /mini-svg-data-uri/1.4.4: + resolution: {integrity: sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==} + hasBin: true + dev: false + + /minimatch/3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + dependencies: + brace-expansion: 1.1.11 + dev: false + + /minimist/1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + dev: false + + /ms/2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + dev: false + + /ms/2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + dev: false + + /mz/2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + dev: false + + /nanoid/3.3.6: + resolution: {integrity: sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + dev: false + + /natural-compare/1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + dev: false + + /next-themes/0.2.1_m2l4knchnqa6bzxbbtfxlzoxdy: + resolution: {integrity: sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A==} + peerDependencies: + next: '*' + react: '*' + react-dom: '*' + dependencies: + next: 13.4.19_biqbaboplfbrettd7655fr4n2y + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 + dev: false + + /next/13.4.19_biqbaboplfbrettd7655fr4n2y: + resolution: {integrity: sha512-HuPSzzAbJ1T4BD8e0bs6B9C1kWQ6gv8ykZoRWs5AQoiIuqbGHHdQO7Ljuvg05Q0Z24E2ABozHe6FxDvI6HfyAw==} + engines: {node: '>=16.8.0'} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + react: ^18.2.0 + react-dom: ^18.2.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + sass: + optional: true + dependencies: + '@next/env': 13.4.19 + '@swc/helpers': 0.5.1 + busboy: 1.6.0 + caniuse-lite: 1.0.30001521 + postcss: 8.4.14 + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 + styled-jsx: 5.1.1_react@18.2.0 + watchpack: 2.4.0 + zod: 3.21.4 + optionalDependencies: + '@next/swc-darwin-arm64': 13.4.19 + '@next/swc-darwin-x64': 13.4.19 + '@next/swc-linux-arm64-gnu': 13.4.19 + '@next/swc-linux-arm64-musl': 13.4.19 + '@next/swc-linux-x64-gnu': 13.4.19 + '@next/swc-linux-x64-musl': 13.4.19 + '@next/swc-win32-arm64-msvc': 13.4.19 + '@next/swc-win32-ia32-msvc': 13.4.19 + '@next/swc-win32-x64-msvc': 13.4.19 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + dev: false + + /no-case/3.0.4: + resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} + dependencies: + lower-case: 2.0.2 + tslib: 2.6.1 + dev: false + + /node-fetch-native/1.0.1: + resolution: {integrity: sha512-VzW+TAk2wE4X9maiKMlT+GsPU4OMmR1U9CrHSmd3DFLn2IcZ9VJ6M6BBugGfYUnPCLSYxXdZy17M0BEJyhUTwg==} + dev: false + + /node-gyp-build/4.6.0: + resolution: {integrity: sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==} + hasBin: true + dev: false + + /node-releases/2.0.13: + resolution: {integrity: sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==} + dev: false + + /normalize-path/3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + dev: false + + /normalize-range/0.1.2: + resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} + engines: {node: '>=0.10.0'} + dev: false + + /nwsapi/2.2.7: + resolution: {integrity: sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==} + dev: false + + /object-assign/4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + dev: false + + /object-hash/3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + dev: false + + /object-inspect/1.12.3: + resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==} + dev: false + + /object-keys/1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + dev: false + + /object.assign/4.1.4: + resolution: {integrity: sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + has-symbols: 1.0.3 + object-keys: 1.1.1 + dev: false + + /object.entries/1.1.6: + resolution: {integrity: sha512-leTPzo4Zvg3pmbQ3rDK69Rl8GQvIqMWubrkxONG9/ojtFE2rD9fjMKfSI5BxW3osRH1m6VdzmqK8oAY9aT4x5w==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.22.1 + dev: false + + /object.fromentries/2.0.6: + resolution: {integrity: sha512-VciD13dswC4j1Xt5394WR4MzmAQmlgN72phd/riNp9vtD7tp4QQWJ0R4wvclXcafgcYK8veHRed2W6XeGBvcfg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.22.1 + dev: false + + /object.groupby/1.0.0: + resolution: {integrity: sha512-70MWG6NfRH9GnbZOikuhPPYzpUpof9iW2J9E4dW7FXTqPNb6rllE6u39SKwwiNh8lCwX3DDb5OgcKGiEBrTTyw==} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.22.1 + get-intrinsic: 1.2.1 + dev: false + + /object.hasown/1.1.2: + resolution: {integrity: sha512-B5UIT3J1W+WuWIU55h0mjlwaqxiE5vYENJXIXZ4VFe05pNYrkKuK0U/6aFcb0pKywYJh7IhfoqUfKVmrJJHZHw==} + dependencies: + define-properties: 1.2.0 + es-abstract: 1.22.1 + dev: false + + /object.values/1.1.6: + resolution: {integrity: sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.22.1 + dev: false + + /once/1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + dependencies: + wrappy: 1.0.2 + dev: false + + /optionator/0.9.3: + resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} + engines: {node: '>= 0.8.0'} + dependencies: + '@aashutoshrathi/word-wrap': 1.2.6 + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + dev: false + + /p-limit/3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + dependencies: + yocto-queue: 0.1.0 + dev: false + + /p-locate/5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + dependencies: + p-limit: 3.1.0 + dev: false + + /pako/0.2.9: + resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==} + dev: false + + /parent-module/1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + dependencies: + callsites: 3.1.0 + dev: false + + /parse-css-color/0.2.1: + resolution: {integrity: sha512-bwS/GGIFV3b6KS4uwpzCFj4w297Yl3uqnSgIPsoQkx7GMLROXfMnWvxfNkL0oh8HVhZA4hvJoEoEIqonfJ3BWg==} + dependencies: + color-name: 1.1.4 + hex-rgb: 4.3.0 + dev: false + + /parse5/7.1.2: + resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==} + dependencies: + entities: 4.5.0 + dev: false + + /path-exists/4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + dev: false + + /path-is-absolute/1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + dev: false + + /path-key/3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + dev: false + + /path-parse/1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + dev: false + + /path-to-regexp/6.2.1: + resolution: {integrity: sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==} + dev: false + + /path-type/4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + dev: false + + /pg-int8/1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + dev: false + + /pg-protocol/1.6.0: + resolution: {integrity: sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q==} + dev: false + + /pg-types/2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.0 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + dev: false + + /picocolors/1.0.0: + resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} + dev: false + + /picomatch/2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + dev: false + + /pify/2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + dev: false + + /pirates/4.0.6: + resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} + engines: {node: '>= 6'} + dev: false + + /postcss-import/15.1.0_postcss@8.4.29: + resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} + engines: {node: '>=14.0.0'} + peerDependencies: + postcss: ^8.0.0 + dependencies: + postcss: 8.4.29 + postcss-value-parser: 4.2.0 + read-cache: 1.0.0 + resolve: 1.22.4 + dev: false + + /postcss-js/4.0.1_postcss@8.4.29: + resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==} + engines: {node: ^12 || ^14 || >= 16} + peerDependencies: + postcss: ^8.4.21 + dependencies: + camelcase-css: 2.0.1 + postcss: 8.4.29 + dev: false + + /postcss-load-config/4.0.1_postcss@8.4.29: + resolution: {integrity: sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==} + engines: {node: '>= 14'} + peerDependencies: + postcss: '>=8.0.9' + ts-node: '>=9.0.0' + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + dependencies: + lilconfig: 2.1.0 + postcss: 8.4.29 + yaml: 2.3.1 + dev: false + + /postcss-nested/6.0.1_postcss@8.4.29: + resolution: {integrity: sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.2.14 + dependencies: + postcss: 8.4.29 + postcss-selector-parser: 6.0.13 + dev: false + + /postcss-selector-parser/6.0.13: + resolution: {integrity: sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==} + engines: {node: '>=4'} + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + dev: false + + /postcss-value-parser/4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + dev: false + + /postcss/8.4.14: + resolution: {integrity: sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.6 + picocolors: 1.0.0 + source-map-js: 1.0.2 + dev: false + + /postcss/8.4.29: + resolution: {integrity: sha512-cbI+jaqIeu/VGqXEarWkRCCffhjgXc0qjBtXpqJhTBohMUjUQnbBr0xqX3vEKudc4iviTewcJo5ajcec5+wdJw==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.6 + picocolors: 1.0.0 + source-map-js: 1.0.2 + dev: false + + /postgres-array/2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + dev: false + + /postgres-bytea/1.0.0: + resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==} + engines: {node: '>=0.10.0'} + dev: false + + /postgres-date/1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + dev: false + + /postgres-interval/1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + dependencies: + xtend: 4.0.2 + dev: false + + /prelude-ls/1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + dev: false + + /pretty-format/29.6.2: + resolution: {integrity: sha512-1q0oC8eRveTg5nnBEWMXAU2qpv65Gnuf2eCQzSjxpWFkPaPARwqZZDGuNE0zPAZfTCHzIk3A8dIjwlQKKLphyg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/schemas': 29.6.0 + ansi-styles: 5.2.0 + react-is: 18.2.0 + dev: false + + /prop-types/15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + dev: false + + /psl/1.9.0: + resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==} + dev: false + + /punycode/2.3.0: + resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} + engines: {node: '>=6'} + dev: false + + /pvtsutils/1.3.3: + resolution: {integrity: sha512-6sAOMlXyrJ+8tRN5IAaYfuYZRp1C2uJ0SyDynEFxL+VY8kCRib9Lpj/+KPaNFpaQWr/iRik5nrzz6iaNlxgEGA==} + dependencies: + tslib: 2.6.1 + dev: false + + /pvutils/1.1.3: + resolution: {integrity: sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==} + engines: {node: '>=6.0.0'} + dev: false + + /querystringify/2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + dev: false + + /queue-microtask/1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + dev: false + + /quick-lru/4.0.1: + resolution: {integrity: sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==} + engines: {node: '>=8'} + dev: false + + /quick-lru/6.1.1: + resolution: {integrity: sha512-S27GBT+F0NTRiehtbrgaSE1idUAJ5bX8dPAQTdylEyNlrdcH5X4Lz7Edz3DYzecbsCluD5zO8ZNEe04z3D3u6Q==} + engines: {node: '>=12'} + dev: false + + /react-dom/18.2.0_react@18.2.0: + resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} + peerDependencies: + react: ^18.2.0 + dependencies: + loose-envify: 1.4.0 + react: 18.2.0 + scheduler: 0.23.0 + dev: false + + /react-icons/4.10.1_react@18.2.0: + resolution: {integrity: sha512-/ngzDP/77tlCfqthiiGNZeYFACw85fUjZtLbedmJ5DTlNDIwETxhwBzdOJ21zj4iJdvc0J3y7yOsX3PpxAJzrw==} + peerDependencies: + react: '*' + dependencies: + react: 18.2.0 + dev: false + + /react-is/16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + dev: false + + /react-is/18.2.0: + resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} + dev: false + + /react-spinners/0.13.8_biqbaboplfbrettd7655fr4n2y: + resolution: {integrity: sha512-3e+k56lUkPj0vb5NDXPVFAOkPC//XyhKPJjvcGjyMNPWsBKpplfeyialP74G7H7+It7KzhtET+MvGqbKgAqpZA==} + peerDependencies: + react: ^16.0.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 + dependencies: + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 + dev: false + + /react/18.2.0: + resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} + engines: {node: '>=0.10.0'} + dependencies: + loose-envify: 1.4.0 + dev: false + + /read-cache/1.0.0: + resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + dependencies: + pify: 2.3.0 + dev: false + + /readdirp/3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + dependencies: + picomatch: 2.3.1 + dev: false + + /reflect.getprototypeof/1.0.3: + resolution: {integrity: sha512-TTAOZpkJ2YLxl7mVHWrNo3iDMEkYlva/kgFcXndqMgbo/AZUmmavEkdXV+hXtE4P8xdyEKRzalaFqZVuwIk/Nw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.22.1 + get-intrinsic: 1.2.1 + globalthis: 1.0.3 + which-builtin-type: 1.1.3 + dev: false + + /regenerator-runtime/0.14.0: + resolution: {integrity: sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==} + dev: false + + /regexp.prototype.flags/1.5.0: + resolution: {integrity: sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + functions-have-names: 1.2.3 + dev: false + + /requires-port/1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + dev: false + + /resolve-from/4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + dev: false + + /resolve-pkg-maps/1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + dev: false + + /resolve/1.22.4: + resolution: {integrity: sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==} + hasBin: true + dependencies: + is-core-module: 2.13.0 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + dev: false + + /resolve/2.0.0-next.4: + resolution: {integrity: sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ==} + hasBin: true + dependencies: + is-core-module: 2.13.0 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + dev: false + + /reusify/1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + dev: false + + /rimraf/3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + hasBin: true + dependencies: + glob: 7.2.3 + dev: false + + /run-parallel/1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + dependencies: + queue-microtask: 1.2.3 + dev: false + + /safe-array-concat/1.0.0: + resolution: {integrity: sha512-9dVEFruWIsnie89yym+xWTAYASdpw3CJV7Li/6zBewGf9z2i1j31rP6jnY0pHEO4QZh6N0K11bFjWmdR8UGdPQ==} + engines: {node: '>=0.4'} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + has-symbols: 1.0.3 + isarray: 2.0.5 + dev: false + + /safe-regex-test/1.0.0: + resolution: {integrity: sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + is-regex: 1.1.4 + dev: false + + /safer-buffer/2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + dev: false + + /satori/0.10.4: + resolution: {integrity: sha512-GJNIsuiXhiC9kWGLvz04Op5DZy2UFYZAWsuUtkTlQt3r15o0K96PeD+FMfGN4luMPUHc4uV9gXqAoPxOK0omSw==} + engines: {node: '>=16'} + dependencies: + '@shuding/opentype.js': 1.4.0-beta.0 + css-background-parser: 0.1.0 + css-box-shadow: 1.0.0-3 + css-to-react-native: 3.2.0 + emoji-regex: 10.2.1 + escape-html: 1.0.3 + linebreak: 1.1.0 + parse-css-color: 0.2.1 + postcss-value-parser: 4.2.0 + yoga-wasm-web: 0.3.3 + dev: false + + /sax/1.2.4: + resolution: {integrity: sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==} + dev: false + + /saxes/6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + dependencies: + xmlchars: 2.2.0 + dev: false + + /scheduler/0.23.0: + resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} + dependencies: + loose-envify: 1.4.0 + dev: false + + /semver/6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + dev: false + + /semver/7.5.4: + resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} + engines: {node: '>=10'} + hasBin: true + dependencies: + lru-cache: 6.0.0 + dev: false + + /shebang-command/2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + dependencies: + shebang-regex: 3.0.0 + dev: false + + /shebang-regex/3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + dev: false + + /short-uuid/4.2.2: + resolution: {integrity: sha512-IE7hDSGV2U/VZoCsjctKX6l5t5ak2jE0+aeGJi3KtvjIUNuZVmHVYUjNBhmo369FIWGDtaieRaO8A83Lvwfpqw==} + engines: {node: '>=8'} + dependencies: + any-base: 1.1.0 + uuid: 8.3.2 + dev: false + + /side-channel/1.0.4: + resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + object-inspect: 1.12.3 + dev: false + + /slash/3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + dev: false + + /snake-case/3.0.4: + resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==} + dependencies: + dot-case: 3.0.4 + tslib: 2.6.1 + dev: false + + /snakecase-keys/3.2.1: + resolution: {integrity: sha512-CjU5pyRfwOtaOITYv5C8DzpZ8XA/ieRsDpr93HI2r6e3YInC6moZpSQbmUtg8cTk58tq2x3jcG2gv+p1IZGmMA==} + engines: {node: '>=8'} + dependencies: + map-obj: 4.3.0 + to-snake-case: 1.0.0 + dev: false + + /snakecase-keys/5.4.4: + resolution: {integrity: sha512-YTywJG93yxwHLgrYLZjlC75moVEX04LZM4FHfihjHe1FCXm+QaLOFfSf535aXOAd0ArVQMWUAe8ZPm4VtWyXaA==} + engines: {node: '>=12'} + dependencies: + map-obj: 4.3.0 + snake-case: 3.0.4 + type-fest: 2.19.0 + dev: false + + /source-map-js/1.0.2: + resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} + engines: {node: '>=0.10.0'} + dev: false + + /source-map/0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + requiresBuild: true + dev: false + optional: true + + /stack-utils/2.0.6: + resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} + engines: {node: '>=10'} + dependencies: + escape-string-regexp: 2.0.0 + dev: false + + /streamsearch/1.1.0: + resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} + engines: {node: '>=10.0.0'} + dev: false + + /string.prototype.codepointat/0.2.1: + resolution: {integrity: sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==} + dev: false + + /string.prototype.matchall/4.0.8: + resolution: {integrity: sha512-6zOCOcJ+RJAQshcTvXPHoxoQGONa3e/Lqx90wUA+wEzX78sg5Bo+1tQo4N0pohS0erG9qtCqJDjNCQBjeWVxyg==} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.22.1 + get-intrinsic: 1.2.1 + has-symbols: 1.0.3 + internal-slot: 1.0.5 + regexp.prototype.flags: 1.5.0 + side-channel: 1.0.4 + dev: false + + /string.prototype.trim/1.2.7: + resolution: {integrity: sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.22.1 + dev: false + + /string.prototype.trimend/1.0.6: + resolution: {integrity: sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.22.1 + dev: false + + /string.prototype.trimstart/1.0.6: + resolution: {integrity: sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.22.1 + dev: false + + /strip-ansi/6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + dependencies: + ansi-regex: 5.0.1 + dev: false + + /strip-bom/3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + dev: false + + /strip-json-comments/3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + dev: false + + /styled-jsx/5.1.1_react@18.2.0: + resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0' + peerDependenciesMeta: + '@babel/core': + optional: true + babel-plugin-macros: + optional: true + dependencies: + client-only: 0.0.1 + react: 18.2.0 + dev: false + + /sucrase/3.34.0: + resolution: {integrity: sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw==} + engines: {node: '>=8'} + hasBin: true + dependencies: + '@jridgewell/gen-mapping': 0.3.3 + commander: 4.1.1 + glob: 7.1.6 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.6 + ts-interface-checker: 0.1.13 + dev: false + + /supports-color/5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + dependencies: + has-flag: 3.0.0 + dev: false + + /supports-color/7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + dependencies: + has-flag: 4.0.0 + dev: false + + /supports-preserve-symlinks-flag/1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + dev: false + + /swr/2.2.0_react@18.2.0: + resolution: {integrity: sha512-AjqHOv2lAhkuUdIiBu9xbuettzAzWXmCEcLONNKJRba87WAefz8Ca9d6ds/SzrPc235n1IxWYdhJ2zF3MNUaoQ==} + peerDependencies: + react: ^16.11.0 || ^17.0.0 || ^18.0.0 + dependencies: + react: 18.2.0 + use-sync-external-store: 1.2.0_react@18.2.0 + dev: false + + /symbol-tree/3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + dev: false + + /tailwindcss/3.3.3: + resolution: {integrity: sha512-A0KgSkef7eE4Mf+nKJ83i75TMyq8HqY3qmFIJSWy8bNt0v1lG7jUcpGpoTFxAwYcWOphcTBLPPJg+bDfhDf52w==} + engines: {node: '>=14.0.0'} + hasBin: true + dependencies: + '@alloc/quick-lru': 5.2.0 + arg: 5.0.2 + chokidar: 3.5.3 + didyoumean: 1.2.2 + dlv: 1.1.3 + fast-glob: 3.3.1 + glob-parent: 6.0.2 + is-glob: 4.0.3 + jiti: 1.19.1 + lilconfig: 2.1.0 + micromatch: 4.0.5 + normalize-path: 3.0.0 + object-hash: 3.0.0 + picocolors: 1.0.0 + postcss: 8.4.29 + postcss-import: 15.1.0_postcss@8.4.29 + postcss-js: 4.0.1_postcss@8.4.29 + postcss-load-config: 4.0.1_postcss@8.4.29 + postcss-nested: 6.0.1_postcss@8.4.29 + postcss-selector-parser: 6.0.13 + resolve: 1.22.4 + sucrase: 3.34.0 + transitivePeerDependencies: + - ts-node + dev: false + + /tapable/2.2.1: + resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} + engines: {node: '>=6'} + dev: false + + /text-table/0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + dev: false + + /thenify-all/1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + dependencies: + thenify: 3.3.1 + dev: false + + /thenify/3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + dependencies: + any-promise: 1.3.0 + dev: false + + /tiny-inflate/1.0.3: + resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} + dev: false + + /to-no-case/1.0.2: + resolution: {integrity: sha512-Z3g735FxuZY8rodxV4gH7LxClE4H0hTIyHNIHdk+vpQxjLm0cwnKXq/OFVZ76SOQmto7txVcwSCwkU5kqp+FKg==} + dev: false + + /to-regex-range/5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + dependencies: + is-number: 7.0.0 + dev: false + + /to-snake-case/1.0.0: + resolution: {integrity: sha512-joRpzBAk1Bhi2eGEYBjukEWHOe/IvclOkiJl3DtA91jV6NwQ3MwXA4FHYeqk8BNp/D8bmi9tcNbRu/SozP0jbQ==} + dependencies: + to-space-case: 1.0.0 + dev: false + + /to-space-case/1.0.0: + resolution: {integrity: sha512-rLdvwXZ39VOn1IxGL3V6ZstoTbwLRckQmn/U8ZDLuWwIXNpuZDhQ3AiRUlhTbOXFVE9C+dR51wM0CBDhk31VcA==} + dependencies: + to-no-case: 1.0.2 + dev: false + + /tough-cookie/4.1.3: + resolution: {integrity: sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==} + engines: {node: '>=6'} + dependencies: + psl: 1.9.0 + punycode: 2.3.0 + universalify: 0.2.0 + url-parse: 1.5.10 + dev: false + + /tr46/3.0.0: + resolution: {integrity: sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==} + engines: {node: '>=12'} + dependencies: + punycode: 2.3.0 + dev: false + + /ts-api-utils/1.0.1_typescript@5.2.2: + resolution: {integrity: sha512-lC/RGlPmwdrIBFTX59wwNzqh7aR2otPNPR/5brHZm/XKFYKsfqxihXUe9pU3JI+3vGkl+vyCoNNnPhJn3aLK1A==} + engines: {node: '>=16.13.0'} + peerDependencies: + typescript: '>=4.2.0' + dependencies: + typescript: 5.2.2 + dev: false + + /ts-exif-parser/0.2.2: + resolution: {integrity: sha512-Z7qGRfvHaISUOErkKfc8Tp+GBnfMWCSyduiLJR0x/b/wKp60oiOvORk73fFML3l8Kf0gVX48mrRsYUcBEb5avQ==} + dependencies: + sax: 1.2.4 + dev: false + + /ts-interface-checker/0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + dev: false + + /tsconfig-paths/3.14.2: + resolution: {integrity: sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==} + dependencies: + '@types/json5': 0.0.29 + json5: 1.0.2 + minimist: 1.2.8 + strip-bom: 3.0.0 + dev: false + + /tslib/2.4.1: + resolution: {integrity: sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==} + dev: false + + /tslib/2.6.1: + resolution: {integrity: sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==} + dev: false + + /type-check/0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.2.1 + dev: false + + /type-detect/4.0.8: + resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} + engines: {node: '>=4'} + dev: false + + /type-fest/0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + dev: false + + /type-fest/2.19.0: + resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} + engines: {node: '>=12.20'} + dev: false + + /type-fest/4.2.0: + resolution: {integrity: sha512-5zknd7Dss75pMSED270A1RQS3KloqRJA9XbXLe0eCxyw7xXFb3rd+9B0UQ/0E+LQT6lnrLviEolYORlRWamn4w==} + engines: {node: '>=16'} + dev: false + + /typed-array-buffer/1.0.0: + resolution: {integrity: sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + is-typed-array: 1.1.12 + dev: false + + /typed-array-byte-length/1.0.0: + resolution: {integrity: sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + for-each: 0.3.3 + has-proto: 1.0.1 + is-typed-array: 1.1.12 + dev: false + + /typed-array-byte-offset/1.0.0: + resolution: {integrity: sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.5 + call-bind: 1.0.2 + for-each: 0.3.3 + has-proto: 1.0.1 + is-typed-array: 1.1.12 + dev: false + + /typed-array-length/1.0.4: + resolution: {integrity: sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==} + dependencies: + call-bind: 1.0.2 + for-each: 0.3.3 + is-typed-array: 1.1.12 + dev: false + + /typescript/5.2.2: + resolution: {integrity: sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==} + engines: {node: '>=14.17'} + hasBin: true + dev: false + + /unbox-primitive/1.0.2: + resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} + dependencies: + call-bind: 1.0.2 + has-bigints: 1.0.2 + has-symbols: 1.0.3 + which-boxed-primitive: 1.0.2 + dev: false + + /undici/5.22.1: + resolution: {integrity: sha512-Ji2IJhFXZY0x/0tVBXeQwgPlLWw13GVzpsWPQ3rV50IFMMof2I55PZZxtm4P6iNq+L5znYN9nSTAq0ZyE6lSJw==} + engines: {node: '>=14.0'} + dependencies: + busboy: 1.6.0 + dev: false + + /unicode-trie/2.0.0: + resolution: {integrity: sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==} + dependencies: + pako: 0.2.9 + tiny-inflate: 1.0.3 + dev: false + + /universalify/0.2.0: + resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} + engines: {node: '>= 4.0.0'} + dev: false + + /update-browserslist-db/1.0.11_browserslist@4.21.10: + resolution: {integrity: sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + dependencies: + browserslist: 4.21.10 + escalade: 3.1.1 + picocolors: 1.0.0 + dev: false + + /uri-js/4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + dependencies: + punycode: 2.3.0 + dev: false + + /url-parse/1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + dev: false + + /use-sync-external-store/1.2.0_react@18.2.0: + resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + dependencies: + react: 18.2.0 + dev: false + + /utf-8-validate/6.0.3: + resolution: {integrity: sha512-uIuGf9TWQ/y+0Lp+KGZCMuJWc3N9BHA+l/UmHd/oUHwJJDeysyTRxNQVkbzsIWfGFbRe3OcgML/i0mvVRPOyDA==} + engines: {node: '>=6.14.2'} + requiresBuild: true + dependencies: + node-gyp-build: 4.6.0 + dev: false + + /util-deprecate/1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + dev: false + + /uuid/8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true + dev: false + + /w3c-xmlserializer/4.0.0: + resolution: {integrity: sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==} + engines: {node: '>=14'} + dependencies: + xml-name-validator: 4.0.0 + dev: false + + /watchpack/2.4.0: + resolution: {integrity: sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==} + engines: {node: '>=10.13.0'} + dependencies: + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + dev: false + + /webcrypto-core/1.7.7: + resolution: {integrity: sha512-7FjigXNsBfopEj+5DV2nhNpfic2vumtjjgPmeDKk45z+MJwXKKfhPB7118Pfzrmh4jqOMST6Ch37iPAHoImg5g==} + dependencies: + '@peculiar/asn1-schema': 2.3.6 + '@peculiar/json-schema': 1.1.12 + asn1js: 3.0.5 + pvtsutils: 1.3.3 + tslib: 2.6.1 + dev: false + + /webidl-conversions/7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + dev: false + + /whatwg-encoding/2.0.0: + resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==} + engines: {node: '>=12'} + dependencies: + iconv-lite: 0.6.3 + dev: false + + /whatwg-mimetype/3.0.0: + resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} + engines: {node: '>=12'} + dev: false + + /whatwg-url/11.0.0: + resolution: {integrity: sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==} + engines: {node: '>=12'} + dependencies: + tr46: 3.0.0 + webidl-conversions: 7.0.0 + dev: false + + /which-boxed-primitive/1.0.2: + resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} + dependencies: + is-bigint: 1.0.4 + is-boolean-object: 1.1.2 + is-number-object: 1.0.7 + is-string: 1.0.7 + is-symbol: 1.0.4 + dev: false + + /which-builtin-type/1.1.3: + resolution: {integrity: sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw==} + engines: {node: '>= 0.4'} + dependencies: + function.prototype.name: 1.1.5 + has-tostringtag: 1.0.0 + is-async-function: 2.0.0 + is-date-object: 1.0.5 + is-finalizationregistry: 1.0.2 + is-generator-function: 1.0.10 + is-regex: 1.1.4 + is-weakref: 1.0.2 + isarray: 2.0.5 + which-boxed-primitive: 1.0.2 + which-collection: 1.0.1 + which-typed-array: 1.1.11 + dev: false + + /which-collection/1.0.1: + resolution: {integrity: sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==} + dependencies: + is-map: 2.0.2 + is-set: 2.0.2 + is-weakmap: 2.0.1 + is-weakset: 2.0.2 + dev: false + + /which-typed-array/1.1.11: + resolution: {integrity: sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.5 + call-bind: 1.0.2 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.0 + dev: false + + /which/2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + dependencies: + isexe: 2.0.0 + dev: false + + /wrappy/1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + dev: false + + /ws/8.13.0: + resolution: {integrity: sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: false + + /ws/8.13.0_2adebc2xdjqbcvbjxkodkhadp4: + resolution: {integrity: sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dependencies: + bufferutil: 4.0.7 + utf-8-validate: 6.0.3 + dev: false + + /xml-name-validator/4.0.0: + resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} + engines: {node: '>=12'} + dev: false + + /xmlchars/2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + dev: false + + /xtend/4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + dev: false + + /yallist/4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + dev: false + + /yaml/2.3.1: + resolution: {integrity: sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==} + engines: {node: '>= 14'} + dev: false + + /yocto-queue/0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + dev: false + + /yoga-wasm-web/0.3.3: + resolution: {integrity: sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA==} + dev: false + + /zod/3.21.4: + resolution: {integrity: sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==} + dev: false diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 00000000..33ad091d --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 00000000..7151da89 Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/favicons/apple-touch-icon.png b/public/favicons/apple-touch-icon.png new file mode 100644 index 00000000..8a6916f2 Binary files /dev/null and b/public/favicons/apple-touch-icon.png differ diff --git a/public/favicons/dark.png b/public/favicons/dark.png new file mode 100644 index 00000000..c1a42303 Binary files /dev/null and b/public/favicons/dark.png differ diff --git a/public/favicons/light.png b/public/favicons/light.png new file mode 100644 index 00000000..5cfdbd0a Binary files /dev/null and b/public/favicons/light.png differ diff --git a/public/fonts/IBMPlexMono-Bold.ttf b/public/fonts/IBMPlexMono-Bold.ttf new file mode 100644 index 00000000..2e437e21 Binary files /dev/null and b/public/fonts/IBMPlexMono-Bold.ttf differ diff --git a/public/fonts/IBMPlexMono-BoldItalic.ttf b/public/fonts/IBMPlexMono-BoldItalic.ttf new file mode 100644 index 00000000..f2695fce Binary files /dev/null and b/public/fonts/IBMPlexMono-BoldItalic.ttf differ diff --git a/public/fonts/IBMPlexMono-ExtraLight.ttf b/public/fonts/IBMPlexMono-ExtraLight.ttf new file mode 100644 index 00000000..573ef764 Binary files /dev/null and b/public/fonts/IBMPlexMono-ExtraLight.ttf differ diff --git a/public/fonts/IBMPlexMono-ExtraLightItalic.ttf b/public/fonts/IBMPlexMono-ExtraLightItalic.ttf new file mode 100644 index 00000000..ea13f86d Binary files /dev/null and b/public/fonts/IBMPlexMono-ExtraLightItalic.ttf differ diff --git a/public/fonts/IBMPlexMono-Italic.ttf b/public/fonts/IBMPlexMono-Italic.ttf new file mode 100644 index 00000000..3cb28a39 Binary files /dev/null and b/public/fonts/IBMPlexMono-Italic.ttf differ diff --git a/public/fonts/IBMPlexMono-Light.ttf b/public/fonts/IBMPlexMono-Light.ttf new file mode 100644 index 00000000..df167f09 Binary files /dev/null and b/public/fonts/IBMPlexMono-Light.ttf differ diff --git a/public/fonts/IBMPlexMono-LightItalic.ttf b/public/fonts/IBMPlexMono-LightItalic.ttf new file mode 100644 index 00000000..c9072e96 Binary files /dev/null and b/public/fonts/IBMPlexMono-LightItalic.ttf differ diff --git a/public/fonts/IBMPlexMono-Medium.ttf b/public/fonts/IBMPlexMono-Medium.ttf new file mode 100644 index 00000000..39f178db Binary files /dev/null and b/public/fonts/IBMPlexMono-Medium.ttf differ diff --git a/public/fonts/IBMPlexMono-MediumItalic.ttf b/public/fonts/IBMPlexMono-MediumItalic.ttf new file mode 100644 index 00000000..0d887f76 Binary files /dev/null and b/public/fonts/IBMPlexMono-MediumItalic.ttf differ diff --git a/public/fonts/IBMPlexMono-Regular.ttf b/public/fonts/IBMPlexMono-Regular.ttf new file mode 100644 index 00000000..81ca3dcc Binary files /dev/null and b/public/fonts/IBMPlexMono-Regular.ttf differ diff --git a/public/fonts/IBMPlexMono-SemiBold.ttf b/public/fonts/IBMPlexMono-SemiBold.ttf new file mode 100644 index 00000000..73dd5a4f Binary files /dev/null and b/public/fonts/IBMPlexMono-SemiBold.ttf differ diff --git a/public/fonts/IBMPlexMono-SemiBoldItalic.ttf b/public/fonts/IBMPlexMono-SemiBoldItalic.ttf new file mode 100644 index 00000000..a41b0d3d Binary files /dev/null and b/public/fonts/IBMPlexMono-SemiBoldItalic.ttf differ diff --git a/public/fonts/IBMPlexMono-Thin.ttf b/public/fonts/IBMPlexMono-Thin.ttf new file mode 100644 index 00000000..e173f5a1 Binary files /dev/null and b/public/fonts/IBMPlexMono-Thin.ttf differ diff --git a/public/fonts/IBMPlexMono-ThinItalic.ttf b/public/fonts/IBMPlexMono-ThinItalic.ttf new file mode 100644 index 00000000..85292757 Binary files /dev/null and b/public/fonts/IBMPlexMono-ThinItalic.ttf differ diff --git a/public/fonts/OFL.txt b/public/fonts/OFL.txt new file mode 100644 index 00000000..379e7356 --- /dev/null +++ b/public/fonts/OFL.txt @@ -0,0 +1,93 @@ +Copyright © 2017 IBM Corp. with Reserved Font Name "Plex" + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/src/app/(auth-state)/admin/photos/[photoId]/edit/page.tsx b/src/app/(auth-state)/admin/photos/[photoId]/edit/page.tsx new file mode 100644 index 00000000..b515eb23 --- /dev/null +++ b/src/app/(auth-state)/admin/photos/[photoId]/edit/page.tsx @@ -0,0 +1,23 @@ +import PhotoForm from '@/photo/PhotoForm'; +import { convertPhotoToFormData } from '@/photo/form'; +import AdminChildPage from '@/components/AdminChildPage'; +import { getPhoto } from '@/services/postgres'; + +export const runtime = 'edge'; + +interface Props { + params: { photoId: string } +} + +export default async function PhotoPageEdit({ params: { photoId } }: Props) { + const photo = await getPhoto(photoId); + + return ( + + + + ); +}; diff --git a/src/app/(auth-state)/admin/photos/page.tsx b/src/app/(auth-state)/admin/photos/page.tsx new file mode 100644 index 00000000..a6655231 --- /dev/null +++ b/src/app/(auth-state)/admin/photos/page.tsx @@ -0,0 +1,185 @@ +import { Fragment, ReactNode } from 'react'; +import PhotoUploadInput from '@/photo/PhotoUploadInput'; +import Link from 'next/link'; +import PhotoTiny from '@/photo/PhotoTiny'; +import { cc } from '@/utility/css'; +import ImageTiny from '@/components/ImageTiny'; +import FormWithConfirm from '@/components/FormWithConfirm'; +import SiteGrid from '@/components/SiteGrid'; +import { + deletePhotoAction, + deleteBlobPhotoAction, +} from '@/photo/actions'; +import { FaRegEdit } from 'react-icons/fa'; +import SubmitButtonWithStatus from '@/components/SubmitButtonWithStatus'; +import { + pathForBlobUrl, + getBlobPhotoUrls, + getBlobUploadUrls, +} from '@/services/blob'; +import { getPhotos } from '@/services/postgres'; +import { routeForPhoto } from '@/site/routes'; + +export const runtime = 'edge'; + +const DEBUG_PHOTO_BLOBS = false; + +export default async function AdminPage() { + const photos = await getPhotos('createdAt'); + + const blobUploadUrls = await getBlobUploadUrls(); + const blobPhotoUrls = DEBUG_PHOTO_BLOBS + ? await getBlobPhotoUrls() + : []; + + return ( + +
+ + {blobUploadUrls.length > 0 && + } + {blobPhotoUrls.length > 0 && + } + + {photos.map(photo => + + + + {photo.title} + {photo.priorityOrder !== null && + + {photo.priorityOrder} + } + +
+ {photo.takenAtNaive} +
+ + + + + + +
)} +
+
+ } + /> + ); +} + +function AdminGrid ({ + title, + children, +}: { + title: string, + children: ReactNode, +}) { + return
+
+ {title} +
+
+
+ {children} +
+
+
; +} + +function EditButton ({ + href, + label = 'Edit', +}: { + href: string, + label?: string, +}) { + return + + {label} + ; +} + +function DeleteButton () { + return Ă—} + > + Delete + ; +} + +function BlobUrls ({ + blobUrls, + label, +}: { + blobUrls: string[], + label: string, +}) { + return + {blobUrls.map(url => { + const href = `/admin/uploads/${encodeURIComponent(url)}`; + const fileName = url.split('/').pop(); + return + + + + + {pathForBlobUrl(url)} + +
+ + + + + + ;})} + ; +} diff --git a/src/app/(auth-state)/admin/uploads/[uploadPath]/page.tsx b/src/app/(auth-state)/admin/uploads/[uploadPath]/page.tsx new file mode 100644 index 00000000..7b4ffc89 --- /dev/null +++ b/src/app/(auth-state)/admin/uploads/[uploadPath]/page.tsx @@ -0,0 +1,42 @@ +import PhotoForm from '@/photo/PhotoForm'; +import { ExifParserFactory } from 'ts-exif-parser'; +import { convertExifToFormData } from '@/photo/form'; +import AdminChildPage from '@/components/AdminChildPage'; +import { getExtensionFromBlobUrl } from '@/services/blob'; + +interface Params { + params: { uploadPath: string } +} + +export default async function UploadPage({ params: { uploadPath } }: Params) { + const url = decodeURIComponent(uploadPath); + + const extension = getExtensionFromBlobUrl(url); + + const fileBytes = uploadPath + ? await fetch(url) + .then(res => res.arrayBuffer()) + : undefined; + + let data; + + if (fileBytes) { + data = ExifParserFactory + .create(Buffer.from(fileBytes)) + .parse(); + } + + return ( + + {data + ? + : null} + + ); +}; diff --git a/src/app/(auth-state)/admin/uploads/blob/route.tsx b/src/app/(auth-state)/admin/uploads/blob/route.tsx new file mode 100644 index 00000000..cdd924aa --- /dev/null +++ b/src/app/(auth-state)/admin/uploads/blob/route.tsx @@ -0,0 +1,39 @@ +import { + ACCEPTED_PHOTO_FILE_TYPES, + isUploadPathnameValid, +} from '@/services/blob'; +import { handleBlobUpload, type HandleBlobUploadBody } from '@vercel/blob'; +import { revalidatePath } from 'next/cache'; +import { NextResponse } from 'next/server'; + +export const runtime = 'edge'; + +export async function POST(request: Request): Promise { + const body = (await request.json()) as HandleBlobUploadBody; + + try { + const jsonResponse = await handleBlobUpload({ + body, + request, + onBeforeGenerateToken: async (pathname) => { + if (isUploadPathnameValid(pathname)) { + return { + maximumSizeInBytes: 40_000_000, + allowedContentTypes: ACCEPTED_PHOTO_FILE_TYPES, + }; + } else { + throw new Error('Invalid upload'); + } + }, + onUploadCompleted: async () => { + revalidatePath('admin/photos'); + }, + }); + return NextResponse.json(jsonResponse); + } catch (error) { + return NextResponse.json( + { error: (error as Error).message }, + { status: 400 }, + ); + } +} diff --git a/src/app/(auth-state)/checklist/page.tsx b/src/app/(auth-state)/checklist/page.tsx new file mode 100644 index 00000000..0e074d62 --- /dev/null +++ b/src/app/(auth-state)/checklist/page.tsx @@ -0,0 +1,14 @@ +import InfoBlock from '@/components/InfoBlock'; +import SiteGrid from '@/components/SiteGrid'; +import { SITE_CHECKLIST_STATUS } from '@/site'; +import SiteChecklist from '@/site/SiteChecklist'; + +export default function ChecklistPage() { + return ( + + + } + /> + ); +} diff --git a/src/app/(auth-state)/layout.tsx b/src/app/(auth-state)/layout.tsx new file mode 100644 index 00000000..25e4f057 --- /dev/null +++ b/src/app/(auth-state)/layout.tsx @@ -0,0 +1,17 @@ +import AuthNav from '@/components/AuthNav'; +import { ClerkProvider } from '@clerk/nextjs'; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + + {children} +
+ +
+
+ ); +} diff --git a/src/app/(auth-state)/sign-in/[[...sign-in]]/page.tsx b/src/app/(auth-state)/sign-in/[[...sign-in]]/page.tsx new file mode 100644 index 00000000..6cbbfaf7 --- /dev/null +++ b/src/app/(auth-state)/sign-in/[[...sign-in]]/page.tsx @@ -0,0 +1,16 @@ +import { cc } from '@/utility/css'; +import { SignIn } from '@clerk/nextjs'; + +export const runtime = 'edge'; + +export default function SignInPage() { + return ( +
+ +
+ ); +} diff --git a/src/app/(isr)/deploy-image/route.tsx b/src/app/(isr)/deploy-image/route.tsx new file mode 100644 index 00000000..255ea1d3 --- /dev/null +++ b/src/app/(isr)/deploy-image/route.tsx @@ -0,0 +1,42 @@ +import DeployImageResponse from '@/photo/image-response/DeployImageResponse'; +import { getPhotos } from '@/services/postgres'; +import { GRID_OG_WIDTH, GRID_OG_HEIGHT } from '@/site'; +import { FONT_FAMILY_IBM_PLEX_MONO, getIBMPlexMonoMedium } from '@/site/font'; +import { ImageResponse } from '@vercel/og'; + +const DEBUG_CACHING: boolean = false; + +export const runtime = 'edge'; + +export async function GET(request: Request) { + const photos = await getPhotos('priority'); + const fontData = await getIBMPlexMonoMedium(); + + return new ImageResponse( + ( + + ), + { + width: GRID_OG_WIDTH, + height: GRID_OG_HEIGHT, + fonts: [ + { + name: FONT_FAMILY_IBM_PLEX_MONO, + data: fontData, + style: 'normal', + }, + ], + ...!DEBUG_CACHING && { + headers: { + 'Cache-Control': 's-maxage=3600, stale-while-revalidate', + }, + }, + }, + ); +} diff --git a/src/app/(isr)/deploy-url/route.tsx b/src/app/(isr)/deploy-url/route.tsx new file mode 100644 index 00000000..cad4bd3e --- /dev/null +++ b/src/app/(isr)/deploy-url/route.tsx @@ -0,0 +1,34 @@ +/* eslint-disable max-len */ +import { NextResponse } from 'next/server'; + +const TITLE = 'Photo Blog'; +const DESCRIPTION = 'Store photos with original camera data'; +const REPO_NAME = 'photo-blog'; + +export function GET() { + const url = new URL('https://vercel.com/new/clone'); + + url.searchParams.set('demo-title', TITLE); + url.searchParams.set('demo-description', DESCRIPTION); + url.searchParams.set('demo-url', 'https://photos.sambecker.com'); + url.searchParams.set('demo-description', DESCRIPTION); + url.searchParams.set('demo-image', 'https://photos.sambecker.com/deploy-image'); + url.searchParams.set('project-name', TITLE); + url.searchParams.set('repository-name', REPO_NAME); + url.searchParams.set('repository-url', `https://github.com/sambecker/${REPO_NAME}`); + url.searchParams.set('from', 'templates'); + url.searchParams.set('skippable-integrations', '1'); + url.searchParams.set('env-description', 'Configure your photo blog meta'); + url.searchParams.set('env-link', 'BLANK'); + url.searchParams.set('env', [ + 'NEXT_PUBLIC_SITE_TITLE', + 'NEXT_PUBLIC_SITE_DOMAIN', + ].join(',')); + url.searchParams.set('teamCreateStatus', 'hidden'); + url.searchParams.set('stores', JSON.stringify([ + { type: 'postgres' }, + { type: 'blob' }, + ])); + + return NextResponse.json(url.toString()); +} diff --git a/src/app/(isr)/grid/[offset]/page.tsx b/src/app/(isr)/grid/[offset]/page.tsx new file mode 100644 index 00000000..97d67e5f --- /dev/null +++ b/src/app/(isr)/grid/[offset]/page.tsx @@ -0,0 +1,29 @@ +import SiteGrid from '@/components/SiteGrid'; +import PhotoGrid from '@/photo/PhotoGrid'; +import { getPhotos } from '@/services/postgres'; + +export const runtime = 'edge'; + +const PHOTOS_PER_PAGE = 6; + +export default async function GridPage( + { params }: { params: Record } +) { + const offset = parseInt(params.offset ?? '0'); + + const photos = await getPhotos( + undefined, + PHOTOS_PER_PAGE, + Number.isNaN(offset) ? 0 : offset, + ); + + return ( + } + /> + ); +} diff --git a/src/app/(isr)/grid/page.tsx b/src/app/(isr)/grid/page.tsx new file mode 100644 index 00000000..18c3e6cb --- /dev/null +++ b/src/app/(isr)/grid/page.tsx @@ -0,0 +1,29 @@ +import SiteGrid from '@/components/SiteGrid'; +import { generateImageMetaForPhoto } from '@/photo'; +import PhotoGrid from '@/photo/PhotoGrid'; +import PhotosEmptyState from '@/photo/PhotosEmptyState'; +import { getPhotos } from '@/services/postgres'; +import { Metadata } from 'next'; + +export const runtime = 'edge'; +export const dynamic = 'force-static'; + +export async function generateMetadata(): Promise { + const photos = await getPhotos(); + return generateImageMetaForPhoto(photos[0]); +} + +export default async function GridPage() { + const photos = await getPhotos(); + + return ( + photos.length > 0 + ? } + /> + : + ); +} diff --git a/src/app/(isr)/layout.tsx b/src/app/(isr)/layout.tsx new file mode 100644 index 00000000..fc0cf2de --- /dev/null +++ b/src/app/(isr)/layout.tsx @@ -0,0 +1,33 @@ +import SiteGrid from '@/components/SiteGrid'; +import ThemeSwitcher from '@/site/ThemeSwitcher'; +import { cc } from '@/utility/css'; +import Link from 'next/link'; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + <> + {children} + +
+ + Admin + +
+ +
} + /> + + ); +} \ No newline at end of file diff --git a/src/app/(isr)/og/page.tsx b/src/app/(isr)/og/page.tsx new file mode 100644 index 00000000..e8272969 --- /dev/null +++ b/src/app/(isr)/og/page.tsx @@ -0,0 +1,14 @@ +import StaggeredPhotos from '@/photo/StaggeredPhotos'; +import { getPhotos } from '@/services/postgres'; + +export const runtime = 'edge'; + +export default async function GridPage() { + const photos = await getPhotos(); + + return ( +
+ +
+ ); +} diff --git a/src/app/(isr)/page.tsx b/src/app/(isr)/page.tsx new file mode 100644 index 00000000..e7cc0590 --- /dev/null +++ b/src/app/(isr)/page.tsx @@ -0,0 +1,36 @@ +import AnimateItems from '@/components/AnimateItems'; +import { generateImageMetaForPhoto } from '@/photo'; +import PhotoLarge from '@/photo/PhotoLarge'; +import PhotosEmptyState from '@/photo/PhotosEmptyState'; +import { getPhotos } from '@/services/postgres'; +import { Metadata } from 'next'; + +export const runtime = 'edge'; +export const dynamic = 'force-static'; + +export async function generateMetadata(): Promise { + const photos = await getPhotos(); + return generateImageMetaForPhoto(photos[0]); +} + +export default async function HomePage() { + const photos = await getPhotos(); + + return ( + photos.length > 0 + ? + )} + /> + : + ); +} diff --git a/src/app/(isr)/photos/[photoId]/image/route.tsx b/src/app/(isr)/photos/[photoId]/image/route.tsx new file mode 100644 index 00000000..92d01739 --- /dev/null +++ b/src/app/(isr)/photos/[photoId]/image/route.tsx @@ -0,0 +1,42 @@ +import PhotoOGImageResponse from '@/photo/image-response/PhotoOGImageResponse'; +import { getPhoto } from '@/services/postgres'; +import { IMAGE_OG_WIDTH, IMAGE_OG_HEIGHT } from '@/site'; +import { FONT_FAMILY_IBM_PLEX_MONO, getIBMPlexMonoMedium } from '@/site/font'; +import { ImageResponse } from '@vercel/og'; + +const DEBUG_CACHING: boolean = false; + +export const runtime = 'edge'; + +export async function GET(request: Request, context: any) { + const photo = await getPhoto(context.params.photoId); + const fontData = await getIBMPlexMonoMedium(); + return new ImageResponse( + ( + + ), + { + width: IMAGE_OG_WIDTH, + height: IMAGE_OG_HEIGHT, + fonts: [ + { + name: FONT_FAMILY_IBM_PLEX_MONO, + data: fontData, + weight: 500, + style: 'normal', + }, + ], + ...!DEBUG_CACHING && { + headers: { + 'Cache-Control': 's-maxage=3600, stale-while-revalidate', + }, + }, + }, + ); +} diff --git a/src/app/(isr)/photos/[photoId]/layout.tsx b/src/app/(isr)/photos/[photoId]/layout.tsx new file mode 100644 index 00000000..935bbf81 --- /dev/null +++ b/src/app/(isr)/photos/[photoId]/layout.tsx @@ -0,0 +1,81 @@ +import { PropsWithChildren } from 'react'; +import AnimateItems from '@/components/AnimateItems'; +import PhotoLinks from '@/photo/PhotoLinks'; +import SiteGrid from '@/components/SiteGrid'; +import { ogImageDescriptionForPhoto, ogImageUrlForPhoto } from '@/photo'; +import PhotoGrid from '@/photo/PhotoGrid'; +import PhotoLarge from '@/photo/PhotoLarge'; +import { cc } from '@/utility/css'; +import { Metadata } from 'next'; +import { BASE_URL } from '@/site/config'; +import { getPhoto, getPhotos } from '@/services/postgres'; + +export const runtime = 'edge'; + +interface Props extends PropsWithChildren { + params: { photoId: string } +} + +export async function generateMetadata( + { params: { photoId } }: Props +): Promise { + const photo = await getPhoto(photoId); + const title = photo.title; + const description = ogImageDescriptionForPhoto(photo); + const images = ogImageUrlForPhoto(photo); + return { + title, + description, + openGraph: { + title, + images, + description, + url: `${BASE_URL}/photos/${photo.idShort}`, + }, + twitter: { + title, + description, + images, + card: 'summary_large_image', + }, + }; +} + +export default async function PhotoPage({ + params: { photoId }, + children, +}: Props) { + const photo = await getPhoto(photoId); + const photos = await getPhotos(); + + return <> + {children} +
+ ]} + /> + } + contentSide={
+ +
} + /> +
+ ; +} diff --git a/src/app/(isr)/photos/[photoId]/page.tsx b/src/app/(isr)/photos/[photoId]/page.tsx new file mode 100644 index 00000000..67e08591 --- /dev/null +++ b/src/app/(isr)/photos/[photoId]/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return null; +} diff --git a/src/app/(isr)/photos/[photoId]/share/page.tsx b/src/app/(isr)/photos/[photoId]/share/page.tsx new file mode 100644 index 00000000..29936898 --- /dev/null +++ b/src/app/(isr)/photos/[photoId]/share/page.tsx @@ -0,0 +1,13 @@ +import PhotoModal from '@/photo/PhotoModal'; +import { getPhoto } from '@/services/postgres'; + +export const runtime = 'edge'; + +interface Props { + params: { photoId: string } +} + +export default async function Share({ params: { photoId }}: Props) { + const photo = await getPhoto(photoId); + return ; +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx new file mode 100644 index 00000000..fe548955 --- /dev/null +++ b/src/app/layout.tsx @@ -0,0 +1,83 @@ +import { Analytics } from '@vercel/analytics/react'; +import { cc } from '@/utility/css'; +import { IBM_Plex_Mono } from 'next/font/google'; +import { Metadata } from 'next'; +import { BASE_URL, SITE_DESCRIPTION, SITE_TITLE } from '@/site/config'; +import StateProvider from '@/state/AppStateProvider'; +import ThemeProviderClient from '@/site/ThemeProviderClient'; +import Nav from '@/components/Nav'; + +import '../site/globals.css'; + +const ibmPlexMono = IBM_Plex_Mono({ + subsets: ['latin'], + weight: ['400', '700'], + variable: '--font-ibm-plex-mono', +}); + +export const metadata: Metadata = { + title: SITE_TITLE, + description: SITE_DESCRIPTION, + metadataBase: new URL(BASE_URL), + openGraph: { + title: SITE_TITLE, + description: SITE_DESCRIPTION, + }, + twitter: { + title: SITE_TITLE, + description: SITE_DESCRIPTION, + }, + icons: [{ + url: '/favicon.ico', + rel: 'icon', + type: 'image/png', + sizes: '180x180', + }, { + url: '/favicons/light.png', + rel: 'icon', + media: '(prefers-color-scheme: light)', + type: 'image/png', + sizes: '32x32', + }, { + url: '/favicons/dark.png', + rel: 'icon', + media: '(prefers-color-scheme: dark)', + type: 'image/png', + sizes: '32x32', + }, { + url: '/favicons/apple-touch-icon.png', + rel: 'icon', + type: 'image/png', + sizes: '180x180', + }], +}; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + + + +
+
+
+ + + ); +} diff --git a/src/cache/index.ts b/src/cache/index.ts new file mode 100644 index 00000000..72bfe360 --- /dev/null +++ b/src/cache/index.ts @@ -0,0 +1,42 @@ +import { getPhoto, getPhotos } from '@/services/postgres'; +import { revalidatePath, revalidateTag, unstable_cache } from 'next/cache'; + +const TAG_PHOTOS = 'photos'; + +const PHOTO_PATHS = [ + '/', + '/grid', + '/photos/[photoId]', + '/photos/[photoId]/image', + '/admin/photos', + '/admin/photos/[photoId]', + '/admin/photos/[photoId]/edit', +]; + +const tagForPhoto = (photoId: string) => `photo-${photoId}`; + +export const revalidatePhotosTag = ( + includePhotoPaths?: boolean +) => { + revalidateTag(TAG_PHOTOS); + if (includePhotoPaths) { revalidateAllPhotoPaths(); } +}; + +export const revalidateAllPhotoPaths = () => + PHOTO_PATHS.forEach(path => revalidatePath(path)); + +export const getPhotosCached: typeof getPhotos = (...args) => + unstable_cache( + () => getPhotos(...args), + [TAG_PHOTOS], { + tags: [TAG_PHOTOS], + } + )(); + +export const getPhotoCached: typeof getPhoto = (...args) => + unstable_cache( + () => getPhoto(...args), + [TAG_PHOTOS, tagForPhoto(...args)], { + tags: [TAG_PHOTOS, tagForPhoto(...args)], + } + )(); diff --git a/src/components/AdminChildPage.tsx b/src/components/AdminChildPage.tsx new file mode 100644 index 00000000..159618ad --- /dev/null +++ b/src/components/AdminChildPage.tsx @@ -0,0 +1,23 @@ +import { ReactNode } from 'react'; +import Link from 'next/link'; +import { FiArrowLeft } from 'react-icons/fi'; + +function AdminChildPage({ + children, +}: { + children: ReactNode, +}) { + return ( +
+ + + Admin + +
+ {children} +
+
+ ); +}; + +export default AdminChildPage; diff --git a/src/components/AnimateItems.tsx b/src/components/AnimateItems.tsx new file mode 100644 index 00000000..6bc5bb4b --- /dev/null +++ b/src/components/AnimateItems.tsx @@ -0,0 +1,120 @@ +'use client'; + +import { useRef } from 'react'; +import { motion } from 'framer-motion'; +import { useAppState } from '@/state'; + +export type AnimationType = 'none' | 'scale' | 'left' | 'right'; + +export interface AnimationConfig { + type?: AnimationType + duration?: number + staggerDelay?: number + scaleOffset?: number + distanceOffset?: number +} + +interface Props extends AnimationConfig { + className?: string + items: JSX.Element[] + animateFromAppState?: boolean + animateOnFirstLoadOnly?: boolean + staggerOnFirstLoadOnly?: boolean +} + +function AnimateItems({ + className, + items, + type = 'scale', + duration = 0.6, + staggerDelay = 0.1, + scaleOffset = 0.9, + distanceOffset = 20, + animateFromAppState, + animateOnFirstLoadOnly, + staggerOnFirstLoadOnly, +}: Props) { + const { + hasLoaded, + nextPhotoAnimation, + clearNextPhotoAnimation, + } = useAppState(); + + const hasLoadedInitial = useRef(hasLoaded); + const nextPhotoAnimationInitial = useRef(nextPhotoAnimation); + + const shouldAnimate = type !== 'none' && + !(animateOnFirstLoadOnly && hasLoadedInitial.current); + const shouldStagger = + !(staggerOnFirstLoadOnly && hasLoadedInitial.current); + + const typeResolved = animateFromAppState + ? (nextPhotoAnimationInitial.current?.type ?? type) + : type; + + const durationResolved = animateFromAppState + ? (nextPhotoAnimationInitial.current?.duration ?? duration) + : duration; + + const getInitialVariant = () => { + switch (typeResolved) { + case 'left': return { + opacity: 0, + translateX: distanceOffset, + }; + case 'right': return { + opacity: 0, + translateX: -distanceOffset, + }; + default: return { + opacity: 0, + scale: scaleOffset, + translateY: distanceOffset, + }; + } + }; + + return ( + { + if (animateFromAppState) { + clearNextPhotoAnimation?.(); + } + }} + > + {items.map((item, index) => + + {item} + )} + + ); +}; + +export default AnimateItems; diff --git a/src/components/AuthNav.tsx b/src/components/AuthNav.tsx new file mode 100644 index 00000000..4c582a2c --- /dev/null +++ b/src/components/AuthNav.tsx @@ -0,0 +1,55 @@ +'use client'; + +import { cc } from '@/utility/css'; +import Link from 'next/link'; +import { useClerk } from '@clerk/nextjs'; +import ThemeSwitcher from '@/site/ThemeSwitcher'; +import SiteGrid from './SiteGrid'; +import { usePathname } from 'next/navigation'; +import { isRouteSignIn } from '@/site/routes'; + +const LINK_STYLE = cc( + 'cursor-pointer', + 'hover:text-gray-600', +); + +export default function AuthNav() { + const { user, signOut } = useClerk(); + + const hasState = signOut !== undefined; + + const path = usePathname(); + + return ( + +
+ {hasState + ? <> + {user === undefined && + <>Loading ...} + {user !== undefined && user !== null && <> +
{user?.emailAddresses[0].emailAddress}
+
signOut()} + className={LINK_STYLE} + > + Sign Out +
+ } + + : + Sign In + } +
+ {!isRouteSignIn(path) && } + } + /> + ); +}; diff --git a/src/components/FieldSet.tsx b/src/components/FieldSet.tsx new file mode 100644 index 00000000..8f8f4b64 --- /dev/null +++ b/src/components/FieldSet.tsx @@ -0,0 +1,40 @@ +export default function FieldSet({ + id, + label, + value, + onChange, + required, + readOnly, +}: { + id: string + label: string + value: string + onChange?: (value: string) => void + required?: boolean + readOnly?: boolean +}) { + return ( +
+ + onChange?.(e.target.value)} + type="text" + autoComplete="off" + readOnly={readOnly} + className="w-full" + /> +
+ ); +}; diff --git a/src/components/FormWithConfirm.tsx b/src/components/FormWithConfirm.tsx new file mode 100644 index 00000000..c2e542bc --- /dev/null +++ b/src/components/FormWithConfirm.tsx @@ -0,0 +1,28 @@ +'use client'; + +import { ReactNode } from 'react'; + +export default function FormWithConfirm({ + action, + confirmText, + children, +}: { + action: (data: FormData) => Promise + confirmText: string + children: ReactNode +}) { + return ( +
{ + if (!confirm(confirmText)) { + e.preventDefault(); + } else { + e.currentTarget.requestSubmit(); + } + }} + > + {children} +
+ ); +}; diff --git a/src/components/ImageLarge.tsx b/src/components/ImageLarge.tsx new file mode 100644 index 00000000..10ed1abf --- /dev/null +++ b/src/components/ImageLarge.tsx @@ -0,0 +1,39 @@ +import { IMAGE_LARGE_WIDTH } from '@/site'; +import Image from 'next/image'; +import Link from 'next/link'; + +export default function ImageLarge({ + className, + href, + src, + alt, + aspectRatio, + blurData, + priority, +}: { + className?: string + href: string + src: string + alt: string + aspectRatio: number + blurData: string + priority?: boolean +}) { + return ( + + + + ); +}; diff --git a/src/components/ImageSmall.tsx b/src/components/ImageSmall.tsx new file mode 100644 index 00000000..9822b02f --- /dev/null +++ b/src/components/ImageSmall.tsx @@ -0,0 +1,28 @@ +import { IMAGE_SMALL_WIDTH } from '@/site'; +import Image from 'next/image'; + +export default function ImageSmall({ + className, + src, + alt, + aspectRatio, + blurData, +}: { + className?: string + src: string + alt: string + aspectRatio: number + blurData: string +}) { + return ( + + ); +}; diff --git a/src/components/ImageTiny.tsx b/src/components/ImageTiny.tsx new file mode 100644 index 00000000..cbed91bc --- /dev/null +++ b/src/components/ImageTiny.tsx @@ -0,0 +1,30 @@ +import { IMAGE_TINY_WIDTH } from '@/site'; +import Image from 'next/image'; + +export default function ImageTiny({ + className, + src, + alt, + aspectRatio, + blurData, +}: { + className?: string + src: string + alt: string + aspectRatio: number + blurData?: string +}) { + return ( + + ); +}; diff --git a/src/components/InfoBlock.tsx b/src/components/InfoBlock.tsx new file mode 100644 index 00000000..d86e29af --- /dev/null +++ b/src/components/InfoBlock.tsx @@ -0,0 +1,27 @@ +import { cc } from '@/utility/css'; +import { ReactNode } from 'react'; + +export default function InfoBlock({ + children, +}: { + children: ReactNode +} ) { + return ( +
+
+ {children} +
+
+ ); +} diff --git a/src/components/LocalDate.tsx b/src/components/LocalDate.tsx new file mode 100644 index 00000000..2f2fd88f --- /dev/null +++ b/src/components/LocalDate.tsx @@ -0,0 +1,11 @@ +'use client'; + +import { formatDate } from '@/utility/date'; + +export default function LocalDate({ date }: { date: Date }) { + return ( + <> + {formatDate(date)} + + ); +}; diff --git a/src/components/Modal.tsx b/src/components/Modal.tsx new file mode 100644 index 00000000..ecb5a819 --- /dev/null +++ b/src/components/Modal.tsx @@ -0,0 +1,62 @@ +'use client'; + +import { ReactNode, useEffect, useRef, useState } from 'react'; +import { motion } from 'framer-motion'; +import { cc } from '@/utility/css'; +import useClickInsideOutside from '@/utility/useClickInsideOutside'; +import { useRouter } from 'next/navigation'; +import AnimateItems from './AnimateItems'; + +export default function Modal({ + onClosePath, + children, +}: { + onClosePath?: string + children: ReactNode +}) { + const router = useRouter(); + + const contentRef = useRef(null); + + const [htmlElements, setHtmlElements] = useState([]); + + useEffect(() => { + if (contentRef.current) { + setHtmlElements([contentRef.current]); + } + }, []); + + useClickInsideOutside({ + htmlElements, + onClickOutside: () => router.push(onClosePath ?? '/'), + }); + + return ( + + + {children} + ]} + /> + + ); +}; diff --git a/src/components/Nav.tsx b/src/components/Nav.tsx new file mode 100644 index 00000000..f000cadd --- /dev/null +++ b/src/components/Nav.tsx @@ -0,0 +1,62 @@ +'use client'; + +import { cc } from '@/utility/css'; +import { usePathname } from 'next/navigation'; +import Link from 'next/link'; +import SiteGrid from './SiteGrid'; +import { SITE_DOMAIN } from '@/site/config'; +import ViewSwitcher, { SwitcherSelection } from '@/photo/ViewSwitcher'; + +export default function Nav({ showTextLinks }: { showTextLinks?: boolean }) { + const isLoggedIn = false; + + const pathname = usePathname(); + + const showNav = !pathname.startsWith('/sign-in'); + + const renderLink = ( + text: string, + linkOrAction: string | (() => void), + ) => + typeof linkOrAction === 'string' + ? {text} + : ; + + const switcherSelectionForPath = (): SwitcherSelection | undefined => { + if (pathname === '/') { + return 'full-frame'; + } else if (pathname === '/grid') { + return 'grid'; + } else if (pathname.startsWith('/admin')) { + return 'admin'; + } + }; + + return ( + + {showNav && <> +
+ + {showTextLinks && <> + {renderLink('Home', '/')} + {renderLink('Admin', '/admin')} + } +
+
+ {renderLink(SITE_DOMAIN, '/')} +
+ } + + } + /> + ); +}; diff --git a/src/components/SiteGrid.tsx b/src/components/SiteGrid.tsx new file mode 100644 index 00000000..9bc2c2ad --- /dev/null +++ b/src/components/SiteGrid.tsx @@ -0,0 +1,36 @@ +import { cc } from '@/utility/css'; + +export default function SiteGrid({ + className, + contentMain, + contentSide, + sideFirstOnMobile, +}: { + className?: string + contentMain: JSX.Element + contentSide?: JSX.Element + sideFirstOnMobile?: boolean +}) { + return ( +
+
+ {contentMain} +
+ {contentSide && +
+ {contentSide} +
} +
+ ); +}; diff --git a/src/components/Spinner.tsx b/src/components/Spinner.tsx new file mode 100644 index 00000000..b7ba3e0c --- /dev/null +++ b/src/components/Spinner.tsx @@ -0,0 +1,21 @@ +import { ClipLoader } from 'react-spinners'; +import resolveConfig from 'tailwindcss/resolveConfig'; +import myConfig from '../../tailwind.config.js'; + +const TAILWIND_COLORS = resolveConfig(myConfig).theme?.colors as any; + +export default function Spinner({ + size = 35, + className, +}: { + size?: number + className?: string +}) { + return ( + + ); +}; diff --git a/src/components/SubmitButtonWithStatus.tsx b/src/components/SubmitButtonWithStatus.tsx new file mode 100644 index 00000000..2ca75ceb --- /dev/null +++ b/src/components/SubmitButtonWithStatus.tsx @@ -0,0 +1,47 @@ +'use client'; + +import { HTMLProps } from 'react'; +import { experimental_useFormStatus as useFormStatus } from 'react-dom'; +import Spinner from './Spinner'; +import { cc } from '@/utility/css'; + +interface Props extends HTMLProps { + icon?: JSX.Element +} + +export default function SubmitButtonWithStatus(props: Props) { + const { + icon, + children, + disabled, + className, + type: _type, + ...buttonProps + } = props; + + const { pending } = useFormStatus(); + + return ( + + ); +}; diff --git a/src/components/Switcher.tsx b/src/components/Switcher.tsx new file mode 100644 index 00000000..54d010c4 --- /dev/null +++ b/src/components/Switcher.tsx @@ -0,0 +1,21 @@ +import { ReactNode } from 'react'; +import { cc } from '@/utility/css'; + +export default function Switcher({ + children, +}: { + children: ReactNode +}) { + return ( +
+ {children} +
+ ); +}; diff --git a/src/components/SwitcherItem.tsx b/src/components/SwitcherItem.tsx new file mode 100644 index 00000000..410d5c91 --- /dev/null +++ b/src/components/SwitcherItem.tsx @@ -0,0 +1,39 @@ +import Link from 'next/link'; +import { cc } from '@/utility/css'; + +export default function SwitcherItem({ + icon, + href, + onClick, + active, + noPadding, +}: { + icon: JSX.Element + href?: string + onClick?: () => void + active?: boolean + noPadding?: boolean +}) { + const className = cc( + 'py-0.5 px-1.5', + 'cursor-pointer', + 'hover:bg-gray-50 active:bg-gray-100 active:text-gray-400', + // eslint-disable-next-line max-len + 'dark:hover:bg-gray-950 dark:active:bg-gray-900/75 dark:active:text-gray-600', + active + ? 'text-black dark:text-white' + : 'text-gray-300 dark:text-gray-700', + ); + + const renderIcon = () => noPadding + ? icon + :
+ {icon} +
; + + return ( + href + ? {renderIcon()} + :
{renderIcon()}
+ ); +}; diff --git a/src/icons/IconFullFrame.tsx b/src/icons/IconFullFrame.tsx new file mode 100644 index 00000000..4c1f510f --- /dev/null +++ b/src/icons/IconFullFrame.tsx @@ -0,0 +1,28 @@ +/* eslint-disable max-len */ + +const INTRINSIC_WIDTH = 28; +const INTRINSIC_HEIGHT = 24; + +export default function IconFullFrame({ + width = INTRINSIC_WIDTH, + includeTitle = true, +}: { + width?: number + includeTitle?: boolean +}) { + return ( + + {includeTitle && Full Frame} + + + + + ); +}; diff --git a/src/icons/IconGrid.tsx b/src/icons/IconGrid.tsx new file mode 100644 index 00000000..df2d355a --- /dev/null +++ b/src/icons/IconGrid.tsx @@ -0,0 +1,29 @@ +/* eslint-disable max-len */ + +const INTRINSIC_WIDTH = 28; +const INTRINSIC_HEIGHT = 24; + +export default function IconGrid({ + width = INTRINSIC_WIDTH, + includeTitle = true, +}: { + width?: number + includeTitle?: boolean +}) { + return ( + + {includeTitle && Grid} + + + + + + ); +}; diff --git a/src/middleware.ts b/src/middleware.ts new file mode 100644 index 00000000..d72cc397 --- /dev/null +++ b/src/middleware.ts @@ -0,0 +1,29 @@ +import { authMiddleware, redirectToSignIn } from '@clerk/nextjs'; +import { NextResponse } from 'next/server'; + +export default authMiddleware({ + afterAuth: (auth, req) => { + if (!( + auth.isPublicRoute || + auth.userId === process.env.CLERK_ADMIN_USER_ID + )) { + return redirectToSignIn({ returnBackUrl: req.url }); + } else { + if (req.nextUrl.pathname === '/admin') { + return NextResponse.redirect(new URL('/admin/photos', req.url)); + } + } + }, + publicRoutes: [ + '/', + '/grid', + '/photos/:photoId', + '/photos/:photoId/share', + '/photos/:photoId/image', + '/deploy-image', + ], +}); + +export const config = { + matcher: ['/((?!.*\\..*|_next).*)'], +}; diff --git a/src/photo/PhotoForm.tsx b/src/photo/PhotoForm.tsx new file mode 100644 index 00000000..b48a7cdb --- /dev/null +++ b/src/photo/PhotoForm.tsx @@ -0,0 +1,137 @@ +'use client'; + +import { useEffect, useRef, useState } from 'react'; +import { + FORM_METADATA_ENTRIES, + PhotoFormData, +} from './form'; +import FieldSet from '@/components/FieldSet'; +import NextImage from 'next/image'; +import { createPhotoAction, updatePhotoAction } from './actions'; +import SubmitButtonWithStatus from '@/components/SubmitButtonWithStatus'; +import Link from 'next/link'; +import { cc } from '@/utility/css'; + +const THUMBNAIL_WIDTH = 300; +const THUMBNAIL_HEIGHT = 200; +const EDGE_BLUR_COMPENSATION = 10; +const BLUR_SCALE = 0.5; +const BLUE_JPEG_QUALITY = 0.9; + +export default function PhotoForm({ + initialPhotoForm, + type = 'create', + debugBlur, +}: { + initialPhotoForm: Partial + type?: 'create' | 'edit' + debugBlur?: boolean +}) { + const [formData, setFormData] = + useState>(initialPhotoForm); + + const [showBlur, setShowBlur] = useState(debugBlur); + + const canvasRef = useRef(null); + + const url = formData.url ?? ''; + + useEffect(() => { + const image = new Image(); + image.crossOrigin = 'anonymous'; + image.src = url; + image.onload = () => { + const canvas = canvasRef.current; + if (canvas) { + canvas.width = THUMBNAIL_WIDTH * BLUR_SCALE; + canvas.height = THUMBNAIL_HEIGHT * BLUR_SCALE; + canvas.style.width = `${THUMBNAIL_WIDTH}px`; + canvas.style.height = `${THUMBNAIL_HEIGHT}px`; + const context = canvasRef.current?.getContext('2d'); + if (context) { + context.scale(BLUR_SCALE, BLUR_SCALE); + context.filter = + 'contrast(1.2) saturate(1.2)' + + `blur(${BLUR_SCALE * 10}px)`; + context.drawImage( + image, + -EDGE_BLUR_COMPENSATION, + -EDGE_BLUR_COMPENSATION, + THUMBNAIL_WIDTH + EDGE_BLUR_COMPENSATION * 2, + THUMBNAIL_WIDTH * image.height / image.width + + EDGE_BLUR_COMPENSATION * 2, + ); + if (type === 'create') { + setFormData(data => ({ + ...data, + blurData: canvas.toDataURL('image/jpeg', BLUE_JPEG_QUALITY), + })); + } + } + } + }; + }, [url, type]); + + const isFormValid = + Boolean(formData.blurData) && + Boolean(formData.title); + + return ( +
+ + setShowBlur(!showBlur)} + /> + {showBlur && formData.blurData && + blur} +
+ {FORM_METADATA_ENTRIES.map(([ + key, + { label, required, readOnly, hideIfEmpty }, + ]) => + (!hideIfEmpty || formData[key]) && +
setFormData({ ...formData, [key]: value })} + required={required} + readOnly={readOnly} + />)} +
+ {type === 'edit' && + + Cancel + } + + {type === 'create' ? 'Create' : 'Update'} + +
+ +
+ ); +}; diff --git a/src/photo/PhotoGrid.tsx b/src/photo/PhotoGrid.tsx new file mode 100644 index 00000000..0a676817 --- /dev/null +++ b/src/photo/PhotoGrid.tsx @@ -0,0 +1,57 @@ +import { Photo } from '.'; +import PhotoSmall from './PhotoSmall'; +import { cc } from '@/utility/css'; +import AnimateItems from '@/components/AnimateItems'; +import Link from 'next/link'; + +const PHOTOS_PER_PAGE = 6; +const PHOTOS_MAX = 35; + +export default function PhotoGrid({ + photos, + selectedPhoto, + offset = 0, + fast, + animateOnFirstLoadOnly, + staggerOnFirstLoadOnly, + showMore, +}: { + photos: Photo[] + selectedPhoto?: Photo + offset?: number + fast?: boolean + animate?: boolean + animateOnFirstLoadOnly?: boolean + staggerOnFirstLoadOnly?: boolean + showMore?: boolean +}) { + return ( + <> + + )} + /> + {showMore && (offset + PHOTOS_PER_PAGE) < PHOTOS_MAX && + + More + } + + ); +}; diff --git a/src/photo/PhotoLarge.tsx b/src/photo/PhotoLarge.tsx new file mode 100644 index 00000000..c4f258b1 --- /dev/null +++ b/src/photo/PhotoLarge.tsx @@ -0,0 +1,105 @@ +import { Photo } from '.'; +import SiteGrid from '@/components/SiteGrid'; +import ImageLarge from '@/components/ImageLarge'; +import { cc } from '@/utility/css'; +import Link from 'next/link'; +import { TbPhotoShare } from 'react-icons/tb'; +import { routeForPhoto } from '@/site/routes'; + +export default function PhotoLarge({ + photo, + priority, + showShare, +}: { + photo: Photo + priority?: boolean + showShare?: boolean +}) { + const renderMiniGrid = (children: JSX.Element) => +
*]:sm:flex-grow', + 'pr-2', + )}> + {children} +
; + + return ( + } + contentSide={ +
+ {renderMiniGrid(<> + + {photo.title} + +
+ {photo.make} {photo.model} +
+ )} + {renderMiniGrid(<> +
    +
  • + {photo.focalLengthFormatted} + {' '} + + {photo.focalLengthIn35MmFormatFormatted} + +
  • +
  • {photo.fNumberFormatted}
  • +
  • {photo.isoFormatted}
  • +
  • {photo.exposureTimeFormatted}
  • +
  • {photo.exposureCompensationFormatted ?? '—'}
  • +
+
+ {photo.takenAtNaiveFormatted} +
+ {showShare && + + + } + )} +
} + /> + ); +}; diff --git a/src/photo/PhotoLink.tsx b/src/photo/PhotoLink.tsx new file mode 100644 index 00000000..ebe3b078 --- /dev/null +++ b/src/photo/PhotoLink.tsx @@ -0,0 +1,40 @@ +'use client'; + +import { ReactNode } from 'react'; +import { Photo } from '@/photo'; +import Link from 'next/link'; +import { AnimationConfig } from '../components/AnimateItems'; +import { useAppState } from '@/state'; +import { routeForPhoto } from '@/site/routes'; + +export default function PhotoLink({ + photo, + prefetch, + nextPhotoAnimation, + children, +}: { + photo?: Photo + prefetch?: boolean + nextPhotoAnimation?: AnimationConfig + children: ReactNode +}) { + const { setNextPhotoAnimation } = useAppState(); + + return ( + photo + ? { + if (nextPhotoAnimation) { + setNextPhotoAnimation?.(nextPhotoAnimation); + } + }} + > + {children} + + : + {children} + + ); +}; diff --git a/src/photo/PhotoLinks.tsx b/src/photo/PhotoLinks.tsx new file mode 100644 index 00000000..ed5c81ed --- /dev/null +++ b/src/photo/PhotoLinks.tsx @@ -0,0 +1,74 @@ +'use client'; + +import { useEffect } from 'react'; +import { Photo, getNextPhoto, getPreviousPhoto } from '@/photo'; +import PhotoLink from './PhotoLink'; +import { usePathname, useRouter } from 'next/navigation'; +import { isRoutePhotoShare, routeForPhoto } from '@/site/routes'; +import { useAppState } from '@/state'; +import { AnimationConfig } from '@/components/AnimateItems'; + +const ANIMATION_LEFT: AnimationConfig = { type: 'left', duration: 0.3 }; +const ANIMATION_RIGHT: AnimationConfig = { type: 'right', duration: 0.3 }; + +export default function PhotoLinks({ + photo, + photos, +}: { + photo: Photo + photos: Photo[] +}) { + const router = useRouter(); + const pathname = usePathname(); + + const { setNextPhotoAnimation } = useAppState(); + + const isRouteShare = isRoutePhotoShare(pathname); + const previousPhoto = getPreviousPhoto(photo, photos); + const nextPhoto = getNextPhoto(photo, photos); + + useEffect(() => { + const onKeyUp = (e: KeyboardEvent) => { + switch (e.key.toUpperCase()) { + case 'ARROWLEFT': + case 'J': + if (previousPhoto) { + setNextPhotoAnimation?.(ANIMATION_RIGHT); + router.push(routeForPhoto(previousPhoto, isRouteShare)); + } + break; + case 'ARROWRIGHT': + case 'L': + if (nextPhoto) { + setNextPhotoAnimation?.(ANIMATION_LEFT); + router.push(routeForPhoto(nextPhoto, isRouteShare)); + } + break; + case 'ESCAPE': + router.push('/grid'); + break; + }; + }; + window.addEventListener('keyup', onKeyUp); + return () => window.removeEventListener('keyup', onKeyUp); + }, [router, setNextPhotoAnimation, previousPhoto, nextPhoto, isRouteShare]); + + return ( + <> + + PREV + + + NEXT + + + ); +}; diff --git a/src/photo/PhotoModal.tsx b/src/photo/PhotoModal.tsx new file mode 100644 index 00000000..9356f4e5 --- /dev/null +++ b/src/photo/PhotoModal.tsx @@ -0,0 +1,64 @@ +'use client'; + +import Modal from '@/components/Modal'; +import PhotoOGTile from '@/photo/PhotoOGTile'; +import { absoluteRouteForPhoto, routeForPhoto } from '@/site/routes'; +import { TbPhotoShare } from 'react-icons/tb'; +import { cc } from '@/utility/css'; +import { BiCopy } from 'react-icons/bi'; +import { useState } from 'react'; +import { Photo } from '.'; + +export default function PhotoModal({ photo }: { photo: Photo }) { + const [copied, setIsCopied] = useState(false); + const shareUrl = absoluteRouteForPhoto(photo); + + return ( + +
+
+ +
+ Share Photo +
+ {copied &&
+ Copied! +
} +
+ +
+
+ {shareUrl} +
+
{ + navigator.clipboard.writeText(shareUrl); + setIsCopied(true); + }} + > + +
+
+
+
+ ); +}; diff --git a/src/photo/PhotoOGTile.tsx b/src/photo/PhotoOGTile.tsx new file mode 100644 index 00000000..91c510d4 --- /dev/null +++ b/src/photo/PhotoOGTile.tsx @@ -0,0 +1,122 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { Photo, ogImageDescriptionForPhoto, ogImageUrlForPhoto } from '@/photo'; +import { cc } from '@/utility/css'; +import Link from 'next/link'; +import { BiError } from 'react-icons/bi'; +import Spinner from '@/components/Spinner'; +import { IMAGE_OG_HEIGHT, IMAGE_OG_RATIO, IMAGE_OG_WIDTH } from '@/site'; +import { routeForPhoto } from '@/site/routes'; + +export type OGLoadingState = 'unloaded' | 'loading' | 'loaded' | 'failed'; + +export default function PhotoOGTile({ + photo, + loadingState: loadingStateExternal, + riseOnHover, + onLoad, + onFail, + retryTime, +}: { + photo: Photo + loadingState?: OGLoadingState + onLoad?: () => void + onFail?: () => void + riseOnHover?: boolean + retryTime?: number +}) { + const [loadingStateInternal, setLoadingStateInternal] = + useState(loadingStateExternal ?? 'unloaded'); + + const loadingState = loadingStateExternal ?? loadingStateInternal; + + useEffect(() => { + if ( + !loadingStateExternal && + loadingStateInternal === 'unloaded' + ) { + setLoadingStateInternal('loading'); + } + }, [loadingStateExternal, loadingStateInternal]); + + return ( + +
+ {loadingState === 'loading' && +
+ +
} + {loadingState === 'failed' && +
+ +
} + {(loadingState === 'loading' || loadingState === 'loaded') && + {`OG { + if (onLoad) { + onLoad(); + } else { + setLoadingStateInternal('loaded'); + } + }} + onError={() => { + if (onFail) { + onFail(); + } else { + setLoadingStateInternal('failed'); + } + if (retryTime !== undefined) { + setTimeout(() => { + setLoadingStateInternal('loading'); + }, retryTime); + } + }} + />} +
+
+
+ {photo.title} +
+
+ {ogImageDescriptionForPhoto(photo)} +
+
+ + ); +}; diff --git a/src/photo/PhotoSmall.tsx b/src/photo/PhotoSmall.tsx new file mode 100644 index 00000000..2241ab7f --- /dev/null +++ b/src/photo/PhotoSmall.tsx @@ -0,0 +1,31 @@ +import { Photo } from '.'; +import ImageSmall from '@/components/ImageSmall'; +import Link from 'next/link'; +import { cc } from '@/utility/css'; +import { routeForPhoto } from '@/site/routes'; + +export default function PhotoSmall({ + photo, + selected, +}: { + photo: Photo + selected?: boolean +}) { + return ( + + + + ); +}; diff --git a/src/photo/PhotoTiny.tsx b/src/photo/PhotoTiny.tsx new file mode 100644 index 00000000..080eef48 --- /dev/null +++ b/src/photo/PhotoTiny.tsx @@ -0,0 +1,34 @@ +import { Photo } from '.'; +import ImageTiny from '@/components/ImageTiny'; +import Link from 'next/link'; +import { cc } from '@/utility/css'; +import { routeForPhoto } from '@/site/routes'; + +export default function PhotoTiny({ + className, + photo, + selected, +}: { + className?: string + photo: Photo + selected?: boolean +}) { + return ( + + + + ); +}; diff --git a/src/photo/PhotoUploadInput.tsx b/src/photo/PhotoUploadInput.tsx new file mode 100644 index 00000000..f70250c7 --- /dev/null +++ b/src/photo/PhotoUploadInput.tsx @@ -0,0 +1,69 @@ +'use client'; + +import { useState } from 'react'; +import Spinner from '@/components/Spinner'; +import { ACCEPTED_PHOTO_FILE_TYPES, putPhoto } from '@/services/blob'; +import { ROUTE_ADMIN_UPLOAD_BLOB_HANDLER } from '@/site/routes'; +import { cc } from '@/utility/css'; +import { useRouter } from 'next/navigation'; + +export default function PhotoUploadInput() { + const [isUploading, setIsUploading] = useState(false); + const [uploadError, setUploadError] = useState(''); + + const router = useRouter(); + + return ( +
+
+
+ { + const file = e.target.files?.[0]; + if (file) { + setIsUploading(true); + setUploadError(''); + const extension = file.name.split('.').pop(); + putPhoto( + file, + extension, + 'upload', + ROUTE_ADMIN_UPLOAD_BLOB_HANDLER, + ) + .then(({ url }) => { + // Refresh page to update upload list, + // relevant only when a photo isn't added + router.refresh(); + // Redirect to photo detail page + router.push(`/admin/uploads/${encodeURIComponent(url)}`); + }) + .catch(error => { + setIsUploading(false); + setUploadError(`Upload Error: ${error.message}`); + }); + + } + }} + disabled={isUploading} + /> + {isUploading && +
+ + Uploading... +
} +
+
+ {uploadError && +
+ {uploadError} +
} +
+ ); +}; diff --git a/src/photo/PhotosEmptyState.tsx b/src/photo/PhotosEmptyState.tsx new file mode 100644 index 00000000..d844b9e5 --- /dev/null +++ b/src/photo/PhotosEmptyState.tsx @@ -0,0 +1,61 @@ +import InfoBlock from '@/components/InfoBlock'; +import SiteGrid from '@/components/SiteGrid'; +import { SITE_CHECKLIST_STATUS } from '@/site'; +import SiteChecklist from '@/site/SiteChecklist'; +import { cc } from '@/utility/css'; +import Link from 'next/link'; +import { HiOutlinePhotograph } from 'react-icons/hi'; + +export default function PhotosEmptyState() { + const showChecklist = Object.values(SITE_CHECKLIST_STATUS).some(v => !v); + + return ( + + +
+ {showChecklist + ? 'Finish Setup' + : 'Welcome!'} +
+ {showChecklist + ? + :
+
+ 1. Visit + {' '} + + /admin + + {' '} + to add your first photo +
+
+ 2. Change the name of this blog and other configuration + by editing environment variables referenced in + {' '} + + src/site/config.ts + +
+
} + } + /> + ); +}; diff --git a/src/photo/StaggeredPhotos.tsx b/src/photo/StaggeredPhotos.tsx new file mode 100644 index 00000000..12a6eeff --- /dev/null +++ b/src/photo/StaggeredPhotos.tsx @@ -0,0 +1,64 @@ +'use client'; + +import { useCallback, useEffect, useState } from 'react'; +import { Photo } from '@/photo'; +import PhotoOGTile, { OGLoadingState } from './PhotoOGTile'; + +const DEFAULT_MAX_CONCURRENCY = 3; + +type PhotoLoadingState = Record; + +export default function StaggeredPhotos({ + photos, + maxConcurrency = DEFAULT_MAX_CONCURRENCY, +}: { + photos: Photo[] + maxConcurrency?: number +}) { + const [loadingState, setLoadingState] = useState( + photos.reduce((acc, photo) => ({ + ...acc, + [photo.id]: 'unloaded' as const, + }), {} as PhotoLoadingState), + ); + + const recomputeLoadingState = useCallback(( + updatedState: PhotoLoadingState = {}, + ) => setLoadingState(currentLoadingState => { + const initialLoadingState = { + ...currentLoadingState, + ...updatedState, + }; + const updatedLoadingState = { + ...currentLoadingState, + ...updatedState, + }; + + let imagesLoadingCount = 0; + Object.entries(initialLoadingState).forEach(([id, state]) => { + if (state === 'loading') { + imagesLoadingCount++; + } else if (imagesLoadingCount < maxConcurrency && state === 'unloaded') { + updatedLoadingState[id] = 'loading'; + imagesLoadingCount++; + } + }); + + return updatedLoadingState; + }) + , [maxConcurrency]); + + useEffect(() => { + recomputeLoadingState(); + }, [recomputeLoadingState]); + + return photos.map(photo => + recomputeLoadingState({ [photo.id]: 'loaded' })} + onFail={() => recomputeLoadingState({ [photo.id]: 'failed' })} + riseOnHover + />); +}; diff --git a/src/photo/ViewSwitcher.tsx b/src/photo/ViewSwitcher.tsx new file mode 100644 index 00000000..07f4693a --- /dev/null +++ b/src/photo/ViewSwitcher.tsx @@ -0,0 +1,38 @@ +import Switcher from '@/components/Switcher'; +import SwitcherItem from '@/components/SwitcherItem'; +import IconFullFrame from '@/icons/IconFullFrame'; +import IconGrid from '@/icons/IconGrid'; +import { BiLockAlt } from 'react-icons/bi'; + +export type SwitcherSelection = 'full-frame' | 'grid' | 'admin'; + +export default function ViewSwitcher({ + currentSelection, + showAdmin, +}: { + currentSelection?: SwitcherSelection + showAdmin?: boolean +}) { + return ( + + } + href="/" + active={currentSelection === 'full-frame'} + noPadding + /> + } + href="/grid" + active={currentSelection === 'grid'} + noPadding + /> + {showAdmin && + } + href="/admin/photos" + active={currentSelection === 'admin'} + />} + + ); +} diff --git a/src/photo/actions.ts b/src/photo/actions.ts new file mode 100644 index 00000000..7d576aa1 --- /dev/null +++ b/src/photo/actions.ts @@ -0,0 +1,51 @@ +'use server'; + +import { revalidatePath } from 'next/cache'; +import { + sqlDeletePhoto, + sqlInsertPhotoIntoDb, + sqlUpdatePhotoInDb, +} from '@/services/postgres'; +import { convertFormDataToPhoto } from './form'; +import { redirect } from 'next/navigation'; +import { + convertUploadToPhoto, + deleteBlobPhoto, +} from '@/services/blob'; +import { revalidatePhotosTag } from '@/cache'; + +export async function createPhotoAction(formData: FormData) { + const photo = convertFormDataToPhoto(formData); + + const updatedUrl = await convertUploadToPhoto(photo.url); + + if (updatedUrl) { photo.url = updatedUrl; } + await sqlInsertPhotoIntoDb(photo); + + revalidatePhotosTag(true); + + redirect('/admin/photos'); +} + +export async function updatePhotoAction(formData: FormData) { + const photo = convertFormDataToPhoto(formData); + + await sqlUpdatePhotoInDb(photo); + + revalidatePhotosTag(true); + + redirect('/admin/photos'); +} + +export async function deletePhotoAction(formData: FormData) { + await deleteBlobPhoto(formData.get('url') as string); + await sqlDeletePhoto(formData.get('id') as string); + + revalidatePhotosTag(true); +}; + +export async function deleteBlobPhotoAction(formData: FormData) { + await deleteBlobPhoto(formData.get('url') as string); + + revalidatePath('/admin/photos'); +}; diff --git a/src/photo/form.ts b/src/photo/form.ts new file mode 100644 index 00000000..141061c7 --- /dev/null +++ b/src/photo/form.ts @@ -0,0 +1,139 @@ +import { ExifData } from 'ts-exif-parser'; +import { Photo, PhotoDbInsert, PhotoExif } from '.'; +import { + convertTimestampToNaivePostgresString, + convertTimestampWithOffsetToPostgresString, +} from '@/utility/date'; +import { getOffsetFromExif } from '@/utility/exif'; +import { toFixedNumber } from '@/utility/number'; + +export type PhotoFormData = Record; + +type FormMeta = { + label: string, + required?: boolean, + readOnly?: boolean, + hideIfEmpty?: boolean, + hideTemporarily?: boolean, +}; + +const FORM_METADATA: Record = { + title: { label: 'title', required: true }, + id: { label: 'id', readOnly: true, hideIfEmpty: true }, + idShort: { label: 'short id', readOnly: true, hideIfEmpty: true }, + url: { label: 'url', readOnly: true }, + extension: { label: 'extension', readOnly: true }, + aspectRatio: { label: 'aspect ratio', readOnly: true }, + blurData: { label: 'blur data', readOnly: true }, + make: { label: 'camera make' }, + model: { label: 'camera model' }, + focalLength: { label: 'focal length' }, + focalLengthIn35MmFormat: { label: 'focal length 35mm-equivalent' }, + fNumber: { label: 'aperture' }, + iso: { label: 'ISO' }, + exposureTime: { label: 'exposure time' }, + exposureCompensation: { label: 'exposure compensation' }, + locationName: { label: 'location name', hideTemporarily: true }, + latitude: { label: 'latitude' }, + longitude: { label: 'longitude' }, + filmSimulation: { label: 'film simulation', hideTemporarily: true }, + priorityOrder: { label: 'priority order' }, + takenAt: { label: 'taken at' }, + takenAtNaive: { label: 'taken at (naive)' }, +}; + +export const FORM_METADATA_ENTRIES = + (Object.entries(FORM_METADATA) as [keyof PhotoFormData, FormMeta][]) + .filter(([_, meta]) => !meta.hideTemporarily); + +export const convertPhotoToFormData = ( + photo: Photo, +): PhotoFormData => { + const valueForKey = (key: keyof Photo, value: any) => { + switch (key) { + case 'takenAt': + return value?.toISOString ? value.toISOString() : value; + default: + return value !== undefined && value !== null + ? value.toString() + : undefined; + } + }; + return Object.entries(photo).reduce((photoForm, [key, value]) => ({ + ...photoForm, + [key]: valueForKey(key as keyof Photo, value), + }), {} as PhotoFormData); +}; + +export const convertExifToFormData = ( + data: ExifData +): Record => ({ + aspectRatio: ( + (data.imageSize?.width ?? 3.0) / + (data.imageSize?.height ?? 2.0) + ).toString(), + make: data.tags?.Make, + model: data.tags?.Model, + focalLength: data.tags?.FocalLength?.toString(), + focalLengthIn35MmFormat: data.tags?.FocalLengthIn35mmFormat?.toString(), + fNumber: data.tags?.FNumber?.toString(), + iso: data.tags?.ISO?.toString(), + exposureTime: data.tags?.ExposureTime?.toString(), + exposureCompensation: data.tags?.ExposureCompensation?.toString(), + latitude: data.tags?.GPSLatitude?.toString(), + longitude: data.tags?.GPSLongitude?.toString(), + filmSimulation: undefined, + takenAt: convertTimestampWithOffsetToPostgresString( + data.tags?.DateTimeOriginal, + getOffsetFromExif(data), + ), + takenAtNaive: convertTimestampToNaivePostgresString( + data.tags?.DateTimeOriginal, + ), +}); + +export const convertFormDataToPhoto = ( + formData: FormData +): PhotoDbInsert => { + const photoForm = Object.fromEntries(formData) as PhotoFormData; + + // Remove Server Action ID + Object.keys(photoForm).forEach(key => { + if (key.startsWith('$ACTION_ID_')) { + delete (photoForm as any)[key]; + } + }); + + return { + ...photoForm, + // Convert form strings to numbers + aspectRatio: toFixedNumber(parseFloat(photoForm.aspectRatio), 6), + focalLength: photoForm.focalLength + ? parseInt(photoForm.focalLength) + : undefined, + focalLengthIn35MmFormat: photoForm.focalLengthIn35MmFormat + ? parseInt(photoForm.focalLengthIn35MmFormat) + : undefined, + fNumber: photoForm.fNumber + ? parseFloat(photoForm.fNumber) + : undefined, + latitude: photoForm.latitude + ? parseFloat(photoForm.latitude) + : undefined, + longitude: photoForm.longitude + ? parseFloat(photoForm.longitude) + : undefined, + iso: photoForm.iso + ? parseInt(photoForm.iso) + : undefined, + exposureTime: photoForm.exposureTime + ? parseFloat(photoForm.exposureTime) + : undefined, + exposureCompensation: photoForm.exposureCompensation + ? parseFloat(photoForm.exposureCompensation) + : undefined, + priorityOrder: photoForm.priorityOrder + ? parseFloat(photoForm.priorityOrder) + : undefined, + }; +}; diff --git a/src/photo/image-response/DeployImageResponse.tsx b/src/photo/image-response/DeployImageResponse.tsx new file mode 100644 index 00000000..de45515f --- /dev/null +++ b/src/photo/image-response/DeployImageResponse.tsx @@ -0,0 +1,92 @@ +import { Photo } from '..'; +import PhotoGridImageResponse from './PhotoGridImageResponse'; +import IconFullFrame from '@/icons/IconFullFrame'; +import IconGrid from '@/icons/IconGrid'; + +export default function DeployImageResponse({ + photos, + request, + width, + height, + fontFamily, + outerMargin = 30, + darkMode = true, +}: { + photos: Photo[] + request: Request + width: number + height: number + fontFamily: string + outerMargin?: number + darkMode?: boolean +}) { + const innerWidth = width - (outerMargin * 2); + + return ( +
+
+
+
+
+ +
+
+ +
+
+
+
+ thephotoblog.vercel.app +
+
+ +
+ ); +} diff --git a/src/photo/image-response/PhotoGridImageResponse.tsx b/src/photo/image-response/PhotoGridImageResponse.tsx new file mode 100644 index 00000000..67f2baca --- /dev/null +++ b/src/photo/image-response/PhotoGridImageResponse.tsx @@ -0,0 +1,54 @@ +import { getNextImageUrlForRequest } from '@/utility/image'; +import { Photo } from '..'; + +const IMAGE_WIDTH = 400; + +export default function PhotoGridImageResponse({ + photos, + request, + width, + colCount, + rowCount, + gap = 10, +}: { + photos: Photo[] + request: Request + width: number + colCount: number + rowCount: number + gap?: number +}) { + const imageWidth = (width - ((colCount - 1) * gap)) / colCount ; + + return ( +
+ {photos + .slice(0, colCount * rowCount) + .map((photo, index) => + {photo.title})} +
+ ); +} diff --git a/src/photo/image-response/PhotoOGImageResponse.tsx b/src/photo/image-response/PhotoOGImageResponse.tsx new file mode 100644 index 00000000..9178239d --- /dev/null +++ b/src/photo/image-response/PhotoOGImageResponse.tsx @@ -0,0 +1,75 @@ +import { Photo } from '..'; +import { getNextImageUrlForRequest } from '@/utility/image'; +import { formatModelShort } from '@/utility/exif'; +import { AiFillApple } from 'react-icons/ai'; + +export default function PhotoOGImageResponse({ + photo, + requestOrPhotoPath, + width, + height, + fontFamily, +}: { + photo: Photo + requestOrPhotoPath: Request | string + width: number + height: number + fontFamily: string +}) { + return ( +
+ {photo.title} +
+ {photo.make === 'Apple' && +
+ +
} +
+ {formatModelShort(photo.model)} +
+
+ {photo.focalLengthFormatted} +
+
+ {photo.fNumberFormatted} +
+
+ {photo.isoFormatted} +
+
+
+ ); +}; diff --git a/src/photo/index.ts b/src/photo/index.ts new file mode 100644 index 00000000..f27d6ef6 --- /dev/null +++ b/src/photo/index.ts @@ -0,0 +1,142 @@ +import { BASE_URL } from '@/site/config'; +import { formatDateFromPostgresString } from '@/utility/date'; +import { + formatAperture, + formatIso, + formatExposureCompensation, + formatExposureTime, + formatFocalLength, +} from '@/utility/exif'; +import camelcaseKeys from 'camelcase-keys'; +import { Metadata } from 'next'; +import short from 'short-uuid'; + +const translator = short(); + +// Core EXIF data +export interface PhotoExif { + aspectRatio: number + make?: string + model?: string + focalLength?: number + focalLengthIn35MmFormat?: number + fNumber?: number + iso?: number + exposureTime?: number + exposureCompensation?: number + latitude?: number + longitude?: number + filmSimulation?: string + takenAt: string + takenAtNaive: string +} + +// Raw db insert +export interface PhotoDbInsert extends PhotoExif { + id: string + idShort: string + url: string + extension: string + blurData: string + title?: string + locationName?: string + priorityOrder?: number +} + +// Raw db response +export interface PhotoDb extends Omit { + updatedAt: Date + createdAt: Date + takenAt: Date +} + +// Parsed db response +export interface Photo extends PhotoDb { + focalLengthFormatted?: string + focalLengthIn35MmFormatFormatted?: string + fNumberFormatted?: string + isoFormatted?: string + exposureTimeFormatted?: string + exposureCompensationFormatted?: string + takenAtNaiveFormatted?: string +} + +export const parsePhotoFromDb = (photoDbRaw: PhotoDb): Photo => { + const photoDb = camelcaseKeys( + photoDbRaw as unknown as Record + ) as unknown as PhotoDb; + return { + ...photoDb, + idShort: + translator.fromUUID(photoDb.id), + focalLengthFormatted: + formatFocalLength(photoDb.focalLength), + focalLengthIn35MmFormatFormatted: + formatFocalLength(photoDb.focalLengthIn35MmFormat), + fNumberFormatted: + formatAperture(photoDb.fNumber), + isoFormatted: + formatIso(photoDb.iso), + exposureTimeFormatted: + formatExposureTime(photoDb.exposureTime), + exposureCompensationFormatted: + formatExposureCompensation(photoDb.exposureCompensation), + takenAtNaiveFormatted: + formatDateFromPostgresString(photoDb.takenAtNaive), + }; +}; + +export const convertPhotoToPhotoDbInsert = ( + photo: Photo, +): PhotoDbInsert => ({ + ...photo, + takenAt: photo.takenAt.toISOString(), +}); + +export const photoStatsAsString = (photo: Photo) => [ + photo.model, + photo.focalLengthFormatted, + photo.fNumberFormatted, + photo.isoFormatted, +].join(' '); + +export const ogImageUrlForPhoto = (photo: Photo) => + `${BASE_URL}/photos/${photo.idShort}/image`; + +export const ogImageDescriptionForPhoto = (photo: Photo) => + photo.takenAtNaiveFormatted?.toUpperCase(); + +export const getPreviousPhoto = (photo: Photo, photos: Photo[]) => { + const index = photos.findIndex(p => p.id === photo.id); + return index > 0 + ? photos[index - 1] + : undefined; +}; + +export const getNextPhoto = (photo: Photo, photos: Photo[]) => { + const index = photos.findIndex(p => p.id === photo.id); + return index < photos.length - 1 + ? photos[index + 1] + : undefined; +}; + +export const generateImageMetaForPhoto = (photo?: Photo): Metadata => photo + ? { + openGraph: { + images: ogImageUrlForPhoto(photo), + }, + twitter: { + card: 'summary_large_image', + images: ogImageUrlForPhoto(photo), + }, + } + : {}; + +const PHOTO_ID_FORWARDING_TABLE: Record = JSON.parse( + process.env.PHOTO_ID_FORWARDING_TABLE || '{}' +); + +export const translatePhotoId = (shortId: string) => { + const id = PHOTO_ID_FORWARDING_TABLE[shortId] || shortId; + return id.length === 22 ? translator.toUUID(id) : id; +}; diff --git a/src/services/blob.ts b/src/services/blob.ts new file mode 100644 index 00000000..ca007e9f --- /dev/null +++ b/src/services/blob.ts @@ -0,0 +1,77 @@ +import { BLOB_BASE_URL } from '@/site'; +import { del, list, put } from '@vercel/blob'; + +export const ACCEPTED_PHOTO_FILE_TYPES = [ + 'image/jpg', + 'image/jpeg', + 'image/png', +]; + +const PREFIX_UPLOAD = 'upload'; +const PREFIX_PHOTO = 'photo'; + +const REGEX_ID = new RegExp( + `\/(?:${PREFIX_UPLOAD}|${PREFIX_PHOTO})-([a-z0-9]+)\.[a-z]{1,4}`, + 'i', +); + +const REGEX_UPLOAD_PATH = new RegExp( + `(?:${PREFIX_UPLOAD})\.[a-z]{1,4}`, + 'i', +); + +export const pathForBlobUrl = (url: string) => + url.replace(`${BLOB_BASE_URL}/`, ''); + +export const getIdFromBlobUrl = (url: string) => + url.match(REGEX_ID)?.[1]; + +export const getExtensionFromBlobUrl = (url: string) => + url.match(/.([a-z]{1,4})$/i)?.[1]; + +export const isUploadPathnameValid = (pathname?: string) => + pathname?.match(REGEX_UPLOAD_PATH); + +export const putPhoto = async ( + file: File | Blob, + extension = 'jpg', + type: 'upload' | 'photo' = 'upload', + handleBlobUploadUrl?: string, +) => + put( + `${type === 'upload' ? PREFIX_UPLOAD : PREFIX_PHOTO}.${extension}`, + file, + { + access: 'public', + handleBlobUploadUrl, + }, + ); + +export const convertUploadToPhoto = async (uploadUrl: string) => { + const file = await fetch(uploadUrl) + .then((response) => response.blob()); + + if (file) { + const { url } = await putPhoto( + file, + uploadUrl.split('.').pop(), + 'photo', + ); + + if (url) { + await del(uploadUrl); + } + + return url; + } +}; + +export const deleteBlobPhoto = (url: string) => del(url); + +export const getBlobUploadUrls = () => + list({ prefix: `${PREFIX_UPLOAD}-` }) + .then(({ blobs }) => blobs.map(({ url }) => url)); + +export const getBlobPhotoUrls = () => + list({ prefix: `${PREFIX_PHOTO}-` }) + .then(({ blobs }) => blobs.map(({ url }) => url)); diff --git a/src/services/postgres.ts b/src/services/postgres.ts new file mode 100644 index 00000000..9fd6765a --- /dev/null +++ b/src/services/postgres.ts @@ -0,0 +1,193 @@ +import { sql } from '@vercel/postgres'; +import { + PhotoDb, + PhotoDbInsert, + translatePhotoId, + parsePhotoFromDb, +} from '@/photo'; + +const PHOTO_DEFAULT_LIMIT = 100; + +const sqlCreatePhotosTable = () => + sql` + CREATE TABLE IF NOT EXISTS photos ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + url VARCHAR(255) NOT NULL, + extension VARCHAR(255) NOT NULL, + aspect_ratio REAL DEFAULT 1.5, + blur_data TEXT, + title VARCHAR(255), + make VARCHAR(255), + model VARCHAR(255), + focal_length SMALLINT, + focal_length_in_35mm_format SMALLINT, + f_number REAL, + iso SMALLINT, + exposure_time DOUBLE PRECISION, + exposure_compensation REAL, + location_name VARCHAR(255), + latitude DOUBLE PRECISION, + longitude DOUBLE PRECISION, + film_simulation VARCHAR(255), + priority_order REAL, + taken_at TIMESTAMP WITH TIME ZONE NOT NULL, + taken_at_naive VARCHAR(255) NOT NULL, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP + ) + `; + +export const sqlInsertPhotoIntoDb = (photo: PhotoDbInsert) => { + return sql` + INSERT INTO photos ( + url, + extension, + aspect_ratio, + blur_data, + title, + make, + model, + focal_length, + focal_length_in_35mm_format, + f_number, + iso, + exposure_time, + exposure_compensation, + location_name, + latitude, + longitude, + film_simulation, + priority_order, + taken_at, + taken_at_naive + ) + VALUES ( + ${photo.url}, + ${photo.extension}, + ${photo.aspectRatio}, + ${photo.blurData}, + ${photo.title}, + ${photo.make}, + ${photo.model}, + ${photo.focalLength}, + ${photo.focalLengthIn35MmFormat}, + ${photo.fNumber}, + ${photo.iso}, + ${photo.exposureTime}, + ${photo.exposureCompensation}, + ${photo.locationName}, + ${photo.latitude}, + ${photo.longitude}, + ${photo.filmSimulation}, + ${photo.priorityOrder}, + ${photo.takenAt}, + ${photo.takenAtNaive} + ) + `; +}; + +export const sqlUpdatePhotoInDb = (photo: PhotoDbInsert) => + sql` + UPDATE photos SET + url=${photo.url}, + extension=${photo.extension}, + aspect_ratio=${photo.aspectRatio}, + blur_data=${photo.blurData}, + title=${photo.title}, + make=${photo.make}, + model=${photo.model}, + focal_length=${photo.focalLength}, + focal_length_in_35mm_format=${photo.focalLengthIn35MmFormat}, + f_number=${photo.fNumber}, + iso=${photo.iso}, + exposure_time=${photo.exposureTime}, + exposure_compensation=${photo.exposureCompensation}, + location_name=${photo.locationName}, + latitude=${photo.latitude}, + longitude=${photo.longitude}, + film_simulation=${photo.filmSimulation}, + priority_order=${photo.priorityOrder}, + taken_at=${photo.takenAt}, + taken_at_naive=${photo.takenAtNaive}, + updated_at=${(new Date()).toISOString()} + WHERE id=${photo.id} + `; + +export const sqlDeletePhoto = (id: string) => + sql`DELETE FROM photos WHERE id=${id}`; + +const sqlGetPhotosFromDb = ( + limit = PHOTO_DEFAULT_LIMIT, + offset = 0, +) => + sql` + SELECT * FROM photos + ORDER BY taken_at DESC + LIMIT ${limit} OFFSET ${offset} + ` + .then(({ rows }) => rows.map(parsePhotoFromDb)); + +const sqlGetPhotosFromDbSortedByCreatedAt = ( + limit = PHOTO_DEFAULT_LIMIT, + offset = 0, +) => + sql` + SELECT * FROM photos + ORDER BY created_at DESC + LIMIT ${limit} OFFSET ${offset} + ` + .then(({ rows }) => rows.map(parsePhotoFromDb)); + +const sqlGetPhotosFromDbSortedByPriority = ( + limit = PHOTO_DEFAULT_LIMIT, + offset = 0, +) => + sql` + SELECT * FROM photos + ORDER BY priority_order ASC + LIMIT ${limit} OFFSET ${offset} + ` + .then(({ rows }) => rows.map(parsePhotoFromDb)); + +const sqlGetPhotoFromDb = (id: string) => + sql`SELECT * FROM photos WHERE id=${id} LIMIT 1` + .then(({ rows }) => rows.map(parsePhotoFromDb)); + +export const getPhotos = async ( + sortBy: 'createdAt' | 'takenAt' | 'priority' = 'takenAt', + limit?: number, + offset?: number, +) => { + let photos; + + const getPhotosRequest = sortBy === 'createdAt' + ? sqlGetPhotosFromDbSortedByCreatedAt + : sortBy === 'priority' + ? sqlGetPhotosFromDbSortedByPriority + : sqlGetPhotosFromDb; + + try { + photos = await getPhotosRequest(limit, offset); + } catch (e: any) { + if (e.message === 'relation "photos" does not exist') { + console.log( + 'Creating table "photos" because it did not exist', + ); + await sqlCreatePhotosTable(); + photos = await getPhotosRequest(limit, offset); + } else { + console.log(`sql get error: ${e.message} `); + throw e; + } + } + + return photos; +}; + +export const getPhoto = (id: string) => + sqlGetPhotoFromDb( + // Check for photo id forwarding + // and convert short ids to uuids + translatePhotoId(id) + ) + .then(photos => photos[0]); diff --git a/src/site/SiteChecklist.tsx b/src/site/SiteChecklist.tsx new file mode 100644 index 00000000..88dd7db0 --- /dev/null +++ b/src/site/SiteChecklist.tsx @@ -0,0 +1,144 @@ +'use client'; + +import { useTransition } from 'react'; +import { useRouter } from 'next/navigation'; +import { cc } from '@/utility/css'; +import SiteChecklistRow from './SiteChecklistRow'; +import { FiExternalLink } from 'react-icons/fi'; + +export default function SiteChecklist({ + hasTitle, + hasDomain, + hasPostgres, + hasBlob, + hasAuth, + showRefreshButton, +}: { + hasTitle: boolean + hasDomain: boolean + hasPostgres: boolean + hasBlob: boolean + hasAuth: boolean + showRefreshButton?: boolean +}) { + const router = useRouter(); + const [isPending, startTransition] = useTransition(); + const refreshSetupStatus = () => { + startTransition(router.refresh); + }; + + const renderLink = (href: string, text: string, external = true) => + <> + + {text} + + {external && + <> +   + + } + ; + + const renderEnvVar = (variables: string[]) => +
+ {variables.map(variable => +
+ + `{variable}` + +
)} +
; + + return ( +
+ + Store in environment variable: + {renderEnvVar(['NEXT_PUBLIC_SITE_TITLE'])} + + + Store in environment variable: + {renderEnvVar(['NEXT_PUBLIC_SITE_DOMAIN'])} + + + {renderLink( + 'https://vercel.com/docs/storage/vercel-postgres/quickstart', + 'Create Vercel Postgres store', + )} + {' '} + and connect to project + + + {renderLink( + 'https://vercel.com/docs/storage/vercel-blob/quickstart', + 'Create Vercel Blob store', + )} + {' '} + and connect to project + + + {renderLink( + 'https://clerk.com/docs/quickstarts/setup-clerk', + 'Create Clerk account', + )} + {' '} + and add environment variables: + {renderEnvVar([ + 'NEXT_PUBLIC_CLERK_SIGN_IN_URL', + 'NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY', + 'CLERK_SECRET_KEY', + 'CLERK_ADMIN_USER_ID', + ])} + +
+
+ Changes to environment variables require a redeploy + or reboot of local dev server +
+ {showRefreshButton && + } +
+
+ ); +} \ No newline at end of file diff --git a/src/site/SiteChecklistRow.tsx b/src/site/SiteChecklistRow.tsx new file mode 100644 index 00000000..d6aed8b0 --- /dev/null +++ b/src/site/SiteChecklistRow.tsx @@ -0,0 +1,37 @@ +import { ReactNode } from 'react'; +import { cc } from '@/utility/css'; +import Spinner from '@/components/Spinner'; + +export default function SiteChecklistRow({ + title, + status, + isPending, + children, +}: { + title: string + status: boolean + isPending: boolean + children: ReactNode +}) { + return ( +
+
+ {isPending + ?
+ +
+ :
+ {status ? '✅' : '❌'} +
} +
+
+
{title}
+
{children}
+
+
+ ); +} diff --git a/src/site/ThemeProviderClient.tsx b/src/site/ThemeProviderClient.tsx new file mode 100644 index 00000000..45996c62 --- /dev/null +++ b/src/site/ThemeProviderClient.tsx @@ -0,0 +1,15 @@ +'use client'; + +import { ThemeProvider } from 'next-themes'; + +export default function ThemeProviderClient({ + children, +}: { + children: React.ReactNode +}) { + return ( + + {children} + + ); +} diff --git a/src/site/ThemeSwitcher.tsx b/src/site/ThemeSwitcher.tsx new file mode 100644 index 00000000..3a2aa831 --- /dev/null +++ b/src/site/ThemeSwitcher.tsx @@ -0,0 +1,41 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { useTheme } from 'next-themes'; +import Switcher from '@/components/Switcher'; +import SwitcherItem from '@/components/SwitcherItem'; +import { BiDesktop, BiMoon, BiSun } from 'react-icons/bi'; + +export default function ThemeSwitcher () { + const [mounted, setMounted] = useState(false); + const { theme, setTheme } = useTheme(); + + // useEffect only runs on the client, so now we can safely show the UI + useEffect(() => { + setMounted(true); + }, []); + + if (!mounted) { + return null; + } + + return ( + + } + onClick={() => setTheme('system')} + active={theme === 'system'} + /> + } + onClick={() => setTheme('light')} + active={theme === 'light'} + /> + } + onClick={() => setTheme('dark')} + active={theme === 'dark'} + /> + + ); +} diff --git a/src/site/config.ts b/src/site/config.ts new file mode 100644 index 00000000..c7178747 --- /dev/null +++ b/src/site/config.ts @@ -0,0 +1,13 @@ +export const SITE_TITLE = process.env.NEXT_PUBLIC_SITE_TITLE + || 'Photo Blog'; + +export const SITE_DOMAIN = process.env.NEXT_PUBLIC_SITE_DOMAIN + || process.env.NEXT_PUBLIC_VERCEL_URL + || SITE_TITLE; + +export const SITE_DESCRIPTION = process.env.NEXT_PUBLIC_SITE_DESCRIPTION + || SITE_DOMAIN; + +export const BASE_URL = process.env.NODE_ENV === 'production' + ? `https://${SITE_DOMAIN}` + : 'http://localhost:3000'; diff --git a/src/site/font.ts b/src/site/font.ts new file mode 100644 index 00000000..57164754 --- /dev/null +++ b/src/site/font.ts @@ -0,0 +1,7 @@ +export const FONT_FAMILY_IBM_PLEX_MONO = 'IBMPlexMono'; + +export const getIBMPlexMonoMedium = () => fetch(new URL( + '/public/fonts/IBMPlexMono-Medium.ttf', + import.meta.url +)) + .then(res => res.arrayBuffer()); diff --git a/src/site/globals.css b/src/site/globals.css new file mode 100644 index 00000000..4026a3c5 --- /dev/null +++ b/src/site/globals.css @@ -0,0 +1,61 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + body { + @apply + font-mono text-sm md:text-base + bg-white dark:bg-black + text-gray-900 dark:text-gray-100 + } + label { + @apply + font-sans font-medium block uppercase text-xs + text-gray-500 dark:text-gray-400 + tracking-wider + } + button, .button, + input[type=text] { + @apply + px-2 py-1.5 + border rounded-md + dark:bg-black + border-gray-200 dark:border-gray-700 + font-mono text-base leading-none + min-h-[2.25rem] + } + input[type=text] { + @apply + min-w-[20rem] read-only:cursor-default + read-only:bg-gray-100 + dark:read-only:bg-gray-900 dark:read-only:text-gray-400 + } + input[type=file] { + @apply + block font-mono w-full text-gray-500 dark:text-gray-400 + file:bg-white dark:file:bg-gray-950 + file:mr-2 file:my-2 file:px-4 file:py-1.5 file:rounded-md + file:border-solid file:border + file:border-gray-200 dark:file:border-gray-700 + file:cursor-pointer + file:shadow-sm + file:active:bg-gray-100 + file:disabled:bg-gray-100 + file:hover:border-gray-300 + file:hover:disabled:border-gray-200 + file:active:disabled:bg-white + file:hover:disabled:cursor-not-allowed + } + button, .button { + @apply + inline-flex gap-2 items-center + px-4 + text-base + shadow-sm + disabled:bg-gray-100 dark:disabled:bg-gray-900 disabled:cursor-not-allowed + active:bg-gray-100 dark:active:bg-gray-900 + hover:border-gray-300 dark:hover:border-gray-600 + hover:disabled:border-gray-200 + } +} diff --git a/src/site/index.ts b/src/site/index.ts new file mode 100644 index 00000000..7fb9b2a0 --- /dev/null +++ b/src/site/index.ts @@ -0,0 +1,38 @@ +// Height determined by intrinsic photo aspect ratio +export const IMAGE_TINY_WIDTH = 50; + +// Height determined by intrinsic photo aspect ratio +export const IMAGE_SMALL_WIDTH = 300; + +// Height determined by intrinsic photo aspect ratio +export const IMAGE_LARGE_WIDTH = 900; + +// 16:9 og image ratio +export const IMAGE_OG_RATIO = 16 / 9; +export const IMAGE_OG_WIDTH = 1200; +export const IMAGE_OG_HEIGHT = IMAGE_OG_WIDTH * (1 / IMAGE_OG_RATIO); + +// 3:2 og grid ratio +export const GRID_OG_RATIO = 1.35; +export const GRID_OG_WIDTH = 1200; +export const GRID_OG_HEIGHT = GRID_OG_WIDTH * (1 / GRID_OG_RATIO); + +const STORE_ID = process.env.BLOB_READ_WRITE_TOKEN?.match( + /^vercel_blob_rw_([a-z0-9]+)_[a-z0-9]+$/i, +)?.[1].toLowerCase(); + +export const BLOB_BASE_URL = + `https://${STORE_ID}.public.blob.vercel-storage.com`; + +export const SITE_CHECKLIST_STATUS = { + hasTitle: (process.env.NEXT_PUBLIC_SITE_TITLE ?? '').length > 0, + hasDomain: (process.env.NEXT_PUBLIC_SITE_DOMAIN ?? '').length > 0, + hasPostgres: (process.env.POSTGRES_HOST ?? '').length > 0, + hasBlob: (process.env.BLOB_READ_WRITE_TOKEN ?? '').length > 0, + hasAuth: ( + (process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY ?? '').length > 0 && + (process.env.NEXT_PUBLIC_CLERK_SIGN_IN_URL ?? '').length > 0 && + (process.env.CLERK_SECRET_KEY ?? '').length > 0 && + (process.env.CLERK_ADMIN_USER_ID ?? '').length > 0 + ), +}; diff --git a/src/site/routes.ts b/src/site/routes.ts new file mode 100644 index 00000000..4ebd97eb --- /dev/null +++ b/src/site/routes.ts @@ -0,0 +1,23 @@ +import { Photo } from '@/photo'; +import { SITE_DOMAIN } from './config'; + +export const ROUTE_ADMIN_UPLOAD = '/admin/uploads'; + +export const ROUTE_ADMIN_UPLOAD_BLOB_HANDLER = '/admin/uploads/blob'; + +export const routeForPhoto = (photo: Photo, share?: boolean) => + share + ? `/photos/${photo.idShort}/share` + : `/photos/${photo.idShort}`; + +export const absoluteRouteForPhoto = (photo: Photo) => + `https://${SITE_DOMAIN}${routeForPhoto(photo)}`; + +export const isRoutePhoto = (pathname = '') => + /^\/photos\/[^/]+\/?$/.test(pathname); + +export const isRoutePhotoShare = (pathname = '') => + /^\/photos\/[^/]+\/share\/?$/.test(pathname); + +export const isRouteSignIn = (pathname = '') => + pathname.startsWith('/sign-in'); diff --git a/src/state/AppStateProvider.tsx b/src/state/AppStateProvider.tsx new file mode 100644 index 00000000..082df160 --- /dev/null +++ b/src/state/AppStateProvider.tsx @@ -0,0 +1,38 @@ +'use client'; + +import { useState, useEffect, ReactNode } from 'react'; +import { AppStateContext } from '.'; +import { AnimationConfig } from '@/components/AnimateItems'; +import usePathnames from '@/utility/usePathnames'; + +export default function StateProvider({ + children, +}: { + children: ReactNode +}) { + const { previousPathname } = usePathnames(); + + const [hasLoaded, setHasLoaded] = useState(false); + + const [nextPhotoAnimation, setNextPhotoAnimation] = + useState(); + + useEffect(() => { + setHasLoaded?.(true); + }, [setHasLoaded]); + + return ( + setNextPhotoAnimation?.(undefined), + }} + > + {children} + + ); +}; diff --git a/src/state/index.ts b/src/state/index.ts new file mode 100644 index 00000000..c8b70912 --- /dev/null +++ b/src/state/index.ts @@ -0,0 +1,15 @@ +import { createContext, useContext } from 'react'; +import { AnimationConfig } from '@/components/AnimateItems'; + +export interface AppStateContext { + previousPathname?: string + hasLoaded?: boolean + setHasLoaded?: (hasLoaded: boolean) => void + nextPhotoAnimation?: AnimationConfig + setNextPhotoAnimation?: (animation?: AnimationConfig) => void + clearNextPhotoAnimation?: () => void +} + +export const AppStateContext = createContext({}); + +export const useAppState = () => useContext(AppStateContext); diff --git a/src/utility/css.ts b/src/utility/css.ts new file mode 100644 index 00000000..4b813415 --- /dev/null +++ b/src/utility/css.ts @@ -0,0 +1,6 @@ +export const cc = ( + ...classes: (string | boolean | undefined)[] +): string => + classes + .filter(s => typeof s === 'string' && s.length) + .join(' '); diff --git a/src/utility/date.ts b/src/utility/date.ts new file mode 100644 index 00000000..d092cfdb --- /dev/null +++ b/src/utility/date.ts @@ -0,0 +1,38 @@ +import { format, parseISO, parse } from 'date-fns'; + +const DATE_STRING_FORMAT = 'd MMM yyyy h:mma'; +const DATE_STRING_FORMAT_POSTGRES = 'yyyy-MM-dd HH:mm:ss'; + +export const formatDate = (date: Date) => + format(date, DATE_STRING_FORMAT); + +export const formatDateFromPostgresString = (date: string) => + formatDate(parse(date, DATE_STRING_FORMAT_POSTGRES, new Date())); + +export const formatDateForPostgres = (date: Date) => + date.toISOString().replace( + /(\d{4}):(\d{2}):(\d{2}) (\d{2}:\d{2}:\d{2})/, + '$1-$2-$3 $4', + ); + +const createNaiveDateWithOffset = ( + dateTimestamp = 0, + offset = '+00:00', +) => { + const date = new Date(dateTimestamp * 1000); + const dateString = `${date.toISOString()}`.replace(/\.[\d]+Z/, offset); + return parseISO(dateString); +}; + +export const convertTimestampWithOffsetToPostgresString = ( + dateTimestamp?: number, + offset?: string, +) => formatDateForPostgres( + createNaiveDateWithOffset(dateTimestamp, offset) +); + +export const convertTimestampToNaivePostgresString = (timestamp = 0) => + new Date(timestamp * 1000).toISOString().replace( + /(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})(.[\d]+Z)*/, + '$1 $2', + ); diff --git a/src/utility/exif.ts b/src/utility/exif.ts new file mode 100644 index 00000000..1734e09d --- /dev/null +++ b/src/utility/exif.ts @@ -0,0 +1,59 @@ +import { ExifData } from 'ts-exif-parser'; + +const OFFSET_REGEX = /[+-]\d\d:\d\d/; + +export const getOffsetFromExif = (data: ExifData) => + Object.values(data.tags as any) + .find((value: any) => + typeof value === 'string' && + OFFSET_REGEX.test(value) + ) as string | undefined; + +export const formatFocalLength = (focalLength?: number) => + focalLength !== undefined ? `${focalLength}mm` : undefined; + +export const formatAperture = (aperture?: number) => + aperture !== undefined ? `ƒ/${aperture}` : undefined; + +export const formatIso = (iso?: number) => + iso !== undefined ? `ISO ${iso}` : undefined; + +export const formatExposureTime = (exposureTime?: number) => + exposureTime !== undefined + ? `Shutter 1/${Math.floor(1 / (exposureTime ?? 1))}` + : undefined; + +const fractionForDecimal = (decimal: number, fractionCharacter?: boolean) => { + switch (Math.abs(Math.floor(decimal * 100))) { + case 33: + return fractionCharacter ? '⅓' : '1/3'; + case 50: + return fractionCharacter ? '½' : '1/2'; + case 66: + case 67: + return fractionCharacter ? '⅔' : '2/3'; + } +}; + +export const formatExposureCompensation = (exposureCompensation?: number) => { + if (exposureCompensation) { + const decimal = exposureCompensation % 1; + const whole = Math.abs(exposureCompensation - decimal); + const fraction = fractionForDecimal(decimal); + const sign = exposureCompensation > 0 ? '+' : '-'; + return `${sign}${whole ? `${whole} ` : ''}${fraction ?? ''} EV`; + } else { + return undefined; + } +}; + +export const formatModelShort = (model?: string) => { + const textLength = model?.length ?? 0; + if (textLength > 0 && textLength <= 8) { + return model; + } else if (model?.includes('iPhone')) { + return model.split('iPhone')[1]; + } else { + return undefined; + } +}; diff --git a/src/utility/image.ts b/src/utility/image.ts new file mode 100644 index 00000000..4312928f --- /dev/null +++ b/src/utility/image.ts @@ -0,0 +1,21 @@ +export const getNextImageUrlForRequest = ( + imageUrl: string, + request: Request, + width: number, + quality = 75, +) => { + const protocol = (request.headers.get('x-forwarded-proto') || 'https') + .split(',')[0]; + + const host = ( + request.headers.get('x-forwarded-host') || + request.headers.get('host') + ); + + const url = new URL(`${protocol}://${host}/_next/image`); + url.searchParams.append('url', imageUrl); + url.searchParams.append('w', width.toString()); + url.searchParams.append('q', quality.toString()); + + return url.toString(); +}; diff --git a/src/utility/number.ts b/src/utility/number.ts new file mode 100644 index 00000000..edaf465d --- /dev/null +++ b/src/utility/number.ts @@ -0,0 +1,7 @@ +export const toFixedNumber = ( + number: number, + digits: number, + base = 10) => { + const pow = Math.pow(base ?? 10, digits); + return Math.round(number * pow) / pow; +}; diff --git a/src/utility/useClickInsideOutside.ts b/src/utility/useClickInsideOutside.ts new file mode 100644 index 00000000..26aedda5 --- /dev/null +++ b/src/utility/useClickInsideOutside.ts @@ -0,0 +1,49 @@ +import { useCallback, useEffect } from 'react'; + +const MOUSE_DOWN = 'mousedown'; + +interface Options { + // HTML reference + htmlElements: (HTMLElement | null)[], + // Callbacks based on click target + onClick?: (event?: MouseEvent) => void, + onClickInside?: (event?: MouseEvent) => void, + onClickOutside?: (event?: MouseEvent) => void, + // Dynamically listen to click events + shouldListenToClicks?: boolean, +} + +const useClickInsideOutside = ({ + htmlElements, + onClick, + onClickInside, + onClickOutside, + shouldListenToClicks = true, +}: Options) => { + const handleClick = useCallback((event: MouseEvent) => { + const target = event.target as HTMLElement; + + const htmlElementsContainTarget = htmlElements + .some(element => element?.contains(target)); + + // On click + onClick?.(event); + // On click inside + if (htmlElementsContainTarget) { + onClickInside?.(event); + } + // On click outside + if (!htmlElementsContainTarget) { + onClickOutside?.(event); + } + }, [onClick, onClickInside, onClickOutside, htmlElements]); + + useEffect(() => { + if (shouldListenToClicks) { + document.addEventListener(MOUSE_DOWN, handleClick); + return () => { document.removeEventListener(MOUSE_DOWN, handleClick); }; + } + }, [htmlElements, shouldListenToClicks, handleClick]); +}; + +export default useClickInsideOutside; diff --git a/src/utility/usePathnames.ts b/src/utility/usePathnames.ts new file mode 100644 index 00000000..cad06d4e --- /dev/null +++ b/src/utility/usePathnames.ts @@ -0,0 +1,21 @@ +import { useEffect, useRef } from 'react'; +import { usePathname } from 'next/navigation'; + +const usePathnames = () => { + const pathname = usePathname(); + + const currentRef = useRef(); + const previousRef = useRef(); + + useEffect(() => { + previousRef.current = currentRef.current; + currentRef.current = pathname; + }, [pathname]); + + return { + currentPathname: currentRef.current, + previousPathname: previousRef.current, + }; +}; + +export default usePathnames; \ No newline at end of file diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 00000000..6bec7c4c --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,32 @@ +const defaultTheme = require('tailwindcss/defaultTheme'); + +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: [ + './src/**/*.{js,ts,jsx,tsx,mdx}', + ], + darkMode: 'class', + theme: { + screens: { + 'xs': '390px', + ...defaultTheme.screens, + }, + fontSize: { + 'xs': '0.75rem', + 'sm': ['0.825rem', '1.15rem'], + 'base': ['0.875rem', '1.275rem'], + 'lg': ['0.925rem', '1.05rem'], + 'xl': '1rem', + '2xl': '1.1rem', + '3xl': ['1.3rem', '1.7rem'], + }, + extend: { + fontFamily: { + 'mono': ['var(--font-ibm-plex-mono)', ...defaultTheme.fontFamily.mono], + }, + }, + }, + plugins: [ + require('@tailwindcss/forms'), + ], +}; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..9ea98fd3 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,41 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": [ + "./src/*" + ] + } + }, + "exclude": [ + "node_modules" + ], + "include": [ + "next-env.d.ts", + ".next/types/**/*.ts", + "**/*.ts", + "**/*.tsx" + ] +}