Merge pull request #242 from sambecker/ai-autofill
Batched generation of missing AI fields
This commit is contained in:
commit
db14acabdf
@ -267,7 +267,7 @@ Vercel Postgres can be switched to another Postgres-compatible, pooling provider
|
|||||||
> There have been reports ([Issue 184](https://github.com/sambecker/exif-photo-blog/issues/184#issuecomment-2629474045) + [185](https://github.com/sambecker/exif-photo-blog/issues/185#issuecomment-2629478570)) that having large photos (over 30MB), or a CDN, e.g., Cloudflare in front of Vercel, may destabilize static optimization.
|
> There have been reports ([Issue 184](https://github.com/sambecker/exif-photo-blog/issues/184#issuecomment-2629474045) + [185](https://github.com/sambecker/exif-photo-blog/issues/185#issuecomment-2629478570)) that having large photos (over 30MB), or a CDN, e.g., Cloudflare in front of Vercel, may destabilize static optimization.
|
||||||
|
|
||||||
#### Why don't my older photos look right?
|
#### Why don't my older photos look right?
|
||||||
> As the template has evolved, EXIF fields (such as lenses) have been added, blur data is generated through a different method, and AI/privacy features have been added. In order to bring older photos up to date, either click the 'sync' button next to a photo or use the outdated photo page (`/admin/outdated`) to make batch updates.
|
> As the template has evolved, EXIF fields (such as lenses) have been added, blur data is generated through a different method, and AI/privacy features have been added. In order to bring older photos up to date, either click the 'sync' button next to a photo or go to photo updates (`/admin/photos/updates`) to sync all photos that need updates.
|
||||||
|
|
||||||
#### Why don't my OG images load when I share a link?
|
#### Why don't my OG images load when I share a link?
|
||||||
> Many services such as iMessage, Slack, and X, require near-instant responses when unfurling link-based content. In order to guarantee sufficient responsiveness, consider rendering pages and image assets ahead of time by enabling static optimization by setting `NEXT_PUBLIC_STATICALLY_OPTIMIZE_PHOTOS = 1` and `NEXT_PUBLIC_STATICALLY_OPTIMIZE_PHOTO_OG_IMAGES = 1`. Keep in mind that this will increase platform usage.
|
> Many services such as iMessage, Slack, and X, require near-instant responses when unfurling link-based content. In order to guarantee sufficient responsiveness, consider rendering pages and image assets ahead of time by enabling static optimization by setting `NEXT_PUBLIC_STATICALLY_OPTIMIZE_PHOTOS = 1` and `NEXT_PUBLIC_STATICALLY_OPTIMIZE_PHOTO_OG_IMAGES = 1`. Keep in mind that this will increase platform usage.
|
||||||
@ -314,6 +314,9 @@ Vercel Postgres can be switched to another Postgres-compatible, pooling provider
|
|||||||
#### I've added my OpenAI key but can't seem to make it work. Why am I seeing connection errors?
|
#### I've added my OpenAI key but can't seem to make it work. Why am I seeing connection errors?
|
||||||
> You may need to pre-purchase credits before accessing the OpenAI API. See [Issue #110](https://github.com/sambecker/exif-photo-blog/issues/110) for discussion.
|
> You may need to pre-purchase credits before accessing the OpenAI API. See [Issue #110](https://github.com/sambecker/exif-photo-blog/issues/110) for discussion.
|
||||||
|
|
||||||
|
#### How do I generate AI text for preexisting photos?
|
||||||
|
> Once AI text generation is configured, photos missing text will show up in photo updates (`/admin/photos/updates`).
|
||||||
|
|
||||||
#### Will there be support for image storage providers beyond Vercel, AWS, and Cloudflare?
|
#### Will there be support for image storage providers beyond Vercel, AWS, and Cloudflare?
|
||||||
> At this time, there are no plans to introduce support for new storage providers. While configuring a new, AWS-compatible provider (e.g., Cloudflare R2) should not be too difficult, there's nuance to consider surrounding details like IAM, CORS, and domain configuration, which can differ slightly from platform to platform. If you’d like to contribute an implementation for a new storage provider, please open a PR.
|
> At this time, there are no plans to introduce support for new storage providers. While configuring a new, AWS-compatible provider (e.g., Cloudflare R2) should not be too difficult, there's nuance to consider surrounding details like IAM, CORS, and domain configuration, which can differ slightly from platform to platform. If you’d like to contribute an implementation for a new storage provider, please open a PR.
|
||||||
|
|
||||||
|
|||||||
@ -1,17 +0,0 @@
|
|||||||
import AdminOutdatedClient from '@/admin/AdminOutdatedClient';
|
|
||||||
import { AI_TEXT_GENERATION_ENABLED } from '@/app/config';
|
|
||||||
import { getOutdatedPhotos } from '@/photo/db/query';
|
|
||||||
|
|
||||||
export const maxDuration = 60;
|
|
||||||
|
|
||||||
export default async function AdminOutdatedPage() {
|
|
||||||
const photos = await getOutdatedPhotos()
|
|
||||||
.catch(() => []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AdminOutdatedClient {...{
|
|
||||||
photos,
|
|
||||||
hasAiTextGeneration: AI_TEXT_GENERATION_ENABLED,
|
|
||||||
}} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,11 +1,10 @@
|
|||||||
import { getStoragePhotoUrlsNoStore } from '@/platforms/storage/cache';
|
import { getStoragePhotoUrlsNoStore } from '@/platforms/storage/cache';
|
||||||
import { getPhotos } from '@/photo/db/query';
|
import { getPhotos, getPhotosInNeedOfSyncCount } from '@/photo/db/query';
|
||||||
import { getPhotosMetaCached } from '@/photo/cache';
|
import { getPhotosMetaCached } from '@/photo/cache';
|
||||||
import AdminPhotosClient from '@/admin/AdminPhotosClient';
|
import AdminPhotosClient from '@/admin/AdminPhotosClient';
|
||||||
import { revalidatePath } from 'next/cache';
|
import { revalidatePath } from 'next/cache';
|
||||||
import { cookies } from 'next/headers';
|
import { cookies } from 'next/headers';
|
||||||
import { TIMEZONE_COOKIE_NAME } from '@/utility/timezone';
|
import { TIMEZONE_COOKIE_NAME } from '@/utility/timezone';
|
||||||
import { getOutdatedPhotosCount } from '@/photo/db/query';
|
|
||||||
import {
|
import {
|
||||||
AI_TEXT_GENERATION_ENABLED,
|
AI_TEXT_GENERATION_ENABLED,
|
||||||
PRESERVE_ORIGINAL_UPLOADS,
|
PRESERVE_ORIGINAL_UPLOADS,
|
||||||
@ -24,7 +23,7 @@ export default async function AdminPhotosPage() {
|
|||||||
const [
|
const [
|
||||||
photos,
|
photos,
|
||||||
photosCount,
|
photosCount,
|
||||||
photosCountOutdated,
|
photosCountNeedsSync,
|
||||||
blobPhotoUrls,
|
blobPhotoUrls,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
getPhotos({
|
getPhotos({
|
||||||
@ -35,7 +34,7 @@ export default async function AdminPhotosPage() {
|
|||||||
getPhotosMetaCached({ hidden: 'include'})
|
getPhotosMetaCached({ hidden: 'include'})
|
||||||
.then(({ count }) => count)
|
.then(({ count }) => count)
|
||||||
.catch(() => 0),
|
.catch(() => 0),
|
||||||
getOutdatedPhotosCount()
|
getPhotosInNeedOfSyncCount()
|
||||||
.catch(() => 0),
|
.catch(() => 0),
|
||||||
DEBUG_PHOTO_BLOBS
|
DEBUG_PHOTO_BLOBS
|
||||||
? getStoragePhotoUrlsNoStore()
|
? getStoragePhotoUrlsNoStore()
|
||||||
@ -46,7 +45,7 @@ export default async function AdminPhotosPage() {
|
|||||||
<AdminPhotosClient {...{
|
<AdminPhotosClient {...{
|
||||||
photos,
|
photos,
|
||||||
photosCount,
|
photosCount,
|
||||||
photosCountOutdated,
|
photosCountNeedsSync,
|
||||||
shouldResize: !PRESERVE_ORIGINAL_UPLOADS,
|
shouldResize: !PRESERVE_ORIGINAL_UPLOADS,
|
||||||
hasAiTextGeneration: AI_TEXT_GENERATION_ENABLED,
|
hasAiTextGeneration: AI_TEXT_GENERATION_ENABLED,
|
||||||
onLastUpload: async () => {
|
onLastUpload: async () => {
|
||||||
|
|||||||
17
app/admin/photos/updates/page.tsx
Normal file
17
app/admin/photos/updates/page.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import AdminPhotosSyncClient from '@/admin/AdminPhotosSyncClient';
|
||||||
|
import { AI_TEXT_GENERATION_ENABLED } from '@/app/config';
|
||||||
|
import { getPhotosInNeedOfSync } from '@/photo/db/query';
|
||||||
|
|
||||||
|
export const maxDuration = 60;
|
||||||
|
|
||||||
|
export default async function AdminUpdatesPage() {
|
||||||
|
const photos = await getPhotosInNeedOfSync()
|
||||||
|
.catch(() => []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AdminPhotosSyncClient {...{
|
||||||
|
photos,
|
||||||
|
hasAiTextGeneration: AI_TEXT_GENERATION_ENABLED,
|
||||||
|
}} />
|
||||||
|
);
|
||||||
|
}
|
||||||
10
package.json
10
package.json
@ -12,10 +12,10 @@
|
|||||||
"@ai-sdk/openai": "^1.3.16",
|
"@ai-sdk/openai": "^1.3.16",
|
||||||
"@aws-sdk/client-s3": "3.787.0",
|
"@aws-sdk/client-s3": "3.787.0",
|
||||||
"@aws-sdk/s3-request-presigner": "3.787.0",
|
"@aws-sdk/s3-request-presigner": "3.787.0",
|
||||||
"@radix-ui/react-dialog": "^1.1.7",
|
"@radix-ui/react-dialog": "^1.1.10",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.7",
|
"@radix-ui/react-dropdown-menu": "^2.1.11",
|
||||||
"@radix-ui/react-tooltip": "^1.2.0",
|
"@radix-ui/react-tooltip": "^1.2.3",
|
||||||
"@radix-ui/react-visually-hidden": "^1.1.3",
|
"@radix-ui/react-visually-hidden": "^1.2.0",
|
||||||
"@upstash/ratelimit": "^2.0.5",
|
"@upstash/ratelimit": "^2.0.5",
|
||||||
"@upstash/redis": "^1.34.8",
|
"@upstash/redis": "^1.34.8",
|
||||||
"@vercel/analytics": "^1.5.0",
|
"@vercel/analytics": "^1.5.0",
|
||||||
@ -62,7 +62,7 @@
|
|||||||
"@types/react-dom": "19.1.2",
|
"@types/react-dom": "19.1.2",
|
||||||
"@types/sanitize-html": "^2.15.0",
|
"@types/sanitize-html": "^2.15.0",
|
||||||
"cross-fetch": "^4.1.0",
|
"cross-fetch": "^4.1.0",
|
||||||
"eslint": "9.24.0",
|
"eslint": "9.25.0",
|
||||||
"eslint-config-next": "15.3.1",
|
"eslint-config-next": "15.3.1",
|
||||||
"eslint-plugin-react-hooks": "^5.2.0",
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
|
|||||||
315
pnpm-lock.yaml
generated
315
pnpm-lock.yaml
generated
@ -18,17 +18,17 @@ importers:
|
|||||||
specifier: 3.787.0
|
specifier: 3.787.0
|
||||||
version: 3.787.0
|
version: 3.787.0
|
||||||
'@radix-ui/react-dialog':
|
'@radix-ui/react-dialog':
|
||||||
specifier: ^1.1.7
|
specifier: ^1.1.10
|
||||||
version: 1.1.7(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
version: 1.1.10(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
'@radix-ui/react-dropdown-menu':
|
'@radix-ui/react-dropdown-menu':
|
||||||
specifier: ^2.1.7
|
specifier: ^2.1.11
|
||||||
version: 2.1.7(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
version: 2.1.11(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
'@radix-ui/react-tooltip':
|
'@radix-ui/react-tooltip':
|
||||||
|
specifier: ^1.2.3
|
||||||
|
version: 1.2.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
|
'@radix-ui/react-visually-hidden':
|
||||||
specifier: ^1.2.0
|
specifier: ^1.2.0
|
||||||
version: 1.2.0(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
version: 1.2.0(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
'@radix-ui/react-visually-hidden':
|
|
||||||
specifier: ^1.1.3
|
|
||||||
version: 1.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
|
||||||
'@upstash/ratelimit':
|
'@upstash/ratelimit':
|
||||||
specifier: ^2.0.5
|
specifier: ^2.0.5
|
||||||
version: 2.0.5(@upstash/redis@1.34.8)
|
version: 2.0.5(@upstash/redis@1.34.8)
|
||||||
@ -163,14 +163,14 @@ importers:
|
|||||||
specifier: ^4.1.0
|
specifier: ^4.1.0
|
||||||
version: 4.1.0
|
version: 4.1.0
|
||||||
eslint:
|
eslint:
|
||||||
specifier: 9.24.0
|
specifier: 9.25.0
|
||||||
version: 9.24.0(jiti@2.4.2)
|
version: 9.25.0(jiti@2.4.2)
|
||||||
eslint-config-next:
|
eslint-config-next:
|
||||||
specifier: 15.3.1
|
specifier: 15.3.1
|
||||||
version: 15.3.1(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3)
|
version: 15.3.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3)
|
||||||
eslint-plugin-react-hooks:
|
eslint-plugin-react-hooks:
|
||||||
specifier: ^5.2.0
|
specifier: ^5.2.0
|
||||||
version: 5.2.0(eslint@9.24.0(jiti@2.4.2))
|
version: 5.2.0(eslint@9.25.0(jiti@2.4.2))
|
||||||
jest:
|
jest:
|
||||||
specifier: ^29.7.0
|
specifier: ^29.7.0
|
||||||
version: 29.7.0(@types/node@22.14.1)(ts-node@10.9.2(@types/node@22.14.1)(typescript@5.8.3))
|
version: 29.7.0(@types/node@22.14.1)(ts-node@10.9.2(@types/node@22.14.1)(typescript@5.8.3))
|
||||||
@ -598,28 +598,28 @@ packages:
|
|||||||
resolution: {integrity: sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==}
|
resolution: {integrity: sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
||||||
'@eslint/config-helpers@0.2.0':
|
'@eslint/config-helpers@0.2.1':
|
||||||
resolution: {integrity: sha512-yJLLmLexii32mGrhW29qvU3QBVTu0GUmEf/J4XsBtVhp4JkIUFN/BjWqTF63yRvGApIDpZm5fa97LtYtINmfeQ==}
|
resolution: {integrity: sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
||||||
'@eslint/core@0.12.0':
|
'@eslint/core@0.13.0':
|
||||||
resolution: {integrity: sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==}
|
resolution: {integrity: sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
||||||
'@eslint/eslintrc@3.3.1':
|
'@eslint/eslintrc@3.3.1':
|
||||||
resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==}
|
resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
||||||
'@eslint/js@9.24.0':
|
'@eslint/js@9.25.0':
|
||||||
resolution: {integrity: sha512-uIY/y3z0uvOGX8cp1C2fiC4+ZmBhp6yZWkojtHL1YEMnRt1Y63HB9TM17proGEmeG7HeUY+UP36F0aknKYTpYA==}
|
resolution: {integrity: sha512-iWhsUS8Wgxz9AXNfvfOPFSW4VfMXdVhp1hjkZVhXCrpgh/aLcc45rX6MPu+tIVUWDw0HfNwth7O28M1xDxNf9w==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
||||||
'@eslint/object-schema@2.1.6':
|
'@eslint/object-schema@2.1.6':
|
||||||
resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==}
|
resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
||||||
'@eslint/plugin-kit@0.2.7':
|
'@eslint/plugin-kit@0.2.8':
|
||||||
resolution: {integrity: sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==}
|
resolution: {integrity: sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
||||||
'@fastify/busboy@2.1.1':
|
'@fastify/busboy@2.1.1':
|
||||||
@ -952,8 +952,8 @@ packages:
|
|||||||
'@radix-ui/primitive@1.1.2':
|
'@radix-ui/primitive@1.1.2':
|
||||||
resolution: {integrity: sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==}
|
resolution: {integrity: sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==}
|
||||||
|
|
||||||
'@radix-ui/react-arrow@1.1.3':
|
'@radix-ui/react-arrow@1.1.4':
|
||||||
resolution: {integrity: sha512-2dvVU4jva0qkNZH6HHWuSz5FN5GeU5tymvCgutF8WaXz9WnD1NgUhy73cqzkjkN4Zkn8lfTPv5JIfrC221W+Nw==}
|
resolution: {integrity: sha512-qz+fxrqgNxG0dYew5l7qR3c7wdgRu1XVUHGnGYX7rg5HM4p9SWaRmJwfgR3J0SgyUKayLmzQIun+N6rWRgiRKw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@types/react': '*'
|
'@types/react': '*'
|
||||||
'@types/react-dom': '*'
|
'@types/react-dom': '*'
|
||||||
@ -965,8 +965,8 @@ packages:
|
|||||||
'@types/react-dom':
|
'@types/react-dom':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@radix-ui/react-collection@1.1.3':
|
'@radix-ui/react-collection@1.1.4':
|
||||||
resolution: {integrity: sha512-mM2pxoQw5HJ49rkzwOs7Y6J4oYH22wS8BfK2/bBxROlI4xuR0c4jEenQP63LlTlDkO6Buj2Vt+QYAYcOgqtrXA==}
|
resolution: {integrity: sha512-cv4vSf7HttqXilDnAnvINd53OTl1/bjUYVZrkFnA7nwmY9Ob2POUy0WY0sfqBAe1s5FyKsyceQlqiEGPYNTadg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@types/react': '*'
|
'@types/react': '*'
|
||||||
'@types/react-dom': '*'
|
'@types/react-dom': '*'
|
||||||
@ -1005,8 +1005,8 @@ packages:
|
|||||||
'@types/react':
|
'@types/react':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@radix-ui/react-dialog@1.1.7':
|
'@radix-ui/react-dialog@1.1.10':
|
||||||
resolution: {integrity: sha512-EIdma8C0C/I6kL6sO02avaCRqi3fmWJpxH6mqbVScorW6nNktzKJT/le7VPho3o/7wCsyRg3z0+Q+Obr0Gy/VQ==}
|
resolution: {integrity: sha512-m6pZb0gEM5uHPSb+i2nKKGQi/HMSVjARMsLMWQfKDP+eJ6B+uqryHnXhpnohTWElw+vEcMk/o4wJODtdRKHwqg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@types/react': '*'
|
'@types/react': '*'
|
||||||
'@types/react-dom': '*'
|
'@types/react-dom': '*'
|
||||||
@ -1027,8 +1027,8 @@ packages:
|
|||||||
'@types/react':
|
'@types/react':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@radix-ui/react-dismissable-layer@1.1.6':
|
'@radix-ui/react-dismissable-layer@1.1.7':
|
||||||
resolution: {integrity: sha512-7gpgMT2gyKym9Jz2ZhlRXSg2y6cNQIK8d/cqBZ0RBCaps8pFryCWXiUKI+uHGFrhMrbGUP7U6PWgiXzIxoyF3Q==}
|
resolution: {integrity: sha512-j5+WBUdhccJsmH5/H0K6RncjDtoALSEr6jbkaZu+bjw6hOPOhHycr6vEUujl+HBK8kjUfWcoCJXxP6e4lUlMZw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@types/react': '*'
|
'@types/react': '*'
|
||||||
'@types/react-dom': '*'
|
'@types/react-dom': '*'
|
||||||
@ -1040,8 +1040,8 @@ packages:
|
|||||||
'@types/react-dom':
|
'@types/react-dom':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@radix-ui/react-dropdown-menu@2.1.7':
|
'@radix-ui/react-dropdown-menu@2.1.11':
|
||||||
resolution: {integrity: sha512-7/1LiuNZuCQE3IzdicGoHdQOHkS2Q08+7p8w6TXZ6ZjgAULaCI85ZY15yPl4o4FVgoKLRT43/rsfNVN8osClQQ==}
|
resolution: {integrity: sha512-wbPE3cFBfLl+S+LCxChWQGX0k14zUxgvep1HEnLhJ9mNhjyO3ETzRviAeKZ3XomT/iVRRZAWFsnFZ3N0wI8OmA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@types/react': '*'
|
'@types/react': '*'
|
||||||
'@types/react-dom': '*'
|
'@types/react-dom': '*'
|
||||||
@ -1062,8 +1062,8 @@ packages:
|
|||||||
'@types/react':
|
'@types/react':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@radix-ui/react-focus-scope@1.1.3':
|
'@radix-ui/react-focus-scope@1.1.4':
|
||||||
resolution: {integrity: sha512-4XaDlq0bPt7oJwR+0k0clCiCO/7lO7NKZTAaJBYxDNQT/vj4ig0/UvctrRscZaFREpRvUTkpKR96ov1e6jptQg==}
|
resolution: {integrity: sha512-r2annK27lIW5w9Ho5NyQgqs0MmgZSTIKXWpVCJaLC1q2kZrZkcqnmHkCHMEmv8XLvsLlurKMPT+kbKkRkm/xVA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@types/react': '*'
|
'@types/react': '*'
|
||||||
'@types/react-dom': '*'
|
'@types/react-dom': '*'
|
||||||
@ -1093,8 +1093,8 @@ packages:
|
|||||||
'@types/react':
|
'@types/react':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@radix-ui/react-menu@2.1.7':
|
'@radix-ui/react-menu@2.1.11':
|
||||||
resolution: {integrity: sha512-tBODsrk68rOi1/iQzbM54toFF+gSw/y+eQgttFflqlGekuSebNqvFNHjJgjqPhiMb4Fw9A0zNFly1QT6ZFdQ+Q==}
|
resolution: {integrity: sha512-sbFI4Qaw02J0ogmR9tOMsSqsdrGNpUanlPYAqTE2JJafow8ecHtykg4fSTjNHBdDl4deiKMK+RhTEwyVhP7UDA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@types/react': '*'
|
'@types/react': '*'
|
||||||
'@types/react-dom': '*'
|
'@types/react-dom': '*'
|
||||||
@ -1106,8 +1106,8 @@ packages:
|
|||||||
'@types/react-dom':
|
'@types/react-dom':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@radix-ui/react-popper@1.2.3':
|
'@radix-ui/react-popper@1.2.4':
|
||||||
resolution: {integrity: sha512-iNb9LYUMkne9zIahukgQmHlSBp9XWGeQQ7FvUGNk45ywzOb6kQa+Ca38OphXlWDiKvyneo9S+KSJsLfLt8812A==}
|
resolution: {integrity: sha512-3p2Rgm/a1cK0r/UVkx5F/K9v/EplfjAeIFCGOPYPO4lZ0jtg4iSQXt/YGTSLWaf4x7NG6Z4+uKFcylcTZjeqDA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@types/react': '*'
|
'@types/react': '*'
|
||||||
'@types/react-dom': '*'
|
'@types/react-dom': '*'
|
||||||
@ -1119,8 +1119,8 @@ packages:
|
|||||||
'@types/react-dom':
|
'@types/react-dom':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@radix-ui/react-portal@1.1.5':
|
'@radix-ui/react-portal@1.1.6':
|
||||||
resolution: {integrity: sha512-ps/67ZqsFm+Mb6lSPJpfhRLrVL2i2fntgCmGMqqth4eaGUf+knAuuRtWVJrNjUhExgmdRqftSgzpf0DF0n6yXA==}
|
resolution: {integrity: sha512-XmsIl2z1n/TsYFLIdYam2rmFwf9OC/Sh2avkbmVMDuBZIe7hSpM0cYnWPAo7nHOVx8zTuwDZGByfcqLdnzp3Vw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@types/react': '*'
|
'@types/react': '*'
|
||||||
'@types/react-dom': '*'
|
'@types/react-dom': '*'
|
||||||
@ -1158,8 +1158,8 @@ packages:
|
|||||||
'@types/react-dom':
|
'@types/react-dom':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@radix-ui/react-primitive@2.0.3':
|
'@radix-ui/react-primitive@2.1.0':
|
||||||
resolution: {integrity: sha512-Pf/t/GkndH7CQ8wE2hbkXA+WyZ83fhQQn5DDmwDiDo6AwN/fhaH8oqZ0jRjMrO2iaMhDi6P1HRx6AZwyMinY1g==}
|
resolution: {integrity: sha512-/J/FhLdK0zVcILOwt5g+dH4KnkonCtkVJsa2G6JmvbbtZfBEI1gMsO3QMjseL4F/SwfAMt1Vc/0XKYKq+xJ1sw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@types/react': '*'
|
'@types/react': '*'
|
||||||
'@types/react-dom': '*'
|
'@types/react-dom': '*'
|
||||||
@ -1171,8 +1171,8 @@ packages:
|
|||||||
'@types/react-dom':
|
'@types/react-dom':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@radix-ui/react-roving-focus@1.1.3':
|
'@radix-ui/react-roving-focus@1.1.7':
|
||||||
resolution: {integrity: sha512-ufbpLUjZiOg4iYgb2hQrWXEPYX6jOLBbR27bDyAff5GYMRrCzcze8lukjuXVUQvJ6HZe8+oL+hhswDcjmcgVyg==}
|
resolution: {integrity: sha512-C6oAg451/fQT3EGbWHbCQjYTtbyjNO1uzQgMzwyivcHT3GKNEmu1q3UuREhN+HzHAVtv3ivMVK08QlC+PkYw9Q==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@types/react': '*'
|
'@types/react': '*'
|
||||||
'@types/react-dom': '*'
|
'@types/react-dom': '*'
|
||||||
@ -1202,8 +1202,8 @@ packages:
|
|||||||
'@types/react':
|
'@types/react':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@radix-ui/react-tooltip@1.2.0':
|
'@radix-ui/react-tooltip@1.2.3':
|
||||||
resolution: {integrity: sha512-b1Sdc75s7zN9B8ONQTGBSHL3XS8+IcjcOIY51fhM4R1Hx8s0YbgqgyNZiri4qcYMVZK8hfCZVBiyCm7N9rs0rw==}
|
resolution: {integrity: sha512-0KX7jUYFA02np01Y11NWkk6Ip6TqMNmD4ijLelYAzeIndl2aVeltjJFJ2gwjNa1P8U/dgjQ+8cr9Y3Ni+ZNoRA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@types/react': '*'
|
'@types/react': '*'
|
||||||
'@types/react-dom': '*'
|
'@types/react-dom': '*'
|
||||||
@ -1224,8 +1224,17 @@ packages:
|
|||||||
'@types/react':
|
'@types/react':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@radix-ui/react-use-controllable-state@1.1.1':
|
'@radix-ui/react-use-controllable-state@1.2.2':
|
||||||
resolution: {integrity: sha512-YnEXIy8/ga01Y1PN0VfaNH//MhA91JlEGVBDxDzROqwrAtG5Yr2QGEPz8A/rJA3C7ZAHryOYGaUv8fLSW2H/mg==}
|
resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@radix-ui/react-use-effect-event@0.0.2':
|
||||||
|
resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@types/react': '*'
|
'@types/react': '*'
|
||||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
@ -1278,8 +1287,8 @@ packages:
|
|||||||
'@types/react':
|
'@types/react':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@radix-ui/react-visually-hidden@1.1.3':
|
'@radix-ui/react-visually-hidden@1.2.0':
|
||||||
resolution: {integrity: sha512-oXSF3ZQRd5fvomd9hmUCb2EHSZbPp3ZSHAHJJU/DlF9XoFkJBBW8RHU/E8WEH+RbSfJd/QFA0sl8ClJXknBwHQ==}
|
resolution: {integrity: sha512-rQj0aAWOpCdCMRbI6pLQm8r7S2BM3YhTa0SzOYD55k+hJA8oo9J+H+9wLM9oMlZWOX/wJWPTzfDfmZkf7LvCfg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@types/react': '*'
|
'@types/react': '*'
|
||||||
'@types/react-dom': '*'
|
'@types/react-dom': '*'
|
||||||
@ -2489,8 +2498,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==}
|
resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
||||||
eslint@9.24.0:
|
eslint@9.25.0:
|
||||||
resolution: {integrity: sha512-eh/jxIEJyZrvbWRe4XuVclLPDYSYYYgLy5zXGGxD6j8zjSAxFEzI2fL/8xNq6O2yKqVt+eF2YhV+hxjV6UKXwQ==}
|
resolution: {integrity: sha512-MsBdObhM4cEwkzCiraDv7A6txFXEqtNXOb877TsSp2FCkBNl8JfVQrmiuDqC1IkejT6JLPzYBXx/xAiYhyzgGA==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -5077,9 +5086,9 @@ snapshots:
|
|||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@eslint-community/eslint-utils@4.4.1(eslint@9.24.0(jiti@2.4.2))':
|
'@eslint-community/eslint-utils@4.4.1(eslint@9.25.0(jiti@2.4.2))':
|
||||||
dependencies:
|
dependencies:
|
||||||
eslint: 9.24.0(jiti@2.4.2)
|
eslint: 9.25.0(jiti@2.4.2)
|
||||||
eslint-visitor-keys: 3.4.3
|
eslint-visitor-keys: 3.4.3
|
||||||
|
|
||||||
'@eslint-community/regexpp@4.12.1': {}
|
'@eslint-community/regexpp@4.12.1': {}
|
||||||
@ -5092,9 +5101,9 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@eslint/config-helpers@0.2.0': {}
|
'@eslint/config-helpers@0.2.1': {}
|
||||||
|
|
||||||
'@eslint/core@0.12.0':
|
'@eslint/core@0.13.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/json-schema': 7.0.15
|
'@types/json-schema': 7.0.15
|
||||||
|
|
||||||
@ -5112,13 +5121,13 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@eslint/js@9.24.0': {}
|
'@eslint/js@9.25.0': {}
|
||||||
|
|
||||||
'@eslint/object-schema@2.1.6': {}
|
'@eslint/object-schema@2.1.6': {}
|
||||||
|
|
||||||
'@eslint/plugin-kit@0.2.7':
|
'@eslint/plugin-kit@0.2.8':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint/core': 0.12.0
|
'@eslint/core': 0.13.0
|
||||||
levn: 0.4.1
|
levn: 0.4.1
|
||||||
|
|
||||||
'@fastify/busboy@2.1.1': {}
|
'@fastify/busboy@2.1.1': {}
|
||||||
@ -5484,20 +5493,20 @@ snapshots:
|
|||||||
|
|
||||||
'@radix-ui/primitive@1.1.2': {}
|
'@radix-ui/primitive@1.1.2': {}
|
||||||
|
|
||||||
'@radix-ui/react-arrow@1.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
'@radix-ui/react-arrow@1.1.4(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/react-primitive': 2.0.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
'@radix-ui/react-primitive': 2.1.0(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
react: 19.1.0
|
react: 19.1.0
|
||||||
react-dom: 19.1.0(react@19.1.0)
|
react-dom: 19.1.0(react@19.1.0)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.1.2
|
'@types/react': 19.1.2
|
||||||
'@types/react-dom': 19.1.2(@types/react@19.1.2)
|
'@types/react-dom': 19.1.2(@types/react@19.1.2)
|
||||||
|
|
||||||
'@radix-ui/react-collection@1.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
'@radix-ui/react-collection@1.1.4(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@19.1.0)
|
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@19.1.0)
|
||||||
'@radix-ui/react-context': 1.1.2(@types/react@19.1.2)(react@19.1.0)
|
'@radix-ui/react-context': 1.1.2(@types/react@19.1.2)(react@19.1.0)
|
||||||
'@radix-ui/react-primitive': 2.0.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
'@radix-ui/react-primitive': 2.1.0(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
'@radix-ui/react-slot': 1.2.0(@types/react@19.1.2)(react@19.1.0)
|
'@radix-ui/react-slot': 1.2.0(@types/react@19.1.2)(react@19.1.0)
|
||||||
react: 19.1.0
|
react: 19.1.0
|
||||||
react-dom: 19.1.0(react@19.1.0)
|
react-dom: 19.1.0(react@19.1.0)
|
||||||
@ -5523,20 +5532,20 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.1.2
|
'@types/react': 19.1.2
|
||||||
|
|
||||||
'@radix-ui/react-dialog@1.1.7(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
'@radix-ui/react-dialog@1.1.10(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/primitive': 1.1.2
|
'@radix-ui/primitive': 1.1.2
|
||||||
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@19.1.0)
|
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@19.1.0)
|
||||||
'@radix-ui/react-context': 1.1.2(@types/react@19.1.2)(react@19.1.0)
|
'@radix-ui/react-context': 1.1.2(@types/react@19.1.2)(react@19.1.0)
|
||||||
'@radix-ui/react-dismissable-layer': 1.1.6(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
'@radix-ui/react-dismissable-layer': 1.1.7(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
'@radix-ui/react-focus-guards': 1.1.2(@types/react@19.1.2)(react@19.1.0)
|
'@radix-ui/react-focus-guards': 1.1.2(@types/react@19.1.2)(react@19.1.0)
|
||||||
'@radix-ui/react-focus-scope': 1.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
'@radix-ui/react-focus-scope': 1.1.4(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
'@radix-ui/react-id': 1.1.1(@types/react@19.1.2)(react@19.1.0)
|
'@radix-ui/react-id': 1.1.1(@types/react@19.1.2)(react@19.1.0)
|
||||||
'@radix-ui/react-portal': 1.1.5(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
'@radix-ui/react-portal': 1.1.6(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
'@radix-ui/react-presence': 1.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
'@radix-ui/react-presence': 1.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
'@radix-ui/react-primitive': 2.0.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
'@radix-ui/react-primitive': 2.1.0(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
'@radix-ui/react-slot': 1.2.0(@types/react@19.1.2)(react@19.1.0)
|
'@radix-ui/react-slot': 1.2.0(@types/react@19.1.2)(react@19.1.0)
|
||||||
'@radix-ui/react-use-controllable-state': 1.1.1(@types/react@19.1.2)(react@19.1.0)
|
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.2)(react@19.1.0)
|
||||||
aria-hidden: 1.2.4
|
aria-hidden: 1.2.4
|
||||||
react: 19.1.0
|
react: 19.1.0
|
||||||
react-dom: 19.1.0(react@19.1.0)
|
react-dom: 19.1.0(react@19.1.0)
|
||||||
@ -5551,11 +5560,11 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.1.2
|
'@types/react': 19.1.2
|
||||||
|
|
||||||
'@radix-ui/react-dismissable-layer@1.1.6(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
'@radix-ui/react-dismissable-layer@1.1.7(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/primitive': 1.1.2
|
'@radix-ui/primitive': 1.1.2
|
||||||
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@19.1.0)
|
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@19.1.0)
|
||||||
'@radix-ui/react-primitive': 2.0.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
'@radix-ui/react-primitive': 2.1.0(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.2)(react@19.1.0)
|
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.2)(react@19.1.0)
|
||||||
'@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.1.2)(react@19.1.0)
|
'@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.1.2)(react@19.1.0)
|
||||||
react: 19.1.0
|
react: 19.1.0
|
||||||
@ -5564,15 +5573,15 @@ snapshots:
|
|||||||
'@types/react': 19.1.2
|
'@types/react': 19.1.2
|
||||||
'@types/react-dom': 19.1.2(@types/react@19.1.2)
|
'@types/react-dom': 19.1.2(@types/react@19.1.2)
|
||||||
|
|
||||||
'@radix-ui/react-dropdown-menu@2.1.7(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
'@radix-ui/react-dropdown-menu@2.1.11(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/primitive': 1.1.2
|
'@radix-ui/primitive': 1.1.2
|
||||||
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@19.1.0)
|
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@19.1.0)
|
||||||
'@radix-ui/react-context': 1.1.2(@types/react@19.1.2)(react@19.1.0)
|
'@radix-ui/react-context': 1.1.2(@types/react@19.1.2)(react@19.1.0)
|
||||||
'@radix-ui/react-id': 1.1.1(@types/react@19.1.2)(react@19.1.0)
|
'@radix-ui/react-id': 1.1.1(@types/react@19.1.2)(react@19.1.0)
|
||||||
'@radix-ui/react-menu': 2.1.7(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
'@radix-ui/react-menu': 2.1.11(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
'@radix-ui/react-primitive': 2.0.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
'@radix-ui/react-primitive': 2.1.0(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
'@radix-ui/react-use-controllable-state': 1.1.1(@types/react@19.1.2)(react@19.1.0)
|
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.2)(react@19.1.0)
|
||||||
react: 19.1.0
|
react: 19.1.0
|
||||||
react-dom: 19.1.0(react@19.1.0)
|
react-dom: 19.1.0(react@19.1.0)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
@ -5585,10 +5594,10 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.1.2
|
'@types/react': 19.1.2
|
||||||
|
|
||||||
'@radix-ui/react-focus-scope@1.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
'@radix-ui/react-focus-scope@1.1.4(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@19.1.0)
|
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@19.1.0)
|
||||||
'@radix-ui/react-primitive': 2.0.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
'@radix-ui/react-primitive': 2.1.0(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.2)(react@19.1.0)
|
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.2)(react@19.1.0)
|
||||||
react: 19.1.0
|
react: 19.1.0
|
||||||
react-dom: 19.1.0(react@19.1.0)
|
react-dom: 19.1.0(react@19.1.0)
|
||||||
@ -5610,22 +5619,22 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.1.2
|
'@types/react': 19.1.2
|
||||||
|
|
||||||
'@radix-ui/react-menu@2.1.7(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
'@radix-ui/react-menu@2.1.11(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/primitive': 1.1.2
|
'@radix-ui/primitive': 1.1.2
|
||||||
'@radix-ui/react-collection': 1.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
'@radix-ui/react-collection': 1.1.4(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@19.1.0)
|
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@19.1.0)
|
||||||
'@radix-ui/react-context': 1.1.2(@types/react@19.1.2)(react@19.1.0)
|
'@radix-ui/react-context': 1.1.2(@types/react@19.1.2)(react@19.1.0)
|
||||||
'@radix-ui/react-direction': 1.1.1(@types/react@19.1.2)(react@19.1.0)
|
'@radix-ui/react-direction': 1.1.1(@types/react@19.1.2)(react@19.1.0)
|
||||||
'@radix-ui/react-dismissable-layer': 1.1.6(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
'@radix-ui/react-dismissable-layer': 1.1.7(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
'@radix-ui/react-focus-guards': 1.1.2(@types/react@19.1.2)(react@19.1.0)
|
'@radix-ui/react-focus-guards': 1.1.2(@types/react@19.1.2)(react@19.1.0)
|
||||||
'@radix-ui/react-focus-scope': 1.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
'@radix-ui/react-focus-scope': 1.1.4(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
'@radix-ui/react-id': 1.1.1(@types/react@19.1.2)(react@19.1.0)
|
'@radix-ui/react-id': 1.1.1(@types/react@19.1.2)(react@19.1.0)
|
||||||
'@radix-ui/react-popper': 1.2.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
'@radix-ui/react-popper': 1.2.4(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
'@radix-ui/react-portal': 1.1.5(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
'@radix-ui/react-portal': 1.1.6(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
'@radix-ui/react-presence': 1.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
'@radix-ui/react-presence': 1.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
'@radix-ui/react-primitive': 2.0.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
'@radix-ui/react-primitive': 2.1.0(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
'@radix-ui/react-roving-focus': 1.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
'@radix-ui/react-roving-focus': 1.1.7(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
'@radix-ui/react-slot': 1.2.0(@types/react@19.1.2)(react@19.1.0)
|
'@radix-ui/react-slot': 1.2.0(@types/react@19.1.2)(react@19.1.0)
|
||||||
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.2)(react@19.1.0)
|
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.2)(react@19.1.0)
|
||||||
aria-hidden: 1.2.4
|
aria-hidden: 1.2.4
|
||||||
@ -5636,13 +5645,13 @@ snapshots:
|
|||||||
'@types/react': 19.1.2
|
'@types/react': 19.1.2
|
||||||
'@types/react-dom': 19.1.2(@types/react@19.1.2)
|
'@types/react-dom': 19.1.2(@types/react@19.1.2)
|
||||||
|
|
||||||
'@radix-ui/react-popper@1.2.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
'@radix-ui/react-popper@1.2.4(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@floating-ui/react-dom': 2.1.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
'@floating-ui/react-dom': 2.1.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
'@radix-ui/react-arrow': 1.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
'@radix-ui/react-arrow': 1.1.4(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@19.1.0)
|
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@19.1.0)
|
||||||
'@radix-ui/react-context': 1.1.2(@types/react@19.1.2)(react@19.1.0)
|
'@radix-ui/react-context': 1.1.2(@types/react@19.1.2)(react@19.1.0)
|
||||||
'@radix-ui/react-primitive': 2.0.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
'@radix-ui/react-primitive': 2.1.0(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.2)(react@19.1.0)
|
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.2)(react@19.1.0)
|
||||||
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.2)(react@19.1.0)
|
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.2)(react@19.1.0)
|
||||||
'@radix-ui/react-use-rect': 1.1.1(@types/react@19.1.2)(react@19.1.0)
|
'@radix-ui/react-use-rect': 1.1.1(@types/react@19.1.2)(react@19.1.0)
|
||||||
@ -5654,9 +5663,9 @@ snapshots:
|
|||||||
'@types/react': 19.1.2
|
'@types/react': 19.1.2
|
||||||
'@types/react-dom': 19.1.2(@types/react@19.1.2)
|
'@types/react-dom': 19.1.2(@types/react@19.1.2)
|
||||||
|
|
||||||
'@radix-ui/react-portal@1.1.5(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
'@radix-ui/react-portal@1.1.6(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/react-primitive': 2.0.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
'@radix-ui/react-primitive': 2.1.0(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.2)(react@19.1.0)
|
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.2)(react@19.1.0)
|
||||||
react: 19.1.0
|
react: 19.1.0
|
||||||
react-dom: 19.1.0(react@19.1.0)
|
react-dom: 19.1.0(react@19.1.0)
|
||||||
@ -5683,7 +5692,7 @@ snapshots:
|
|||||||
'@types/react': 19.1.2
|
'@types/react': 19.1.2
|
||||||
'@types/react-dom': 19.1.2(@types/react@19.1.2)
|
'@types/react-dom': 19.1.2(@types/react@19.1.2)
|
||||||
|
|
||||||
'@radix-ui/react-primitive@2.0.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
'@radix-ui/react-primitive@2.1.0(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/react-slot': 1.2.0(@types/react@19.1.2)(react@19.1.0)
|
'@radix-ui/react-slot': 1.2.0(@types/react@19.1.2)(react@19.1.0)
|
||||||
react: 19.1.0
|
react: 19.1.0
|
||||||
@ -5692,17 +5701,17 @@ snapshots:
|
|||||||
'@types/react': 19.1.2
|
'@types/react': 19.1.2
|
||||||
'@types/react-dom': 19.1.2(@types/react@19.1.2)
|
'@types/react-dom': 19.1.2(@types/react@19.1.2)
|
||||||
|
|
||||||
'@radix-ui/react-roving-focus@1.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
'@radix-ui/react-roving-focus@1.1.7(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/primitive': 1.1.2
|
'@radix-ui/primitive': 1.1.2
|
||||||
'@radix-ui/react-collection': 1.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
'@radix-ui/react-collection': 1.1.4(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@19.1.0)
|
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@19.1.0)
|
||||||
'@radix-ui/react-context': 1.1.2(@types/react@19.1.2)(react@19.1.0)
|
'@radix-ui/react-context': 1.1.2(@types/react@19.1.2)(react@19.1.0)
|
||||||
'@radix-ui/react-direction': 1.1.1(@types/react@19.1.2)(react@19.1.0)
|
'@radix-ui/react-direction': 1.1.1(@types/react@19.1.2)(react@19.1.0)
|
||||||
'@radix-ui/react-id': 1.1.1(@types/react@19.1.2)(react@19.1.0)
|
'@radix-ui/react-id': 1.1.1(@types/react@19.1.2)(react@19.1.0)
|
||||||
'@radix-ui/react-primitive': 2.0.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
'@radix-ui/react-primitive': 2.1.0(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.2)(react@19.1.0)
|
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.2)(react@19.1.0)
|
||||||
'@radix-ui/react-use-controllable-state': 1.1.1(@types/react@19.1.2)(react@19.1.0)
|
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.2)(react@19.1.0)
|
||||||
react: 19.1.0
|
react: 19.1.0
|
||||||
react-dom: 19.1.0(react@19.1.0)
|
react-dom: 19.1.0(react@19.1.0)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
@ -5723,20 +5732,20 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.1.2
|
'@types/react': 19.1.2
|
||||||
|
|
||||||
'@radix-ui/react-tooltip@1.2.0(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
'@radix-ui/react-tooltip@1.2.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/primitive': 1.1.2
|
'@radix-ui/primitive': 1.1.2
|
||||||
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@19.1.0)
|
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@19.1.0)
|
||||||
'@radix-ui/react-context': 1.1.2(@types/react@19.1.2)(react@19.1.0)
|
'@radix-ui/react-context': 1.1.2(@types/react@19.1.2)(react@19.1.0)
|
||||||
'@radix-ui/react-dismissable-layer': 1.1.6(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
'@radix-ui/react-dismissable-layer': 1.1.7(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
'@radix-ui/react-id': 1.1.1(@types/react@19.1.2)(react@19.1.0)
|
'@radix-ui/react-id': 1.1.1(@types/react@19.1.2)(react@19.1.0)
|
||||||
'@radix-ui/react-popper': 1.2.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
'@radix-ui/react-popper': 1.2.4(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
'@radix-ui/react-portal': 1.1.5(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
'@radix-ui/react-portal': 1.1.6(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
'@radix-ui/react-presence': 1.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
'@radix-ui/react-presence': 1.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
'@radix-ui/react-primitive': 2.0.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
'@radix-ui/react-primitive': 2.1.0(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
'@radix-ui/react-slot': 1.2.0(@types/react@19.1.2)(react@19.1.0)
|
'@radix-ui/react-slot': 1.2.0(@types/react@19.1.2)(react@19.1.0)
|
||||||
'@radix-ui/react-use-controllable-state': 1.1.1(@types/react@19.1.2)(react@19.1.0)
|
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.2)(react@19.1.0)
|
||||||
'@radix-ui/react-visually-hidden': 1.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
'@radix-ui/react-visually-hidden': 1.2.0(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
react: 19.1.0
|
react: 19.1.0
|
||||||
react-dom: 19.1.0(react@19.1.0)
|
react-dom: 19.1.0(react@19.1.0)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
@ -5749,9 +5758,17 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.1.2
|
'@types/react': 19.1.2
|
||||||
|
|
||||||
'@radix-ui/react-use-controllable-state@1.1.1(@types/react@19.1.2)(react@19.1.0)':
|
'@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.1.2)(react@19.1.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.2)(react@19.1.0)
|
'@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.1.2)(react@19.1.0)
|
||||||
|
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.2)(react@19.1.0)
|
||||||
|
react: 19.1.0
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 19.1.2
|
||||||
|
|
||||||
|
'@radix-ui/react-use-effect-event@0.0.2(@types/react@19.1.2)(react@19.1.0)':
|
||||||
|
dependencies:
|
||||||
|
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.2)(react@19.1.0)
|
||||||
react: 19.1.0
|
react: 19.1.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.1.2
|
'@types/react': 19.1.2
|
||||||
@ -5789,9 +5806,9 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.1.2
|
'@types/react': 19.1.2
|
||||||
|
|
||||||
'@radix-ui/react-visually-hidden@1.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
'@radix-ui/react-visually-hidden@1.2.0(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/react-primitive': 2.0.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
'@radix-ui/react-primitive': 2.1.0(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
react: 19.1.0
|
react: 19.1.0
|
||||||
react-dom: 19.1.0(react@19.1.0)
|
react-dom: 19.1.0(react@19.1.0)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
@ -6357,15 +6374,15 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@types/yargs-parser': 21.0.3
|
'@types/yargs-parser': 21.0.3
|
||||||
|
|
||||||
'@typescript-eslint/eslint-plugin@8.24.1(@typescript-eslint/parser@8.24.1(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3)':
|
'@typescript-eslint/eslint-plugin@8.24.1(@typescript-eslint/parser@8.24.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint-community/regexpp': 4.12.1
|
'@eslint-community/regexpp': 4.12.1
|
||||||
'@typescript-eslint/parser': 8.24.1(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3)
|
'@typescript-eslint/parser': 8.24.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3)
|
||||||
'@typescript-eslint/scope-manager': 8.24.1
|
'@typescript-eslint/scope-manager': 8.24.1
|
||||||
'@typescript-eslint/type-utils': 8.24.1(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3)
|
'@typescript-eslint/type-utils': 8.24.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3)
|
||||||
'@typescript-eslint/utils': 8.24.1(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3)
|
'@typescript-eslint/utils': 8.24.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3)
|
||||||
'@typescript-eslint/visitor-keys': 8.24.1
|
'@typescript-eslint/visitor-keys': 8.24.1
|
||||||
eslint: 9.24.0(jiti@2.4.2)
|
eslint: 9.25.0(jiti@2.4.2)
|
||||||
graphemer: 1.4.0
|
graphemer: 1.4.0
|
||||||
ignore: 5.3.2
|
ignore: 5.3.2
|
||||||
natural-compare: 1.4.0
|
natural-compare: 1.4.0
|
||||||
@ -6374,14 +6391,14 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@typescript-eslint/parser@8.24.1(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3)':
|
'@typescript-eslint/parser@8.24.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/scope-manager': 8.24.1
|
'@typescript-eslint/scope-manager': 8.24.1
|
||||||
'@typescript-eslint/types': 8.24.1
|
'@typescript-eslint/types': 8.24.1
|
||||||
'@typescript-eslint/typescript-estree': 8.24.1(typescript@5.8.3)
|
'@typescript-eslint/typescript-estree': 8.24.1(typescript@5.8.3)
|
||||||
'@typescript-eslint/visitor-keys': 8.24.1
|
'@typescript-eslint/visitor-keys': 8.24.1
|
||||||
debug: 4.4.0
|
debug: 4.4.0
|
||||||
eslint: 9.24.0(jiti@2.4.2)
|
eslint: 9.25.0(jiti@2.4.2)
|
||||||
typescript: 5.8.3
|
typescript: 5.8.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@ -6391,12 +6408,12 @@ snapshots:
|
|||||||
'@typescript-eslint/types': 8.24.1
|
'@typescript-eslint/types': 8.24.1
|
||||||
'@typescript-eslint/visitor-keys': 8.24.1
|
'@typescript-eslint/visitor-keys': 8.24.1
|
||||||
|
|
||||||
'@typescript-eslint/type-utils@8.24.1(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3)':
|
'@typescript-eslint/type-utils@8.24.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/typescript-estree': 8.24.1(typescript@5.8.3)
|
'@typescript-eslint/typescript-estree': 8.24.1(typescript@5.8.3)
|
||||||
'@typescript-eslint/utils': 8.24.1(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3)
|
'@typescript-eslint/utils': 8.24.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3)
|
||||||
debug: 4.4.0
|
debug: 4.4.0
|
||||||
eslint: 9.24.0(jiti@2.4.2)
|
eslint: 9.25.0(jiti@2.4.2)
|
||||||
ts-api-utils: 2.0.1(typescript@5.8.3)
|
ts-api-utils: 2.0.1(typescript@5.8.3)
|
||||||
typescript: 5.8.3
|
typescript: 5.8.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@ -6418,13 +6435,13 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@typescript-eslint/utils@8.24.1(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3)':
|
'@typescript-eslint/utils@8.24.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint-community/eslint-utils': 4.4.1(eslint@9.24.0(jiti@2.4.2))
|
'@eslint-community/eslint-utils': 4.4.1(eslint@9.25.0(jiti@2.4.2))
|
||||||
'@typescript-eslint/scope-manager': 8.24.1
|
'@typescript-eslint/scope-manager': 8.24.1
|
||||||
'@typescript-eslint/types': 8.24.1
|
'@typescript-eslint/types': 8.24.1
|
||||||
'@typescript-eslint/typescript-estree': 8.24.1(typescript@5.8.3)
|
'@typescript-eslint/typescript-estree': 8.24.1(typescript@5.8.3)
|
||||||
eslint: 9.24.0(jiti@2.4.2)
|
eslint: 9.25.0(jiti@2.4.2)
|
||||||
typescript: 5.8.3
|
typescript: 5.8.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@ -6778,7 +6795,7 @@ snapshots:
|
|||||||
cmdk@1.1.1(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
|
cmdk@1.1.1(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/react-compose-refs': 1.1.1(@types/react@19.1.2)(react@19.1.0)
|
'@radix-ui/react-compose-refs': 1.1.1(@types/react@19.1.2)(react@19.1.0)
|
||||||
'@radix-ui/react-dialog': 1.1.7(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
'@radix-ui/react-dialog': 1.1.10(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
'@radix-ui/react-id': 1.1.0(@types/react@19.1.2)(react@19.1.0)
|
'@radix-ui/react-id': 1.1.0(@types/react@19.1.2)(react@19.1.0)
|
||||||
'@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
'@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
react: 19.1.0
|
react: 19.1.0
|
||||||
@ -7109,19 +7126,19 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
source-map: 0.6.1
|
source-map: 0.6.1
|
||||||
|
|
||||||
eslint-config-next@15.3.1(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3):
|
eslint-config-next@15.3.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@next/eslint-plugin-next': 15.3.1
|
'@next/eslint-plugin-next': 15.3.1
|
||||||
'@rushstack/eslint-patch': 1.10.5
|
'@rushstack/eslint-patch': 1.10.5
|
||||||
'@typescript-eslint/eslint-plugin': 8.24.1(@typescript-eslint/parser@8.24.1(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3)
|
'@typescript-eslint/eslint-plugin': 8.24.1(@typescript-eslint/parser@8.24.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3)
|
||||||
'@typescript-eslint/parser': 8.24.1(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3)
|
'@typescript-eslint/parser': 8.24.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3)
|
||||||
eslint: 9.24.0(jiti@2.4.2)
|
eslint: 9.25.0(jiti@2.4.2)
|
||||||
eslint-import-resolver-node: 0.3.9
|
eslint-import-resolver-node: 0.3.9
|
||||||
eslint-import-resolver-typescript: 3.8.1(eslint-plugin-import@2.31.0)(eslint@9.24.0(jiti@2.4.2))
|
eslint-import-resolver-typescript: 3.8.1(eslint-plugin-import@2.31.0)(eslint@9.25.0(jiti@2.4.2))
|
||||||
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.24.1(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.8.1)(eslint@9.24.0(jiti@2.4.2))
|
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.24.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.8.1)(eslint@9.25.0(jiti@2.4.2))
|
||||||
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.24.0(jiti@2.4.2))
|
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.25.0(jiti@2.4.2))
|
||||||
eslint-plugin-react: 7.37.4(eslint@9.24.0(jiti@2.4.2))
|
eslint-plugin-react: 7.37.4(eslint@9.25.0(jiti@2.4.2))
|
||||||
eslint-plugin-react-hooks: 5.2.0(eslint@9.24.0(jiti@2.4.2))
|
eslint-plugin-react-hooks: 5.2.0(eslint@9.25.0(jiti@2.4.2))
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
typescript: 5.8.3
|
typescript: 5.8.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@ -7137,33 +7154,33 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
eslint-import-resolver-typescript@3.8.1(eslint-plugin-import@2.31.0)(eslint@9.24.0(jiti@2.4.2)):
|
eslint-import-resolver-typescript@3.8.1(eslint-plugin-import@2.31.0)(eslint@9.25.0(jiti@2.4.2)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nolyfill/is-core-module': 1.0.39
|
'@nolyfill/is-core-module': 1.0.39
|
||||||
debug: 4.4.0
|
debug: 4.4.0
|
||||||
enhanced-resolve: 5.18.1
|
enhanced-resolve: 5.18.1
|
||||||
eslint: 9.24.0(jiti@2.4.2)
|
eslint: 9.25.0(jiti@2.4.2)
|
||||||
get-tsconfig: 4.10.0
|
get-tsconfig: 4.10.0
|
||||||
is-bun-module: 1.3.0
|
is-bun-module: 1.3.0
|
||||||
stable-hash: 0.0.4
|
stable-hash: 0.0.4
|
||||||
tinyglobby: 0.2.11
|
tinyglobby: 0.2.11
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.24.1(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.8.1)(eslint@9.24.0(jiti@2.4.2))
|
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.24.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.8.1)(eslint@9.25.0(jiti@2.4.2))
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
eslint-module-utils@2.12.0(@typescript-eslint/parser@8.24.1(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.1)(eslint@9.24.0(jiti@2.4.2)):
|
eslint-module-utils@2.12.0(@typescript-eslint/parser@8.24.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.1)(eslint@9.25.0(jiti@2.4.2)):
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 3.2.7
|
debug: 3.2.7
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@typescript-eslint/parser': 8.24.1(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3)
|
'@typescript-eslint/parser': 8.24.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3)
|
||||||
eslint: 9.24.0(jiti@2.4.2)
|
eslint: 9.25.0(jiti@2.4.2)
|
||||||
eslint-import-resolver-node: 0.3.9
|
eslint-import-resolver-node: 0.3.9
|
||||||
eslint-import-resolver-typescript: 3.8.1(eslint-plugin-import@2.31.0)(eslint@9.24.0(jiti@2.4.2))
|
eslint-import-resolver-typescript: 3.8.1(eslint-plugin-import@2.31.0)(eslint@9.25.0(jiti@2.4.2))
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.24.1(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.8.1)(eslint@9.24.0(jiti@2.4.2)):
|
eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.24.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.8.1)(eslint@9.25.0(jiti@2.4.2)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@rtsao/scc': 1.1.0
|
'@rtsao/scc': 1.1.0
|
||||||
array-includes: 3.1.8
|
array-includes: 3.1.8
|
||||||
@ -7172,9 +7189,9 @@ snapshots:
|
|||||||
array.prototype.flatmap: 1.3.3
|
array.prototype.flatmap: 1.3.3
|
||||||
debug: 3.2.7
|
debug: 3.2.7
|
||||||
doctrine: 2.1.0
|
doctrine: 2.1.0
|
||||||
eslint: 9.24.0(jiti@2.4.2)
|
eslint: 9.25.0(jiti@2.4.2)
|
||||||
eslint-import-resolver-node: 0.3.9
|
eslint-import-resolver-node: 0.3.9
|
||||||
eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.24.1(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.1)(eslint@9.24.0(jiti@2.4.2))
|
eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.24.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.1)(eslint@9.25.0(jiti@2.4.2))
|
||||||
hasown: 2.0.2
|
hasown: 2.0.2
|
||||||
is-core-module: 2.16.1
|
is-core-module: 2.16.1
|
||||||
is-glob: 4.0.3
|
is-glob: 4.0.3
|
||||||
@ -7186,13 +7203,13 @@ snapshots:
|
|||||||
string.prototype.trimend: 1.0.9
|
string.prototype.trimend: 1.0.9
|
||||||
tsconfig-paths: 3.15.0
|
tsconfig-paths: 3.15.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@typescript-eslint/parser': 8.24.1(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3)
|
'@typescript-eslint/parser': 8.24.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- eslint-import-resolver-typescript
|
- eslint-import-resolver-typescript
|
||||||
- eslint-import-resolver-webpack
|
- eslint-import-resolver-webpack
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
eslint-plugin-jsx-a11y@6.10.2(eslint@9.24.0(jiti@2.4.2)):
|
eslint-plugin-jsx-a11y@6.10.2(eslint@9.25.0(jiti@2.4.2)):
|
||||||
dependencies:
|
dependencies:
|
||||||
aria-query: 5.3.2
|
aria-query: 5.3.2
|
||||||
array-includes: 3.1.8
|
array-includes: 3.1.8
|
||||||
@ -7202,7 +7219,7 @@ snapshots:
|
|||||||
axobject-query: 4.1.0
|
axobject-query: 4.1.0
|
||||||
damerau-levenshtein: 1.0.8
|
damerau-levenshtein: 1.0.8
|
||||||
emoji-regex: 9.2.2
|
emoji-regex: 9.2.2
|
||||||
eslint: 9.24.0(jiti@2.4.2)
|
eslint: 9.25.0(jiti@2.4.2)
|
||||||
hasown: 2.0.2
|
hasown: 2.0.2
|
||||||
jsx-ast-utils: 3.3.5
|
jsx-ast-utils: 3.3.5
|
||||||
language-tags: 1.0.9
|
language-tags: 1.0.9
|
||||||
@ -7211,11 +7228,11 @@ snapshots:
|
|||||||
safe-regex-test: 1.1.0
|
safe-regex-test: 1.1.0
|
||||||
string.prototype.includes: 2.0.1
|
string.prototype.includes: 2.0.1
|
||||||
|
|
||||||
eslint-plugin-react-hooks@5.2.0(eslint@9.24.0(jiti@2.4.2)):
|
eslint-plugin-react-hooks@5.2.0(eslint@9.25.0(jiti@2.4.2)):
|
||||||
dependencies:
|
dependencies:
|
||||||
eslint: 9.24.0(jiti@2.4.2)
|
eslint: 9.25.0(jiti@2.4.2)
|
||||||
|
|
||||||
eslint-plugin-react@7.37.4(eslint@9.24.0(jiti@2.4.2)):
|
eslint-plugin-react@7.37.4(eslint@9.25.0(jiti@2.4.2)):
|
||||||
dependencies:
|
dependencies:
|
||||||
array-includes: 3.1.8
|
array-includes: 3.1.8
|
||||||
array.prototype.findlast: 1.2.5
|
array.prototype.findlast: 1.2.5
|
||||||
@ -7223,7 +7240,7 @@ snapshots:
|
|||||||
array.prototype.tosorted: 1.1.4
|
array.prototype.tosorted: 1.1.4
|
||||||
doctrine: 2.1.0
|
doctrine: 2.1.0
|
||||||
es-iterator-helpers: 1.2.1
|
es-iterator-helpers: 1.2.1
|
||||||
eslint: 9.24.0(jiti@2.4.2)
|
eslint: 9.25.0(jiti@2.4.2)
|
||||||
estraverse: 5.3.0
|
estraverse: 5.3.0
|
||||||
hasown: 2.0.2
|
hasown: 2.0.2
|
||||||
jsx-ast-utils: 3.3.5
|
jsx-ast-utils: 3.3.5
|
||||||
@ -7246,16 +7263,16 @@ snapshots:
|
|||||||
|
|
||||||
eslint-visitor-keys@4.2.0: {}
|
eslint-visitor-keys@4.2.0: {}
|
||||||
|
|
||||||
eslint@9.24.0(jiti@2.4.2):
|
eslint@9.25.0(jiti@2.4.2):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint-community/eslint-utils': 4.4.1(eslint@9.24.0(jiti@2.4.2))
|
'@eslint-community/eslint-utils': 4.4.1(eslint@9.25.0(jiti@2.4.2))
|
||||||
'@eslint-community/regexpp': 4.12.1
|
'@eslint-community/regexpp': 4.12.1
|
||||||
'@eslint/config-array': 0.20.0
|
'@eslint/config-array': 0.20.0
|
||||||
'@eslint/config-helpers': 0.2.0
|
'@eslint/config-helpers': 0.2.1
|
||||||
'@eslint/core': 0.12.0
|
'@eslint/core': 0.13.0
|
||||||
'@eslint/eslintrc': 3.3.1
|
'@eslint/eslintrc': 3.3.1
|
||||||
'@eslint/js': 9.24.0
|
'@eslint/js': 9.25.0
|
||||||
'@eslint/plugin-kit': 0.2.7
|
'@eslint/plugin-kit': 0.2.8
|
||||||
'@humanfs/node': 0.16.6
|
'@humanfs/node': 0.16.6
|
||||||
'@humanwhocodes/module-importer': 1.0.1
|
'@humanwhocodes/module-importer': 1.0.1
|
||||||
'@humanwhocodes/retry': 0.4.2
|
'@humanwhocodes/retry': 0.4.2
|
||||||
|
|||||||
@ -30,6 +30,7 @@ import AdminLink from './AdminLink';
|
|||||||
import ScoreCardContainer from '@/components/ScoreCardContainer';
|
import ScoreCardContainer from '@/components/ScoreCardContainer';
|
||||||
import { capitalize, deparameterize } from '@/utility/string';
|
import { capitalize, deparameterize } from '@/utility/string';
|
||||||
import { DEFAULT_CATEGORY_KEYS, getHiddenCategories } from '@/category';
|
import { DEFAULT_CATEGORY_KEYS, getHiddenCategories } from '@/category';
|
||||||
|
import { AI_AUTO_GENERATED_FIELDS_ALL } from '@/photo/ai';
|
||||||
|
|
||||||
export default function AdminAppConfigurationClient({
|
export default function AdminAppConfigurationClient({
|
||||||
// Storage
|
// Storage
|
||||||
@ -365,6 +366,28 @@ export default function AdminAppConfigurationClient({
|
|||||||
and improve accessibility:
|
and improve accessibility:
|
||||||
{renderEnvVars(['OPENAI_SECRET_KEY'])}
|
{renderEnvVars(['OPENAI_SECRET_KEY'])}
|
||||||
</ChecklistRow>
|
</ChecklistRow>
|
||||||
|
<ChecklistRow
|
||||||
|
title={'Auto-generated fields'}
|
||||||
|
status={hasAiTextAutoGeneratedFields}
|
||||||
|
optional
|
||||||
|
>
|
||||||
|
{hasAiTextAutoGeneratedFields &&
|
||||||
|
AI_AUTO_GENERATED_FIELDS_ALL.map(field =>
|
||||||
|
<Fragment key={field}>
|
||||||
|
{renderSubStatus(
|
||||||
|
aiTextAutoGeneratedFields.includes(field)
|
||||||
|
? 'checked'
|
||||||
|
: 'optional',
|
||||||
|
capitalize(field),
|
||||||
|
)}
|
||||||
|
</Fragment>)}
|
||||||
|
Comma-separated fields to auto-generate when
|
||||||
|
uploading photos. Accepted values: title, caption,
|
||||||
|
tags, description, all, or none
|
||||||
|
{' '}
|
||||||
|
(default: {'"title,tags,semantic"'}):
|
||||||
|
{renderEnvVars(['AI_TEXT_AUTO_GENERATED_FIELDS'])}
|
||||||
|
</ChecklistRow>
|
||||||
<ChecklistRow
|
<ChecklistRow
|
||||||
title={hasRedisStorage && isAnalyzingConfiguration
|
title={hasRedisStorage && isAnalyzingConfiguration
|
||||||
? 'Testing Redis connection'
|
? 'Testing Redis connection'
|
||||||
@ -380,19 +403,6 @@ export default function AdminAppConfigurationClient({
|
|||||||
on Vercel dashboard and connect to this project
|
on Vercel dashboard and connect to this project
|
||||||
to enable rate limiting
|
to enable rate limiting
|
||||||
</ChecklistRow>
|
</ChecklistRow>
|
||||||
<ChecklistRow
|
|
||||||
// eslint-disable-next-line max-len
|
|
||||||
title={`Auto-generated fields: ${aiTextAutoGeneratedFields.join(',')}`}
|
|
||||||
status={hasAiTextAutoGeneratedFields}
|
|
||||||
optional
|
|
||||||
>
|
|
||||||
Comma-separated fields to auto-generate when
|
|
||||||
uploading photos. Accepted values: title, caption,
|
|
||||||
tags, description, all, or none
|
|
||||||
{' '}
|
|
||||||
(default: {'"title,tags,semantic"'}):
|
|
||||||
{renderEnvVars(['AI_TEXT_AUTO_GENERATED_FIELDS'])}
|
|
||||||
</ChecklistRow>
|
|
||||||
</ChecklistGroup>
|
</ChecklistGroup>
|
||||||
<ChecklistGroup
|
<ChecklistGroup
|
||||||
title="Performance"
|
title="Performance"
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import {
|
|||||||
PATH_ADMIN_CONFIGURATION,
|
PATH_ADMIN_CONFIGURATION,
|
||||||
PATH_ADMIN_INSIGHTS,
|
PATH_ADMIN_INSIGHTS,
|
||||||
PATH_ADMIN_PHOTOS,
|
PATH_ADMIN_PHOTOS,
|
||||||
|
PATH_ADMIN_PHOTOS_UPDATES,
|
||||||
PATH_ADMIN_RECIPES,
|
PATH_ADMIN_RECIPES,
|
||||||
PATH_ADMIN_TAGS,
|
PATH_ADMIN_TAGS,
|
||||||
PATH_ADMIN_UPLOADS,
|
PATH_ADMIN_UPLOADS,
|
||||||
@ -26,6 +27,8 @@ import IconSignOut from '@/components/icons/IconSignOut';
|
|||||||
import IconLock from '@/components/icons/IconLock';
|
import IconLock from '@/components/icons/IconLock';
|
||||||
import { IoMdCheckboxOutline } from 'react-icons/io';
|
import { IoMdCheckboxOutline } from 'react-icons/io';
|
||||||
import Spinner from '@/components/Spinner';
|
import Spinner from '@/components/Spinner';
|
||||||
|
import IconBroom from '@/components/icons/IconBroom';
|
||||||
|
import InsightsIndicatorDot from './insights/InsightsIndicatorDot';
|
||||||
|
|
||||||
export default function AdminAppMenu({
|
export default function AdminAppMenu({
|
||||||
active,
|
active,
|
||||||
@ -38,6 +41,7 @@ export default function AdminAppMenu({
|
|||||||
}) {
|
}) {
|
||||||
const {
|
const {
|
||||||
photosCountTotal = 0,
|
photosCountTotal = 0,
|
||||||
|
photosCountNeedSync = 0,
|
||||||
uploadsCount = 0,
|
uploadsCount = 0,
|
||||||
tagsCount = 0,
|
tagsCount = 0,
|
||||||
recipesCount = 0,
|
recipesCount = 0,
|
||||||
@ -71,11 +75,30 @@ export default function AdminAppMenu({
|
|||||||
annotation: `${uploadsCount}`,
|
annotation: `${uploadsCount}`,
|
||||||
icon: <IconFolder
|
icon: <IconFolder
|
||||||
size={16}
|
size={16}
|
||||||
className="translate-y-[0.5px]"
|
className="translate-x-[1px] translate-y-[0.5px]"
|
||||||
/>,
|
/>,
|
||||||
href: PATH_ADMIN_UPLOADS,
|
href: PATH_ADMIN_UPLOADS,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (photosCountNeedSync) {
|
||||||
|
items.push({
|
||||||
|
label: 'Updates',
|
||||||
|
annotation: <>
|
||||||
|
<span className="mr-3">
|
||||||
|
{photosCountNeedSync}
|
||||||
|
</span>
|
||||||
|
<InsightsIndicatorDot
|
||||||
|
className="inline-block translate-y-[-0.5px]"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</>,
|
||||||
|
icon: <IconBroom
|
||||||
|
size={17}
|
||||||
|
className="translate-y-[0.5px]"
|
||||||
|
/>,
|
||||||
|
href: PATH_ADMIN_PHOTOS_UPDATES,
|
||||||
|
});
|
||||||
|
}
|
||||||
if (photosCountTotal) {
|
if (photosCountTotal) {
|
||||||
items.push({
|
items.push({
|
||||||
label: 'Manage Photos',
|
label: 'Manage Photos',
|
||||||
@ -121,7 +144,10 @@ export default function AdminAppMenu({
|
|||||||
size={18}
|
size={18}
|
||||||
className="translate-x-[-1px] translate-y-[0.5px]"
|
className="translate-x-[-1px] translate-y-[0.5px]"
|
||||||
/>
|
/>
|
||||||
: <IoMdCheckboxOutline size={17} className="translate-x-[-0.5px]" />,
|
: <IoMdCheckboxOutline
|
||||||
|
size={16}
|
||||||
|
className="translate-x-[-0.5px]"
|
||||||
|
/>,
|
||||||
href: PATH_GRID_INFERRED,
|
href: PATH_GRID_INFERRED,
|
||||||
action: () => {
|
action: () => {
|
||||||
if (isSelecting) {
|
if (isSelecting) {
|
||||||
|
|||||||
@ -1,109 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { Photo } from '@/photo';
|
|
||||||
import AdminPhotosTable from '@/admin/AdminPhotosTable';
|
|
||||||
import LoaderButton from '@/components/primitives/LoaderButton';
|
|
||||||
import IconGrSync from '@/components/icons/IconGrSync';
|
|
||||||
import Note from '@/components/Note';
|
|
||||||
import AdminChildPage from '@/components/AdminChildPage';
|
|
||||||
import { PATH_ADMIN_PHOTOS } from '@/app/paths';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { syncPhotosAction } from '@/photo/actions';
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
import ResponsiveText from '@/components/primitives/ResponsiveText';
|
|
||||||
import { LiaBroomSolid } from 'react-icons/lia';
|
|
||||||
|
|
||||||
const UPDATE_BATCH_SIZE_MAX = 4;
|
|
||||||
|
|
||||||
export default function AdminOutdatedClient({
|
|
||||||
photos,
|
|
||||||
hasAiTextGeneration,
|
|
||||||
}: {
|
|
||||||
photos: Photo[]
|
|
||||||
hasAiTextGeneration: boolean
|
|
||||||
}) {
|
|
||||||
const updateBatchSize = Math.min(UPDATE_BATCH_SIZE_MAX, photos.length);
|
|
||||||
|
|
||||||
const [photoIdsSyncing, setPhotoIdsSyncing] = useState<string[]>([]);
|
|
||||||
|
|
||||||
const arePhotoIdsSyncing = photoIdsSyncing.length > 0;
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AdminChildPage
|
|
||||||
backLabel="Photos"
|
|
||||||
backPath={PATH_ADMIN_PHOTOS}
|
|
||||||
breadcrumb={<>
|
|
||||||
<span className="hidden sm:inline-block">
|
|
||||||
Outdated ({photos.length})
|
|
||||||
</span>
|
|
||||||
<span className="sm:hidden">
|
|
||||||
Outdated
|
|
||||||
</span>
|
|
||||||
</>}
|
|
||||||
accessory={<LoaderButton
|
|
||||||
primary
|
|
||||||
icon={<IconGrSync className="translate-y-[1px]" />}
|
|
||||||
hideTextOnMobile={false}
|
|
||||||
onClick={async () => {
|
|
||||||
if (window.confirm(
|
|
||||||
// eslint-disable-next-line max-len
|
|
||||||
`Are you sure you want to sync the oldest ${updateBatchSize} photos? This action cannot be undone.`,
|
|
||||||
)) {
|
|
||||||
const photosToSync = photos
|
|
||||||
.slice(0, updateBatchSize)
|
|
||||||
.map(photo => photo.id);
|
|
||||||
const isFinalBatch = photosToSync.length >= photos.length;
|
|
||||||
setPhotoIdsSyncing(photosToSync);
|
|
||||||
syncPhotosAction(photosToSync)
|
|
||||||
.finally(() => {
|
|
||||||
if (isFinalBatch) {
|
|
||||||
router.push(PATH_ADMIN_PHOTOS);
|
|
||||||
} else {
|
|
||||||
setPhotoIdsSyncing([]);
|
|
||||||
router.refresh();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
isLoading={arePhotoIdsSyncing}
|
|
||||||
disabled={!updateBatchSize}
|
|
||||||
>
|
|
||||||
{arePhotoIdsSyncing
|
|
||||||
? 'Syncing'
|
|
||||||
: <ResponsiveText shortText={`Sync Next ${updateBatchSize}`}>
|
|
||||||
Sync Next {updateBatchSize} Photos
|
|
||||||
</ResponsiveText>}
|
|
||||||
</LoaderButton>}
|
|
||||||
>
|
|
||||||
<div className="space-y-6">
|
|
||||||
<Note
|
|
||||||
color="blue"
|
|
||||||
icon={<LiaBroomSolid size={18}/>}
|
|
||||||
>
|
|
||||||
<div className="space-y-1.5">
|
|
||||||
<div className="font-bold">
|
|
||||||
{photos.length} outdated
|
|
||||||
{' '}
|
|
||||||
{photos.length === 1 ? 'photo' : 'photos'} found
|
|
||||||
</div>
|
|
||||||
Sync photos to import newer EXIF fields, improve blur data,
|
|
||||||
{' '}
|
|
||||||
and leverage AI-generated text where possible
|
|
||||||
</div>
|
|
||||||
</Note>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<AdminPhotosTable
|
|
||||||
photos={photos}
|
|
||||||
photoIdsSyncing={photoIdsSyncing}
|
|
||||||
hasAiTextGeneration={hasAiTextGeneration}
|
|
||||||
canEdit={false}
|
|
||||||
canDelete={false}
|
|
||||||
showUpdatedAt
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</AdminChildPage>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -21,10 +21,10 @@ import { RevalidatePhoto } from '@/photo/InfinitePhotoScroll';
|
|||||||
import { MdOutlineFileDownload } from 'react-icons/md';
|
import { MdOutlineFileDownload } from 'react-icons/md';
|
||||||
import MoreMenuItem from '@/components/more/MoreMenuItem';
|
import MoreMenuItem from '@/components/more/MoreMenuItem';
|
||||||
import IconGrSync from '@/components/icons/IconGrSync';
|
import IconGrSync from '@/components/icons/IconGrSync';
|
||||||
import { isPhotoOutdated } from '@/photo/outdated';
|
|
||||||
import InsightsIndicatorDot from './insights/InsightsIndicatorDot';
|
import InsightsIndicatorDot from './insights/InsightsIndicatorDot';
|
||||||
import IconFavs from '@/components/icons/IconFavs';
|
import IconFavs from '@/components/icons/IconFavs';
|
||||||
import IconEdit from '@/components/icons/IconEdit';
|
import IconEdit from '@/components/icons/IconEdit';
|
||||||
|
import { photoNeedsToBeSynced } from '@/photo/sync';
|
||||||
|
|
||||||
export default function AdminPhotoMenu({
|
export default function AdminPhotoMenu({
|
||||||
photo,
|
photo,
|
||||||
@ -79,10 +79,11 @@ export default function AdminPhotoMenu({
|
|||||||
label: 'Sync',
|
label: 'Sync',
|
||||||
labelComplex: <span className="inline-flex items-center gap-2">
|
labelComplex: <span className="inline-flex items-center gap-2">
|
||||||
<span>Sync</span>
|
<span>Sync</span>
|
||||||
{isPhotoOutdated(photo) &&
|
{photoNeedsToBeSynced(photo) &&
|
||||||
<InsightsIndicatorDot
|
<InsightsIndicatorDot
|
||||||
colorOverride="blue"
|
colorOverride="blue"
|
||||||
className="translate-y-[1.5px]"
|
className="ml-1 translate-y-[1.5px]"
|
||||||
|
size="small"
|
||||||
/>}
|
/>}
|
||||||
</span>,
|
</span>,
|
||||||
icon: <IconGrSync className="translate-x-[-1px]" />,
|
icon: <IconGrSync className="translate-x-[-1px]" />,
|
||||||
|
|||||||
@ -5,19 +5,21 @@ import AppGrid from '@/components/AppGrid';
|
|||||||
import AdminPhotosTable from '@/admin/AdminPhotosTable';
|
import AdminPhotosTable from '@/admin/AdminPhotosTable';
|
||||||
import AdminPhotosTableInfinite from '@/admin/AdminPhotosTableInfinite';
|
import AdminPhotosTableInfinite from '@/admin/AdminPhotosTableInfinite';
|
||||||
import PathLoaderButton from '@/components/primitives/PathLoaderButton';
|
import PathLoaderButton from '@/components/primitives/PathLoaderButton';
|
||||||
import { PATH_ADMIN_OUTDATED } from '@/app/paths';
|
import { PATH_ADMIN_PHOTOS_UPDATES } from '@/app/paths';
|
||||||
import { Photo } from '@/photo';
|
import { Photo } from '@/photo';
|
||||||
import { StorageListResponse } from '@/platforms/storage';
|
import { StorageListResponse } from '@/platforms/storage';
|
||||||
import { LiaBroomSolid } from 'react-icons/lia';
|
|
||||||
import AdminUploadsTable from './AdminUploadsTable';
|
import AdminUploadsTable from './AdminUploadsTable';
|
||||||
import { Timezone } from '@/utility/timezone';
|
import { Timezone } from '@/utility/timezone';
|
||||||
import { useAppState } from '@/state/AppState';
|
import { useAppState } from '@/state/AppState';
|
||||||
import PhotoUploadWithStatus from '@/photo/PhotoUploadWithStatus';
|
import PhotoUploadWithStatus from '@/photo/PhotoUploadWithStatus';
|
||||||
|
import { pluralize } from '@/utility/string';
|
||||||
|
import IconBroom from '@/components/icons/IconBroom';
|
||||||
|
import ResponsiveText from '@/components/primitives/ResponsiveText';
|
||||||
|
|
||||||
export default function AdminPhotosClient({
|
export default function AdminPhotosClient({
|
||||||
photos,
|
photos,
|
||||||
photosCount,
|
photosCount,
|
||||||
photosCountOutdated,
|
photosCountNeedsSync,
|
||||||
blobPhotoUrls,
|
blobPhotoUrls,
|
||||||
shouldResize,
|
shouldResize,
|
||||||
hasAiTextGeneration,
|
hasAiTextGeneration,
|
||||||
@ -28,7 +30,7 @@ export default function AdminPhotosClient({
|
|||||||
}: {
|
}: {
|
||||||
photos: Photo[]
|
photos: Photo[]
|
||||||
photosCount: number
|
photosCount: number
|
||||||
photosCountOutdated: number
|
photosCountNeedsSync: number
|
||||||
blobPhotoUrls: StorageListResponse
|
blobPhotoUrls: StorageListResponse
|
||||||
shouldResize: boolean
|
shouldResize: boolean
|
||||||
hasAiTextGeneration: boolean
|
hasAiTextGeneration: boolean
|
||||||
@ -51,14 +53,17 @@ export default function AdminPhotosClient({
|
|||||||
onLastUpload={onLastUpload}
|
onLastUpload={onLastUpload}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{photosCountOutdated > 0 &&
|
{photosCountNeedsSync > 0 &&
|
||||||
<PathLoaderButton
|
<PathLoaderButton
|
||||||
path={PATH_ADMIN_OUTDATED}
|
path={PATH_ADMIN_PHOTOS_UPDATES}
|
||||||
icon={<LiaBroomSolid
|
icon={<IconBroom
|
||||||
size={18}
|
size={18}
|
||||||
className="translate-y-[-1px]"
|
className="translate-x-[-1px]"
|
||||||
/>}
|
/>}
|
||||||
title={`${photosCountOutdated} Outdated Photos`}
|
tooltip={(
|
||||||
|
pluralize(photosCountNeedsSync, 'photo') +
|
||||||
|
' missing data or AI-generated text'
|
||||||
|
)}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'text-blue-600 dark:text-blue-400',
|
'text-blue-600 dark:text-blue-400',
|
||||||
'border border-blue-200 dark:border-blue-800/60',
|
'border border-blue-200 dark:border-blue-800/60',
|
||||||
@ -70,7 +75,9 @@ export default function AdminPhotosClient({
|
|||||||
spinnerClassName="text-blue-200 dark:text-blue-600/40"
|
spinnerClassName="text-blue-200 dark:text-blue-600/40"
|
||||||
hideTextOnMobile={false}
|
hideTextOnMobile={false}
|
||||||
>
|
>
|
||||||
{photosCountOutdated}
|
<ResponsiveText shortText={photosCountNeedsSync}>
|
||||||
|
{pluralize(photosCountNeedsSync, 'Update')}
|
||||||
|
</ResponsiveText>
|
||||||
</PathLoaderButton>}
|
</PathLoaderButton>}
|
||||||
</div>
|
</div>
|
||||||
{blobPhotoUrls.length > 0 &&
|
{blobPhotoUrls.length > 0 &&
|
||||||
|
|||||||
140
src/admin/AdminPhotosSyncClient.tsx
Normal file
140
src/admin/AdminPhotosSyncClient.tsx
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Photo } from '@/photo';
|
||||||
|
import AdminPhotosTable from '@/admin/AdminPhotosTable';
|
||||||
|
import IconGrSync from '@/components/icons/IconGrSync';
|
||||||
|
import Note from '@/components/Note';
|
||||||
|
import AdminChildPage from '@/components/AdminChildPage';
|
||||||
|
import { PATH_ADMIN_PHOTOS } from '@/app/paths';
|
||||||
|
import { useMemo, useRef, useState } from 'react';
|
||||||
|
import { syncPhotosAction } from '@/photo/actions';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import ResponsiveText from '@/components/primitives/ResponsiveText';
|
||||||
|
import { LiaBroomSolid } from 'react-icons/lia';
|
||||||
|
import ProgressButton from '@/components/primitives/ProgressButton';
|
||||||
|
import ErrorNote from '@/components/ErrorNote';
|
||||||
|
import { pluralize } from '@/utility/string';
|
||||||
|
import { getPhotosSyncStatusText } from '@/photo/sync';
|
||||||
|
|
||||||
|
const SYNC_BATCH_SIZE_MAX = 3;
|
||||||
|
|
||||||
|
export default function AdminPhotosSyncClient({
|
||||||
|
photos,
|
||||||
|
hasAiTextGeneration,
|
||||||
|
}: {
|
||||||
|
photos: Photo[]
|
||||||
|
hasAiTextGeneration: boolean
|
||||||
|
}) {
|
||||||
|
// Use refs for non-reactive while loop state
|
||||||
|
const photoIdsToSync = useRef(photos.map(photo => photo.id));
|
||||||
|
const errorRef = useRef<Error>(undefined);
|
||||||
|
|
||||||
|
// Use state for updating progress button and error UI
|
||||||
|
const [photoIdsSyncing, setPhotoIdsSyncing] = useState<string[]>([]);
|
||||||
|
const [error, setError] = useState<Error>();
|
||||||
|
const [progress, setProgress] = useState(0);
|
||||||
|
|
||||||
|
const arePhotoIdsSyncing = photoIdsSyncing.length > 0;
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const statusText = useMemo(() => getPhotosSyncStatusText(photos), [photos]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AdminChildPage
|
||||||
|
backLabel="Photos"
|
||||||
|
backPath={PATH_ADMIN_PHOTOS}
|
||||||
|
breadcrumb={<ResponsiveText shortText="Updates">
|
||||||
|
Updates ({photos.length})
|
||||||
|
</ResponsiveText>}
|
||||||
|
accessory={<ProgressButton
|
||||||
|
primary
|
||||||
|
icon={<IconGrSync className="translate-y-[1px]" />}
|
||||||
|
hideTextOnMobile={false}
|
||||||
|
progress={progress}
|
||||||
|
tooltip={`Sync data for all ${pluralize(photos.length, 'photo')}`}
|
||||||
|
onClick={async () => {
|
||||||
|
if (window.confirm([
|
||||||
|
'Are you sure you want to sync',
|
||||||
|
photos.length === 1
|
||||||
|
? '1 photo?'
|
||||||
|
: `all ${photos.length} photos?`,
|
||||||
|
'Browser must remain open while syncing.',
|
||||||
|
'This action cannot be undone.',
|
||||||
|
].join(' '))) {
|
||||||
|
errorRef.current = undefined;
|
||||||
|
setError(undefined);
|
||||||
|
while (photoIdsToSync.current.length > 0) {
|
||||||
|
const photoIds = photoIdsToSync.current
|
||||||
|
.slice(0, SYNC_BATCH_SIZE_MAX);
|
||||||
|
setPhotoIdsSyncing(photoIds);
|
||||||
|
await syncPhotosAction(photoIds)
|
||||||
|
.then(() => {
|
||||||
|
photoIdsToSync.current = photoIdsToSync.current.filter(
|
||||||
|
id => !photoIds.includes(id),
|
||||||
|
);
|
||||||
|
setProgress(
|
||||||
|
(photos.length - photoIdsToSync.current.length) /
|
||||||
|
photos.length,
|
||||||
|
);
|
||||||
|
router.refresh();
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
errorRef.current = e;
|
||||||
|
setError(e);
|
||||||
|
});
|
||||||
|
if (errorRef.current) { break; }
|
||||||
|
}
|
||||||
|
if (!errorRef.current) {
|
||||||
|
router.push(PATH_ADMIN_PHOTOS);
|
||||||
|
} else {
|
||||||
|
setProgress(0);
|
||||||
|
setPhotoIdsSyncing([]);
|
||||||
|
router.refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
isLoading={arePhotoIdsSyncing}
|
||||||
|
disabled={photoIdsSyncing.length > 0}
|
||||||
|
>
|
||||||
|
{arePhotoIdsSyncing
|
||||||
|
? 'Syncing ...'
|
||||||
|
: 'Sync All'}
|
||||||
|
</ProgressButton>}
|
||||||
|
>
|
||||||
|
<div className="space-y-6">
|
||||||
|
{error && <ErrorNote>
|
||||||
|
<span className="font-bold">
|
||||||
|
Issue syncing:
|
||||||
|
</span>
|
||||||
|
{' '}
|
||||||
|
{error.message}
|
||||||
|
</ErrorNote>}
|
||||||
|
<Note
|
||||||
|
color="blue"
|
||||||
|
icon={<LiaBroomSolid size={18}/>}
|
||||||
|
>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<div className="font-bold">
|
||||||
|
Photo updates: {statusText}
|
||||||
|
</div>
|
||||||
|
Sync to capture new EXIF fields, improve blur data,
|
||||||
|
{' '}
|
||||||
|
use AI to generate missing text (if configured)
|
||||||
|
</div>
|
||||||
|
</Note>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<AdminPhotosTable
|
||||||
|
photos={photos}
|
||||||
|
photoIdsSyncing={photoIdsSyncing}
|
||||||
|
hasAiTextGeneration={hasAiTextGeneration}
|
||||||
|
canEdit={false}
|
||||||
|
canDelete={false}
|
||||||
|
dateType="updatedAt"
|
||||||
|
shouldScrollIntoViewOnExternalSync
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AdminChildPage>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -15,6 +15,8 @@ import PhotoSyncButton from './PhotoSyncButton';
|
|||||||
import DeletePhotoButton from './DeletePhotoButton';
|
import DeletePhotoButton from './DeletePhotoButton';
|
||||||
import { Timezone } from '@/utility/timezone';
|
import { Timezone } from '@/utility/timezone';
|
||||||
import IconHidden from '@/components/icons/IconHidden';
|
import IconHidden from '@/components/icons/IconHidden';
|
||||||
|
import Tooltip from '@/components/Tooltip';
|
||||||
|
import { photoNeedsToBeSynced, getPhotoSyncStatusText } from '@/photo/sync';
|
||||||
|
|
||||||
export default function AdminPhotosTable({
|
export default function AdminPhotosTable({
|
||||||
photos,
|
photos,
|
||||||
@ -22,20 +24,22 @@ export default function AdminPhotosTable({
|
|||||||
revalidatePhoto,
|
revalidatePhoto,
|
||||||
photoIdsSyncing = [],
|
photoIdsSyncing = [],
|
||||||
hasAiTextGeneration,
|
hasAiTextGeneration,
|
||||||
showUpdatedAt,
|
dateType = 'createdAt',
|
||||||
canEdit = true,
|
canEdit = true,
|
||||||
canDelete = true,
|
canDelete = true,
|
||||||
timezone,
|
timezone,
|
||||||
|
shouldScrollIntoViewOnExternalSync,
|
||||||
}: {
|
}: {
|
||||||
photos: Photo[],
|
photos: Photo[],
|
||||||
onLastPhotoVisible?: () => void
|
onLastPhotoVisible?: () => void
|
||||||
revalidatePhoto?: RevalidatePhoto
|
revalidatePhoto?: RevalidatePhoto
|
||||||
photoIdsSyncing?: string[]
|
photoIdsSyncing?: string[]
|
||||||
hasAiTextGeneration: boolean
|
hasAiTextGeneration: boolean
|
||||||
showUpdatedAt?: boolean
|
dateType?: 'createdAt' | 'updatedAt'
|
||||||
canEdit?: boolean
|
canEdit?: boolean
|
||||||
canDelete?: boolean
|
canDelete?: boolean
|
||||||
timezone?: Timezone
|
timezone?: Timezone
|
||||||
|
shouldScrollIntoViewOnExternalSync?: boolean
|
||||||
}) {
|
}) {
|
||||||
const { invalidateSwr } = useAppState();
|
const { invalidateSwr } = useAppState();
|
||||||
|
|
||||||
@ -68,7 +72,7 @@ export default function AdminPhotosTable({
|
|||||||
<span className={clsx(
|
<span className={clsx(
|
||||||
photo.hidden && 'text-dim',
|
photo.hidden && 'text-dim',
|
||||||
)}>
|
)}>
|
||||||
{titleForPhoto(photo)}
|
{titleForPhoto(photo, false)}
|
||||||
{photo.hidden && <span className="whitespace-nowrap">
|
{photo.hidden && <span className="whitespace-nowrap">
|
||||||
{' '}
|
{' '}
|
||||||
<IconHidden
|
<IconHidden
|
||||||
@ -90,11 +94,18 @@ export default function AdminPhotosTable({
|
|||||||
'lg:w-[50%] uppercase',
|
'lg:w-[50%] uppercase',
|
||||||
'text-dim',
|
'text-dim',
|
||||||
)}>
|
)}>
|
||||||
<PhotoDate {...{
|
{<>
|
||||||
photo,
|
<PhotoDate {...{ photo, dateType, timezone }} />
|
||||||
dateType: showUpdatedAt ? 'updatedAt' : 'createdAt',
|
{photoNeedsToBeSynced(photo) &&
|
||||||
timezone,
|
<Tooltip
|
||||||
}} />
|
content={getPhotoSyncStatusText(photo)}
|
||||||
|
classNameTrigger={clsx(
|
||||||
|
'translate-y-1 ml-1.5',
|
||||||
|
'text-blue-600 dark:text-blue-400',
|
||||||
|
)}
|
||||||
|
supportMobile
|
||||||
|
/>}
|
||||||
|
</>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={clsx(
|
<div className={clsx(
|
||||||
@ -113,6 +124,8 @@ export default function AdminPhotosTable({
|
|||||||
className={opacityForPhotoId(photo.id)}
|
className={opacityForPhotoId(photo.id)}
|
||||||
shouldConfirm
|
shouldConfirm
|
||||||
shouldToast
|
shouldToast
|
||||||
|
shouldScrollIntoViewOnExternalSync={
|
||||||
|
shouldScrollIntoViewOnExternalSync}
|
||||||
/>
|
/>
|
||||||
{canDelete &&
|
{canDelete &&
|
||||||
<DeletePhotoButton
|
<DeletePhotoButton
|
||||||
|
|||||||
@ -2,8 +2,10 @@ import LoaderButton from '@/components/primitives/LoaderButton';
|
|||||||
import { syncPhotoAction } from '@/photo/actions';
|
import { syncPhotoAction } from '@/photo/actions';
|
||||||
import IconGrSync from '@/components/icons/IconGrSync';
|
import IconGrSync from '@/components/icons/IconGrSync';
|
||||||
import { toastSuccess } from '@/toast';
|
import { toastSuccess } from '@/toast';
|
||||||
import { ComponentProps, useState } from 'react';
|
import { ComponentProps, useRef, useState } from 'react';
|
||||||
import Tooltip from '@/components/Tooltip';
|
import Tooltip from '@/components/Tooltip';
|
||||||
|
import clsx from 'clsx/lite';
|
||||||
|
import useScrollIntoView from '@/utility/useScrollIntoView';
|
||||||
|
|
||||||
export default function PhotoSyncButton({
|
export default function PhotoSyncButton({
|
||||||
photoId,
|
photoId,
|
||||||
@ -15,6 +17,7 @@ export default function PhotoSyncButton({
|
|||||||
disabled,
|
disabled,
|
||||||
shouldConfirm,
|
shouldConfirm,
|
||||||
shouldToast,
|
shouldToast,
|
||||||
|
shouldScrollIntoViewOnExternalSync,
|
||||||
}: {
|
}: {
|
||||||
photoId: string
|
photoId: string
|
||||||
photoTitle?: string
|
photoTitle?: string
|
||||||
@ -23,7 +26,10 @@ export default function PhotoSyncButton({
|
|||||||
hasAiTextGeneration?: boolean
|
hasAiTextGeneration?: boolean
|
||||||
shouldConfirm?: boolean
|
shouldConfirm?: boolean
|
||||||
shouldToast?: boolean
|
shouldToast?: boolean
|
||||||
|
shouldScrollIntoViewOnExternalSync?: boolean
|
||||||
} & ComponentProps<typeof LoaderButton>) {
|
} & ComponentProps<typeof LoaderButton>) {
|
||||||
|
const ref = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
const [isSyncing, setIsSyncing] = useState(false);
|
const [isSyncing, setIsSyncing] = useState(false);
|
||||||
|
|
||||||
const confirmText = ['Overwrite'];
|
const confirmText = ['Overwrite'];
|
||||||
@ -33,10 +39,18 @@ export default function PhotoSyncButton({
|
|||||||
'AI text will be generated for undefined fields.'); }
|
'AI text will be generated for undefined fields.'); }
|
||||||
confirmText.push('This action cannot be undone.');
|
confirmText.push('This action cannot be undone.');
|
||||||
|
|
||||||
|
useScrollIntoView({
|
||||||
|
ref,
|
||||||
|
shouldScrollIntoView:
|
||||||
|
isSyncingExternal &&
|
||||||
|
shouldScrollIntoViewOnExternalSync,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip content="Regenerate photo data">
|
<Tooltip content="Regenerate photo data">
|
||||||
<LoaderButton
|
<LoaderButton
|
||||||
className={className}
|
ref={ref}
|
||||||
|
className={clsx('scroll-mt-8', className)}
|
||||||
icon={<IconGrSync
|
icon={<IconGrSync
|
||||||
className="translate-y-[0.5px] translate-x-[0.5px]"
|
className="translate-y-[0.5px] translate-x-[0.5px]"
|
||||||
/>}
|
/>}
|
||||||
|
|||||||
@ -7,12 +7,16 @@ import { testDatabaseConnection } from '@/platforms/postgres';
|
|||||||
import { testStorageConnection } from '@/platforms/storage';
|
import { testStorageConnection } from '@/platforms/storage';
|
||||||
import { APP_CONFIGURATION } from '@/app/config';
|
import { APP_CONFIGURATION } from '@/app/config';
|
||||||
import { getStorageUploadUrlsNoStore } from '@/platforms/storage/cache';
|
import { getStorageUploadUrlsNoStore } from '@/platforms/storage/cache';
|
||||||
import { getInsightsIndicatorStatus } from '@/admin/insights/server';
|
|
||||||
import {
|
import {
|
||||||
getPhotosMeta,
|
getPhotosMeta,
|
||||||
getUniqueTags,
|
getUniqueTags,
|
||||||
getUniqueRecipes,
|
getUniqueRecipes,
|
||||||
|
getPhotosInNeedOfSyncCount,
|
||||||
} from '@/photo/db/query';
|
} from '@/photo/db/query';
|
||||||
|
import {
|
||||||
|
getGitHubMetaForCurrentApp,
|
||||||
|
indicatorStatusForSignificantInsights,
|
||||||
|
} from './insights';
|
||||||
|
|
||||||
export type AdminData = Awaited<ReturnType<typeof getAdminDataAction>>;
|
export type AdminData = Awaited<ReturnType<typeof getAdminDataAction>>;
|
||||||
|
|
||||||
@ -21,10 +25,11 @@ export const getAdminDataAction = async () =>
|
|||||||
const [
|
const [
|
||||||
photosCount,
|
photosCount,
|
||||||
photosCountHidden,
|
photosCountHidden,
|
||||||
|
photosCountNeedSync,
|
||||||
|
codeMeta,
|
||||||
uploadsCount,
|
uploadsCount,
|
||||||
tagsCount,
|
tagsCount,
|
||||||
recipesCount,
|
recipesCount,
|
||||||
insightsIndicatorStatus,
|
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
getPhotosMeta()
|
getPhotosMeta()
|
||||||
.then(({ count }) => count)
|
.then(({ count }) => count)
|
||||||
@ -32,6 +37,8 @@ export const getAdminDataAction = async () =>
|
|||||||
getPhotosMeta({ hidden: 'only' })
|
getPhotosMeta({ hidden: 'only' })
|
||||||
.then(({ count }) => count)
|
.then(({ count }) => count)
|
||||||
.catch(() => 0),
|
.catch(() => 0),
|
||||||
|
getPhotosInNeedOfSyncCount(),
|
||||||
|
getGitHubMetaForCurrentApp(),
|
||||||
getStorageUploadUrlsNoStore()
|
getStorageUploadUrlsNoStore()
|
||||||
.then(urls => urls.length)
|
.then(urls => urls.length)
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
@ -44,9 +51,13 @@ export const getAdminDataAction = async () =>
|
|||||||
getUniqueRecipes()
|
getUniqueRecipes()
|
||||||
.then(recipes => recipes.length)
|
.then(recipes => recipes.length)
|
||||||
.catch(() => 0),
|
.catch(() => 0),
|
||||||
getInsightsIndicatorStatus(),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const insightsIndicatorStatus = indicatorStatusForSignificantInsights({
|
||||||
|
codeMeta,
|
||||||
|
photosCountNeedSync,
|
||||||
|
});
|
||||||
|
|
||||||
const photosCountTotal = (
|
const photosCountTotal = (
|
||||||
photosCount !== undefined &&
|
photosCount !== undefined &&
|
||||||
photosCountHidden !== undefined
|
photosCountHidden !== undefined
|
||||||
@ -57,12 +68,13 @@ export const getAdminDataAction = async () =>
|
|||||||
return {
|
return {
|
||||||
photosCount,
|
photosCount,
|
||||||
photosCountHidden,
|
photosCountHidden,
|
||||||
|
photosCountNeedSync,
|
||||||
photosCountTotal,
|
photosCountTotal,
|
||||||
uploadsCount,
|
uploadsCount,
|
||||||
tagsCount,
|
tagsCount,
|
||||||
recipesCount,
|
recipesCount,
|
||||||
insightsIndicatorStatus,
|
insightsIndicatorStatus,
|
||||||
};
|
} as const;
|
||||||
});
|
});
|
||||||
|
|
||||||
const scanForError = (
|
const scanForError = (
|
||||||
|
|||||||
@ -9,13 +9,13 @@ import {
|
|||||||
} from '@/photo/db/query';
|
} from '@/photo/db/query';
|
||||||
import AdminAppInsightsClient from './AdminAppInsightsClient';
|
import AdminAppInsightsClient from './AdminAppInsightsClient';
|
||||||
import { getAllInsights, getGitHubMetaForCurrentApp } from '.';
|
import { getAllInsights, getGitHubMetaForCurrentApp } from '.';
|
||||||
import { getOutdatedPhotosCount } from '@/photo/db/query';
|
import { getPhotosInNeedOfSyncCount } from '@/photo/db/query';
|
||||||
|
|
||||||
export default async function AdminAppInsights() {
|
export default async function AdminAppInsights() {
|
||||||
const [
|
const [
|
||||||
{ count: photosCount, dateRange },
|
{ count: photosCount, dateRange },
|
||||||
{ count: photosCountHidden },
|
{ count: photosCountHidden },
|
||||||
photosCountOutdated,
|
photosCountNeedSync,
|
||||||
{ count: photosCountPortrait },
|
{ count: photosCountPortrait },
|
||||||
codeMeta,
|
codeMeta,
|
||||||
cameras,
|
cameras,
|
||||||
@ -27,7 +27,7 @@ export default async function AdminAppInsights() {
|
|||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
getPhotosMeta({ hidden: 'include' }),
|
getPhotosMeta({ hidden: 'include' }),
|
||||||
getPhotosMeta({ hidden: 'only' }),
|
getPhotosMeta({ hidden: 'only' }),
|
||||||
getOutdatedPhotosCount(),
|
getPhotosInNeedOfSyncCount(),
|
||||||
getPhotosMeta({ maximumAspectRatio: 0.9 }),
|
getPhotosMeta({ maximumAspectRatio: 0.9 }),
|
||||||
getGitHubMetaForCurrentApp(),
|
getGitHubMetaForCurrentApp(),
|
||||||
getUniqueCameras(),
|
getUniqueCameras(),
|
||||||
@ -44,14 +44,14 @@ export default async function AdminAppInsights() {
|
|||||||
insights={getAllInsights({
|
insights={getAllInsights({
|
||||||
codeMeta,
|
codeMeta,
|
||||||
photosCount,
|
photosCount,
|
||||||
photosCountOutdated,
|
photosCountNeedSync,
|
||||||
photosCountPortrait,
|
photosCountPortrait,
|
||||||
tagsCount: tags.length,
|
tagsCount: tags.length,
|
||||||
})}
|
})}
|
||||||
photoStats={{
|
photoStats={{
|
||||||
photosCount,
|
photosCount,
|
||||||
photosCountHidden,
|
photosCountHidden,
|
||||||
photosCountOutdated,
|
photosCountNeedSync,
|
||||||
camerasCount: cameras.length,
|
camerasCount: cameras.length,
|
||||||
lensesCount: lenses.length,
|
lensesCount: lenses.length,
|
||||||
tagsCount: tags.length,
|
tagsCount: tags.length,
|
||||||
|
|||||||
@ -28,7 +28,7 @@ import {
|
|||||||
import EnvVar from '@/components/EnvVar';
|
import EnvVar from '@/components/EnvVar';
|
||||||
import { IoSyncCircle } from 'react-icons/io5';
|
import { IoSyncCircle } from 'react-icons/io5';
|
||||||
import clsx from 'clsx/lite';
|
import clsx from 'clsx/lite';
|
||||||
import { PATH_ADMIN_OUTDATED } from '@/app/paths';
|
import { PATH_ADMIN_PHOTOS_UPDATES } from '@/app/paths';
|
||||||
import { LiaBroomSolid } from 'react-icons/lia';
|
import { LiaBroomSolid } from 'react-icons/lia';
|
||||||
import { IoMdGrid } from 'react-icons/io';
|
import { IoMdGrid } from 'react-icons/io';
|
||||||
import { RiSpeedMiniLine } from 'react-icons/ri';
|
import { RiSpeedMiniLine } from 'react-icons/ri';
|
||||||
@ -46,11 +46,12 @@ import IconFocalLength from '@/components/icons/IconFocalLength';
|
|||||||
import IconTag from '@/components/icons/IconTag';
|
import IconTag from '@/components/icons/IconTag';
|
||||||
import IconPhoto from '@/components/icons/IconPhoto';
|
import IconPhoto from '@/components/icons/IconPhoto';
|
||||||
import { HiOutlineDocumentText } from 'react-icons/hi';
|
import { HiOutlineDocumentText } from 'react-icons/hi';
|
||||||
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
const DEBUG_COMMIT_SHA = '4cd29ed';
|
const DEBUG_COMMIT_SHA = '4cd29ed';
|
||||||
const DEBUG_COMMIT_MESSAGE = 'Long commit message for debugging purposes';
|
const DEBUG_COMMIT_MESSAGE = 'Long commit message for debugging purposes';
|
||||||
const DEBUG_BEHIND_BY = 9;
|
const DEBUG_BEHIND_BY = 9;
|
||||||
const DEBUG_PHOTOS_COUNT_OUTDATED = 7;
|
const DEBUG_PHOTOS_NEED_SYNC_COUNT = 7;
|
||||||
|
|
||||||
const TEXT_COLOR_WARNING = 'text-amber-600 dark:text-amber-500';
|
const TEXT_COLOR_WARNING = 'text-amber-600 dark:text-amber-500';
|
||||||
const TEXT_COLOR_BLUE = 'text-blue-600 dark:text-blue-500';
|
const TEXT_COLOR_BLUE = 'text-blue-600 dark:text-blue-500';
|
||||||
@ -91,7 +92,7 @@ export default function AdminAppInsightsClient({
|
|||||||
photoStats: {
|
photoStats: {
|
||||||
photosCount,
|
photosCount,
|
||||||
photosCountHidden,
|
photosCountHidden,
|
||||||
photosCountOutdated,
|
photosCountNeedSync,
|
||||||
camerasCount,
|
camerasCount,
|
||||||
lensesCount,
|
lensesCount,
|
||||||
tagsCount,
|
tagsCount,
|
||||||
@ -114,7 +115,7 @@ export default function AdminAppInsightsClient({
|
|||||||
noAiRateLimiting,
|
noAiRateLimiting,
|
||||||
noConfiguredDomain,
|
noConfiguredDomain,
|
||||||
noConfiguredMeta,
|
noConfiguredMeta,
|
||||||
outdatedPhotos,
|
photosNeedSync,
|
||||||
photoMatting,
|
photoMatting,
|
||||||
camerasFirst,
|
camerasFirst,
|
||||||
gridFirst,
|
gridFirst,
|
||||||
@ -131,6 +132,13 @@ export default function AdminAppInsightsClient({
|
|||||||
{codeMeta?.branch ?? TEMPLATE_REPO_BRANCH}
|
{codeMeta?.branch ?? TEMPLATE_REPO_BRANCH}
|
||||||
</a>;
|
</a>;
|
||||||
|
|
||||||
|
const renderTooltipContent = (content: ReactNode) =>
|
||||||
|
<Tooltip
|
||||||
|
content={content}
|
||||||
|
classNameTrigger="translate-y-[-1.5px] ml-2 h-3"
|
||||||
|
supportMobile
|
||||||
|
/>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScoreCardContainer>
|
<ScoreCardContainer>
|
||||||
{(codeMeta || debug) && <>
|
{(codeMeta || debug) && <>
|
||||||
@ -143,11 +151,9 @@ export default function AdminAppInsightsClient({
|
|||||||
/>}
|
/>}
|
||||||
content={<>
|
content={<>
|
||||||
<span>Could not analyze source code</span>
|
<span>Could not analyze source code</span>
|
||||||
<Tooltip
|
{renderTooltipContent(
|
||||||
content="Could not connect to GitHub API. Try refreshing."
|
'Could not connect to GitHub API. Try refreshing.',
|
||||||
classNameTrigger="translate-y-[-1.5px] ml-2 h-3"
|
)}
|
||||||
supportMobile
|
|
||||||
/>
|
|
||||||
</>}
|
</>}
|
||||||
/>}
|
/>}
|
||||||
{((!codeMeta?.didError && noFork) || debug) &&
|
{((!codeMeta?.didError && noFork) || debug) &&
|
||||||
@ -417,7 +423,7 @@ export default function AdminAppInsightsClient({
|
|||||||
</AdminEmptyState>}
|
</AdminEmptyState>}
|
||||||
</ScoreCard>
|
</ScoreCard>
|
||||||
<ScoreCard title="Library Stats">
|
<ScoreCard title="Library Stats">
|
||||||
{(outdatedPhotos || debug) && <ScoreCardRow
|
{(photosNeedSync || debug) && <ScoreCardRow
|
||||||
icon={<LiaBroomSolid
|
icon={<LiaBroomSolid
|
||||||
size={19}
|
size={19}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
@ -425,14 +431,21 @@ export default function AdminAppInsightsClient({
|
|||||||
TEXT_COLOR_BLUE,
|
TEXT_COLOR_BLUE,
|
||||||
)}
|
)}
|
||||||
/>}
|
/>}
|
||||||
content={renderHighlightText(
|
content={<>
|
||||||
pluralize(
|
{renderHighlightText(
|
||||||
photosCountOutdated || DEBUG_PHOTOS_COUNT_OUTDATED,
|
pluralize(
|
||||||
'outdated photo',
|
photosCountNeedSync || DEBUG_PHOTOS_NEED_SYNC_COUNT,
|
||||||
),
|
'photo',
|
||||||
'blue',
|
),
|
||||||
)}
|
'blue',
|
||||||
expandPath={PATH_ADMIN_OUTDATED}
|
)}
|
||||||
|
{' '}
|
||||||
|
with updates
|
||||||
|
{renderTooltipContent(<>
|
||||||
|
Missing data or AI‑generated text
|
||||||
|
</>)}
|
||||||
|
</>}
|
||||||
|
expandPath={PATH_ADMIN_PHOTOS_UPDATES}
|
||||||
/>}
|
/>}
|
||||||
<ScoreCardRow
|
<ScoreCardRow
|
||||||
icon={<IconPhoto
|
icon={<IconPhoto
|
||||||
|
|||||||
@ -39,7 +39,7 @@ const _INSIGHTS_TEMPLATE = [
|
|||||||
type AdminAppInsightRecommendation = typeof _INSIGHTS_TEMPLATE[number];
|
type AdminAppInsightRecommendation = typeof _INSIGHTS_TEMPLATE[number];
|
||||||
|
|
||||||
const _INSIGHTS_LIBRARY = [
|
const _INSIGHTS_LIBRARY = [
|
||||||
'outdatedPhotos',
|
'photosNeedSync',
|
||||||
] as const;
|
] as const;
|
||||||
type AdminAppInsightLibrary = typeof _INSIGHTS_LIBRARY[number];
|
type AdminAppInsightLibrary = typeof _INSIGHTS_LIBRARY[number];
|
||||||
|
|
||||||
@ -58,7 +58,7 @@ export const hasTemplateRecommendations = (insights: AdminAppInsights) =>
|
|||||||
export interface PhotoStats {
|
export interface PhotoStats {
|
||||||
photosCount: number
|
photosCount: number
|
||||||
photosCountHidden: number
|
photosCountHidden: number
|
||||||
photosCountOutdated: number
|
photosCountNeedSync: number
|
||||||
camerasCount: number
|
camerasCount: number
|
||||||
lensesCount: number
|
lensesCount: number
|
||||||
tagsCount: number
|
tagsCount: number
|
||||||
@ -80,10 +80,10 @@ export const getGitHubMetaForCurrentApp = () =>
|
|||||||
|
|
||||||
export const getSignificantInsights = ({
|
export const getSignificantInsights = ({
|
||||||
codeMeta,
|
codeMeta,
|
||||||
photosCountOutdated,
|
photosCountNeedSync,
|
||||||
}: {
|
}: {
|
||||||
codeMeta: Awaited<ReturnType<typeof getGitHubMetaForCurrentApp>>
|
codeMeta: Awaited<ReturnType<typeof getGitHubMetaForCurrentApp>>
|
||||||
photosCountOutdated: number
|
photosCountNeedSync: number
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
isAiTextGenerationEnabled,
|
isAiTextGenerationEnabled,
|
||||||
@ -95,30 +95,38 @@ export const getSignificantInsights = ({
|
|||||||
forkBehind: Boolean(codeMeta?.isBehind),
|
forkBehind: Boolean(codeMeta?.isBehind),
|
||||||
noAiRateLimiting: isAiTextGenerationEnabled && !hasRedisStorage,
|
noAiRateLimiting: isAiTextGenerationEnabled && !hasRedisStorage,
|
||||||
noConfiguredDomain: !hasDomain,
|
noConfiguredDomain: !hasDomain,
|
||||||
outdatedPhotos: Boolean(photosCountOutdated),
|
photosNeedSync: Boolean(photosCountNeedSync),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const indicatorStatusForSignificantInsights = (
|
export const indicatorStatusForSignificantInsights = ({
|
||||||
insights: Awaited<ReturnType<typeof getSignificantInsights>>,
|
codeMeta,
|
||||||
) => {
|
photosCountNeedSync,
|
||||||
|
}: Parameters<typeof getSignificantInsights>[0] & {
|
||||||
|
photosCountNeedSync: number
|
||||||
|
}) => {
|
||||||
|
const insights = getSignificantInsights({
|
||||||
|
codeMeta,
|
||||||
|
photosCountNeedSync,
|
||||||
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
forkBehind,
|
forkBehind,
|
||||||
noAiRateLimiting,
|
noAiRateLimiting,
|
||||||
noConfiguredDomain,
|
noConfiguredDomain,
|
||||||
outdatedPhotos,
|
photosNeedSync,
|
||||||
} = insights;
|
} = insights;
|
||||||
|
|
||||||
if (noAiRateLimiting || noConfiguredDomain) {
|
if (noAiRateLimiting || noConfiguredDomain) {
|
||||||
return 'yellow';
|
return 'yellow';
|
||||||
} else if (forkBehind || outdatedPhotos) {
|
} else if (forkBehind || photosNeedSync) {
|
||||||
return 'blue';
|
return 'blue';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getAllInsights = ({
|
export const getAllInsights = ({
|
||||||
codeMeta,
|
codeMeta,
|
||||||
photosCountOutdated,
|
photosCountNeedSync,
|
||||||
photosCount,
|
photosCount,
|
||||||
photosCountPortrait,
|
photosCountPortrait,
|
||||||
tagsCount,
|
tagsCount,
|
||||||
@ -127,7 +135,7 @@ export const getAllInsights = ({
|
|||||||
photosCountPortrait: number
|
photosCountPortrait: number
|
||||||
tagsCount: number
|
tagsCount: number
|
||||||
}) => ({
|
}) => ({
|
||||||
...getSignificantInsights({ codeMeta, photosCountOutdated }),
|
...getSignificantInsights({ codeMeta, photosCountNeedSync }),
|
||||||
noFork: !codeMeta?.isForkedFromBase && !codeMeta?.isBaseRepo,
|
noFork: !codeMeta?.isForkedFromBase && !codeMeta?.isBaseRepo,
|
||||||
noAi: !AI_TEXT_GENERATION_ENABLED,
|
noAi: !AI_TEXT_GENERATION_ENABLED,
|
||||||
noConfiguredMeta:
|
noConfiguredMeta:
|
||||||
|
|||||||
@ -1,23 +0,0 @@
|
|||||||
import { getOutdatedPhotosCount } from '@/photo/db/query';
|
|
||||||
import {
|
|
||||||
getSignificantInsights,
|
|
||||||
indicatorStatusForSignificantInsights,
|
|
||||||
} from '.';
|
|
||||||
import { getGitHubMetaForCurrentApp } from '.';
|
|
||||||
|
|
||||||
export const getInsightsIndicatorStatus = async () => {
|
|
||||||
const [
|
|
||||||
codeMeta,
|
|
||||||
photosCountOutdated,
|
|
||||||
] = await Promise.all([
|
|
||||||
getGitHubMetaForCurrentApp(),
|
|
||||||
getOutdatedPhotosCount(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const significantInsights = getSignificantInsights({
|
|
||||||
codeMeta,
|
|
||||||
photosCountOutdated,
|
|
||||||
});
|
|
||||||
|
|
||||||
return indicatorStatusForSignificantInsights(significantInsights);
|
|
||||||
};
|
|
||||||
@ -7,50 +7,54 @@ import { TAG_HIDDEN } from '@/tag';
|
|||||||
import { Lens } from '@/lens';
|
import { Lens } from '@/lens';
|
||||||
|
|
||||||
// Core paths
|
// Core paths
|
||||||
export const PATH_ROOT = '/';
|
export const PATH_ROOT = '/';
|
||||||
export const PATH_GRID = '/grid';
|
export const PATH_GRID = '/grid';
|
||||||
export const PATH_FEED = '/feed';
|
export const PATH_FEED = '/feed';
|
||||||
export const PATH_ADMIN = '/admin';
|
export const PATH_ADMIN = '/admin';
|
||||||
export const PATH_API = '/api';
|
export const PATH_API = '/api';
|
||||||
export const PATH_SIGN_IN = '/sign-in';
|
export const PATH_SIGN_IN = '/sign-in';
|
||||||
export const PATH_OG = '/og';
|
export const PATH_OG = '/og';
|
||||||
// eslint-disable-next-line max-len
|
|
||||||
export const PATH_GRID_INFERRED = GRID_HOMEPAGE_ENABLED ? PATH_ROOT : PATH_GRID;
|
export const PATH_GRID_INFERRED = GRID_HOMEPAGE_ENABLED
|
||||||
// eslint-disable-next-line max-len
|
? PATH_ROOT
|
||||||
export const PATH_FEED_INFERRED = GRID_HOMEPAGE_ENABLED ? PATH_FEED : PATH_ROOT;
|
: PATH_GRID;
|
||||||
|
|
||||||
|
export const PATH_FEED_INFERRED = GRID_HOMEPAGE_ENABLED
|
||||||
|
? PATH_FEED
|
||||||
|
: PATH_ROOT;
|
||||||
|
|
||||||
// Path prefixes
|
// Path prefixes
|
||||||
export const PREFIX_PHOTO = '/p';
|
export const PREFIX_PHOTO = '/p';
|
||||||
export const PREFIX_CAMERA = '/shot-on';
|
export const PREFIX_CAMERA = '/shot-on';
|
||||||
export const PREFIX_LENS = '/lens';
|
export const PREFIX_LENS = '/lens';
|
||||||
export const PREFIX_TAG = '/tag';
|
export const PREFIX_TAG = '/tag';
|
||||||
export const PREFIX_RECIPE = '/recipe';
|
export const PREFIX_RECIPE = '/recipe';
|
||||||
export const PREFIX_FILM = '/film';
|
export const PREFIX_FILM = '/film';
|
||||||
export const PREFIX_FOCAL_LENGTH = '/focal';
|
export const PREFIX_FOCAL_LENGTH = '/focal';
|
||||||
|
|
||||||
// Dynamic paths
|
// Dynamic paths
|
||||||
const PATH_PHOTO_DYNAMIC = `${PREFIX_PHOTO}/[photoId]`;
|
const PATH_PHOTO_DYNAMIC = `${PREFIX_PHOTO}/[photoId]`;
|
||||||
const PATH_CAMERA_DYNAMIC = `${PREFIX_CAMERA}/[make]/[model]`;
|
const PATH_CAMERA_DYNAMIC = `${PREFIX_CAMERA}/[make]/[model]`;
|
||||||
const PATH_LENS_DYNAMIC = `${PREFIX_LENS}/[make]/[model]`;
|
const PATH_LENS_DYNAMIC = `${PREFIX_LENS}/[make]/[model]`;
|
||||||
const PATH_TAG_DYNAMIC = `${PREFIX_TAG}/[tag]`;
|
const PATH_TAG_DYNAMIC = `${PREFIX_TAG}/[tag]`;
|
||||||
const PATH_FILM_DYNAMIC = `${PREFIX_FILM}/[film]`;
|
const PATH_FILM_DYNAMIC = `${PREFIX_FILM}/[film]`;
|
||||||
const PATH_FOCAL_LENGTH_DYNAMIC = `${PREFIX_FOCAL_LENGTH}/[focal]`;
|
const PATH_FOCAL_LENGTH_DYNAMIC = `${PREFIX_FOCAL_LENGTH}/[focal]`;
|
||||||
const PATH_RECIPE_DYNAMIC = `${PREFIX_RECIPE}/[recipe]`;
|
const PATH_RECIPE_DYNAMIC = `${PREFIX_RECIPE}/[recipe]`;
|
||||||
|
|
||||||
// Admin paths
|
// Admin paths
|
||||||
export const PATH_ADMIN_PHOTOS = `${PATH_ADMIN}/photos`;
|
export const PATH_ADMIN_PHOTOS = `${PATH_ADMIN}/photos`;
|
||||||
export const PATH_ADMIN_OUTDATED = `${PATH_ADMIN}/outdated`;
|
export const PATH_ADMIN_PHOTOS_UPDATES = `${PATH_ADMIN_PHOTOS}/updates`;
|
||||||
export const PATH_ADMIN_UPLOADS = `${PATH_ADMIN}/uploads`;
|
export const PATH_ADMIN_UPLOADS = `${PATH_ADMIN}/uploads`;
|
||||||
export const PATH_ADMIN_TAGS = `${PATH_ADMIN}/tags`;
|
export const PATH_ADMIN_TAGS = `${PATH_ADMIN}/tags`;
|
||||||
export const PATH_ADMIN_RECIPES = `${PATH_ADMIN}/recipes`;
|
export const PATH_ADMIN_RECIPES = `${PATH_ADMIN}/recipes`;
|
||||||
export const PATH_ADMIN_CONFIGURATION = `${PATH_ADMIN}/configuration`;
|
export const PATH_ADMIN_CONFIGURATION = `${PATH_ADMIN}/configuration`;
|
||||||
export const PATH_ADMIN_INSIGHTS = `${PATH_ADMIN}/insights`;
|
export const PATH_ADMIN_INSIGHTS = `${PATH_ADMIN}/insights`;
|
||||||
export const PATH_ADMIN_BASELINE = `${PATH_ADMIN}/baseline`;
|
export const PATH_ADMIN_BASELINE = `${PATH_ADMIN}/baseline`;
|
||||||
export const PATH_ADMIN_COMPONENTS = `${PATH_ADMIN}/components`;
|
export const PATH_ADMIN_COMPONENTS = `${PATH_ADMIN}/components`;
|
||||||
|
|
||||||
// Debug paths
|
// Debug paths
|
||||||
export const PATH_OG_ALL = `${PATH_OG}/all`;
|
export const PATH_OG_ALL = `${PATH_OG}/all`;
|
||||||
export const PATH_OG_SAMPLE = `${PATH_OG}/sample`;
|
export const PATH_OG_SAMPLE = `${PATH_OG}/sample`;
|
||||||
|
|
||||||
// API paths
|
// API paths
|
||||||
export const PATH_API_STORAGE = `${PATH_API}/storage`;
|
export const PATH_API_STORAGE = `${PATH_API}/storage`;
|
||||||
@ -66,6 +70,7 @@ export const MISSING_FIELD = '-';
|
|||||||
export const PATHS_ADMIN = [
|
export const PATHS_ADMIN = [
|
||||||
PATH_ADMIN,
|
PATH_ADMIN,
|
||||||
PATH_ADMIN_PHOTOS,
|
PATH_ADMIN_PHOTOS,
|
||||||
|
PATH_ADMIN_PHOTOS_UPDATES,
|
||||||
PATH_ADMIN_UPLOADS,
|
PATH_ADMIN_UPLOADS,
|
||||||
PATH_ADMIN_TAGS,
|
PATH_ADMIN_TAGS,
|
||||||
PATH_ADMIN_RECIPES,
|
PATH_ADMIN_RECIPES,
|
||||||
@ -94,9 +99,6 @@ type PhotoPathParams = { photo: PhotoOrPhotoId } & PhotoSetCategory & {
|
|||||||
showRecipe?: boolean
|
showRecipe?: boolean
|
||||||
};
|
};
|
||||||
|
|
||||||
// Absolute paths
|
|
||||||
export const ABSOLUTE_PATH_FOR_HOME_IMAGE = `${BASE_URL}/home-image`;
|
|
||||||
|
|
||||||
export const pathForAdminUploadUrl = (url: string) =>
|
export const pathForAdminUploadUrl = (url: string) =>
|
||||||
`${PATH_ADMIN_UPLOADS}/${encodeURIComponent(url)}`;
|
`${PATH_ADMIN_UPLOADS}/${encodeURIComponent(url)}`;
|
||||||
|
|
||||||
@ -164,6 +166,9 @@ export const pathForFocalLength = (focal: number) =>
|
|||||||
export const pathForRecipe = (recipe: string) =>
|
export const pathForRecipe = (recipe: string) =>
|
||||||
`${PREFIX_RECIPE}/${recipe}`;
|
`${PREFIX_RECIPE}/${recipe}`;
|
||||||
|
|
||||||
|
// Absolute paths
|
||||||
|
export const ABSOLUTE_PATH_FOR_HOME_IMAGE = `${BASE_URL}/home-image`;
|
||||||
|
|
||||||
export const absolutePathForPhoto = (params: PhotoPathParams) =>
|
export const absolutePathForPhoto = (params: PhotoPathParams) =>
|
||||||
`${BASE_URL}${pathForPhoto(params)}`;
|
`${BASE_URL}${pathForPhoto(params)}`;
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import { BiCopy } from 'react-icons/bi';
|
|||||||
import LoaderButton from './primitives/LoaderButton';
|
import LoaderButton from './primitives/LoaderButton';
|
||||||
import clsx from 'clsx/lite';
|
import clsx from 'clsx/lite';
|
||||||
import { toastSuccess } from '@/toast';
|
import { toastSuccess } from '@/toast';
|
||||||
import Tooltip from './Tooltip';
|
|
||||||
import { ComponentProps } from 'react';
|
import { ComponentProps } from 'react';
|
||||||
|
|
||||||
export default function CopyButton({
|
export default function CopyButton({
|
||||||
@ -10,20 +9,18 @@ export default function CopyButton({
|
|||||||
text,
|
text,
|
||||||
subtle,
|
subtle,
|
||||||
iconSize = 15,
|
iconSize = 15,
|
||||||
tooltip,
|
|
||||||
tooltipColor,
|
|
||||||
className,
|
className,
|
||||||
|
...props
|
||||||
}: {
|
}: {
|
||||||
label: string
|
label: string
|
||||||
text?: string,
|
text?: string,
|
||||||
subtle?: boolean
|
subtle?: boolean
|
||||||
iconSize?: number
|
iconSize?: number
|
||||||
tooltip?: string
|
|
||||||
tooltipColor?: ComponentProps<typeof Tooltip>['color']
|
|
||||||
className?: string
|
className?: string
|
||||||
}) {
|
} & ComponentProps<typeof LoaderButton>) {
|
||||||
const button =
|
return (
|
||||||
<LoaderButton
|
<LoaderButton
|
||||||
|
{...props}
|
||||||
icon={<BiCopy size={iconSize} />}
|
icon={<BiCopy size={iconSize} />}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
subtle && 'text-gray-300 dark:text-gray-700',
|
subtle && 'text-gray-300 dark:text-gray-700',
|
||||||
@ -37,13 +34,6 @@ export default function CopyButton({
|
|||||||
: undefined}
|
: undefined}
|
||||||
styleAs="link"
|
styleAs="link"
|
||||||
disabled={!text}
|
disabled={!text}
|
||||||
/>;
|
/>
|
||||||
|
|
||||||
return (
|
|
||||||
tooltip
|
|
||||||
? <Tooltip content={tooltip} color={tooltipColor}>
|
|
||||||
{button}
|
|
||||||
</Tooltip>
|
|
||||||
: button
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
6
src/components/icons/IconBroom.tsx
Normal file
6
src/components/icons/IconBroom.tsx
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { IconBaseProps } from 'react-icons';
|
||||||
|
import { LiaBroomSolid } from 'react-icons/lia';
|
||||||
|
|
||||||
|
export default function IconBroom(props: IconBaseProps) {
|
||||||
|
return <LiaBroomSolid {...props} />;
|
||||||
|
}
|
||||||
@ -21,7 +21,7 @@ export default function MoreMenuItem({
|
|||||||
}: {
|
}: {
|
||||||
label: string
|
label: string
|
||||||
labelComplex?: ReactNode
|
labelComplex?: ReactNode
|
||||||
annotation?: string
|
annotation?: ReactNode
|
||||||
icon?: ReactNode
|
icon?: ReactNode
|
||||||
href?: string
|
href?: string
|
||||||
hrefDownloadName?: string
|
hrefDownloadName?: string
|
||||||
|
|||||||
@ -2,9 +2,36 @@
|
|||||||
|
|
||||||
import Spinner, { SpinnerColor } from '@/components/Spinner';
|
import Spinner, { SpinnerColor } from '@/components/Spinner';
|
||||||
import { clsx } from 'clsx/lite';
|
import { clsx } from 'clsx/lite';
|
||||||
import { ButtonHTMLAttributes, ReactNode } from 'react';
|
import {
|
||||||
|
ButtonHTMLAttributes,
|
||||||
|
ComponentProps,
|
||||||
|
ReactNode,
|
||||||
|
RefObject,
|
||||||
|
} from 'react';
|
||||||
|
import Tooltip from '../Tooltip';
|
||||||
|
|
||||||
export default function LoaderButton(props: {
|
export default function LoaderButton({
|
||||||
|
ref,
|
||||||
|
children,
|
||||||
|
isLoading,
|
||||||
|
icon,
|
||||||
|
spinnerColor,
|
||||||
|
spinnerClassName,
|
||||||
|
styleAs = 'button',
|
||||||
|
hideTextOnMobile = true,
|
||||||
|
confirmText,
|
||||||
|
shouldPreventDefault,
|
||||||
|
primary,
|
||||||
|
hideFocusOutline,
|
||||||
|
type = 'button',
|
||||||
|
onClick,
|
||||||
|
disabled,
|
||||||
|
className,
|
||||||
|
tooltip,
|
||||||
|
tooltipColor,
|
||||||
|
...rest
|
||||||
|
}: {
|
||||||
|
ref?: RefObject<HTMLButtonElement | null>
|
||||||
isLoading?: boolean
|
isLoading?: boolean
|
||||||
icon?: ReactNode
|
icon?: ReactNode
|
||||||
spinnerColor?: SpinnerColor
|
spinnerColor?: SpinnerColor
|
||||||
@ -15,29 +42,13 @@ export default function LoaderButton(props: {
|
|||||||
shouldPreventDefault?: boolean
|
shouldPreventDefault?: boolean
|
||||||
primary?: boolean
|
primary?: boolean
|
||||||
hideFocusOutline?: boolean
|
hideFocusOutline?: boolean
|
||||||
|
tooltip?: string
|
||||||
|
tooltipColor?: ComponentProps<typeof Tooltip>['color']
|
||||||
} & ButtonHTMLAttributes<HTMLButtonElement>) {
|
} & ButtonHTMLAttributes<HTMLButtonElement>) {
|
||||||
const {
|
const button =
|
||||||
children,
|
|
||||||
isLoading,
|
|
||||||
icon,
|
|
||||||
spinnerColor,
|
|
||||||
spinnerClassName,
|
|
||||||
styleAs = 'button',
|
|
||||||
hideTextOnMobile = true,
|
|
||||||
confirmText,
|
|
||||||
shouldPreventDefault,
|
|
||||||
primary,
|
|
||||||
hideFocusOutline,
|
|
||||||
type = 'button',
|
|
||||||
onClick,
|
|
||||||
disabled,
|
|
||||||
className,
|
|
||||||
...rest
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
<button
|
||||||
{...rest}
|
{...rest}
|
||||||
|
ref={ref}
|
||||||
type={type}
|
type={type}
|
||||||
onClick={e => {
|
onClick={e => {
|
||||||
if (shouldPreventDefault) { e.preventDefault(); }
|
if (shouldPreventDefault) { e.preventDefault(); }
|
||||||
@ -86,6 +97,13 @@ export default function LoaderButton(props: {
|
|||||||
)}>
|
)}>
|
||||||
{children}
|
{children}
|
||||||
</span>}
|
</span>}
|
||||||
</button>
|
</button>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
tooltip
|
||||||
|
? <Tooltip content={tooltip} color={tooltipColor}>
|
||||||
|
{button}
|
||||||
|
</Tooltip>
|
||||||
|
: button
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -374,7 +374,7 @@ export const getExifDataAction = async (
|
|||||||
// - strip GPS data if necessary
|
// - strip GPS data if necessary
|
||||||
// - update blur data (or destroy if blur is disabled)
|
// - update blur data (or destroy if blur is disabled)
|
||||||
// - generate AI text data, if enabled, and auto-generated fields are empty
|
// - generate AI text data, if enabled, and auto-generated fields are empty
|
||||||
export const syncPhotoAction = async (photoId: string) =>
|
export const syncPhotoAction = async (photoId: string, isBatch?: boolean) =>
|
||||||
runAuthenticatedAdminServerAction(async () => {
|
runAuthenticatedAdminServerAction(async () => {
|
||||||
const photo = await getPhoto(photoId ?? '', true);
|
const photo = await getPhoto(photoId ?? '', true);
|
||||||
|
|
||||||
@ -414,7 +414,8 @@ export const syncPhotoAction = async (photoId: string) =>
|
|||||||
semanticDescription: aiSemanticDescription,
|
semanticDescription: aiSemanticDescription,
|
||||||
} = await generateAiImageQueries(
|
} = await generateAiImageQueries(
|
||||||
imageResizedBase64,
|
imageResizedBase64,
|
||||||
AI_TEXT_AUTO_GENERATED_FIELDS,
|
photo.syncStatus.missingAiTextFields,
|
||||||
|
isBatch,
|
||||||
);
|
);
|
||||||
|
|
||||||
const formDataFromPhoto = convertPhotoToFormData(photo);
|
const formDataFromPhoto = convertPhotoToFormData(photo);
|
||||||
@ -451,7 +452,7 @@ export const syncPhotoAction = async (photoId: string) =>
|
|||||||
export const syncPhotosAction = async (photoIds: string[]) =>
|
export const syncPhotosAction = async (photoIds: string[]) =>
|
||||||
runAuthenticatedAdminServerAction(async () => {
|
runAuthenticatedAdminServerAction(async () => {
|
||||||
for (const photoId of photoIds) {
|
for (const photoId of photoIds) {
|
||||||
await syncPhotoAction(photoId);
|
await syncPhotoAction(photoId, true);
|
||||||
}
|
}
|
||||||
revalidateAllKeysAndPaths();
|
revalidateAllKeysAndPaths();
|
||||||
});
|
});
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import { getUniqueTags } from '../db/query';
|
|||||||
export const generateAiImageQueries = async (
|
export const generateAiImageQueries = async (
|
||||||
imageBase64?: string,
|
imageBase64?: string,
|
||||||
textFieldsToGenerate: AiAutoGeneratedField[] = [],
|
textFieldsToGenerate: AiAutoGeneratedField[] = [],
|
||||||
|
isBatch?: boolean,
|
||||||
): Promise<{
|
): Promise<{
|
||||||
title?: string
|
title?: string
|
||||||
caption?: string
|
caption?: string
|
||||||
@ -31,6 +32,7 @@ export const generateAiImageQueries = async (
|
|||||||
const titleAndCaption = await generateOpenAiImageQuery(
|
const titleAndCaption = await generateOpenAiImageQuery(
|
||||||
imageBase64,
|
imageBase64,
|
||||||
getAiImageQuery('title-and-caption'),
|
getAiImageQuery('title-and-caption'),
|
||||||
|
isBatch,
|
||||||
);
|
);
|
||||||
if (titleAndCaption) {
|
if (titleAndCaption) {
|
||||||
const titleAndCaptionParsed = parseTitleAndCaption(titleAndCaption);
|
const titleAndCaptionParsed = parseTitleAndCaption(titleAndCaption);
|
||||||
@ -42,12 +44,14 @@ export const generateAiImageQueries = async (
|
|||||||
title = await generateOpenAiImageQuery(
|
title = await generateOpenAiImageQuery(
|
||||||
imageBase64,
|
imageBase64,
|
||||||
getAiImageQuery('title'),
|
getAiImageQuery('title'),
|
||||||
|
isBatch,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (textFieldsToGenerate.includes('caption')) {
|
if (textFieldsToGenerate.includes('caption')) {
|
||||||
caption = await generateOpenAiImageQuery(
|
caption = await generateOpenAiImageQuery(
|
||||||
imageBase64,
|
imageBase64,
|
||||||
getAiImageQuery('caption'),
|
getAiImageQuery('caption'),
|
||||||
|
isBatch,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -57,6 +61,7 @@ export const generateAiImageQueries = async (
|
|||||||
tags = await generateOpenAiImageQuery(
|
tags = await generateOpenAiImageQuery(
|
||||||
imageBase64,
|
imageBase64,
|
||||||
getAiImageQuery('tags', existingTags),
|
getAiImageQuery('tags', existingTags),
|
||||||
|
isBatch,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,6 +69,7 @@ export const generateAiImageQueries = async (
|
|||||||
semanticDescription = await generateOpenAiImageQuery(
|
semanticDescription = await generateOpenAiImageQuery(
|
||||||
imageBase64,
|
imageBase64,
|
||||||
getAiImageQuery('description-small'),
|
getAiImageQuery('description-small'),
|
||||||
|
isBatch,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,13 +7,17 @@ import { Lens } from '@/lens';
|
|||||||
export const GENERATE_STATIC_PARAMS_LIMIT = 1000;
|
export const GENERATE_STATIC_PARAMS_LIMIT = 1000;
|
||||||
export const PHOTO_DEFAULT_LIMIT = 100;
|
export const PHOTO_DEFAULT_LIMIT = 100;
|
||||||
|
|
||||||
// Trim whitespace
|
const DB_PARAMETERIZE_REPLACEMENTS = [
|
||||||
// Make lowercase
|
[',', ''],
|
||||||
// Remove commas, slashes
|
['/', ''],
|
||||||
// Replace spaces with dashes
|
['+', '-'],
|
||||||
|
[' ', '-'],
|
||||||
|
];
|
||||||
|
|
||||||
const parameterizeForDb = (field: string) =>
|
const parameterizeForDb = (field: string) =>
|
||||||
// eslint-disable-next-line max-len
|
DB_PARAMETERIZE_REPLACEMENTS.reduce((acc, [from, to]) =>
|
||||||
`REPLACE(REPLACE(REPLACE(LOWER(TRIM(${field})), ',', ''), '/', ''), ' ', '-')`;
|
`REPLACE(${acc}, '${from}', '${to}')`
|
||||||
|
, `LOWER(TRIM(${field}))`);
|
||||||
|
|
||||||
export type GetPhotosOptions = {
|
export type GetPhotosOptions = {
|
||||||
sortBy?: 'createdAt' | 'createdAtAsc' | 'takenAt' | 'priority'
|
sortBy?: 'createdAt' | 'createdAtAsc' | 'takenAt' | 'priority'
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable quotes */
|
||||||
import {
|
import {
|
||||||
sql,
|
sql,
|
||||||
query,
|
query,
|
||||||
@ -14,7 +15,11 @@ import {
|
|||||||
import { Cameras, createCameraKey } from '@/camera';
|
import { Cameras, createCameraKey } from '@/camera';
|
||||||
import { Tags } from '@/tag';
|
import { Tags } from '@/tag';
|
||||||
import { Films } from '@/film';
|
import { Films } from '@/film';
|
||||||
import { ADMIN_SQL_DEBUG_ENABLED } from '@/app/config';
|
import {
|
||||||
|
ADMIN_SQL_DEBUG_ENABLED,
|
||||||
|
AI_TEXT_AUTO_GENERATED_FIELDS,
|
||||||
|
AI_TEXT_GENERATION_ENABLED,
|
||||||
|
} from '@/app/config';
|
||||||
import {
|
import {
|
||||||
GetPhotosOptions,
|
GetPhotosOptions,
|
||||||
getLimitAndOffsetFromOptions,
|
getLimitAndOffsetFromOptions,
|
||||||
@ -24,7 +29,11 @@ import { getWheresFromOptions } from '.';
|
|||||||
import { FocalLengths } from '@/focal';
|
import { FocalLengths } from '@/focal';
|
||||||
import { Lenses, createLensKey } from '@/lens';
|
import { Lenses, createLensKey } from '@/lens';
|
||||||
import { migrationForError } from './migration';
|
import { migrationForError } from './migration';
|
||||||
import { UPDATED_BEFORE_01, UPDATED_BEFORE_02 } from '../outdated';
|
import {
|
||||||
|
SYNC_QUERY_LIMIT,
|
||||||
|
UPDATED_BEFORE_01,
|
||||||
|
UPDATED_BEFORE_02,
|
||||||
|
} from '../sync';
|
||||||
import { MAKE_FUJIFILM } from '@/platforms/fujifilm';
|
import { MAKE_FUJIFILM } from '@/platforms/fujifilm';
|
||||||
import { Recipes } from '@/recipe';
|
import { Recipes } from '@/recipe';
|
||||||
|
|
||||||
@ -568,38 +577,55 @@ export const getPhoto = async (
|
|||||||
.then(photos => photos.length > 0 ? photos[0] : undefined);
|
.then(photos => photos.length > 0 ? photos[0] : undefined);
|
||||||
}, 'getPhoto');
|
}, 'getPhoto');
|
||||||
|
|
||||||
// Outdated queries
|
// Sync queries
|
||||||
|
|
||||||
const outdatedWhereClause =
|
const outdatedWhereClauses = [
|
||||||
// eslint-disable-next-line quotes
|
`updated_at < $1`,
|
||||||
`WHERE updated_at < $1 OR (updated_at < $2 AND make = $3)`;
|
`(updated_at < $2 AND make = $3)`,
|
||||||
|
];
|
||||||
|
|
||||||
const outdatedValues = [
|
const outdatedWhereValues = [
|
||||||
UPDATED_BEFORE_01.toISOString(),
|
UPDATED_BEFORE_01.toISOString(),
|
||||||
UPDATED_BEFORE_02.toISOString(),
|
UPDATED_BEFORE_02.toISOString(),
|
||||||
MAKE_FUJIFILM,
|
MAKE_FUJIFILM,
|
||||||
];
|
];
|
||||||
|
|
||||||
export const getOutdatedPhotos = () => safelyQueryPhotos(
|
const needsAiTextWhereClauses =
|
||||||
|
AI_TEXT_GENERATION_ENABLED
|
||||||
|
? AI_TEXT_AUTO_GENERATED_FIELDS
|
||||||
|
.map(field => {
|
||||||
|
switch (field) {
|
||||||
|
case 'title': return `(title <> '') IS NOT TRUE`;
|
||||||
|
case 'caption': return `(caption <> '') IS NOT TRUE`;
|
||||||
|
case 'tags': return `(tags IS NULL OR array_length(tags, 1) = 0)`;
|
||||||
|
case 'semantic': return `(semantic_description <> '') IS NOT TRUE`;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const needsSyncWhereStatement =
|
||||||
|
`WHERE ${outdatedWhereClauses.concat(needsAiTextWhereClauses).join(' OR ')}`;
|
||||||
|
|
||||||
|
export const getPhotosInNeedOfSync = () => safelyQueryPhotos(
|
||||||
() => query(`
|
() => query(`
|
||||||
SELECT * FROM photos
|
SELECT * FROM photos
|
||||||
${outdatedWhereClause}
|
${needsSyncWhereStatement}
|
||||||
ORDER BY created_at DESC
|
ORDER BY created_at DESC
|
||||||
LIMIT 1000
|
LIMIT ${SYNC_QUERY_LIMIT}
|
||||||
`,
|
`,
|
||||||
outdatedValues,
|
outdatedWhereValues,
|
||||||
)
|
)
|
||||||
.then(({ rows }) => rows.map(parsePhotoFromDb)),
|
.then(({ rows }) => rows.map(parsePhotoFromDb)),
|
||||||
'getOutdatedPhotos',
|
'getPhotosInNeedOfSync',
|
||||||
);
|
);
|
||||||
|
|
||||||
export const getOutdatedPhotosCount = () => safelyQueryPhotos(
|
export const getPhotosInNeedOfSyncCount = () => safelyQueryPhotos(
|
||||||
() => query(`
|
() => query(`
|
||||||
SELECT COUNT(*) FROM photos
|
SELECT COUNT(*) FROM photos
|
||||||
${outdatedWhereClause}
|
${needsSyncWhereStatement}
|
||||||
`,
|
`,
|
||||||
outdatedValues,
|
outdatedWhereValues,
|
||||||
)
|
)
|
||||||
.then(({ rows }) => parseInt(rows[0].count, 10)),
|
.then(({ rows }) => parseInt(rows[0].count, 10)),
|
||||||
'getOutdatedPhotosCount',
|
'getPhotosInNeedOfSyncCount',
|
||||||
);
|
);
|
||||||
|
|||||||
@ -23,6 +23,7 @@ import { isBefore } from 'date-fns';
|
|||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
import { FujifilmRecipe } from '@/platforms/fujifilm/recipe';
|
import { FujifilmRecipe } from '@/platforms/fujifilm/recipe';
|
||||||
import { FujifilmSimulation } from '@/platforms/fujifilm/simulation';
|
import { FujifilmSimulation } from '@/platforms/fujifilm/simulation';
|
||||||
|
import { PhotoSyncStatus, generatePhotoSyncStatus } from './sync';
|
||||||
|
|
||||||
// INFINITE SCROLL: FEED
|
// INFINITE SCROLL: FEED
|
||||||
export const INFINITE_SCROLL_FEED_INITIAL =
|
export const INFINITE_SCROLL_FEED_INITIAL =
|
||||||
@ -96,7 +97,7 @@ export interface PhotoDb extends
|
|||||||
updatedAt: Date
|
updatedAt: Date
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
takenAt: Date
|
takenAt: Date
|
||||||
tags: string[]
|
tags: string[] | null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parsed db response
|
// Parsed db response
|
||||||
@ -108,7 +109,9 @@ export interface Photo extends Omit<PhotoDb, 'recipeData'> {
|
|||||||
exposureTimeFormatted?: string
|
exposureTimeFormatted?: string
|
||||||
exposureCompensationFormatted?: string
|
exposureCompensationFormatted?: string
|
||||||
takenAtNaiveFormatted: string
|
takenAtNaiveFormatted: string
|
||||||
|
tags: string[]
|
||||||
recipeData?: FujifilmRecipe
|
recipeData?: FujifilmRecipe
|
||||||
|
syncStatus: PhotoSyncStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
export const parsePhotoFromDb = (photoDbRaw: PhotoDb): Photo => {
|
export const parsePhotoFromDb = (photoDbRaw: PhotoDb): Photo => {
|
||||||
@ -130,15 +133,16 @@ export const parsePhotoFromDb = (photoDbRaw: PhotoDb): Photo => {
|
|||||||
formatExposureTime(photoDb.exposureTime),
|
formatExposureTime(photoDb.exposureTime),
|
||||||
exposureCompensationFormatted:
|
exposureCompensationFormatted:
|
||||||
formatExposureCompensation(photoDb.exposureCompensation),
|
formatExposureCompensation(photoDb.exposureCompensation),
|
||||||
|
takenAtNaiveFormatted:
|
||||||
|
formatDateFromPostgresString(photoDb.takenAtNaive),
|
||||||
recipeData: photoDb.recipeData
|
recipeData: photoDb.recipeData
|
||||||
// Legacy check on escaped, string-based JSON
|
// Legacy check on escaped, string-based JSON
|
||||||
? typeof photoDb.recipeData === 'string'
|
? typeof photoDb.recipeData === 'string'
|
||||||
? JSON.parse(photoDb.recipeData)
|
? JSON.parse(photoDb.recipeData)
|
||||||
: photoDb.recipeData
|
: photoDb.recipeData
|
||||||
: undefined,
|
: undefined,
|
||||||
takenAtNaiveFormatted:
|
syncStatus: generatePhotoSyncStatus(photoDb),
|
||||||
formatDateFromPostgresString(photoDb.takenAtNaive),
|
} as Photo;
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const parseCachedPhotoDates = (photo: Photo) => ({
|
export const parseCachedPhotoDates = (photo: Photo) => ({
|
||||||
|
|||||||
@ -1,13 +0,0 @@
|
|||||||
import { MAKE_FUJIFILM } from '@/platforms/fujifilm';
|
|
||||||
import { Photo } from '.';
|
|
||||||
|
|
||||||
export const UPDATED_BEFORE_01 = new Date('2024-06-16');
|
|
||||||
// UTC 2025-02-24 05:30:00
|
|
||||||
export const UPDATED_BEFORE_02 = new Date(Date.UTC(2025, 1, 24, 5, 30, 0));
|
|
||||||
|
|
||||||
export const isPhotoOutdated = (photo: Photo) => {
|
|
||||||
return photo.updatedAt < UPDATED_BEFORE_01 || (
|
|
||||||
photo.updatedAt < UPDATED_BEFORE_02 &&
|
|
||||||
photo.make === MAKE_FUJIFILM
|
|
||||||
);
|
|
||||||
};
|
|
||||||
87
src/photo/sync.ts
Normal file
87
src/photo/sync.ts
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import { MAKE_FUJIFILM } from '@/platforms/fujifilm';
|
||||||
|
import { Photo, PhotoDb } from '.';
|
||||||
|
import {
|
||||||
|
AI_TEXT_AUTO_GENERATED_FIELDS,
|
||||||
|
AI_TEXT_GENERATION_ENABLED,
|
||||||
|
} from '@/app/config';
|
||||||
|
import { AiAutoGeneratedField } from './ai';
|
||||||
|
|
||||||
|
export interface PhotoSyncStatus {
|
||||||
|
isOutdated: boolean;
|
||||||
|
missingAiTextFields: AiAutoGeneratedField[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SYNC_QUERY_LIMIT = 1000;
|
||||||
|
|
||||||
|
export const UPDATED_BEFORE_01 = new Date('2024-06-16');
|
||||||
|
// UTC 2025-02-24 05:30:00
|
||||||
|
export const UPDATED_BEFORE_02 = new Date(Date.UTC(2025, 1, 24, 5, 30, 0));
|
||||||
|
|
||||||
|
const isPhotoOutdated = (photo: PhotoDb) =>
|
||||||
|
photo.updatedAt < UPDATED_BEFORE_01 || (
|
||||||
|
photo.updatedAt < UPDATED_BEFORE_02 &&
|
||||||
|
photo.make === MAKE_FUJIFILM
|
||||||
|
);
|
||||||
|
|
||||||
|
const getMissingAiTextFields = ({
|
||||||
|
title,
|
||||||
|
caption,
|
||||||
|
tags,
|
||||||
|
semanticDescription,
|
||||||
|
}: PhotoDb | Photo): AiAutoGeneratedField[] =>
|
||||||
|
AI_TEXT_GENERATION_ENABLED
|
||||||
|
? AI_TEXT_AUTO_GENERATED_FIELDS.reduce((fields, field) => {
|
||||||
|
switch (field) {
|
||||||
|
case 'title':
|
||||||
|
return !title ? [...fields, 'title'] : fields;
|
||||||
|
case 'caption':
|
||||||
|
return !caption ? [...fields, 'caption'] : fields;
|
||||||
|
case 'tags':
|
||||||
|
return (tags ?? []).length === 0 ? [...fields, 'tags'] : fields;
|
||||||
|
case 'semantic':
|
||||||
|
return !semanticDescription ? [...fields, 'semantic'] : fields;
|
||||||
|
}
|
||||||
|
}, [] as AiAutoGeneratedField[])
|
||||||
|
: [];
|
||||||
|
|
||||||
|
export const generatePhotoSyncStatus = (photo: PhotoDb): PhotoSyncStatus => ({
|
||||||
|
isOutdated: isPhotoOutdated(photo),
|
||||||
|
missingAiTextFields: getMissingAiTextFields(photo),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const photoNeedsToBeSynced = (photo: Photo) =>
|
||||||
|
photo.syncStatus.isOutdated ||
|
||||||
|
photo.syncStatus.missingAiTextFields.length > 0;
|
||||||
|
|
||||||
|
export const getPhotoSyncStatusText = (photo: Photo) => {
|
||||||
|
const { isOutdated, missingAiTextFields } = photo.syncStatus;
|
||||||
|
const text: string[] = [];
|
||||||
|
if (isOutdated) {
|
||||||
|
text.push('Outdated Data');
|
||||||
|
} else if (missingAiTextFields.length > 0) {
|
||||||
|
const missingFieldsText = missingAiTextFields
|
||||||
|
.map(field => field.toLocaleUpperCase())
|
||||||
|
.join(', ');
|
||||||
|
text.push(`Missing AI Text (${missingFieldsText})`);
|
||||||
|
}
|
||||||
|
return text.join(' and ');
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getPhotosSyncStatusText = (photos: Photo[]) => {
|
||||||
|
const statusText = [] as string[];
|
||||||
|
|
||||||
|
const photosCountOutdated = photos.filter(
|
||||||
|
photo => photo.syncStatus.isOutdated,
|
||||||
|
).length;
|
||||||
|
const photosCountMissingAiText = photos.filter(
|
||||||
|
photo => photo.syncStatus.missingAiTextFields.length > 0,
|
||||||
|
).length;
|
||||||
|
|
||||||
|
if (photosCountOutdated > 0) {
|
||||||
|
statusText.push(`${photosCountOutdated} outdated`);
|
||||||
|
}
|
||||||
|
if (photosCountMissingAiText > 0) {
|
||||||
|
statusText.push(`${photosCountMissingAiText} missing AI text`);
|
||||||
|
}
|
||||||
|
return statusText.join(', ');
|
||||||
|
};
|
||||||
@ -13,7 +13,6 @@ import { cleanUpAiTextResponse } from '@/photo/ai';
|
|||||||
const redis = HAS_REDIS_STORAGE ? Redis.fromEnv() : undefined;
|
const redis = HAS_REDIS_STORAGE ? Redis.fromEnv() : undefined;
|
||||||
|
|
||||||
const RATE_LIMIT_IDENTIFIER = 'openai-image-query';
|
const RATE_LIMIT_IDENTIFIER = 'openai-image-query';
|
||||||
const RATE_LIMIT_MAX_QUERIES_PER_HOUR = 100;
|
|
||||||
const MODEL = 'gpt-4o';
|
const MODEL = 'gpt-4o';
|
||||||
|
|
||||||
const openai = AI_TEXT_GENERATION_ENABLED
|
const openai = AI_TEXT_GENERATION_ENABLED
|
||||||
@ -21,18 +20,24 @@ const openai = AI_TEXT_GENERATION_ENABLED
|
|||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const ratelimit = redis
|
const ratelimit = redis
|
||||||
? new Ratelimit({
|
? {
|
||||||
redis,
|
basic: new Ratelimit({
|
||||||
limiter: Ratelimit.slidingWindow(RATE_LIMIT_MAX_QUERIES_PER_HOUR, '1h'),
|
redis,
|
||||||
})
|
limiter: Ratelimit.slidingWindow(100, '1h'),
|
||||||
|
}),
|
||||||
|
batch: new Ratelimit({
|
||||||
|
redis,
|
||||||
|
limiter: Ratelimit.slidingWindow(1200, '1d'),
|
||||||
|
}),
|
||||||
|
}
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
// Allows 100 requests per hour
|
const checkRateLimitAndThrow = async (isBatch?: boolean) => {
|
||||||
const checkRateLimitAndThrow = async () => {
|
|
||||||
if (ratelimit) {
|
if (ratelimit) {
|
||||||
let success = false;
|
let success = false;
|
||||||
try {
|
try {
|
||||||
success = (await ratelimit.limit(RATE_LIMIT_IDENTIFIER)).success;
|
const limiter = isBatch ? ratelimit.batch : ratelimit.basic;
|
||||||
|
success = (await limiter.limit(RATE_LIMIT_IDENTIFIER)).success;
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error('Failed to rate limit OpenAI', e);
|
console.error('Failed to rate limit OpenAI', e);
|
||||||
throw new Error('Failed to rate limit OpenAI');
|
throw new Error('Failed to rate limit OpenAI');
|
||||||
@ -92,8 +97,9 @@ export const streamOpenAiImageQuery = async (
|
|||||||
export const generateOpenAiImageQuery = async (
|
export const generateOpenAiImageQuery = async (
|
||||||
imageBase64: string,
|
imageBase64: string,
|
||||||
query: string,
|
query: string,
|
||||||
|
isBatch?: boolean,
|
||||||
) => {
|
) => {
|
||||||
await checkRateLimitAndThrow();
|
await checkRateLimitAndThrow(isBatch);
|
||||||
|
|
||||||
const args = getImageTextArgs(imageBase64, query);
|
const args = getImageTextArgs(imageBase64, query);
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { RefObject, useCallback, useEffect, useState } from 'react';
|
import { RefObject, useCallback, useMemo, useState } from 'react';
|
||||||
import { isElementEntirelyInViewport } from '@/utility/dom';
|
|
||||||
import useClickInsideOutside from '@/utility/useClickInsideOutside';
|
import useClickInsideOutside from '@/utility/useClickInsideOutside';
|
||||||
|
import useScrollIntoView from '@/utility/useScrollIntoView';
|
||||||
|
|
||||||
export default function useRecipeOverlay({
|
export default function useRecipeOverlay({
|
||||||
ref,
|
ref,
|
||||||
@ -19,16 +19,18 @@ export default function useRecipeOverlay({
|
|||||||
setIsShowingRecipeOverlay(current => !current),
|
setIsShowingRecipeOverlay(current => !current),
|
||||||
[]);
|
[]);
|
||||||
|
|
||||||
|
const htmlElements = useMemo(() =>
|
||||||
|
[ref, ...refTriggers], [ref, refTriggers]);
|
||||||
|
|
||||||
useClickInsideOutside({
|
useClickInsideOutside({
|
||||||
htmlElements: [ref, ...refTriggers],
|
htmlElements,
|
||||||
onClickOutside: hideRecipeOverlay,
|
onClickOutside: hideRecipeOverlay,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useScrollIntoView({
|
||||||
if (isShowingRecipeOverlay && !isElementEntirelyInViewport(ref?.current)) {
|
ref,
|
||||||
ref?.current?.scrollIntoView({ behavior: 'smooth' });
|
shouldScrollIntoView: isShowingRecipeOverlay,
|
||||||
}
|
});
|
||||||
}, [ref, isShowingRecipeOverlay]);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isShowingRecipeOverlay,
|
isShowingRecipeOverlay,
|
||||||
|
|||||||
@ -151,7 +151,7 @@ export default function AppStateProvider({
|
|||||||
if (userEmail) {
|
if (userEmail) {
|
||||||
storeAuthEmailCookie(userEmail);
|
storeAuthEmailCookie(userEmail);
|
||||||
}
|
}
|
||||||
}, [userEmail, adminData]);
|
}, [userEmail]);
|
||||||
|
|
||||||
const registerAdminUpdate = useCallback(() =>
|
const registerAdminUpdate = useCallback(() =>
|
||||||
setAdminUpdateTimes(updates => [...updates, new Date()])
|
setAdminUpdateTimes(updates => [...updates, new Date()])
|
||||||
|
|||||||
@ -22,11 +22,11 @@ export const parameterize = (
|
|||||||
) =>
|
) =>
|
||||||
string
|
string
|
||||||
.trim()
|
.trim()
|
||||||
// Replaces spaces, underscores, slashes,and dashes with dashes
|
// Replace spaces, underscores, slashes, pluses, dashes with dashes
|
||||||
.replaceAll(/[\s_–—]/gi, '-')
|
.replaceAll(/[\s_–—+]/gi, '-')
|
||||||
// Removes punctuation
|
// Remove punctuation
|
||||||
.replaceAll(/['"!@#$%^&*()_+=[\]{};:/?,<>\\/|`~]/gi, '')
|
.replaceAll(/['"!@#$%^&*()=[\]{};:/?,<>\\/|`~]/gi, '')
|
||||||
// Removes all non-alphanumeric characters
|
// Removes non-alphanumeric characters, if configured
|
||||||
.replaceAll(
|
.replaceAll(
|
||||||
shouldRemoveNonAlphanumeric
|
shouldRemoveNonAlphanumeric
|
||||||
? /([^a-z0-9-])/gi
|
? /([^a-z0-9-])/gi
|
||||||
|
|||||||
20
src/utility/useScrollIntoView.ts
Normal file
20
src/utility/useScrollIntoView.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { RefObject, useEffect } from 'react';
|
||||||
|
import { isElementEntirelyInViewport } from '@/utility/dom';
|
||||||
|
|
||||||
|
export default function useScrollIntoView({
|
||||||
|
ref,
|
||||||
|
shouldScrollIntoView,
|
||||||
|
}: {
|
||||||
|
ref?: RefObject<HTMLElement | null>
|
||||||
|
shouldScrollIntoView?: boolean
|
||||||
|
}) {
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
ref?.current &&
|
||||||
|
!isElementEntirelyInViewport(ref.current) &&
|
||||||
|
shouldScrollIntoView
|
||||||
|
) {
|
||||||
|
ref.current.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
}, [ref, shouldScrollIntoView]);
|
||||||
|
}
|
||||||
@ -1,6 +1,4 @@
|
|||||||
import { useState } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
import { useEffect } from 'react';
|
|
||||||
|
|
||||||
export default function useVisualViewportHeight() {
|
export default function useVisualViewportHeight() {
|
||||||
const [viewportHeight, setViewportHeight] = useState<number>();
|
const [viewportHeight, setViewportHeight] = useState<number>();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user