diff --git a/.vscode/settings.json b/.vscode/settings.json index 03182fcf..1e20aba9 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,6 +10,7 @@ "cloudflarestorage", "cmdk", "CredentialsSignin", + "datetime", "Eterna", "exif", "exifr", @@ -20,9 +21,11 @@ "headlessui", "hgetall", "hset", + "ILIKE", "jpgs", "Lightbox", "Makernote", + "mitigations", "nanoids", "nextjs", "parameterizes", @@ -30,19 +33,22 @@ "Provia", "qaub", "QRSTUVWXYZ", + "ratelimit", + "ratelimiter", "Reala", "skippable", "sonner", + "Streamable", "thephotoblog", "trpc", "unnest", + "upstash", "UsKSGcbt", "Velvia", "WRHGZC", "wxyz", "zadd", - "zrange", - "datetime" + "zrange" ], "files.associations": { "*.css": "tailwindcss" diff --git a/README.md b/README.md index 9f4e0b2f..c00de089 100644 --- a/README.md +++ b/README.md @@ -10,12 +10,14 @@ https://photos.sambecker.com Features - +- Built-in auth - Photo upload with EXIF extraction - Organize photos by tag and camera model - Infinite scroll -- Built-in auth - Light/dark mode +- CMD-K menu with photo search - Automatic OG image generation +- Experimental support for AI-generated descriptions - Support for Fujifilm simulations OG Image Preview @@ -66,7 +68,25 @@ Installation 2. Click "Speed Insights" tab 3. Follow "Enable Speed Insights" instructions (`@vercel/speed-insights` already included) -### 7. Optional configuration +### 7. Add experimental AI text generation + +_⚠️ READ BEFORE PROCEEDING_ + +> _Usage of this feature will result in fees from OpenAI. When enabling AI text generation, follow all recommended mitigations in order to avoid unexpected charges and attacks. Make sure your OpenAI secret key environment variable is not prefixed with NEXT_PUBLIC._ + +1. Setup OpenAI + - If you don't already have one, create an [OpenAI](https://openai.com) account + - Generate an API key and store in environment variable `OPENAI_SECRET_KEY` + - Setup usage limits to avoid unexpected charges (_recommended_) +2. Add rate limiting (_recommended_) + - As an additional precaution, create a [Vercel KV](https://vercel.com/docs/storage/vercel-kv/quickstart#create-a-kv-database) store and link it to your project in order to enable rate limiting +3. Configure auto-generated fields (optional) + - Set which text fields should auto-generate when uploading a photo by storing a comma-separated list, e.g., `AI_TEXT_AUTO_GENERATED_FIELDS = title, semantic` + - Accepted values: title, caption, tags, description, all, or none (default is "all") + +### 8. Optional configuration + +Application behavior can be changed by configuring the following environment variables: - `NEXT_PUBLIC_PRO_MODE = 1` enables higher quality image storage for jpgs (results in increased storage usage) - `NEXT_PUBLIC_STATICALLY_OPTIMIZE = 1` enables PPR and static optimization, i.e., building pages ahead of time (results in increased storage usage)—⚠️ _Experimental_ diff --git a/__tests__/ai.test.ts b/__tests__/ai.test.ts new file mode 100644 index 00000000..b70c93a0 --- /dev/null +++ b/__tests__/ai.test.ts @@ -0,0 +1,42 @@ +/* eslint-disable quotes */ +import { parseTitleAndCaption } from "@/photo/ai"; + +describe('AI text parses', () => { + it('titles and captions', () => { + // Complex case + expect(parseTitleAndCaption( + `'Title: "Ephemeral Beauty" Caption: "Roses bask in fleeting sunlight."'` + )).toStrictEqual({ + title: 'Ephemeral Beauty', + caption: 'Roses bask in fleeting sunlight', + }); + // Without surrounding single quotes + expect(parseTitleAndCaption( + `Title: "Ephemeral Beauty" Caption: "Roses bask in fleeting sunlight."` + )).toStrictEqual({ + title: 'Ephemeral Beauty', + caption: 'Roses bask in fleeting sunlight', + }); + // Without trailing period + expect(parseTitleAndCaption( + `Title: "Ephemeral Beauty" Caption: "Roses bask in fleeting sunlight"` + )).toStrictEqual({ + title: 'Ephemeral Beauty', + caption: 'Roses bask in fleeting sunlight', + }); + // Without and quotes + expect(parseTitleAndCaption( + `Title: Ephemeral Beauty Caption: Roses bask in fleeting sunlight` + )).toStrictEqual({ + title: 'Ephemeral Beauty', + caption: 'Roses bask in fleeting sunlight', + }); + // With single space + expect(parseTitleAndCaption( + `Title: Ephemeral Beauty Caption: Roses bask in fleeting sunlight` + )).toStrictEqual({ + title: 'Ephemeral Beauty', + caption: 'Roses bask in fleeting sunlight', + }); + }); +}); diff --git a/package.json b/package.json index ce562cab..9f6cff03 100644 --- a/package.json +++ b/package.json @@ -23,10 +23,13 @@ "@types/react-dom": "18.2.21", "@typescript-eslint/eslint-plugin": "^7.2.0", "@typescript-eslint/parser": "^7.2.0", + "@upstash/ratelimit": "^1.0.1", "@vercel/analytics": "^1.2.2", "@vercel/blob": "^0.22.1", + "@vercel/kv": "^1.0.1", "@vercel/postgres": "0.7.2", "@vercel/speed-insights": "^1.0.10", + "ai": "^3.0.13", "autoprefixer": "10.4.18", "camelcase-keys": "^9.1.3", "clsx": "^2.1.0", @@ -39,9 +42,10 @@ "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "nanoid": "^5.0.6", - "next": "14.2.0-canary.31", + "next": "14.2.0-canary.39", "next-auth": "5.0.0-beta.15", "next-themes": "^0.3.0", + "openai": "^4.29.2", "postcss": "8.4.35", "react": "18.2.0", "react-dom": "18.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8d0186e0..704ae7e3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -47,18 +47,27 @@ dependencies: '@typescript-eslint/parser': specifier: ^7.2.0 version: 7.2.0(eslint@8.57.0)(typescript@5.4.2) + '@upstash/ratelimit': + specifier: ^1.0.1 + version: 1.0.1 '@vercel/analytics': specifier: ^1.2.2 - version: 1.2.2(next@14.2.0-canary.31)(react@18.2.0) + version: 1.2.2(next@14.2.0-canary.39)(react@18.2.0) '@vercel/blob': specifier: ^0.22.1 version: 0.22.1 + '@vercel/kv': + specifier: ^1.0.1 + version: 1.0.1 '@vercel/postgres': specifier: 0.7.2 version: 0.7.2 '@vercel/speed-insights': specifier: ^1.0.10 - version: 1.0.10(next@14.2.0-canary.31)(react@18.2.0) + version: 1.0.10(next@14.2.0-canary.39)(react@18.2.0)(svelte@4.2.12)(vue@3.4.21) + ai: + specifier: ^3.0.13 + version: 3.0.13(react@18.2.0)(solid-js@1.8.15)(svelte@4.2.12)(vue@3.4.21)(zod@3.22.4) autoprefixer: specifier: 10.4.18 version: 10.4.18(postcss@8.4.35) @@ -96,14 +105,17 @@ dependencies: specifier: ^5.0.6 version: 5.0.6 next: - specifier: 14.2.0-canary.31 - version: 14.2.0-canary.31(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0) + specifier: 14.2.0-canary.39 + version: 14.2.0-canary.39(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0) next-auth: specifier: 5.0.0-beta.15 - version: 5.0.0-beta.15(next@14.2.0-canary.31)(react@18.2.0) + version: 5.0.0-beta.15(next@14.2.0-canary.39)(react@18.2.0) next-themes: specifier: ^0.3.0 version: 0.3.0(react-dom@18.2.0)(react@18.2.0) + openai: + specifier: ^4.29.2 + version: 4.29.2 postcss: specifier: 8.4.35 version: 8.4.35 @@ -1551,8 +1563,8 @@ packages: - utf-8-validate dev: false - /@next/env@14.2.0-canary.31: - resolution: {integrity: sha512-xdUjSv8c5e1QPiB010TcyW1zPL3bK7FySHQDu6NjzZuUkYwm8W9c9NGIdJLB2UQv0rfpaFBKfWNlGbakicrE+g==} + /@next/env@14.2.0-canary.39: + resolution: {integrity: sha512-ROeqwq9mybhzfdzNDbz9/0e3fFB6gtC25NZNC/rhZzvgkTvUuYXUbJOJSvvtsoUjQolTCFOhZqKmopX+QgwYwQ==} dev: false /@next/eslint-plugin-next@14.1.3: @@ -1561,8 +1573,8 @@ packages: glob: 10.3.10 dev: false - /@next/swc-darwin-arm64@14.2.0-canary.31: - resolution: {integrity: sha512-9NRPNOWxY/ecv1hxZej9nVvggIemdCqkwlgmkVv+M2TkAJzff5bwY4IzTuyPxQmioxHs43gPyKh/wpVsH4cuog==} + /@next/swc-darwin-arm64@14.2.0-canary.39: + resolution: {integrity: sha512-ImAEFQBac/jYFCQYAEOxLZlzZfoa0GnbmXlGruzyNXl7RG3gJ3OBXx6G/puySAdytp54tArmr+0h+xoEXbop2Q==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] @@ -1570,8 +1582,8 @@ packages: dev: false optional: true - /@next/swc-darwin-x64@14.2.0-canary.31: - resolution: {integrity: sha512-8xbOircQJzJx39GZ4iNd/6PIyOI/qZ8TpjJ9qzA1FpVZUDAMXZIDnQIHIsnxvn0HkP85PByw3tcKZLLKcw5k4g==} + /@next/swc-darwin-x64@14.2.0-canary.39: + resolution: {integrity: sha512-2q0F3L261vYPOrn7KXLX5SzfMe8yPRs0plnExpV2MwQjikt5OhlUdGwRRyEFT0DS0S0cyaKw00nENxBuDi7VyA==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] @@ -1579,8 +1591,8 @@ packages: dev: false optional: true - /@next/swc-linux-arm64-gnu@14.2.0-canary.31: - resolution: {integrity: sha512-zpA19+op2KxnIutpoZbvQiplmoJHsUUPIhynC+PpZtSqA1IWBZoem+T4IhnmHr+F0DQZRqO9lmZZkHdEpRAzbQ==} + /@next/swc-linux-arm64-gnu@14.2.0-canary.39: + resolution: {integrity: sha512-efraDAfAjQosfUdW8ZMjnrH3/mveQQxs055BdGfh+L0+hlTf05ECUH07tg3AKqihhnk+sgJUqigR5ZSsUYrqsw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] @@ -1588,8 +1600,8 @@ packages: dev: false optional: true - /@next/swc-linux-arm64-musl@14.2.0-canary.31: - resolution: {integrity: sha512-cZ1GuBi6YOHdEw8/lhuxZsObJVpry7irgf1PCsOLigpIqCAyMhbAcQ32FkTA3N5MKWpOkrmS9xQ2B6K1ZaQzCg==} + /@next/swc-linux-arm64-musl@14.2.0-canary.39: + resolution: {integrity: sha512-Eb6+d3XkhwaEd69OoTOa4/scqQJtCUiZrmWjR0sVbW3QJ0wWu2o5gz8mInYsLeLwxN+HDy1aDuQSl3hp2PwBbQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] @@ -1597,8 +1609,8 @@ packages: dev: false optional: true - /@next/swc-linux-x64-gnu@14.2.0-canary.31: - resolution: {integrity: sha512-YxWC/fipzs/3cTeGcsSSU3GXEAJ16TI3yo4jgwofWko2jSF2/kpCOZSnRYbqX3eNyBCD4K9/g/9v6rAx2zHiew==} + /@next/swc-linux-x64-gnu@14.2.0-canary.39: + resolution: {integrity: sha512-HKkx1WCMsycDFOp76avVMCIGm/E0jw3yugfyIc/g1vRIh6fTOZ9iyLd1Uannu4MorTxGWS4g1ZRr1C5/9Ve8kg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] @@ -1606,8 +1618,8 @@ packages: dev: false optional: true - /@next/swc-linux-x64-musl@14.2.0-canary.31: - resolution: {integrity: sha512-CoZVZyx08myeDXmoSEuk+eXrwMYenevXP03Rz/+6+BT6zOrq+1s1rFKUZVd6/3AnD9Q7vNCuTGtaL5jaF5koGw==} + /@next/swc-linux-x64-musl@14.2.0-canary.39: + resolution: {integrity: sha512-9W/UTFugvG0fYhNK5IqahiwldH3JSXmF2iCzQMbGMpyhjvOn1UirEZPwkMXz6tdSGXVHwxvvsuhZhZgBIt8csw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] @@ -1615,8 +1627,8 @@ packages: dev: false optional: true - /@next/swc-win32-arm64-msvc@14.2.0-canary.31: - resolution: {integrity: sha512-m1nuZmu8DOJKJvVrrz7KxMH1K3IU1UC7av1jD55cFf3ZM5ur06Mx2PvtbKSnSLCjK7Ga8LHMYXBXQWAbkD6Bcg==} + /@next/swc-win32-arm64-msvc@14.2.0-canary.39: + resolution: {integrity: sha512-rtG2wYP3Sa67F2AqaX2qISefZbc/KN0fj5gPx3ReFIuK8/p6tR/L063xvyNmBZs22DZuc07EaFCQ9Px7EB0C2Q==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] @@ -1624,8 +1636,8 @@ packages: dev: false optional: true - /@next/swc-win32-ia32-msvc@14.2.0-canary.31: - resolution: {integrity: sha512-OFmjN8wK6eSViHUqsh7VLzI8H8d3G6esjn3zOHoSirg821MJLyWVGAXMBjykDz4kTP6VmGdCkohBP6nf/uy94Q==} + /@next/swc-win32-ia32-msvc@14.2.0-canary.39: + resolution: {integrity: sha512-Qh3vNCQQqghFuX4XhKuBhlleaRNIVFTspFMMKdQKFoATVVZh5n/PEeGEIgwjZjsjwfLPI82fkIvxhZkPujcAgg==} engines: {node: '>= 10'} cpu: [ia32] os: [win32] @@ -1633,8 +1645,8 @@ packages: dev: false optional: true - /@next/swc-win32-x64-msvc@14.2.0-canary.31: - resolution: {integrity: sha512-Dv+FC2zYh8aEKsFUpq6815grRS0dcRw9uJ9hxULAZ9EuFcw0iu5zKbEPWZ+klxDrAWA8bw7WYMZfkvEH7bSLOA==} + /@next/swc-win32-x64-msvc@14.2.0-canary.39: + resolution: {integrity: sha512-CPFzgcPYamtJpHtrHr55LsZ9g95l9vnm85OckaDQCK+359z4sgWk5Jp2ortPN/ZorDk+KjiixrE8x1Ix07Mk9g==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -2785,6 +2797,14 @@ packages: resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} dev: false + /@types/diff-match-patch@1.0.36: + resolution: {integrity: sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==} + dev: false + + /@types/estree@1.0.5: + resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} + dev: false + /@types/graceful-fs@4.1.9: resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} dependencies: @@ -2830,6 +2850,19 @@ packages: resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} dev: false + /@types/node-fetch@2.6.11: + resolution: {integrity: sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==} + dependencies: + '@types/node': 20.11.26 + form-data: 4.0.0 + dev: false + + /@types/node@18.19.24: + resolution: {integrity: sha512-eghAz3gnbQbvnHqB+mgB2ZR3aH6RhdEmHGS48BnV75KceQPHqabkxKI0BbUSsqhqy2Ddhc2xD/VAR9ySZd57Lw==} + dependencies: + undici-types: 5.26.5 + dev: false + /@types/node@20.11.26: resolution: {integrity: sha512-YwOMmyhNnAWijOBQweOJnQPl068Oqd4K3OFbTc6AHJwzweUwwWG3GIFY74OKks2PJUDkQPeddOQES9mLn1CTEQ==} dependencies: @@ -3088,7 +3121,32 @@ packages: resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} dev: false - /@vercel/analytics@1.2.2(next@14.2.0-canary.31)(react@18.2.0): + /@upstash/core-analytics@0.0.7: + resolution: {integrity: sha512-lC2j5efqb1haX/fpTGaPUx1rue1WUkOZBVHDzCB7eMIVsRdFFp4xiHtyH/G9omiR1zj39fU5SCTWFiKJH3KOpw==} + engines: {node: '>=16.0.0'} + dependencies: + '@upstash/redis': 1.28.4 + dev: false + + /@upstash/ratelimit@1.0.1: + resolution: {integrity: sha512-G9LZ7idhlkuYknbUngCB3qzd7QnkK1xDkFG5jRtEJZuOUS5UKJ0UTKbhalCtp39eX2wu2Ubv8W7HCeaJQOWM0A==} + dependencies: + '@upstash/core-analytics': 0.0.7 + dev: false + + /@upstash/redis@1.25.1: + resolution: {integrity: sha512-ACj0GhJ4qrQyBshwFgPod6XufVEfKX2wcaihsEvSdLYnY+m+pa13kGt1RXm/yTHKf4TQi/Dy2A8z/y6WUEOmlg==} + dependencies: + crypto-js: 4.2.0 + dev: false + + /@upstash/redis@1.28.4: + resolution: {integrity: sha512-UalkSAny/dz1m8giEhD3Y5ru1o+CPHI32wFyS3MyzDzj2TRvEN+lTw+mPwi20ojk0H2gs8TBW3qsrvwuLLy+pA==} + dependencies: + crypto-js: 4.2.0 + dev: false + + /@vercel/analytics@1.2.2(next@14.2.0-canary.39)(react@18.2.0): resolution: {integrity: sha512-X0rctVWkQV1e5Y300ehVNqpOfSOufo7ieA5PIdna8yX/U7Vjz0GFsGf4qvAhxV02uQ2CVt7GYcrFfddXXK2Y4A==} peerDependencies: next: '>= 13' @@ -3099,7 +3157,7 @@ packages: react: optional: true dependencies: - next: 14.2.0-canary.31(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0) + next: 14.2.0-canary.39(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 server-only: 0.0.1 dev: false @@ -3114,6 +3172,13 @@ packages: undici: 5.28.3 dev: false + /@vercel/kv@1.0.1: + resolution: {integrity: sha512-uTKddsqVYS2GRAM/QMNNXCTuw9N742mLoGRXoNDcyECaxEXvIHG0dEY+ZnYISV4Vz534VwJO+64fd9XeSggSKw==} + engines: {node: '>=14.6'} + dependencies: + '@upstash/redis': 1.25.1 + dev: false + /@vercel/postgres@0.7.2: resolution: {integrity: sha512-IqR/ZAvoPGcPaXl9eWWB5KaA+w/81RzZa/18P4izQRHpNBkTGt9HwGfYi9+wut5UgxNq4QSX9A7HIQR6QDvX2Q==} engines: {node: '>=14.6'} @@ -3124,7 +3189,7 @@ packages: ws: 8.14.2(bufferutil@4.0.8)(utf-8-validate@6.0.3) dev: false - /@vercel/speed-insights@1.0.10(next@14.2.0-canary.31)(react@18.2.0): + /@vercel/speed-insights@1.0.10(next@14.2.0-canary.39)(react@18.2.0)(svelte@4.2.12)(vue@3.4.21): resolution: {integrity: sha512-4uzdKB0RW6Ff2FkzshzjZ+RlJfLPxgm/00i0XXgxfMPhwnnsk92YgtqsxT9OcPLdJUyVU1DqFlSWWjIQMPkh0g==} requiresBuild: true peerDependencies: @@ -3148,8 +3213,83 @@ packages: vue-router: optional: true dependencies: - next: 14.2.0-canary.31(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0) + next: 14.2.0-canary.39(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 + svelte: 4.2.12 + vue: 3.4.21(typescript@5.4.2) + dev: false + + /@vue/compiler-core@3.4.21: + resolution: {integrity: sha512-MjXawxZf2SbZszLPYxaFCjxfibYrzr3eYbKxwpLR9EQN+oaziSu3qKVbwBERj1IFIB8OLUewxB5m/BFzi613og==} + dependencies: + '@babel/parser': 7.23.9 + '@vue/shared': 3.4.21 + entities: 4.5.0 + estree-walker: 2.0.2 + source-map-js: 1.0.2 + dev: false + + /@vue/compiler-dom@3.4.21: + resolution: {integrity: sha512-IZC6FKowtT1sl0CR5DpXSiEB5ayw75oT2bma1BEhV7RRR1+cfwLrxc2Z8Zq/RGFzJ8w5r9QtCOvTjQgdn0IKmA==} + dependencies: + '@vue/compiler-core': 3.4.21 + '@vue/shared': 3.4.21 + dev: false + + /@vue/compiler-sfc@3.4.21: + resolution: {integrity: sha512-me7epoTxYlY+2CUM7hy9PCDdpMPfIwrOvAXud2Upk10g4YLv9UBW7kL798TvMeDhPthkZ0CONNrK2GoeI1ODiQ==} + dependencies: + '@babel/parser': 7.23.9 + '@vue/compiler-core': 3.4.21 + '@vue/compiler-dom': 3.4.21 + '@vue/compiler-ssr': 3.4.21 + '@vue/shared': 3.4.21 + estree-walker: 2.0.2 + magic-string: 0.30.8 + postcss: 8.4.35 + source-map-js: 1.0.2 + dev: false + + /@vue/compiler-ssr@3.4.21: + resolution: {integrity: sha512-M5+9nI2lPpAsgXOGQobnIueVqc9sisBFexh5yMIMRAPYLa7+5wEJs8iqOZc1WAa9WQbx9GR2twgznU8LTIiZ4Q==} + dependencies: + '@vue/compiler-dom': 3.4.21 + '@vue/shared': 3.4.21 + dev: false + + /@vue/reactivity@3.4.21: + resolution: {integrity: sha512-UhenImdc0L0/4ahGCyEzc/pZNwVgcglGy9HVzJ1Bq2Mm9qXOpP8RyNTjookw/gOCUlXSEtuZ2fUg5nrHcoqJcw==} + dependencies: + '@vue/shared': 3.4.21 + dev: false + + /@vue/runtime-core@3.4.21: + resolution: {integrity: sha512-pQthsuYzE1XcGZznTKn73G0s14eCJcjaLvp3/DKeYWoFacD9glJoqlNBxt3W2c5S40t6CCcpPf+jG01N3ULyrA==} + dependencies: + '@vue/reactivity': 3.4.21 + '@vue/shared': 3.4.21 + dev: false + + /@vue/runtime-dom@3.4.21: + resolution: {integrity: sha512-gvf+C9cFpevsQxbkRBS1NpU8CqxKw0ebqMvLwcGQrNpx6gqRDodqKqA+A2VZZpQ9RpK2f9yfg8VbW/EpdFUOJw==} + dependencies: + '@vue/runtime-core': 3.4.21 + '@vue/shared': 3.4.21 + csstype: 3.1.3 + dev: false + + /@vue/server-renderer@3.4.21(vue@3.4.21): + resolution: {integrity: sha512-aV1gXyKSN6Rz+6kZ6kr5+Ll14YzmIbeuWe7ryJl5muJ4uwSwY/aStXTixx76TwkZFJLm1aAlA/HSWEJ4EyiMkg==} + peerDependencies: + vue: 3.4.21 + dependencies: + '@vue/compiler-ssr': 3.4.21 + '@vue/shared': 3.4.21 + vue: 3.4.21(typescript@5.4.2) + dev: false + + /@vue/shared@3.4.21: + resolution: {integrity: sha512-PuJe7vDIi6VYSinuEbUIQgMIRZGgM8e4R+G+/dQTk0X1NEdvgvvgv7m+rfmDH1gZzyA1OjjoWskvHlfRNfQf3g==} dev: false /abab@2.0.6: @@ -3157,6 +3297,13 @@ packages: deprecated: Use your platform's native atob() and btoa() methods instead dev: false + /abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + dependencies: + event-target-shim: 5.0.1 + dev: false + /acorn-globals@7.0.1: resolution: {integrity: sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==} dependencies: @@ -3192,6 +3339,50 @@ packages: - supports-color dev: false + /agentkeepalive@4.5.0: + resolution: {integrity: sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==} + engines: {node: '>= 8.0.0'} + dependencies: + humanize-ms: 1.2.1 + dev: false + + /ai@3.0.13(react@18.2.0)(solid-js@1.8.15)(svelte@4.2.12)(vue@3.4.21)(zod@3.22.4): + resolution: {integrity: sha512-fDrYnVTdMJuS/qYUq0T/CX3WDuTfcZFie9LkgnoQ2layfUG2Wzh/mpfkfYXFEq/mqnpep3xUtECOB1weyyvwUg==} + engines: {node: '>=14.6'} + peerDependencies: + react: ^18.2.0 + solid-js: ^1.7.7 + svelte: ^3.0.0 || ^4.0.0 + vue: ^3.3.4 + zod: ^3.0.0 + peerDependenciesMeta: + react: + optional: true + solid-js: + optional: true + svelte: + optional: true + vue: + optional: true + zod: + optional: true + dependencies: + eventsource-parser: 1.0.0 + jsondiffpatch: 0.6.0 + nanoid: 3.3.6 + react: 18.2.0 + solid-js: 1.8.15 + solid-swr-store: 0.10.7(solid-js@1.8.15)(swr-store@0.10.6) + sswr: 2.0.0(svelte@4.2.12) + svelte: 4.2.12 + swr: 2.2.0(react@18.2.0) + swr-store: 0.10.6 + swrv: 1.0.4(vue@3.4.21) + vue: 3.4.21(typescript@5.4.2) + zod: 3.22.4 + zod-to-json-schema: 3.22.4(zod@3.22.4) + dev: false + /ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} dependencies: @@ -3429,6 +3620,12 @@ packages: dequal: 2.0.3 dev: false + /axobject-query@4.0.0: + resolution: {integrity: sha512-+60uv1hiVFhHZeO+Lz0RYzsVHy5Wr1ayX0mwda9KPDVLNJgZ1T9Ny7VmFbLDzxsH0D87I86vgj3gFrjTJUYznw==} + dependencies: + dequal: 2.0.3 + dev: false + /babel-jest@29.7.0(@babel/core@7.23.9): resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -3505,6 +3702,10 @@ packages: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} dev: false + /base-64@0.1.0: + resolution: {integrity: sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==} + dev: false + /binary-extensions@2.2.0: resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} engines: {node: '>=8'} @@ -3660,11 +3861,20 @@ packages: supports-color: 7.2.0 dev: false + /chalk@5.3.0: + resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + dev: false + /char-regex@1.0.2: resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} engines: {node: '>=10'} dev: false + /charenc@0.0.2: + resolution: {integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==} + dev: false + /chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} @@ -3727,6 +3937,16 @@ packages: engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} dev: false + /code-red@1.0.4: + resolution: {integrity: sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==} + dependencies: + '@jridgewell/sourcemap-codec': 1.4.15 + '@types/estree': 1.0.5 + acorn: 8.11.3 + estree-walker: 3.0.3 + periscopic: 3.1.0 + dev: false + /collect-v8-coverage@1.0.2: resolution: {integrity: sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==} dev: false @@ -3810,6 +4030,22 @@ packages: which: 2.0.2 dev: false + /crypt@0.0.2: + resolution: {integrity: sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==} + dev: false + + /crypto-js@4.2.0: + resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==} + dev: false + + /css-tree@2.3.1: + resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + dependencies: + mdn-data: 2.0.30 + source-map-js: 1.0.2 + dev: false + /css.escape@1.5.1: resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} dev: false @@ -3971,11 +4207,22 @@ packages: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} dev: false + /diff-match-patch@1.0.5: + resolution: {integrity: sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==} + dev: false + /diff-sequences@29.6.3: resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dev: false + /digest-fetch@1.3.0: + resolution: {integrity: sha512-CGJuv6iKNM7QyZlM2T3sPAdZWd/p9zQiRNS9G+9COUCwzWFTs0Xp8NF5iePx7wtvhDykReiRRrSeNb4oMmB8lA==} + dependencies: + base-64: 0.1.0 + md5: 2.3.0 + dev: false + /dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -4513,11 +4760,31 @@ packages: engines: {node: '>=4.0'} dev: false + /estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + dev: false + + /estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + dependencies: + '@types/estree': 1.0.5 + dev: false + /esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} dev: false + /event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + dev: false + + /eventsource-parser@1.0.0: + resolution: {integrity: sha512-9jgfSCa3dmEme2ES3mPByGXfgZ87VbP97tng1G2nWwWx6bV2nYxm2AWCrbQjXToSe+yYlqaZNtxffR9IeQr95g==} + engines: {node: '>=14.18'} + dev: false + /execa@5.1.1: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} @@ -4652,6 +4919,10 @@ packages: signal-exit: 4.1.0 dev: false + /form-data-encoder@1.7.2: + resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==} + dev: false + /form-data@4.0.0: resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} engines: {node: '>= 6'} @@ -4661,6 +4932,14 @@ packages: mime-types: 2.1.35 dev: false + /formdata-node@4.4.1: + resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==} + engines: {node: '>= 12.20'} + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 4.0.0-beta.3 + dev: false + /fraction.js@4.3.7: resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} dev: false @@ -4935,6 +5214,12 @@ packages: engines: {node: '>=10.17.0'} dev: false + /humanize-ms@1.2.1: + resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} + dependencies: + ms: 2.1.3 + dev: false + /iconv-lite@0.6.3: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} @@ -5048,6 +5333,10 @@ packages: has-tostringtag: 1.0.2 dev: false + /is-buffer@1.1.6: + resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} + dev: false + /is-buffer@2.0.5: resolution: {integrity: sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==} engines: {node: '>=4'} @@ -5141,6 +5430,12 @@ packages: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} dev: false + /is-reference@3.0.2: + resolution: {integrity: sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==} + dependencies: + '@types/estree': 1.0.5 + dev: false + /is-regex@1.1.4: resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} engines: {node: '>= 0.4'} @@ -5825,6 +6120,16 @@ packages: hasBin: true dev: false + /jsondiffpatch@0.6.0: + resolution: {integrity: sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + dependencies: + '@types/diff-match-patch': 1.0.36 + chalk: 5.3.0 + diff-match-patch: 1.0.5 + dev: false + /jsx-ast-utils@3.3.5: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} @@ -5884,6 +6189,10 @@ packages: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} dev: false + /locate-character@3.0.0: + resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} + dev: false + /locate-path@5.0.0: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} @@ -5936,6 +6245,13 @@ packages: hasBin: true dev: false + /magic-string@0.30.8: + resolution: {integrity: sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==} + engines: {node: '>=12'} + dependencies: + '@jridgewell/sourcemap-codec': 1.4.15 + dev: false + /make-dir@4.0.0: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} @@ -5954,6 +6270,18 @@ packages: engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} dev: false + /md5@2.3.0: + resolution: {integrity: sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==} + dependencies: + charenc: 0.0.2 + crypt: 0.0.2 + is-buffer: 1.1.6 + dev: false + + /mdn-data@2.0.30: + resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} + dev: false + /merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} dev: false @@ -6041,6 +6369,12 @@ packages: 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 + /nanoid@3.3.7: resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -6057,7 +6391,7 @@ packages: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} dev: false - /next-auth@5.0.0-beta.15(next@14.2.0-canary.31)(react@18.2.0): + /next-auth@5.0.0-beta.15(next@14.2.0-canary.39)(react@18.2.0): resolution: {integrity: sha512-UQggNq8CDu3/w8CYkihKLLnRPNXel98K0j7mtjj9a6XTNYo4Hni8xg/2h1YhElW6vXE8mgtvmH11rU8NKw86jQ==} peerDependencies: '@simplewebauthn/browser': ^9.0.1 @@ -6074,7 +6408,7 @@ packages: optional: true dependencies: '@auth/core': 0.28.0 - next: 14.2.0-canary.31(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0) + next: 14.2.0-canary.39(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 dev: false @@ -6088,8 +6422,8 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false - /next@14.2.0-canary.31(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-aKls3+4raMbu8ex6YDuQFa8U4ajKojXpn8GvdlFtgNUyBHJ7IrVuaFJw6rU9s9OibfgpFnAbsmvKzYQmAAjmlg==} + /next@14.2.0-canary.39(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-sTAsUnf7ihBdvN0XwiPKe6kfqxUeEZJaHVOR5RIt2LJ2OnI1mVAp875hjKNxDeOxg2TjpxQCWiEEeKE8IV/tvw==} engines: {node: '>=18.17.0'} hasBin: true peerDependencies: @@ -6103,7 +6437,7 @@ packages: sass: optional: true dependencies: - '@next/env': 14.2.0-canary.31 + '@next/env': 14.2.0-canary.39 '@swc/helpers': 0.5.5 busboy: 1.6.0 caniuse-lite: 1.0.30001591 @@ -6113,20 +6447,37 @@ packages: react-dom: 18.2.0(react@18.2.0) styled-jsx: 5.1.1(@babel/core@7.23.9)(react@18.2.0) optionalDependencies: - '@next/swc-darwin-arm64': 14.2.0-canary.31 - '@next/swc-darwin-x64': 14.2.0-canary.31 - '@next/swc-linux-arm64-gnu': 14.2.0-canary.31 - '@next/swc-linux-arm64-musl': 14.2.0-canary.31 - '@next/swc-linux-x64-gnu': 14.2.0-canary.31 - '@next/swc-linux-x64-musl': 14.2.0-canary.31 - '@next/swc-win32-arm64-msvc': 14.2.0-canary.31 - '@next/swc-win32-ia32-msvc': 14.2.0-canary.31 - '@next/swc-win32-x64-msvc': 14.2.0-canary.31 + '@next/swc-darwin-arm64': 14.2.0-canary.39 + '@next/swc-darwin-x64': 14.2.0-canary.39 + '@next/swc-linux-arm64-gnu': 14.2.0-canary.39 + '@next/swc-linux-arm64-musl': 14.2.0-canary.39 + '@next/swc-linux-x64-gnu': 14.2.0-canary.39 + '@next/swc-linux-x64-musl': 14.2.0-canary.39 + '@next/swc-win32-arm64-msvc': 14.2.0-canary.39 + '@next/swc-win32-ia32-msvc': 14.2.0-canary.39 + '@next/swc-win32-x64-msvc': 14.2.0-canary.39 transitivePeerDependencies: - '@babel/core' - babel-plugin-macros dev: false + /node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + dev: false + + /node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + dependencies: + whatwg-url: 5.0.0 + dev: false + /node-gyp-build@4.8.0: resolution: {integrity: sha512-u6fs2AEUljNho3EYTJNBfImO5QTo/J/1Etd+NVdCj7qWKUSN/bSLkZwhDv7I+w/MSC6qJ4cknepkAYykDdK8og==} hasBin: true @@ -6259,6 +6610,23 @@ packages: mimic-fn: 2.1.0 dev: false + /openai@4.29.2: + resolution: {integrity: sha512-cPkT6zjEcE4qU5OW/SoDDuXEsdOLrXlAORhzmaguj5xZSPlgKvLhi27sFWhLKj07Y6WKNWxcwIbzm512FzTBNQ==} + hasBin: true + dependencies: + '@types/node': 18.19.24 + '@types/node-fetch': 2.6.11 + abort-controller: 3.0.0 + agentkeepalive: 4.5.0 + digest-fetch: 1.3.0 + form-data-encoder: 1.7.2 + formdata-node: 4.4.1 + node-fetch: 2.7.0 + web-streams-polyfill: 3.3.3 + transitivePeerDependencies: + - encoding + dev: false + /opener@1.5.2: resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==} hasBin: true @@ -6364,6 +6732,14 @@ packages: engines: {node: '>=8'} dev: false + /periscopic@3.1.0: + resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==} + dependencies: + '@types/estree': 1.0.5 + estree-walker: 3.0.3 + is-reference: 3.0.2 + dev: false + /pg-int8@1.0.1: resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} engines: {node: '>=4.0.0'} @@ -6858,6 +7234,20 @@ packages: lru-cache: 6.0.0 dev: false + /seroval-plugins@1.0.5(seroval@1.0.5): + resolution: {integrity: sha512-8+pDC1vOedPXjKG7oz8o+iiHrtF2WswaMQJ7CKFpccvSYfrzmvKY9zOJWCg+881722wIHfwkdnRmiiDm9ym+zQ==} + engines: {node: '>=10'} + peerDependencies: + seroval: ^1.0 + dependencies: + seroval: 1.0.5 + dev: false + + /seroval@1.0.5: + resolution: {integrity: sha512-TM+Z11tHHvQVQKeNlOUonOWnsNM+2IBwZ4vwoi4j3zKzIpc5IDw8WPwCfcc8F17wy6cBcJGbZbFOR0UCuTZHQA==} + engines: {node: '>=10'} + dev: false + /server-only@0.0.1: resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==} dev: false @@ -6932,6 +7322,25 @@ packages: engines: {node: '>=8'} dev: false + /solid-js@1.8.15: + resolution: {integrity: sha512-d0QP/efr3UVcwGgWVPveQQ0IHOH6iU7yUhc2piy8arNG8wxKmvUy1kFxyF8owpmfCWGB87usDKMaVnsNYZm+Vw==} + dependencies: + csstype: 3.1.3 + seroval: 1.0.5 + seroval-plugins: 1.0.5(seroval@1.0.5) + dev: false + + /solid-swr-store@0.10.7(solid-js@1.8.15)(swr-store@0.10.6): + resolution: {integrity: sha512-A6d68aJmRP471aWqKKPE2tpgOiR5fH4qXQNfKIec+Vap+MGQm3tvXlT8n0I8UgJSlNAsSAUuw2VTviH2h3Vv5g==} + engines: {node: '>=10'} + peerDependencies: + solid-js: ^1.2 + swr-store: ^0.10 + dependencies: + solid-js: 1.8.15 + swr-store: 0.10.6 + dev: false + /sonner@1.4.3(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-SArYlHbkjqRuLiR0iGY2ZSr09oOrxw081ZZkQPfXrs8aZQLIBOLOdzTYxGJB5yIZ7qL56UEPmrX1YqbODwG0Lw==} peerDependencies: @@ -6963,6 +7372,15 @@ packages: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} dev: false + /sswr@2.0.0(svelte@4.2.12): + resolution: {integrity: sha512-mV0kkeBHcjcb0M5NqKtKVg/uTIYNlIIniyDfSGrSfxpEdM9C365jK0z55pl9K0xAkNTJi2OAOVFQpgMPUk+V0w==} + peerDependencies: + svelte: ^4.0.0 + dependencies: + svelte: 4.2.12 + swrev: 4.0.0 + dev: false + /stack-utils@2.0.6: resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} engines: {node: '>=10'} @@ -7150,6 +7568,54 @@ packages: engines: {node: '>= 0.4'} dev: false + /svelte@4.2.12: + resolution: {integrity: sha512-d8+wsh5TfPwqVzbm4/HCXC783/KPHV60NvwitJnyTA5lWn1elhXMNWhXGCJ7PwPa8qFUnyJNIyuIRt2mT0WMug==} + engines: {node: '>=16'} + dependencies: + '@ampproject/remapping': 2.2.1 + '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/trace-mapping': 0.3.22 + '@types/estree': 1.0.5 + acorn: 8.11.3 + aria-query: 5.3.0 + axobject-query: 4.0.0 + code-red: 1.0.4 + css-tree: 2.3.1 + estree-walker: 3.0.3 + is-reference: 3.0.2 + locate-character: 3.0.0 + magic-string: 0.30.8 + periscopic: 3.1.0 + dev: false + + /swr-store@0.10.6: + resolution: {integrity: sha512-xPjB1hARSiRaNNlUQvWSVrG5SirCjk2TmaUyzzvk69SZQan9hCJqw/5rG9iL7xElHU784GxRPISClq4488/XVw==} + engines: {node: '>=10'} + dependencies: + dequal: 2.0.3 + 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 + + /swrev@4.0.0: + resolution: {integrity: sha512-LqVcOHSB4cPGgitD1riJ1Hh4vdmITOp+BkmfmXRh4hSF/t7EnS4iD+SOTmq7w5pPm/SiPeto4ADbKS6dHUDWFA==} + dev: false + + /swrv@1.0.4(vue@3.4.21): + resolution: {integrity: sha512-zjEkcP8Ywmj+xOJW3lIT65ciY/4AL4e/Or7Gj0MzU3zBJNMdJiT8geVZhINavnlHRMMCcJLHhraLTAiDOTmQ9g==} + peerDependencies: + vue: '>=3.2.26 < 4' + dependencies: + vue: 3.4.21(typescript@5.4.2) + dev: false + /symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} dev: false @@ -7247,6 +7713,10 @@ packages: url-parse: 1.5.10 dev: false + /tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + dev: false + /tr46@3.0.0: resolution: {integrity: sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==} engines: {node: '>=12'} @@ -7461,6 +7931,14 @@ packages: tslib: 2.6.2 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'} @@ -7487,6 +7965,22 @@ packages: convert-source-map: 2.0.0 dev: false + /vue@3.4.21(typescript@5.4.2): + resolution: {integrity: sha512-5hjyV/jLEIKD/jYl4cavMcnzKwjMKohureP8ejn3hhEjwhWIhWeuzL2kJAjzl/WyVsgPY56Sy4Z40C3lVshxXA==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@vue/compiler-dom': 3.4.21 + '@vue/compiler-sfc': 3.4.21 + '@vue/runtime-dom': 3.4.21 + '@vue/server-renderer': 3.4.21(vue@3.4.21) + '@vue/shared': 3.4.21 + typescript: 5.4.2 + dev: false + /w3c-xmlserializer@4.0.0: resolution: {integrity: sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==} engines: {node: '>=14'} @@ -7500,6 +7994,20 @@ packages: makeerror: 1.0.12 dev: false + /web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + dev: false + + /web-streams-polyfill@4.0.0-beta.3: + resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==} + engines: {node: '>= 14'} + dev: false + + /webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + dev: false + /webidl-conversions@7.0.0: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} engines: {node: '>=12'} @@ -7548,6 +8056,13 @@ packages: webidl-conversions: 7.0.0 dev: false + /whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + dev: false + /which-boxed-primitive@1.0.2: resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} dependencies: @@ -7730,3 +8245,15 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} dev: false + + /zod-to-json-schema@3.22.4(zod@3.22.4): + resolution: {integrity: sha512-2Ed5dJ+n/O3cU383xSY28cuVi0BCQhF8nYqWU5paEpl7fVdqdAmiLdqLyfblbNdfOFwFfi/mqU4O1pwc60iBhQ==} + peerDependencies: + zod: ^3.22.4 + dependencies: + zod: 3.22.4 + dev: false + + /zod@3.22.4: + resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} + dev: false diff --git a/src/app/admin/photos/[photoId]/edit/page.tsx b/src/app/admin/photos/[photoId]/edit/page.tsx index 2a5c2d6c..c9a5c68c 100644 --- a/src/app/admin/photos/[photoId]/edit/page.tsx +++ b/src/app/admin/photos/[photoId]/edit/page.tsx @@ -2,6 +2,7 @@ import { redirect } from 'next/navigation'; import { getPhotoNoStore, getUniqueTagsCached } from '@/photo/cache'; import { PATH_ADMIN } from '@/site/paths'; import PhotoEditPageClient from '@/photo/PhotoEditPageClient'; +import { AI_TEXT_GENERATION_ENABLED } from '@/site/config'; export default async function PhotoEditPage({ params: { photoId }, @@ -14,7 +15,13 @@ export default async function PhotoEditPage({ const uniqueTags = await getUniqueTagsCached(); + const hasAiTextGeneration = AI_TEXT_GENERATION_ENABLED; + return ( - + ); }; diff --git a/src/app/admin/uploads/[uploadPath]/page.tsx b/src/app/admin/uploads/[uploadPath]/page.tsx index b59ccc45..a2faee67 100644 --- a/src/app/admin/uploads/[uploadPath]/page.tsx +++ b/src/app/admin/uploads/[uploadPath]/page.tsx @@ -3,6 +3,10 @@ import { extractExifDataFromBlobPath } from '@/photo/server'; import { redirect } from 'next/navigation'; import { getUniqueTagsCached } from '@/photo/cache'; import UploadPageClient from '@/photo/UploadPageClient'; +import { + AI_TEXT_AUTO_GENERATED_FIELDS, + AI_TEXT_GENERATION_ENABLED, +} from '@/site/config'; interface Params { params: { uploadPath: string } @@ -14,11 +18,21 @@ export default async function UploadPage({ params: { uploadPath } }: Params) { photoFormExif, } = await extractExifDataFromBlobPath(uploadPath); - const uniqueTags = await getUniqueTagsCached(); - if (!photoFormExif) { redirect(PATH_ADMIN); } + const uniqueTags = await getUniqueTagsCached(); + + const hasAiTextGeneration = AI_TEXT_GENERATION_ENABLED; + + const textFieldsToAutoGenerate = AI_TEXT_AUTO_GENERATED_FIELDS; + return ( - + ); }; diff --git a/src/auth/index.ts b/src/auth/index.ts index 4ea60774..20ae70af 100644 --- a/src/auth/index.ts +++ b/src/auth/index.ts @@ -44,7 +44,7 @@ export const { }, }); -export const safelyRunServerAdminAction = async ( +export const safelyRunAdminServerAction = async ( callback: () => T, ): Promise => { const session = await auth(); diff --git a/src/components/AdminChildPage.tsx b/src/components/AdminChildPage.tsx index 6a96900e..a0b4861e 100644 --- a/src/components/AdminChildPage.tsx +++ b/src/components/AdminChildPage.tsx @@ -10,6 +10,7 @@ function AdminChildPage({ backPath, backLabel, breadcrumb, + breadcrumbEllipsis, accessory, isLoading, children, @@ -17,6 +18,7 @@ function AdminChildPage({ backPath?: string backLabel?: string breadcrumb?: ReactNode + breadcrumbEllipsis?: boolean accessory?: ReactNode isLoading?: boolean children: ReactNode, @@ -27,12 +29,14 @@ function AdminChildPage({
{(backPath || breadcrumb || accessory) &&
{backPath && - {backLabel || 'Back'} + + {backLabel || 'Back'} + } {breadcrumb && <> / - + {breadcrumb} } diff --git a/src/components/Badge.tsx b/src/components/Badge.tsx index 69c1fc8a..e2ce75fb 100644 --- a/src/components/Badge.tsx +++ b/src/components/Badge.tsx @@ -8,6 +8,7 @@ export default function Badge({ highContrast, uppercase, interactive, + className, }: { children: React.ReactNode className?: string @@ -16,6 +17,7 @@ export default function Badge({ highContrast?: boolean uppercase?: boolean interactive?: boolean + className?: string }) { const stylesForType = () => { switch (type) { @@ -47,6 +49,7 @@ export default function Badge({ 'leading-none', stylesForType(), uppercase && 'uppercase tracking-wider', + className, )}> {children} diff --git a/src/components/CanvasBlurCapture.tsx b/src/components/CanvasBlurCapture.tsx index a4541e04..cce1ed31 100644 --- a/src/components/CanvasBlurCapture.tsx +++ b/src/components/CanvasBlurCapture.tsx @@ -6,7 +6,9 @@ const RETRY_DELAY = 2000; export default function CanvasBlurCapture({ imageUrl, + onLoad, onCapture, + onError, width, height, hidden = true, @@ -15,7 +17,9 @@ export default function CanvasBlurCapture({ quality = 0.9, }: { imageUrl: string - onCapture: (blurData: string) => void + onLoad?: (imageData: string) => void + onCapture: (imageData: string) => void + onError?: (error: string) => void width: number height: number hidden?: boolean @@ -44,7 +48,18 @@ export default function CanvasBlurCapture({ canvas.style.height = `${height}px`; const context = refCanvas.current?.getContext('2d'); if (context) { + // Draw scaled image context.scale(scale, scale); + context.drawImage( + refImage.current, + -edgeCompensation, + -edgeCompensation, + width + edgeCompensation * 2, + width * refImage.current.height / refImage.current.width + + edgeCompensation * 2, + ); + onLoad?.(canvas.toDataURL('image/jpeg', quality)); + // Draw blurred image context.filter = 'contrast(1.2) saturate(1.2) ' + `blur(${scale * 10}px)`; @@ -56,17 +71,21 @@ export default function CanvasBlurCapture({ width * refImage.current.height / refImage.current.width + edgeCompensation * 2, ); - refTimeouts.current.forEach(clearTimeout); onCapture(canvas.toDataURL('image/jpeg', quality)); + onError?.(''); + refTimeouts.current.forEach(clearTimeout); refShouldCapture.current = false; } else { - console.error('Cannot get 2d context'); + console.error('Cannot get 2d context ... retrying'); + onError?.('Cannot get 2d context ... retrying'); // Retry capture in case canvas is not available refTimeouts.current.push(setTimeout(capture, RETRY_DELAY)); } } else { // eslint-disable-next-line max-len - console.error('Cannot generate blur data: canvas/image not ready'); + console.error('Cannot generate blur data: canvas/image not ready ... retrying'); + // eslint-disable-next-line max-len + onError?.('Cannot generate blur data: canvas/image not ready ... retrying'); // Retry capture in case canvas is not available refTimeouts.current.push(setTimeout(capture, RETRY_DELAY)); } @@ -92,6 +111,8 @@ export default function CanvasBlurCapture({ }, [ imageUrl, onCapture, + onLoad, + onError, width, height, edgeCompensation, diff --git a/src/components/Checklist.tsx b/src/components/Checklist.tsx index cb80dded..62a23bab 100644 --- a/src/components/Checklist.tsx +++ b/src/components/Checklist.tsx @@ -1,30 +1,36 @@ import { ReactNode } from 'react'; import { clsx } from 'clsx/lite'; +import ExperimentalBadge from './ExperimentalBadge'; +import Badge from './Badge'; export default function Checklist({ title, icon, optional, + experimental, children, }: { title: string icon?: ReactNode optional?: boolean + experimental?: boolean children: ReactNode }) { return (
- {icon} -
-
{title}
+ {icon} + + {title} {optional && -
(Optional)
} -
+ Optional} + {experimental && + } +
{title} {experimental && - - Experimental - } + }
{children} diff --git a/src/components/CommandKClient.tsx b/src/components/CommandKClient.tsx index ae7dad4a..5bae5a13 100644 --- a/src/components/CommandKClient.tsx +++ b/src/components/CommandKClient.tsx @@ -28,6 +28,7 @@ export type CommandKSection = { accessory?: ReactNode items: { label: string + keywords?: string[] annotation?: ReactNode annotationAria?: string accessory?: ReactNode @@ -156,8 +157,13 @@ export default function CommandKClient({ open={isOpen} onOpenChange={setIsOpen} label="Global Command Menu" - filter={(value, search) => - value.toLowerCase().includes(search.toLowerCase()) ? 1 : 0} + filter={(value, search, keywords) => { + const searchFormatted = search.trim().toLocaleLowerCase(); + return ( + value.toLocaleLowerCase().includes(searchFormatted) || + keywords?.includes(searchFormatted) + ) ? 1 : 0 ; + }} loop > {items.map(({ - accessory, label, + keywords, annotation, annotationAria, + accessory, path, action, }) => + Experimental + + ); +} diff --git a/src/components/FieldSetWithStatus.tsx b/src/components/FieldSetWithStatus.tsx index 19e7ab01..3c7bdd7f 100644 --- a/src/components/FieldSetWithStatus.tsx +++ b/src/components/FieldSetWithStatus.tsx @@ -24,6 +24,7 @@ export default function FieldSetWithStatus({ capitalize, type = 'text', inputRef, + accessory, }: { id: string label: string @@ -41,6 +42,7 @@ export default function FieldSetWithStatus({ capitalize?: boolean type?: FieldSetType inputRef?: LegacyRef + accessory?: React.ReactNode }) { const { pending } = useFormStatus(); @@ -68,58 +70,76 @@ export default function FieldSetWithStatus({ } - {selectOptions - ? - : tagOptions - ? + {selectOptions + ? onChange?.(type === 'checkbox' - ? e.target.value === 'true' ? 'false' : 'true' - : e.target.value)} - type={type} - autoComplete="off" - autoCapitalize={!capitalize ? 'off' : undefined} - readOnly={readOnly || pending} + onChange={e => onChange?.(e.target.value)} className={clsx( - type === 'text' && 'w-full', - Boolean(error) && 'error', + 'w-full', + clsx(Boolean(error) && 'error'), + // Use special class because `select` can't be readonly + readOnly || pending && 'disabled-select', )} - />} + > + {selectOptionsDefaultLabel && + } + {selectOptions.map(({ value: optionValue, label: optionLabel }) => + )} + + : tagOptions + ? + : type === 'textarea' + ?