diff --git a/__tests__/github.test.ts b/__tests__/github.test.ts
index 2f7a0442..6aa8a2a0 100644
--- a/__tests__/github.test.ts
+++ b/__tests__/github.test.ts
@@ -1,47 +1,44 @@
-import { getGitHubMetaWithFallback, getGitHubPublicFork } from '@/admin/github';
-import { TEMPLATE_BASE_OWNER, TEMPLATE_BASE_REPO } from '@/site/config';
+import {
+ getGitHubMeta,
+ getGitHubPublicFork,
+} from '@/platforms/github';
+import { TEMPLATE_REPO_OWNER, TEMPLATE_REPO_NAME } from '@/app-core/config';
describe('GitHub', () => {
it('fetches base repo meta', async () => {
- const meta = await getGitHubMetaWithFallback({
- owner: TEMPLATE_BASE_OWNER,
- repo: TEMPLATE_BASE_REPO,
+ const meta = await getGitHubMeta({
+ owner: TEMPLATE_REPO_OWNER,
+ repo: TEMPLATE_REPO_NAME,
});
expect(meta).toBeDefined();
- expect(meta.url).toBeDefined();
+ expect(meta.urlRepo).toBeDefined();
expect(meta.isForkedFromBase).toEqual(false);
- expect(meta.label).toBeDefined();
- expect(meta.description).toBeDefined();
expect(meta.isBehind).toEqual(false);
expect(meta.isBaseRepo).toBe(true);
});
it('fetches fork meta', async () => {
const fork = await getGitHubPublicFork();
- const metaFork = await getGitHubMetaWithFallback(fork);
+ const metaFork = await getGitHubMeta(fork);
expect(metaFork.isForkedFromBase).toEqual(true);
});
it('handles nonexistent repos', async () => {
- const meta = await getGitHubMetaWithFallback({
+ const meta = await getGitHubMeta({
owner: 'nonexistent',
repo: 'nonexistent',
});
expect(meta).toBeDefined();
- expect(meta.url).toBeDefined();
+ expect(meta.urlRepo).toBeDefined();
expect(meta.isForkedFromBase).toEqual(false);
- expect(meta.label).toEqual('Unknown');
- expect(meta.description).toEqual('Unknown');
expect(meta.isBehind).toBeUndefined();
});
it('handles fetch errors', async () => {
- const meta = await getGitHubMetaWithFallback({
+ const meta = await getGitHubMeta({
owner: 'gibberish / / *',
repo: 'bad text for a url.com',
});
expect(meta).toBeDefined();
- expect(meta.url).toBeDefined();
+ expect(meta.urlRepo).toBeDefined();
expect(meta.isForkedFromBase).toEqual(false);
- expect(meta.label).toEqual('Unknown');
- expect(meta.description).toEqual('Unknown');
expect(meta.isBehind).toBeUndefined();
});
});
diff --git a/__tests__/path.test.ts b/__tests__/path.test.ts
index 5aeead2f..77f2c737 100644
--- a/__tests__/path.test.ts
+++ b/__tests__/path.test.ts
@@ -12,7 +12,7 @@ import {
isPathProtected,
isPathTag,
isPathTagPhoto,
-} from '@/site/paths';
+} from '@/app-core/paths';
import { TAG_HIDDEN } from '@/tag';
const PHOTO_ID = 'UsKSGcbt';
diff --git a/package.json b/package.json
index 2e9a2012..66a658aa 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "exif-photo-blog",
"scripts": {
- "dev": "next dev",
+ "dev": "next dev --turbo",
"build": "next build",
"start": "next start",
"lint": "next lint",
@@ -17,19 +17,19 @@
"@radix-ui/react-tooltip": "^1.1.8",
"@radix-ui/react-visually-hidden": "^1.1.2",
"@upstash/ratelimit": "^2.0.5",
- "@vercel/analytics": "^1.4.1",
+ "@vercel/analytics": "^1.5.0",
"@vercel/blob": "^0.27.1",
"@vercel/kv": "^3.0.0",
- "@vercel/speed-insights": "^1.1.0",
- "ai": "^4.1.26",
+ "@vercel/speed-insights": "^1.2.0",
+ "ai": "^4.1.34",
"camelcase-keys": "^9.1.3",
"cmdk": "^1.0.4",
"date-fns": "^4.1.0",
"date-fns-tz": "^3.2.0",
"exifr": "^7.1.3",
- "framer-motion": "^12.4.1",
+ "framer-motion": "^12.4.2",
"nanoid": "^5.0.9",
- "next": "15.1.6",
+ "next": "15.1.7",
"next-auth": "5.0.0-beta.25",
"next-themes": "^0.4.4",
"pg": "^8.13.1",
@@ -45,10 +45,10 @@
"viewerjs": "^1.11.7"
},
"devDependencies": {
- "@next/bundle-analyzer": "15.1.6",
+ "@next/bundle-analyzer": "15.1.7",
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/forms": "^0.5.10",
- "@tailwindcss/postcss": "^4.0.5",
+ "@tailwindcss/postcss": "^4.0.6",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0",
@@ -61,11 +61,11 @@
"clsx": "^2.1.1",
"cross-fetch": "^4.1.0",
"eslint": "9.20.0",
- "eslint-config-next": "15.1.6",
+ "eslint-config-next": "15.1.7",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
- "postcss": "8.5.1",
- "tailwindcss": "4.0.5",
+ "postcss": "8.5.2",
+ "tailwindcss": "4.0.6",
"ts-node": "^10.9.2",
"typescript": "5.7.3"
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index d9d808fa..84632744 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -33,8 +33,8 @@ importers:
specifier: ^2.0.5
version: 2.0.5(@upstash/redis@1.34.3)
'@vercel/analytics':
- specifier: ^1.4.1
- version: 1.4.1(next@15.1.6(@babel/core@7.24.5)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)(svelte@4.2.17)(vue@3.4.27(typescript@5.7.3))
+ specifier: ^1.5.0
+ version: 1.5.0(next@15.1.7(@babel/core@7.24.5)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)(svelte@4.2.17)(vue@3.4.27(typescript@5.7.3))
'@vercel/blob':
specifier: ^0.27.1
version: 0.27.1
@@ -42,11 +42,11 @@ importers:
specifier: ^3.0.0
version: 3.0.0
'@vercel/speed-insights':
- specifier: ^1.1.0
- version: 1.1.0(next@15.1.6(@babel/core@7.24.5)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)(svelte@4.2.17)(vue@3.4.27(typescript@5.7.3))
+ specifier: ^1.2.0
+ version: 1.2.0(next@15.1.7(@babel/core@7.24.5)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)(svelte@4.2.17)(vue@3.4.27(typescript@5.7.3))
ai:
- specifier: ^4.1.26
- version: 4.1.26(react@19.0.0)(zod@3.23.8)
+ specifier: ^4.1.34
+ version: 4.1.34(react@19.0.0)(zod@3.23.8)
camelcase-keys:
specifier: ^9.1.3
version: 9.1.3
@@ -63,17 +63,17 @@ importers:
specifier: ^7.1.3
version: 7.1.3
framer-motion:
- specifier: ^12.4.1
- version: 12.4.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+ specifier: ^12.4.2
+ version: 12.4.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
nanoid:
specifier: ^5.0.9
version: 5.0.9
next:
- specifier: 15.1.6
- version: 15.1.6(@babel/core@7.24.5)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+ specifier: 15.1.7
+ version: 15.1.7(@babel/core@7.24.5)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
next-auth:
specifier: 5.0.0-beta.25
- version: 5.0.0-beta.25(next@15.1.6(@babel/core@7.24.5)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)
+ version: 5.0.0-beta.25(next@15.1.7(@babel/core@7.24.5)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)
next-themes:
specifier: ^0.4.4
version: 0.4.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
@@ -112,17 +112,17 @@ importers:
version: 1.11.7
devDependencies:
'@next/bundle-analyzer':
- specifier: 15.1.6
- version: 15.1.6
+ specifier: 15.1.7
+ version: 15.1.7
'@tailwindcss/container-queries':
specifier: ^0.1.1
- version: 0.1.1(tailwindcss@4.0.5)
+ version: 0.1.1(tailwindcss@4.0.6)
'@tailwindcss/forms':
specifier: ^0.5.10
- version: 0.5.10(tailwindcss@4.0.5)
+ version: 0.5.10(tailwindcss@4.0.6)
'@tailwindcss/postcss':
- specifier: ^4.0.5
- version: 4.0.5
+ specifier: ^4.0.6
+ version: 4.0.6
'@testing-library/dom':
specifier: ^10.4.0
version: 10.4.0
@@ -160,8 +160,8 @@ importers:
specifier: 9.20.0
version: 9.20.0(jiti@2.4.2)
eslint-config-next:
- specifier: 15.1.6
- version: 15.1.6(eslint@9.20.0(jiti@2.4.2))(typescript@5.7.3)
+ specifier: 15.1.7
+ version: 15.1.7(eslint@9.20.0(jiti@2.4.2))(typescript@5.7.3)
jest:
specifier: ^29.7.0
version: 29.7.0(@types/node@22.13.1)(ts-node@10.9.2(@types/node@22.13.1)(typescript@5.7.3))
@@ -169,11 +169,11 @@ importers:
specifier: ^29.7.0
version: 29.7.0
postcss:
- specifier: 8.5.1
- version: 8.5.1
+ specifier: 8.5.2
+ version: 8.5.2
tailwindcss:
- specifier: 4.0.5
- version: 4.0.5
+ specifier: 4.0.6
+ version: 4.0.6
ts-node:
specifier: ^10.9.2
version: 10.9.2(@types/node@22.13.1)(typescript@5.7.3)
@@ -205,8 +205,8 @@ packages:
resolution: {integrity: sha512-q1PJEZ0qD9rVR+8JFEd01/QM++csMT5UVwYXSN2u54BrVw/D8TZLTeg2FEfKK00DgAx0UtWd8XOhhwITP9BT5g==}
engines: {node: '>=18'}
- '@ai-sdk/react@1.1.10':
- resolution: {integrity: sha512-RTkEVYKq7qO6Ct3XdVTgbaCTyjX+q1HLqb+t2YvZigimzMCQbHkpZCtt2H2Fgpt1UOTqnAAlXjEAgTW3X60Y9g==}
+ '@ai-sdk/react@1.1.11':
+ resolution: {integrity: sha512-vfjZ7w2M+Me83HTMMrnnrmXotz39UDCMd27YQSrvt2f1YCLPloVpLhP+Y9TLZeFE/QiiRCrPYLDQm6aQJYJ9PQ==}
engines: {node: '>=18'}
peerDependencies:
react: ^18 || ^19 || ^19.0.0-rc
@@ -217,8 +217,8 @@ packages:
zod:
optional: true
- '@ai-sdk/ui-utils@1.1.10':
- resolution: {integrity: sha512-x+A1Nfy8RTSatdCe+7nRpHAZVzPFB6H+r+2JKoapSvrwsu9mw2pAbmFgV8Zaj94TsmUdTlO0/j97e63f+yYuWg==}
+ '@ai-sdk/ui-utils@1.1.11':
+ resolution: {integrity: sha512-1SC9W4VZLcJtxHRv4Y0aX20EFeaEP6gUvVqoKLBBtMLOgtcZrv/F/HQRjGavGugiwlS3dsVza4X+E78fiwtlTA==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.0.0
@@ -866,59 +866,59 @@ packages:
'@jridgewell/trace-mapping@0.3.9':
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
- '@next/bundle-analyzer@15.1.6':
- resolution: {integrity: sha512-hGzQyDqJzFHcHNCyTqM3o05BpVq5tGnRODccZBVJDBf5Miv/26UJPMB0wh9L9j3ylgHC+0/v8BaBnBBek1rC6Q==}
+ '@next/bundle-analyzer@15.1.7':
+ resolution: {integrity: sha512-tESiAwTUEpzzxKMLDbQuPHvD+PFDjY+0O3R4T5bpjIo0cr5fvppbbllQbtksQbBEquT55Eu8JmDoOlc9YFv6Kw==}
- '@next/env@15.1.6':
- resolution: {integrity: sha512-d9AFQVPEYNr+aqokIiPLNK/MTyt3DWa/dpKveiAaVccUadFbhFEvY6FXYX2LJO2Hv7PHnLBu2oWwB4uBuHjr/w==}
+ '@next/env@15.1.7':
+ resolution: {integrity: sha512-d9jnRrkuOH7Mhi+LHav2XW91HOgTAWHxjMPkXMGBc9B2b7614P7kjt8tAplRvJpbSt4nbO1lugcT/kAaWzjlLQ==}
- '@next/eslint-plugin-next@15.1.6':
- resolution: {integrity: sha512-+slMxhTgILUntZDGNgsKEYHUvpn72WP1YTlkmEhS51vnVd7S9jEEy0n9YAMcI21vUG4akTw9voWH02lrClt/yw==}
+ '@next/eslint-plugin-next@15.1.7':
+ resolution: {integrity: sha512-kRP7RjSxfTO13NE317ek3mSGzoZlI33nc/i5hs1KaWpK+egs85xg0DJ4p32QEiHnR0mVjuUfhRIun7awqfL7pQ==}
- '@next/swc-darwin-arm64@15.1.6':
- resolution: {integrity: sha512-u7lg4Mpl9qWpKgy6NzEkz/w0/keEHtOybmIl0ykgItBxEM5mYotS5PmqTpo+Rhg8FiOiWgwr8USxmKQkqLBCrw==}
+ '@next/swc-darwin-arm64@15.1.7':
+ resolution: {integrity: sha512-hPFwzPJDpA8FGj7IKV3Yf1web3oz2YsR8du4amKw8d+jAOHfYHYFpMkoF6vgSY4W6vB29RtZEklK9ayinGiCmQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
- '@next/swc-darwin-x64@15.1.6':
- resolution: {integrity: sha512-x1jGpbHbZoZ69nRuogGL2MYPLqohlhnT9OCU6E6QFewwup+z+M6r8oU47BTeJcWsF2sdBahp5cKiAcDbwwK/lg==}
+ '@next/swc-darwin-x64@15.1.7':
+ resolution: {integrity: sha512-2qoas+fO3OQKkU0PBUfwTiw/EYpN+kdAx62cePRyY1LqKtP09Vp5UcUntfZYajop5fDFTjSxCHfZVRxzi+9FYQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
- '@next/swc-linux-arm64-gnu@15.1.6':
- resolution: {integrity: sha512-jar9sFw0XewXsBzPf9runGzoivajeWJUc/JkfbLTC4it9EhU8v7tCRLH7l5Y1ReTMN6zKJO0kKAGqDk8YSO2bg==}
+ '@next/swc-linux-arm64-gnu@15.1.7':
+ resolution: {integrity: sha512-sKLLwDX709mPdzxMnRIXLIT9zaX2w0GUlkLYQnKGoXeWUhcvpCrK+yevcwCJPdTdxZEUA0mOXGLdPsGkudGdnA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
- '@next/swc-linux-arm64-musl@15.1.6':
- resolution: {integrity: sha512-+n3u//bfsrIaZch4cgOJ3tXCTbSxz0s6brJtU3SzLOvkJlPQMJ+eHVRi6qM2kKKKLuMY+tcau8XD9CJ1OjeSQQ==}
+ '@next/swc-linux-arm64-musl@15.1.7':
+ resolution: {integrity: sha512-zblK1OQbQWdC8fxdX4fpsHDw+VSpBPGEUX4PhSE9hkaWPrWoeIJn+baX53vbsbDRaDKd7bBNcXRovY1hEhFd7w==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
- '@next/swc-linux-x64-gnu@15.1.6':
- resolution: {integrity: sha512-SpuDEXixM3PycniL4iVCLyUyvcl6Lt0mtv3am08sucskpG0tYkW1KlRhTgj4LI5ehyxriVVcfdoxuuP8csi3kQ==}
+ '@next/swc-linux-x64-gnu@15.1.7':
+ resolution: {integrity: sha512-GOzXutxuLvLHFDAPsMP2zDBMl1vfUHHpdNpFGhxu90jEzH6nNIgmtw/s1MDwpTOiM+MT5V8+I1hmVFeAUhkbgQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
- '@next/swc-linux-x64-musl@15.1.6':
- resolution: {integrity: sha512-L4druWmdFSZIIRhF+G60API5sFB7suTbDRhYWSjiw0RbE+15igQvE2g2+S973pMGvwN3guw7cJUjA/TmbPWTHQ==}
+ '@next/swc-linux-x64-musl@15.1.7':
+ resolution: {integrity: sha512-WrZ7jBhR7ATW1z5iEQ0ZJfE2twCNSXbpCSaAunF3BKcVeHFADSI/AW1y5Xt3DzTqPF1FzQlwQTewqetAABhZRQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
- '@next/swc-win32-arm64-msvc@15.1.6':
- resolution: {integrity: sha512-s8w6EeqNmi6gdvM19tqKKWbCyOBvXFbndkGHl+c9YrzsLARRdCHsD9S1fMj8gsXm9v8vhC8s3N8rjuC/XrtkEg==}
+ '@next/swc-win32-arm64-msvc@15.1.7':
+ resolution: {integrity: sha512-LDnj1f3OVbou1BqvvXVqouJZKcwq++mV2F+oFHptToZtScIEnhNRJAhJzqAtTE2dB31qDYL45xJwrc+bLeKM2Q==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
- '@next/swc-win32-x64-msvc@15.1.6':
- resolution: {integrity: sha512-6xomMuu54FAFxttYr5PJbEfu96godcxBTRk1OhAvJq0/EnmFU/Ybiax30Snis4vdWZ9LGpf7Roy5fSs7v/5ROQ==}
+ '@next/swc-win32-x64-msvc@15.1.7':
+ resolution: {integrity: sha512-dC01f1quuf97viOfW05/K8XYv2iuBgAxJZl7mbCKEjMgdQl5JjAKJ0D2qMKZCgPWDeFbFT0Q0nYWwytEW0DWTQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
@@ -1515,81 +1515,81 @@ packages:
peerDependencies:
tailwindcss: '>=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1'
- '@tailwindcss/node@4.0.5':
- resolution: {integrity: sha512-ffTz4DX1cgr4XPuqjhm32YV6Lyx58R1CxAAnSFTamg6wXwfk3oWdb6exgAbGesPzvUgicTO0gwUdQGSsg4nNog==}
+ '@tailwindcss/node@4.0.6':
+ resolution: {integrity: sha512-jb6E0WeSq7OQbVYcIJ6LxnZTeC4HjMvbzFBMCrQff4R50HBlo/obmYNk6V2GCUXDeqiXtvtrQgcIbT+/boB03Q==}
- '@tailwindcss/oxide-android-arm64@4.0.5':
- resolution: {integrity: sha512-kK/ik8aIAKWDIEYDZGUCJcnU1qU5sPoMBlVzPvtsUqiV6cSHcnVRUdkcLwKqTeUowzZtjjRiamELLd9Gb0x5BQ==}
+ '@tailwindcss/oxide-android-arm64@4.0.6':
+ resolution: {integrity: sha512-xDbym6bDPW3D2XqQqX3PjqW3CKGe1KXH7Fdkc60sX5ZLVUbzPkFeunQaoP+BuYlLc2cC1FoClrIRYnRzof9Sow==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [android]
- '@tailwindcss/oxide-darwin-arm64@4.0.5':
- resolution: {integrity: sha512-vkbXFv0FfAEbrSa5NBjFEE+xi06ha7mxuxjY8LRn7d7/tBGrAZOEJnnsEbB6M1+x2pGRTjjei0XyTIXdVCglJA==}
+ '@tailwindcss/oxide-darwin-arm64@4.0.6':
+ resolution: {integrity: sha512-1f71/ju/tvyGl5c2bDkchZHy8p8EK/tDHCxlpYJ1hGNvsYihZNurxVpZ0DefpN7cNc9RTT8DjrRoV8xXZKKRjg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
- '@tailwindcss/oxide-darwin-x64@4.0.5':
- resolution: {integrity: sha512-PedA64rHBXEa4e6abBWE4Yj4gHulfPb5T+rBNnX+WGkjjge5Txa2oS99TLmJ5BPDkXXqz/Ba7oweWIDDG7i5NQ==}
+ '@tailwindcss/oxide-darwin-x64@4.0.6':
+ resolution: {integrity: sha512-s/hg/ZPgxFIrGMb0kqyeaqZt505P891buUkSezmrDY6lxv2ixIELAlOcUVTkVh245SeaeEiUVUPiUN37cwoL2g==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
- '@tailwindcss/oxide-freebsd-x64@4.0.5':
- resolution: {integrity: sha512-silz3nuZdEYDfic3v/ooVUQChj9hbxDSee43GCQNwr/iD9L4K/JsZtoNqr0w69pUkvWcKINOGOG0r7WqUqkAeg==}
+ '@tailwindcss/oxide-freebsd-x64@4.0.6':
+ resolution: {integrity: sha512-Z3Wo8FWZnmio8+xlcbb7JUo/hqRMSmhQw8IGIRoRJ7GmLR0C+25Wq+bEX/135xe/yEle2lFkhu9JBHd4wZYiig==}
engines: {node: '>= 10'}
cpu: [x64]
os: [freebsd]
- '@tailwindcss/oxide-linux-arm-gnueabihf@4.0.5':
- resolution: {integrity: sha512-ElneG75XS64B9I2G83A/Hc7EtNVOD5xahs7avq0aeW7mEX6CtMc8m8RCXMn3jGhz8enFE52l6QU0wO7iVkEtXQ==}
+ '@tailwindcss/oxide-linux-arm-gnueabihf@4.0.6':
+ resolution: {integrity: sha512-SNSwkkim1myAgmnbHs4EjXsPL7rQbVGtjcok5EaIzkHkCAVK9QBQsWeP2Jm2/JJhq4wdx8tZB9Y7psMzHYWCkA==}
engines: {node: '>= 10'}
cpu: [arm]
os: [linux]
- '@tailwindcss/oxide-linux-arm64-gnu@4.0.5':
- resolution: {integrity: sha512-8yoXpWTeIFaByUaKy2qRAppznLVaDHP9xYCAbS3FG7+uUwHi8CHE4TcomM7eyamo0U7dbUIDgKMGoAX5s2iVrA==}
+ '@tailwindcss/oxide-linux-arm64-gnu@4.0.6':
+ resolution: {integrity: sha512-tJ+mevtSDMQhKlwCCuhsFEFg058kBiSy4TkoeBG921EfrHKmexOaCyFKYhVXy4JtkaeeOcjJnCLasEeqml4i+Q==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
- '@tailwindcss/oxide-linux-arm64-musl@4.0.5':
- resolution: {integrity: sha512-BDlVSiiJ08GRz9KKnXgaPFs2fkukPF3pym6uK3oWEKW45jKlVGgybLqulcV5nLEqREOuyq4Rn4vnZss4/bbQ/g==}
+ '@tailwindcss/oxide-linux-arm64-musl@4.0.6':
+ resolution: {integrity: sha512-IoArz1vfuTR4rALXMUXI/GWWfx2EaO4gFNtBNkDNOYhlTD4NVEwE45nbBoojYiTulajI4c2XH8UmVEVJTOJKxA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
- '@tailwindcss/oxide-linux-x64-gnu@4.0.5':
- resolution: {integrity: sha512-DYgieNDRkTy69bWPgdsc47nAXa74P63P/RetUwYM9vYj5USyOfHCEcqIthkCuYw3dXKBhjgwe697TmL2g2jpAw==}
+ '@tailwindcss/oxide-linux-x64-gnu@4.0.6':
+ resolution: {integrity: sha512-QtsUfLkEAeWAC3Owx9Kg+7JdzE+k9drPhwTAXbXugYB9RZUnEWWx5x3q/au6TvUYcL+n0RBqDEO2gucZRvRFgQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
- '@tailwindcss/oxide-linux-x64-musl@4.0.5':
- resolution: {integrity: sha512-z2RzUvOQl0ZqrZqmCFP53tJbBXQ3UmLD/E6J7+q0e+4VaFnXCcIYTfQbHgI8f3fash+q6gK80Ko/ywEQ+bvv6Q==}
+ '@tailwindcss/oxide-linux-x64-musl@4.0.6':
+ resolution: {integrity: sha512-QthvJqIji2KlGNwLcK/PPYo7w1Wsi/8NK0wAtRGbv4eOPdZHkQ9KUk+oCoP20oPO7i2a6X1aBAFQEL7i08nNMA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
- '@tailwindcss/oxide-win32-arm64-msvc@4.0.5':
- resolution: {integrity: sha512-ho1dJ4o5Q8nAOxdMkbfBu5aSqI+/bzQ0jEeHcXaEdEJzf2fSWs3HY7bIKtE6vQS8c4SmSBvls7IhGPuJxNg+2Q==}
+ '@tailwindcss/oxide-win32-arm64-msvc@4.0.6':
+ resolution: {integrity: sha512-+oka+dYX8jy9iP00DJ9Y100XsqvbqR5s0yfMZJuPR1H/lDVtDfsZiSix1UFBQ3X1HWxoEEl6iXNJHWd56TocVw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
- '@tailwindcss/oxide-win32-x64-msvc@4.0.5':
- resolution: {integrity: sha512-yjw6JhtyDXr+G0aZrj3L3NlEV7CobSqOdPyfo6G3d91WEZ5b8PyGm86IAreX08Jp9DChGXEd53gWysVpWCTs+w==}
+ '@tailwindcss/oxide-win32-x64-msvc@4.0.6':
+ resolution: {integrity: sha512-+o+juAkik4p8Ue/0LiflQXPmVatl6Av3LEZXpBTfg4qkMIbZdhCGWFzHdt2NjoMiLOJCFDddoV6GYaimvK1Olw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
- '@tailwindcss/oxide@4.0.5':
- resolution: {integrity: sha512-iWGyOCu0TuzvCBisWbGv2K9+7QCfE0ztgtrZOvb9iF7V7ChVkD15Obe3HevZrhjngAc34jDA+OMSuSvkrpTy4A==}
+ '@tailwindcss/oxide@4.0.6':
+ resolution: {integrity: sha512-lVyKV2y58UE9CeKVcYykULe9QaE1dtKdxDEdrTPIdbzRgBk6bdxHNAoDqvcqXbIGXubn3VOl1O/CFF77v/EqSA==}
engines: {node: '>= 10'}
- '@tailwindcss/postcss@4.0.5':
- resolution: {integrity: sha512-U7IPb+KMASETtUvISwePM+1h+jLQspXf2ncfX/LmP/4AaH7b7DJQhqXzDCaJQd/MIh54dRUO93i9q4+Xm7dlVg==}
+ '@tailwindcss/postcss@4.0.6':
+ resolution: {integrity: sha512-noTaGPHjGCXTCc487TWnfAEN0VMjqDAecssWDOsfxV2hFrcZR0AHthX7IdY/0xHTg/EtpmIPdssddlZ5/B7JnQ==}
'@testing-library/dom@10.4.0':
resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==}
@@ -1769,8 +1769,8 @@ packages:
'@upstash/redis@1.34.3':
resolution: {integrity: sha512-VT25TyODGy/8ljl7GADnJoMmtmJ1F8d84UXfGonRRF8fWYJz7+2J6GzW+a6ETGtk4OyuRTt7FRSvFG5GvrfSdQ==}
- '@vercel/analytics@1.4.1':
- resolution: {integrity: sha512-ekpL4ReX2TH3LnrRZTUKjHHNpNy9S1I7QmS+g/RQXoSUQ8ienzosuX7T9djZ/s8zPhBx1mpHP/Rw5875N+zQIQ==}
+ '@vercel/analytics@1.5.0':
+ resolution: {integrity: sha512-MYsBzfPki4gthY5HnYN7jgInhAZ7Ac1cYDoRWFomwGHWEX7odTEzbtg9kf/QSo7XEsEAqlQugA6gJ2WS2DEa3g==}
peerDependencies:
'@remix-run/react': ^2
'@sveltejs/kit': ^1 || ^2
@@ -1803,8 +1803,8 @@ packages:
resolution: {integrity: sha512-pKT8fRnfyYk2MgvyB6fn6ipJPCdfZwiKDdw7vB+HL50rjboEBHDVBEcnwfkEpVSp2AjNtoaOUH7zG+bVC/rvSg==}
engines: {node: '>=14.6'}
- '@vercel/speed-insights@1.1.0':
- resolution: {integrity: sha512-rAXxuhhO4mlRGC9noa5F7HLMtGg8YF1zAN6Pjd1Ny4pII4cerhtwSG4vympbCl+pWkH7nBS9kVXRD4FAn54dlg==}
+ '@vercel/speed-insights@1.2.0':
+ resolution: {integrity: sha512-y9GVzrUJ2xmgtQlzFP2KhVRoCglwfRQgjyfY607aU0hh0Un6d0OUyrJkjuAlsV18qR4zfoFPs/BiIj9YDS6Wzw==}
peerDependencies:
'@sveltejs/kit': ^1 || ^2
next: '>= 13'
@@ -1885,8 +1885,8 @@ packages:
resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
engines: {node: '>= 6.0.0'}
- ai@4.1.26:
- resolution: {integrity: sha512-Mww6mJbGwmMK0qAKR67WfVK1WyaUjfFlPZ2rhUUmDns3WhI+DVgMM7gLmuo0rA+I5qq69g7YE1OCgUwMRKKjMw==}
+ ai@4.1.34:
+ resolution: {integrity: sha512-9IB5duz6VbXvjibqNrvKz6++PwE8Ui5UfbOC9/CtcQN5Z9sudUQErss+maj7ptoPysD2NPjj99e0Hp183Cz5LQ==}
engines: {node: '>=18'}
peerDependencies:
react: ^18 || ^19 || ^19.0.0-rc
@@ -2378,10 +2378,6 @@ packages:
emoji-regex@9.2.2:
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
- enhanced-resolve@5.16.1:
- resolution: {integrity: sha512-4U5pNsuDl0EhuZpq46M5xPslstkviJuhrdobaRDBk2Jy2KO37FDAJl4lb2KlNabxT0m4MTK2UHNrsAcphE8nyw==}
- engines: {node: '>=10.13.0'}
-
enhanced-resolve@5.18.1:
resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==}
engines: {node: '>=10.13.0'}
@@ -2445,8 +2441,8 @@ packages:
engines: {node: '>=6.0'}
hasBin: true
- eslint-config-next@15.1.6:
- resolution: {integrity: sha512-Wd1uy6y7nBbXUSg9QAuQ+xYEKli5CgUhLjz1QHW11jLDis5vK5XB3PemL6jEmy7HrdhaRFDz+GTZ/3FoH+EUjg==}
+ eslint-config-next@15.1.7:
+ resolution: {integrity: sha512-zXoMnYUIy3XHaAoOhrcYkT9UQWvXqWju2K7NNsmb5wd/7XESDwof61eUdW4QhERr3eJ9Ko/vnXqIrj8kk/drYw==}
peerDependencies:
eslint: ^7.23.0 || ^8.0.0 || ^9.0.0
typescript: '>=3.3.1'
@@ -2646,8 +2642,8 @@ packages:
resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==}
engines: {node: '>= 6'}
- framer-motion@12.4.1:
- resolution: {integrity: sha512-5Ijbea3topSZjadQ0hgc/TcWj2ldMZmNREM7RvAhvsThYOA1HHOA8TT1yKvMu1YXP3jWaFwoZ6Vo9Nw+DUZrzA==}
+ framer-motion@12.4.2:
+ resolution: {integrity: sha512-pW307cQKjDqEuO1flEoIFf6TkuJRfKr+c7qsHAJhDo4368N/5U8/7WU8J+xhd9+gjmOgJfgp+46evxRRFM39dA==}
peerDependencies:
'@emotion/is-prop-valid': '*'
react: ^18.0.0 || ^19.0.0
@@ -3441,8 +3437,8 @@ packages:
react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
- next@15.1.6:
- resolution: {integrity: sha512-Hch4wzbaX0vKQtalpXvUiw5sYivBy4cm5rzUKrBnUB/y436LGrvOUqYvlSeNVCWFO/770gDlltR9gqZH62ct4Q==}
+ next@15.1.7:
+ resolution: {integrity: sha512-GNeINPGS9c6OZKCvKypbL8GTsT5GhWPp4DM0fzkXJuXMilOO2EeFxuAY6JZbtk6XIl6Ws10ag3xRINDjSO5+wg==}
engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
hasBin: true
peerDependencies:
@@ -3661,8 +3657,8 @@ packages:
resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==}
engines: {node: ^10 || ^12 || >=14}
- postcss@8.5.1:
- resolution: {integrity: sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==}
+ postcss@8.5.2:
+ resolution: {integrity: sha512-MjOadfU3Ys9KYoX0AdkBlFEF1Vx37uCCeN4ZHnmwm9FfpbsGWMZeBLMmmpY+6Ocqod7mkdZ0DT31OlbsFrLlkA==}
engines: {node: ^10 || ^12 || >=14}
postgres-array@2.0.0:
@@ -4088,8 +4084,8 @@ packages:
symbol-tree@3.2.4:
resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
- tailwindcss@4.0.5:
- resolution: {integrity: sha512-DZZIKX3tA23LGTjHdnwlJOTxfICD1cPeykLLsYF1RQBI9QsCR3i0szohJfJDVjr6aNRAIio5WVO7FGB77fRHwg==}
+ tailwindcss@4.0.6:
+ resolution: {integrity: sha512-mysewHYJKaXgNOW6pp5xon/emCsfAMnO8WMaGKZZ35fomnR/T5gYnRg2/yRTTrtXiEl1tiVkeRt0eMO6HxEZqw==}
tapable@2.2.1:
resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==}
@@ -4439,17 +4435,17 @@ snapshots:
dependencies:
json-schema: 0.4.0
- '@ai-sdk/react@1.1.10(react@19.0.0)(zod@3.23.8)':
+ '@ai-sdk/react@1.1.11(react@19.0.0)(zod@3.23.8)':
dependencies:
'@ai-sdk/provider-utils': 2.1.6(zod@3.23.8)
- '@ai-sdk/ui-utils': 1.1.10(zod@3.23.8)
+ '@ai-sdk/ui-utils': 1.1.11(zod@3.23.8)
swr: 2.3.2(react@19.0.0)
throttleit: 2.1.0
optionalDependencies:
react: 19.0.0
zod: 3.23.8
- '@ai-sdk/ui-utils@1.1.10(zod@3.23.8)':
+ '@ai-sdk/ui-utils@1.1.11(zod@3.23.8)':
dependencies:
'@ai-sdk/provider': 1.0.7
'@ai-sdk/provider-utils': 2.1.6(zod@3.23.8)
@@ -5512,41 +5508,41 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.4.15
- '@next/bundle-analyzer@15.1.6':
+ '@next/bundle-analyzer@15.1.7':
dependencies:
webpack-bundle-analyzer: 4.10.1
transitivePeerDependencies:
- bufferutil
- utf-8-validate
- '@next/env@15.1.6': {}
+ '@next/env@15.1.7': {}
- '@next/eslint-plugin-next@15.1.6':
+ '@next/eslint-plugin-next@15.1.7':
dependencies:
fast-glob: 3.3.1
- '@next/swc-darwin-arm64@15.1.6':
+ '@next/swc-darwin-arm64@15.1.7':
optional: true
- '@next/swc-darwin-x64@15.1.6':
+ '@next/swc-darwin-x64@15.1.7':
optional: true
- '@next/swc-linux-arm64-gnu@15.1.6':
+ '@next/swc-linux-arm64-gnu@15.1.7':
optional: true
- '@next/swc-linux-arm64-musl@15.1.6':
+ '@next/swc-linux-arm64-musl@15.1.7':
optional: true
- '@next/swc-linux-x64-gnu@15.1.6':
+ '@next/swc-linux-x64-gnu@15.1.7':
optional: true
- '@next/swc-linux-x64-musl@15.1.6':
+ '@next/swc-linux-x64-musl@15.1.7':
optional: true
- '@next/swc-win32-arm64-msvc@15.1.6':
+ '@next/swc-win32-arm64-msvc@15.1.7':
optional: true
- '@next/swc-win32-x64-msvc@15.1.6':
+ '@next/swc-win32-x64-msvc@15.1.7':
optional: true
'@nodelib/fs.scandir@2.1.5':
@@ -6223,76 +6219,76 @@ snapshots:
dependencies:
tslib: 2.8.1
- '@tailwindcss/container-queries@0.1.1(tailwindcss@4.0.5)':
+ '@tailwindcss/container-queries@0.1.1(tailwindcss@4.0.6)':
dependencies:
- tailwindcss: 4.0.5
+ tailwindcss: 4.0.6
- '@tailwindcss/forms@0.5.10(tailwindcss@4.0.5)':
+ '@tailwindcss/forms@0.5.10(tailwindcss@4.0.6)':
dependencies:
mini-svg-data-uri: 1.4.4
- tailwindcss: 4.0.5
+ tailwindcss: 4.0.6
- '@tailwindcss/node@4.0.5':
+ '@tailwindcss/node@4.0.6':
dependencies:
enhanced-resolve: 5.18.1
jiti: 2.4.2
- tailwindcss: 4.0.5
+ tailwindcss: 4.0.6
- '@tailwindcss/oxide-android-arm64@4.0.5':
+ '@tailwindcss/oxide-android-arm64@4.0.6':
optional: true
- '@tailwindcss/oxide-darwin-arm64@4.0.5':
+ '@tailwindcss/oxide-darwin-arm64@4.0.6':
optional: true
- '@tailwindcss/oxide-darwin-x64@4.0.5':
+ '@tailwindcss/oxide-darwin-x64@4.0.6':
optional: true
- '@tailwindcss/oxide-freebsd-x64@4.0.5':
+ '@tailwindcss/oxide-freebsd-x64@4.0.6':
optional: true
- '@tailwindcss/oxide-linux-arm-gnueabihf@4.0.5':
+ '@tailwindcss/oxide-linux-arm-gnueabihf@4.0.6':
optional: true
- '@tailwindcss/oxide-linux-arm64-gnu@4.0.5':
+ '@tailwindcss/oxide-linux-arm64-gnu@4.0.6':
optional: true
- '@tailwindcss/oxide-linux-arm64-musl@4.0.5':
+ '@tailwindcss/oxide-linux-arm64-musl@4.0.6':
optional: true
- '@tailwindcss/oxide-linux-x64-gnu@4.0.5':
+ '@tailwindcss/oxide-linux-x64-gnu@4.0.6':
optional: true
- '@tailwindcss/oxide-linux-x64-musl@4.0.5':
+ '@tailwindcss/oxide-linux-x64-musl@4.0.6':
optional: true
- '@tailwindcss/oxide-win32-arm64-msvc@4.0.5':
+ '@tailwindcss/oxide-win32-arm64-msvc@4.0.6':
optional: true
- '@tailwindcss/oxide-win32-x64-msvc@4.0.5':
+ '@tailwindcss/oxide-win32-x64-msvc@4.0.6':
optional: true
- '@tailwindcss/oxide@4.0.5':
+ '@tailwindcss/oxide@4.0.6':
optionalDependencies:
- '@tailwindcss/oxide-android-arm64': 4.0.5
- '@tailwindcss/oxide-darwin-arm64': 4.0.5
- '@tailwindcss/oxide-darwin-x64': 4.0.5
- '@tailwindcss/oxide-freebsd-x64': 4.0.5
- '@tailwindcss/oxide-linux-arm-gnueabihf': 4.0.5
- '@tailwindcss/oxide-linux-arm64-gnu': 4.0.5
- '@tailwindcss/oxide-linux-arm64-musl': 4.0.5
- '@tailwindcss/oxide-linux-x64-gnu': 4.0.5
- '@tailwindcss/oxide-linux-x64-musl': 4.0.5
- '@tailwindcss/oxide-win32-arm64-msvc': 4.0.5
- '@tailwindcss/oxide-win32-x64-msvc': 4.0.5
+ '@tailwindcss/oxide-android-arm64': 4.0.6
+ '@tailwindcss/oxide-darwin-arm64': 4.0.6
+ '@tailwindcss/oxide-darwin-x64': 4.0.6
+ '@tailwindcss/oxide-freebsd-x64': 4.0.6
+ '@tailwindcss/oxide-linux-arm-gnueabihf': 4.0.6
+ '@tailwindcss/oxide-linux-arm64-gnu': 4.0.6
+ '@tailwindcss/oxide-linux-arm64-musl': 4.0.6
+ '@tailwindcss/oxide-linux-x64-gnu': 4.0.6
+ '@tailwindcss/oxide-linux-x64-musl': 4.0.6
+ '@tailwindcss/oxide-win32-arm64-msvc': 4.0.6
+ '@tailwindcss/oxide-win32-x64-msvc': 4.0.6
- '@tailwindcss/postcss@4.0.5':
+ '@tailwindcss/postcss@4.0.6':
dependencies:
'@alloc/quick-lru': 5.2.0
- '@tailwindcss/node': 4.0.5
- '@tailwindcss/oxide': 4.0.5
+ '@tailwindcss/node': 4.0.6
+ '@tailwindcss/oxide': 4.0.6
lightningcss: 1.29.1
- postcss: 8.5.1
- tailwindcss: 4.0.5
+ postcss: 8.5.2
+ tailwindcss: 4.0.6
'@testing-library/dom@10.4.0':
dependencies:
@@ -6519,9 +6515,9 @@ snapshots:
dependencies:
crypto-js: 4.2.0
- '@vercel/analytics@1.4.1(next@15.1.6(@babel/core@7.24.5)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)(svelte@4.2.17)(vue@3.4.27(typescript@5.7.3))':
+ '@vercel/analytics@1.5.0(next@15.1.7(@babel/core@7.24.5)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)(svelte@4.2.17)(vue@3.4.27(typescript@5.7.3))':
optionalDependencies:
- next: 15.1.6(@babel/core@7.24.5)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+ next: 15.1.7(@babel/core@7.24.5)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
react: 19.0.0
svelte: 4.2.17
vue: 3.4.27(typescript@5.7.3)
@@ -6538,9 +6534,9 @@ snapshots:
dependencies:
'@upstash/redis': 1.34.3
- '@vercel/speed-insights@1.1.0(next@15.1.6(@babel/core@7.24.5)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)(svelte@4.2.17)(vue@3.4.27(typescript@5.7.3))':
+ '@vercel/speed-insights@1.2.0(next@15.1.7(@babel/core@7.24.5)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)(svelte@4.2.17)(vue@3.4.27(typescript@5.7.3))':
optionalDependencies:
- next: 15.1.6(@babel/core@7.24.5)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+ next: 15.1.7(@babel/core@7.24.5)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
react: 19.0.0
svelte: 4.2.17
vue: 3.4.27(typescript@5.7.3)
@@ -6569,7 +6565,7 @@ snapshots:
'@vue/shared': 3.4.27
estree-walker: 2.0.2
magic-string: 0.30.10
- postcss: 8.5.1
+ postcss: 8.5.2
source-map-js: 1.2.1
optional: true
@@ -6630,12 +6626,12 @@ snapshots:
transitivePeerDependencies:
- supports-color
- ai@4.1.26(react@19.0.0)(zod@3.23.8):
+ ai@4.1.34(react@19.0.0)(zod@3.23.8):
dependencies:
'@ai-sdk/provider': 1.0.7
'@ai-sdk/provider-utils': 2.1.6(zod@3.23.8)
- '@ai-sdk/react': 1.1.10(react@19.0.0)(zod@3.23.8)
- '@ai-sdk/ui-utils': 1.1.10(zod@3.23.8)
+ '@ai-sdk/react': 1.1.11(react@19.0.0)(zod@3.23.8)
+ '@ai-sdk/ui-utils': 1.1.11(zod@3.23.8)
'@opentelemetry/api': 1.9.0
jsondiffpatch: 0.6.0
optionalDependencies:
@@ -7160,11 +7156,6 @@ snapshots:
emoji-regex@9.2.2: {}
- enhanced-resolve@5.16.1:
- dependencies:
- graceful-fs: 4.2.11
- tapable: 2.2.1
-
enhanced-resolve@5.18.1:
dependencies:
graceful-fs: 4.2.11
@@ -7285,9 +7276,9 @@ snapshots:
optionalDependencies:
source-map: 0.6.1
- eslint-config-next@15.1.6(eslint@9.20.0(jiti@2.4.2))(typescript@5.7.3):
+ eslint-config-next@15.1.7(eslint@9.20.0(jiti@2.4.2))(typescript@5.7.3):
dependencies:
- '@next/eslint-plugin-next': 15.1.6
+ '@next/eslint-plugin-next': 15.1.7
'@rushstack/eslint-patch': 1.10.3
'@typescript-eslint/eslint-plugin': 8.19.0(@typescript-eslint/parser@8.19.0(eslint@9.20.0(jiti@2.4.2))(typescript@5.7.3))(eslint@9.20.0(jiti@2.4.2))(typescript@5.7.3)
'@typescript-eslint/parser': 8.19.0(eslint@9.20.0(jiti@2.4.2))(typescript@5.7.3)
@@ -7315,7 +7306,7 @@ snapshots:
eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@8.19.0(eslint@9.20.0(jiti@2.4.2))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.20.0(jiti@2.4.2)):
dependencies:
debug: 4.3.4
- enhanced-resolve: 5.16.1
+ enhanced-resolve: 5.18.1
eslint: 9.20.0(jiti@2.4.2)
eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.19.0(eslint@9.20.0(jiti@2.4.2))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@9.20.0(jiti@2.4.2))
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.19.0(eslint@9.20.0(jiti@2.4.2))(typescript@5.7.3))(eslint-import-resolver-typescript@3.6.1)(eslint@9.20.0(jiti@2.4.2))
@@ -7591,7 +7582,7 @@ snapshots:
combined-stream: 1.0.8
mime-types: 2.1.35
- framer-motion@12.4.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
+ framer-motion@12.4.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
dependencies:
motion-dom: 12.0.0
motion-utils: 12.0.0
@@ -8523,10 +8514,10 @@ snapshots:
natural-compare@1.4.0: {}
- next-auth@5.0.0-beta.25(next@15.1.6(@babel/core@7.24.5)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0):
+ next-auth@5.0.0-beta.25(next@15.1.7(@babel/core@7.24.5)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0):
dependencies:
'@auth/core': 0.37.2
- next: 15.1.6(@babel/core@7.24.5)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+ next: 15.1.7(@babel/core@7.24.5)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
react: 19.0.0
next-themes@0.4.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
@@ -8534,9 +8525,9 @@ snapshots:
react: 19.0.0
react-dom: 19.0.0(react@19.0.0)
- next@15.1.6(@babel/core@7.24.5)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
+ next@15.1.7(@babel/core@7.24.5)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
dependencies:
- '@next/env': 15.1.6
+ '@next/env': 15.1.7
'@swc/counter': 0.1.3
'@swc/helpers': 0.5.15
busboy: 1.6.0
@@ -8546,14 +8537,14 @@ snapshots:
react-dom: 19.0.0(react@19.0.0)
styled-jsx: 5.1.6(@babel/core@7.24.5)(react@19.0.0)
optionalDependencies:
- '@next/swc-darwin-arm64': 15.1.6
- '@next/swc-darwin-x64': 15.1.6
- '@next/swc-linux-arm64-gnu': 15.1.6
- '@next/swc-linux-arm64-musl': 15.1.6
- '@next/swc-linux-x64-gnu': 15.1.6
- '@next/swc-linux-x64-musl': 15.1.6
- '@next/swc-win32-arm64-msvc': 15.1.6
- '@next/swc-win32-x64-msvc': 15.1.6
+ '@next/swc-darwin-arm64': 15.1.7
+ '@next/swc-darwin-x64': 15.1.7
+ '@next/swc-linux-arm64-gnu': 15.1.7
+ '@next/swc-linux-arm64-musl': 15.1.7
+ '@next/swc-linux-x64-gnu': 15.1.7
+ '@next/swc-linux-x64-musl': 15.1.7
+ '@next/swc-win32-arm64-msvc': 15.1.7
+ '@next/swc-win32-x64-msvc': 15.1.7
'@opentelemetry/api': 1.9.0
sharp: 0.33.5
transitivePeerDependencies:
@@ -8756,7 +8747,7 @@ snapshots:
picocolors: 1.1.1
source-map-js: 1.2.1
- postcss@8.5.1:
+ postcss@8.5.2:
dependencies:
nanoid: 3.3.8
picocolors: 1.1.1
@@ -8958,7 +8949,7 @@ snapshots:
htmlparser2: 8.0.2
is-plain-object: 5.0.0
parse-srcset: 1.0.2
- postcss: 8.5.1
+ postcss: 8.5.2
sax@1.2.4: {}
@@ -9217,7 +9208,7 @@ snapshots:
symbol-tree@3.2.4: {}
- tailwindcss@4.0.5: {}
+ tailwindcss@4.0.6: {}
tapable@2.2.1: {}
diff --git a/src/admin/AdminAddAllUploads.tsx b/src/admin/AdminAddAllUploads.tsx
index b14f6939..71b31d56 100644
--- a/src/admin/AdminAddAllUploads.tsx
+++ b/src/admin/AdminAddAllUploads.tsx
@@ -4,7 +4,7 @@ import ErrorNote from '@/components/ErrorNote';
import FieldSetWithStatus from '@/components/FieldSetWithStatus';
import Container from '@/components/Container';
import { addAllUploadsAction } from '@/photo/actions';
-import { PATH_ADMIN_PHOTOS } from '@/site/paths';
+import { PATH_ADMIN_PHOTOS } from '@/app-core/paths';
import { Tags } from '@/tag';
import {
generateLocalNaivePostgresString,
diff --git a/src/admin/AdminAppConfiguration.tsx b/src/admin/AdminAppConfiguration.tsx
new file mode 100644
index 00000000..bc4dbc69
--- /dev/null
+++ b/src/admin/AdminAppConfiguration.tsx
@@ -0,0 +1,20 @@
+import { Suspense } from 'react';
+import { APP_CONFIGURATION } from '@/app-core/config';
+import AdminAppConfigurationServer from './AdminAppConfigurationServer';
+import AdminAppConfigurationClient from './AdminAppConfigurationClient';
+
+export default function AdminAppConfiguration({
+ simplifiedView,
+}: {
+ simplifiedView?: boolean
+}) {
+ return (
+ }>
+
+
+ );
+}
diff --git a/src/site/SiteChecklistClient.tsx b/src/admin/AdminAppConfigurationClient.tsx
similarity index 85%
rename from src/site/SiteChecklistClient.tsx
rename to src/admin/AdminAppConfigurationClient.tsx
index 3aa5c4d8..51e2322f 100644
--- a/src/site/SiteChecklistClient.tsx
+++ b/src/admin/AdminAppConfigurationClient.tsx
@@ -4,18 +4,16 @@ import {
ComponentProps,
ReactNode,
} from 'react';
-import { clsx } from 'clsx/lite';
import ChecklistRow from '../components/ChecklistRow';
-import { FiExternalLink } from 'react-icons/fi';
import {
- BiCog,
BiData,
BiHide,
BiLockAlt,
BiPencil,
} from 'react-icons/bi';
-import Checklist from '@/components/Checklist';
-import { ConfigChecklistStatus } from './config';
+import { HiOutlineCog } from 'react-icons/hi';
+import ChecklistGroup from '@/components/ChecklistGroup';
+import { ConfigChecklistStatus } from '../app-core/config';
import StatusIcon from '@/components/StatusIcon';
import { labelForStorage } from '@/services/storage';
import { HiSparkles } from 'react-icons/hi';
@@ -23,14 +21,14 @@ import { testConnectionsAction } from '@/admin/actions';
import ErrorNote from '@/components/ErrorNote';
import WarningNote from '@/components/WarningNote';
import { RiSpeedMiniLine } from 'react-icons/ri';
-import Link from 'next/link';
-import SecretGenerator from './SecretGenerator';
-import CopyButton from '@/components/CopyButton';
+import SecretGenerator from '../app-core/SecretGenerator';
import { PiPaintBrushHousehold } from 'react-icons/pi';
import { IoMdGrid } from 'react-icons/io';
import { CgDebug } from 'react-icons/cg';
+import EnvVar from '@/components/EnvVar';
+import AdminLink from './AdminLink';
-export default function SiteChecklistClient({
+export default function AdminAppConfigurationClient({
// Storage
hasDatabase,
isPostgresSslEnabled,
@@ -79,7 +77,7 @@ export default function SiteChecklistClient({
isGridHomepageEnabled,
gridAspectRatio,
hasGridAspectRatio,
- gridDensity,
+ hasHighGridDensity,
hasGridDensityPreference,
// Settings
isGeoPrivacyEnabled,
@@ -94,9 +92,6 @@ export default function SiteChecklistClient({
isAdminSqlDebugEnabled,
// Misc
baseUrl,
- commitSha,
- commitMessage,
- commitUrl,
// Connection status
databaseError,
storageError,
@@ -110,54 +105,10 @@ export default function SiteChecklistClient({
simplifiedView?: boolean
isAnalyzingConfiguration?: boolean
}) {
- const renderLink = (href: string, text: string, external = true) =>
- <>
-
- {text}
-
- {external &&
- <>
-
-
- >}
- >;
-
- const renderEnvVar = (
- variable: string,
- minimal?: boolean,
- ) =>
-
-
-
- `{variable}`
-
- {!minimal && }
-
-
;
-
const renderEnvVars = (variables: string[]) =>
-
- {variables.map(envVar => renderEnvVar(envVar))}
+
+ {variables.map(envVar =>
+ )}
;
const renderSubStatus = (
@@ -181,7 +132,7 @@ export default function SiteChecklistClient({
renderSubStatus(
type,
renderEnvVars([variable]),
- 'translate-y-[5px]',
+ 'translate-y-[3px]',
);
const renderError = ({
@@ -213,9 +164,9 @@ export default function SiteChecklistClient({
;
return (
-
+ <>
-
}
>
@@ -234,11 +185,13 @@ export default function SiteChecklistClient({
: renderSubStatus('optional', <>
Vercel Postgres:
{' '}
- {renderLink(
+
+ create store
+
{' '}
and connect to project
>)}
@@ -270,11 +223,13 @@ export default function SiteChecklistClient({
: renderSubStatus('optional', <>
{labelForStorage('vercel-blob')}:
{' '}
- {renderLink(
+
+ create store
+
{' '}
and connect to project
>,
@@ -284,24 +239,29 @@ export default function SiteChecklistClient({
: renderSubStatus('optional', <>
{labelForStorage('cloudflare-r2')}:
{' '}
- {renderLink(
- 'https://github.com/sambecker/exif-photo-blog#cloudflare-r2',
- 'create/configure bucket',
- )}
+
+ create/configure bucket
+
>)}
{hasAwsS3Storage
? renderSubStatus('checked', 'AWS S3: connected')
: renderSubStatus('optional', <>
{labelForStorage('aws-s3')}:
{' '}
- {renderLink(
- 'https://github.com/sambecker/exif-photo-blog#aws-s3',
- 'create/configure bucket',
- )}
+
+ create/configure bucket
+
>)}
-
-
+ }
>
@@ -331,8 +291,8 @@ export default function SiteChecklistClient({
'ADMIN_PASSWORD',
])}
-
-
+ }
>
@@ -373,9 +333,9 @@ export default function SiteChecklistClient({
Store in environment variable (seen in grid sidebar):
{renderEnvVars(['NEXT_PUBLIC_SITE_ABOUT'])}
-
+
{!simplifiedView && <>
-
}
@@ -408,11 +368,13 @@ export default function SiteChecklistClient({
{kvError && renderError({
connection: { provider: 'Vercel KV', error: kvError},
})}
- {renderLink(
+
+ Create Vercel KV store
+
{' '}
and connect to project in order to enable rate limiting
@@ -429,8 +391,8 @@ export default function SiteChecklistClient({
(default: {'"title, tags, semantic"'}):
{renderEnvVars(['AI_TEXT_AUTO_GENERATED_FIELDS'])}
-
-
+ }
optional
@@ -490,8 +452,8 @@ export default function SiteChecklistClient({
image blur data being stored and displayed:
{renderEnvVars(['NEXT_PUBLIC_BLUR_DISABLED'])}
-
-
+ }
optional
@@ -518,8 +480,8 @@ export default function SiteChecklistClient({
of each photo, and display a surrounding border:
{renderEnvVars(['NEXT_PUBLIC_MATTE_PHOTOS'])}
-
-
+ }
optional
@@ -578,8 +540,8 @@ export default function SiteChecklistClient({
Set environment variable to {'"1"'} to hide footer link:
{renderEnvVars(['NEXT_PUBLIC_HIDE_REPO_LINK'])}
-
-
+ }
optional
@@ -604,7 +566,7 @@ export default function SiteChecklistClient({
{renderEnvVars(['NEXT_PUBLIC_GRID_ASPECT_RATIO'])}
@@ -613,10 +575,10 @@ export default function SiteChecklistClient({
aspect ratio):
{renderEnvVars(['NEXT_PUBLIC_SHOW_LARGE_THUMBNAILS'])}
-
-
+ }
+ icon={}
optional
>
-
+
{areInternalToolsEnabled &&
-
}
optional
@@ -698,7 +660,7 @@ export default function SiteChecklistClient({
console output for all sql queries:
{renderEnvVars(['ADMIN_SQL_DEBUG'])}
- }
+ }
>}
@@ -715,23 +677,8 @@ export default function SiteChecklistClient({
{baseUrl || 'Not Defined'}
-
- Commit
-
- {commitSha
- ? commitUrl
- ?
- {commitSha}
-
- : {commitSha}
- : 'Not Found'}
-
}
-
+ >
);
}
diff --git a/src/site/SiteChecklistServer.tsx b/src/admin/AdminAppConfigurationServer.tsx
similarity index 51%
rename from src/site/SiteChecklistServer.tsx
rename to src/admin/AdminAppConfigurationServer.tsx
index e50255ab..0f19eb2c 100644
--- a/src/site/SiteChecklistServer.tsx
+++ b/src/admin/AdminAppConfigurationServer.tsx
@@ -1,8 +1,8 @@
-import SiteChecklistClient from './SiteChecklistClient';
-import { CONFIG_CHECKLIST_STATUS } from '@/site/config';
+import AdminAppConfigurationClient from './AdminAppConfigurationClient';
+import { APP_CONFIGURATION } from '@/app-core/config';
import { testConnectionsAction } from '@/admin/actions';
-export default async function SiteChecklistServer({
+export default async function AdminAppConfigurationServer({
simplifiedView,
}: {
simplifiedView?: boolean
@@ -10,8 +10,8 @@ export default async function SiteChecklistServer({
const connectionErrors = await testConnectionsAction().catch(() => ({}));
return (
-
diff --git a/src/admin/AdminAppMenu.tsx b/src/admin/AdminAppMenu.tsx
index 1835ea05..de7e9f4d 100644
--- a/src/admin/AdminAppMenu.tsx
+++ b/src/admin/AdminAppMenu.tsx
@@ -1,11 +1,16 @@
'use client';
import MoreMenu from '@/components/more/MoreMenu';
-import { PATH_ADMIN_CONFIGURATION, PATH_GRID_INFERRED } from '@/site/paths';
+import {
+ PATH_ADMIN_CONFIGURATION,
+ PATH_ADMIN_INSIGHTS,
+ PATH_GRID_INFERRED,
+} from '@/app-core/paths';
import { useAppState } from '@/state/AppState';
-import { BiCog } from 'react-icons/bi';
import { ImCheckboxUnchecked } from 'react-icons/im';
import { IoCloseSharp } from 'react-icons/io5';
+import AdminAppInsightsIcon from './insights/AdminAppInsightsIcon';
+import { LuCog } from 'react-icons/lu';
export default function AdminAppMenu() {
const {
@@ -18,19 +23,27 @@ export default function AdminAppMenu() {
return (
,
+ label: 'Insights',
+ icon:
+
+ ,
+ href: PATH_ADMIN_INSIGHTS,
+ }, {
+ label: 'Configuration',
+ icon: ,
href: PATH_ADMIN_CONFIGURATION,
}, {
label: isSelecting
? 'Exit Select'
- : 'Select Multiple',
+ : 'Select',
icon: isSelecting
?
: ,
href: PATH_GRID_INFERRED,
action: () => {
diff --git a/src/admin/AdminBatchEditPanelClient.tsx b/src/admin/AdminBatchEditPanelClient.tsx
index 91a487bc..e108c6cd 100644
--- a/src/admin/AdminBatchEditPanelClient.tsx
+++ b/src/admin/AdminBatchEditPanelClient.tsx
@@ -9,7 +9,7 @@ import { IoCloseSharp } from 'react-icons/io5';
import { useState } from 'react';
import { TAG_FAVS, Tags } from '@/tag';
import { usePathname } from 'next/navigation';
-import { PATH_GRID_INFERRED } from '@/site/paths';
+import { PATH_GRID_INFERRED } from '@/app-core/paths';
import PhotoTagFieldset from './PhotoTagFieldset';
import { tagMultiplePhotosAction } from '@/photo/actions';
import { toastSuccess } from '@/toast';
diff --git a/src/admin/AdminCTA.tsx b/src/admin/AdminCTA.tsx
index 92fae7b4..74ea1015 100644
--- a/src/admin/AdminCTA.tsx
+++ b/src/admin/AdminCTA.tsx
@@ -1,7 +1,7 @@
'use client';
import PhotoUpload from '@/photo/PhotoUpload';
-import { PATH_ADMIN_PHOTOS } from '@/site/paths';
+import { PATH_ADMIN_PHOTOS } from '@/app-core/paths';
import { useAppState } from '@/state/AppState';
import Link from 'next/link';
import { useState } from 'react';
diff --git a/src/admin/AdminEmptyState.tsx b/src/admin/AdminEmptyState.tsx
new file mode 100644
index 00000000..3eaef77f
--- /dev/null
+++ b/src/admin/AdminEmptyState.tsx
@@ -0,0 +1,29 @@
+import clsx from 'clsx/lite';
+import { ReactNode } from 'react';
+import { IoInformationCircleOutline } from 'react-icons/io5';
+
+export default function AdminEmptyState({
+ icon,
+ children,
+ includeContainer = true,
+}: {
+ icon?: ReactNode
+ children: ReactNode
+ includeContainer?: boolean
+}) {
+ return (
+
+
+ {icon ?? }
+
+ {children}
+
+ );
+}
\ No newline at end of file
diff --git a/src/admin/AdminInfoPage.tsx b/src/admin/AdminInfoPage.tsx
new file mode 100644
index 00000000..3e046c22
--- /dev/null
+++ b/src/admin/AdminInfoPage.tsx
@@ -0,0 +1,32 @@
+import Container from '@/components/Container';
+import SiteGrid from '@/components/SiteGrid';
+import { ReactNode } from 'react';
+
+export default function AdminInfoPage({
+ title,
+ accessory,
+ children,
+}: {
+ title: string
+ accessory?: ReactNode
+ children: ReactNode
+}) {
+ return (
+
+
+
+ {title}
+
+ {accessory}
+
+
+
+ {children}
+
+
+ }
+ />
+ );
+}
diff --git a/src/admin/AdminLink.tsx b/src/admin/AdminLink.tsx
new file mode 100644
index 00000000..51e53f84
--- /dev/null
+++ b/src/admin/AdminLink.tsx
@@ -0,0 +1,38 @@
+import clsx from 'clsx/lite';
+import Link from 'next/link';
+import { ComponentProps } from 'react';
+import { FiExternalLink } from 'react-icons/fi';
+export default function AdminLink({
+ href,
+ className,
+ children,
+ externalIcon,
+ ...props
+}: ComponentProps & {
+ externalIcon?: boolean
+}) {
+ return (
+ <>
+
+ {children}
+
+ {externalIcon &&
+ <>
+
+
+ >}
+ >
+ );
+}
diff --git a/src/admin/AdminNav.tsx b/src/admin/AdminNav.tsx
index a87a15d9..ceef18c9 100644
--- a/src/admin/AdminNav.tsx
+++ b/src/admin/AdminNav.tsx
@@ -8,7 +8,7 @@ import {
PATH_ADMIN_PHOTOS,
PATH_ADMIN_TAGS,
PATH_ADMIN_UPLOADS,
-} from '@/site/paths';
+} from '@/app-core/paths';
import AdminNavClient from './AdminNavClient';
export default async function AdminNav() {
@@ -31,6 +31,8 @@ export default async function AdminNav() {
getPhotosMostRecentUpdateCached().catch(() => undefined),
]);
+ const includeInsights = countPhotos > 0;
+
// Photos
const items = [{
label: 'Photos',
@@ -53,6 +55,10 @@ export default async function AdminNav() {
}); }
return (
-
+
);
}
diff --git a/src/admin/AdminNavClient.tsx b/src/admin/AdminNavClient.tsx
index 723afcc9..b112dc3e 100644
--- a/src/admin/AdminNavClient.tsx
+++ b/src/admin/AdminNavClient.tsx
@@ -7,17 +7,20 @@ import SiteGrid from '@/components/SiteGrid';
import Spinner from '@/components/Spinner';
import {
PATH_ADMIN_CONFIGURATION,
+ PATH_ADMIN_INSIGHTS,
checkPathPrefix,
isPathAdminConfiguration,
+ isPathAdminInsights,
isPathTopLevelAdmin,
-} from '@/site/paths';
+} from '@/app-core/paths';
import { useAppState } from '@/state/AppState';
import { clsx } from 'clsx/lite';
import { differenceInMinutes } from 'date-fns';
import { usePathname } from 'next/navigation';
import { useEffect, useMemo, useState } from 'react';
-import { BiCog } from 'react-icons/bi';
import { FaRegClock } from 'react-icons/fa';
+import AdminAppInsightsIcon from './insights/AdminAppInsightsIcon';
+import { LuCog } from 'react-icons/lu';
// Updates considered recent if they occurred in past 5 minutes
const areTimesRecent = (dates: Date[]) => dates
@@ -26,6 +29,7 @@ const areTimesRecent = (dates: Date[]) => dates
export default function AdminNavClient({
items,
mostRecentPhotoUpdateTime,
+ includeInsights,
}: {
items: {
label: string,
@@ -33,6 +37,7 @@ export default function AdminNavClient({
count: number,
}[]
mostRecentPhotoUpdateTime?: Date
+ includeInsights?: boolean
}) {
const pathname = usePathname();
@@ -86,19 +91,33 @@ export default function AdminNavClient({
({count})}
)}
- }
- >
-
-
+
+ {includeInsights &&
+
}
+ >
+
+ }
+
}
+ >
+
+
+
{shouldShowBanner &&
}>
diff --git a/src/admin/AdminOutdatedClient.tsx b/src/admin/AdminOutdatedClient.tsx
index b09653ac..734471b7 100644
--- a/src/admin/AdminOutdatedClient.tsx
+++ b/src/admin/AdminOutdatedClient.tsx
@@ -3,10 +3,10 @@
import { OUTDATED_THRESHOLD, Photo } from '@/photo';
import AdminPhotosTable from '@/admin/AdminPhotosTable';
import LoaderButton from '@/components/primitives/LoaderButton';
-import IconGrSync from '@/site/IconGrSync';
+import IconGrSync from '@/app-core/IconGrSync';
import Note from '@/components/Note';
import AdminChildPage from '@/components/AdminChildPage';
-import { PATH_ADMIN_PHOTOS } from '@/site/paths';
+import { PATH_ADMIN_PHOTOS } from '@/app-core/paths';
import { useState } from 'react';
import { syncPhotosAction } from '@/photo/actions';
import { useRouter } from 'next/navigation';
@@ -67,6 +67,7 @@ export default function AdminOutdatedClient({
}
}}
isLoading={arePhotoIdsSyncing}
+ disabled={!updateBatchSize}
>
{arePhotoIdsSyncing
? 'Syncing'
@@ -78,6 +79,9 @@ export default function AdminOutdatedClient({
+
+ Outdated photos found
+
{photos.length}
{' '}
{photos.length === 1 ? 'photo' : 'photos'}
@@ -87,8 +91,7 @@ export default function AdminOutdatedClient({
{' '}
may have: missing EXIF fields, inaccurate blur data,
{' '}
- undesired privacy settings
- {hasAiTextGeneration && ', missing AI-generated text'}
+ undesired privacy settings, or missing AI-generated text
diff --git a/src/admin/AdminPhotoMenuClient.tsx b/src/admin/AdminPhotoMenuClient.tsx
index e73f0629..2feade73 100644
--- a/src/admin/AdminPhotoMenuClient.tsx
+++ b/src/admin/AdminPhotoMenuClient.tsx
@@ -1,7 +1,7 @@
'use client';
import { ComponentProps, useMemo } from 'react';
-import { pathForAdminPhotoEdit, pathForPhoto } from '@/site/paths';
+import { pathForAdminPhotoEdit, pathForPhoto } from '@/app-core/paths';
import { deletePhotoAction, toggleFavoritePhotoAction } from '@/photo/actions';
import { FaRegEdit, FaRegStar, FaStar } from 'react-icons/fa';
import {
diff --git a/src/admin/AdminPhotosClient.tsx b/src/admin/AdminPhotosClient.tsx
index 073004d1..35015494 100644
--- a/src/admin/AdminPhotosClient.tsx
+++ b/src/admin/AdminPhotosClient.tsx
@@ -6,11 +6,11 @@ import SiteGrid from '@/components/SiteGrid';
import {
AI_TEXT_GENERATION_ENABLED,
PRESERVE_ORIGINAL_UPLOADS,
-} from '@/site/config';
+} from '@/app-core/config';
import AdminPhotosTable from '@/admin/AdminPhotosTable';
import AdminPhotosTableInfinite from '@/admin/AdminPhotosTableInfinite';
import PathLoaderButton from '@/components/primitives/PathLoaderButton';
-import { PATH_ADMIN_OUTDATED } from '@/site/paths';
+import { PATH_ADMIN_OUTDATED } from '@/app-core/paths';
import { Photo } from '@/photo';
import { StorageListResponse } from '@/services/storage';
import { useState } from 'react';
diff --git a/src/admin/AdminPhotosTable.tsx b/src/admin/AdminPhotosTable.tsx
index 357ae9c1..0e8d8ca6 100644
--- a/src/admin/AdminPhotosTable.tsx
+++ b/src/admin/AdminPhotosTable.tsx
@@ -5,7 +5,7 @@ import AdminTable from './AdminTable';
import { Fragment } from 'react';
import PhotoSmall from '@/photo/PhotoSmall';
import { clsx } from 'clsx/lite';
-import { pathForAdminPhotoEdit, pathForPhoto } from '@/site/paths';
+import { pathForAdminPhotoEdit, pathForPhoto } from '@/app-core/paths';
import Link from 'next/link';
import { AiOutlineEyeInvisible } from 'react-icons/ai';
import PhotoDate from '@/photo/PhotoDate';
diff --git a/src/admin/AdminPhotosTableInfinite.tsx b/src/admin/AdminPhotosTableInfinite.tsx
index 3dcc7c9d..65376dfe 100644
--- a/src/admin/AdminPhotosTableInfinite.tsx
+++ b/src/admin/AdminPhotosTableInfinite.tsx
@@ -1,6 +1,6 @@
'use client';
-import { PATH_ADMIN_PHOTOS } from '@/site/paths';
+import { PATH_ADMIN_PHOTOS } from '@/app-core/paths';
import InfinitePhotoScroll from '../photo/InfinitePhotoScroll';
import AdminPhotosTable from './AdminPhotosTable';
import { ComponentProps } from 'react';
diff --git a/src/admin/AdminTagTable.tsx b/src/admin/AdminTagTable.tsx
index a5bdfc5d..cf93cf99 100644
--- a/src/admin/AdminTagTable.tsx
+++ b/src/admin/AdminTagTable.tsx
@@ -6,7 +6,7 @@ import DeleteFormButton from '@/admin/DeleteFormButton';
import { photoQuantityText } from '@/photo';
import { Tags, formatTag, sortTagsObject } from '@/tag';
import EditButton from '@/admin/EditButton';
-import { pathForAdminTagEdit } from '@/site/paths';
+import { pathForAdminTagEdit } from '@/app-core/paths';
import { clsx } from 'clsx/lite';
import AdminTagBadge from './AdminTagBadge';
diff --git a/src/admin/AdminUploadsTable.tsx b/src/admin/AdminUploadsTable.tsx
index edeb0603..027c4a53 100644
--- a/src/admin/AdminUploadsTable.tsx
+++ b/src/admin/AdminUploadsTable.tsx
@@ -5,7 +5,7 @@ import Spinner from '@/components/Spinner';
import { getIdFromStorageUrl } from '@/services/storage';
import { clsx } from 'clsx/lite';
import { FaRegCircleCheck } from 'react-icons/fa6';
-import { pathForAdminUploadUrl } from '@/site/paths';
+import { pathForAdminUploadUrl } from '@/app-core/paths';
import AddButton from './AddButton';
import { UrlAddStatus } from './AdminUploadsClient';
import ResponsiveDate from '@/components/ResponsiveDate';
diff --git a/src/admin/DeleteBlobButton.tsx b/src/admin/DeleteBlobButton.tsx
index 8e30e030..10d483f0 100644
--- a/src/admin/DeleteBlobButton.tsx
+++ b/src/admin/DeleteBlobButton.tsx
@@ -3,7 +3,7 @@
import { deleteUploadAction } from '@/photo/actions';
import DeleteButton from './DeleteButton';
import { useRouter } from 'next/navigation';
-import { PATH_ADMIN_PHOTOS } from '@/site/paths';
+import { PATH_ADMIN_PHOTOS } from '@/app-core/paths';
import { useState } from 'react';
export default function DeleteUploadButton({
diff --git a/src/admin/ExifSyncButton.tsx b/src/admin/ExifSyncButton.tsx
index 517bcbe5..f968e5d2 100644
--- a/src/admin/ExifSyncButton.tsx
+++ b/src/admin/ExifSyncButton.tsx
@@ -4,7 +4,7 @@ import LoaderButton from '@/components/primitives/LoaderButton';
import SubmitButtonWithStatus from '@/components/SubmitButtonWithStatus';
import { getExifDataAction } from '@/photo/actions';
import { PhotoFormData } from '@/photo/form';
-import IconGrSync from '@/site/IconGrSync';
+import IconGrSync from '@/app-core/IconGrSync';
import { clsx } from 'clsx/lite';
import { ComponentProps, useState } from 'react';
diff --git a/src/admin/PhotoSyncButton.tsx b/src/admin/PhotoSyncButton.tsx
index ea410d82..07e0709d 100644
--- a/src/admin/PhotoSyncButton.tsx
+++ b/src/admin/PhotoSyncButton.tsx
@@ -1,6 +1,6 @@
import LoaderButton from '@/components/primitives/LoaderButton';
import { syncPhotoAction } from '@/photo/actions';
-import IconGrSync from '@/site/IconGrSync';
+import IconGrSync from '@/app-core/IconGrSync';
import { toastSuccess } from '@/toast';
import { ComponentProps, useState } from 'react';
diff --git a/src/admin/actions.ts b/src/admin/actions.ts
index e2ddd1ae..ee366c7a 100644
--- a/src/admin/actions.ts
+++ b/src/admin/actions.ts
@@ -5,7 +5,7 @@ import { testKvConnection } from '@/services/kv';
import { testOpenAiConnection } from '@/services/openai';
import { testDatabaseConnection } from '@/services/postgres';
import { testStorageConnection } from '@/services/storage';
-import { CONFIG_CHECKLIST_STATUS } from '@/site/config';
+import { APP_CONFIGURATION } from '@/app-core/config';
const scanForError = (
shouldCheck: boolean,
@@ -24,7 +24,7 @@ export const testConnectionsAction = async () =>
hasStorageProvider,
hasVercelKv,
isAiTextGenerationEnabled,
- } = CONFIG_CHECKLIST_STATUS;
+ } = APP_CONFIGURATION;
const [
databaseError,
diff --git a/src/admin/github/GitHubForkStatusBadge.tsx b/src/admin/github/GitHubForkStatusBadge.tsx
deleted file mode 100644
index 0af8dbbb..00000000
--- a/src/admin/github/GitHubForkStatusBadge.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import { Suspense } from 'react';
-import GitHubForkStatusBadgeClient from './GitHubForkStatusBadgeClient';
-import GitHubForkStatusBadgeServer from './GitHubForkStatusBadgeServer';
-import { IS_DEVELOPMENT } from '@/site/config';
-
-export default function GitHubForkStatusBadge() {
- return IS_DEVELOPMENT
- ?
- :
-
- ;
-}
diff --git a/src/admin/github/GitHubForkStatusBadgeClient.tsx b/src/admin/github/GitHubForkStatusBadgeClient.tsx
deleted file mode 100644
index 3bfe8593..00000000
--- a/src/admin/github/GitHubForkStatusBadgeClient.tsx
+++ /dev/null
@@ -1,65 +0,0 @@
-import Spinner from '@/components/Spinner';
-import Tooltip from '@/components/Tooltip';
-import clsx from 'clsx/lite';
-import { ReactNode } from 'react';
-import { BiLogoGithub } from 'react-icons/bi';
-
-export default function GitHubForkStatusBadgeClient({
- label,
- style = 'mono',
- tooltip,
-}: {
- label?: ReactNode
- style?: 'success' | 'warning' | 'error' | 'mono'
- tooltip?: ReactNode
-}) {
- const classNameForStyle = () => {
- switch (style) {
- case 'success': return clsx(
- 'text-green-700 hover:text-green-700',
- 'dark:text-green-400 dark:hover:text-green-400',
- 'bg-green-100/40 dark:bg-green-900/25',
- 'border-green-300/40 dark:border-green-900/50',
- );
- case 'warning': return clsx(
- 'text-amber-800/90 hover:text-amber-800/90',
- 'dark:text-amber-400 dark:hover:text-amber-400',
- 'bg-amber-100/40 dark:bg-amber-900/25',
- 'border-amber-300/40 dark:border-amber-900/50',
- );
- case 'error': return clsx(
- 'text-red-700/90 hover:text-red-700/90',
- 'dark:text-red-400 dark:hover:text-red-400',
- 'bg-red-100/20 dark:bg-red-900/25',
- 'border-red-300/40 dark:border-red-900/50',
- );
- default: return clsx(
- 'text-main',
- 'bg-white dark:bg-transparent',
- 'border-main',
- );
- }
- };
-
- return (
-
-
- {!label
- ?
- : }
- {label ?? 'Checking'}
-
-
- );
-}
diff --git a/src/admin/github/GitHubForkStatusBadgeServer.tsx b/src/admin/github/GitHubForkStatusBadgeServer.tsx
deleted file mode 100644
index 5b7657fe..00000000
--- a/src/admin/github/GitHubForkStatusBadgeServer.tsx
+++ /dev/null
@@ -1,62 +0,0 @@
-import GitHubForkStatusBadgeClient from './GitHubForkStatusBadgeClient';
-import {
- VERCEL_GIT_BRANCH,
- VERCEL_GIT_REPO_OWNER,
- VERCEL_GIT_REPO_SLUG,
- VERCEL_GIT_COMMIT_SHA,
-} from '@/site/config';
-import { getGitHubMetaWithFallback, getGitHubRepoUrl } from '.';
-
-export default async function GitHubForkStatusBadgeServer() {
- const owner = VERCEL_GIT_REPO_OWNER;
- const repo = VERCEL_GIT_REPO_SLUG;
- const branch = VERCEL_GIT_BRANCH;
- const commit = VERCEL_GIT_COMMIT_SHA;
-
- const {
- url,
- isForkedFromBase,
- isBaseRepo,
- isBehind,
- label,
- description,
- didError,
- } = await getGitHubMetaWithFallback({ owner, repo, branch, commit });
-
- const repoLink = (text: string) =>
-
- {text}
- ;
-
- const isBehindContent = <>
- {' '}
- {repoLink('Sync on GitHub')} for latest updates.
- >;
-
- const didErrorContent = <>
- {' '}
- Could not connect to {repoLink('GitHub')}.
- >;
-
- return isForkedFromBase || isBaseRepo
- ?
- {description}
- {didError
- ? didErrorContent
- : isBehind
- ? isBehindContent
- : null}
- >,
- style: didError || isBehind === undefined || isBehind
- ? 'warning'
- : 'mono',
- }} />
- : null;
-}
diff --git a/src/admin/github/index.ts b/src/admin/github/index.ts
deleted file mode 100644
index f04491d5..00000000
--- a/src/admin/github/index.ts
+++ /dev/null
@@ -1,172 +0,0 @@
-import {
- TEMPLATE_BASE_OWNER,
- TEMPLATE_BASE_REPO,
- TEMPLATE_BASE_BRANCH,
-} from '@/site/config';
-
-const DEFAULT_BRANCH = 'main';
-const FALLBACK_TEXT = 'Unknown';
-const CACHE_GITHUB_REQUESTS = false;
-
-// Cache all results for 2 minutes to avoid rate limiting
-// GitHub API requests limited to 60 requests per hour
-const FETCH_CONFIG: RequestInit | undefined= CACHE_GITHUB_REQUESTS
- ? { next: { revalidate: 120 } } : undefined;
-
-interface RepoParams {
- owner?: string
- repo?: string
- branch?: string
- commit?: string
-};
-
-// Website urls
-
-export const getGitHubRepoUrl = ({
- owner = TEMPLATE_BASE_OWNER,
- repo = TEMPLATE_BASE_REPO,
-}: RepoParams = {}) =>
- `https://github.com/${owner}/${repo}`;
-
-export const getGitHubCompareUrl = ({
- owner,
- repo,
- branch = DEFAULT_BRANCH,
-}: RepoParams = {}) =>
- // eslint-disable-next-line max-len
- `${getGitHubRepoUrl({ owner, repo })}/compare/${branch}...${TEMPLATE_BASE_OWNER}:${TEMPLATE_BASE_REPO}:${TEMPLATE_BASE_BRANCH}`;
-
-// API urls
-
-const getGitHubApiRepoUrl = ({
- owner = TEMPLATE_BASE_OWNER,
- repo = TEMPLATE_BASE_REPO,
-}: RepoParams = {}) =>
- `https://api.github.com/repos/${owner}/${repo}`;
-
-const getGitHubApiCommitsUrl = (params?: RepoParams) =>
- `${getGitHubApiRepoUrl(params)}/commits/${params?.branch || DEFAULT_BRANCH}`;
-
-const getGitHubApiForksUrl = (params?: RepoParams) =>
- `${getGitHubApiRepoUrl(params)}/forks`;
-
-const getGitHubApiCompareToRepoUrl = ({
- owner,
- repo,
- branch = DEFAULT_BRANCH,
-}: RepoParams = {}) =>
- // eslint-disable-next-line max-len
- `${getGitHubApiRepoUrl()}/compare/${TEMPLATE_BASE_BRANCH}...${owner}:${repo}:${branch}`;
-
-const getGitHubApiCompareToCommitUrl = ({ commit }: RepoParams = {}) =>
- `${getGitHubApiRepoUrl()}/compare/${TEMPLATE_BASE_BRANCH}...${commit}`;
-
-// Requests
-
-export const getLatestBaseRepoCommitSha = async () => {
- const response = await fetch(getGitHubApiCommitsUrl(), FETCH_CONFIG);
- const data = await response.json();
- return data.sha ? data.sha.slice(0, 7) as string : undefined;
-};
-
-const getIsRepoForkedFromBase = async (params: RepoParams) => {
- const response = await fetch(getGitHubApiRepoUrl(params), FETCH_CONFIG);
- const data = await response.json();
- return (
- Boolean(data.fork) &&
- data.source?.full_name === `${TEMPLATE_BASE_OWNER}/${TEMPLATE_BASE_REPO}`
- );
-};
-
-const getGitHubCommitsBehindFromRepo = async (params?: RepoParams) => {
- const response = await fetch(
- getGitHubApiCompareToRepoUrl(params),
- FETCH_CONFIG,
- );
- const data = await response.json();
- return data.behind_by as number;
-};
-
-const getGitHubCommitsBehindFromCommit = async (params?: RepoParams) => {
- const response = await fetch(
- getGitHubApiCompareToCommitUrl(params),
- FETCH_CONFIG,
- );
- const data = await response.json();
- return data.behind_by as number;
-};
-
-const isRepoBaseRepo = ({ owner, repo }: RepoParams) =>
- owner?.toLowerCase() === TEMPLATE_BASE_OWNER &&
- repo?.toLowerCase() === TEMPLATE_BASE_REPO;
-
-export const getGitHubPublicFork = async (
- params?: RepoParams,
-): Promise => {
- const response = await fetch(getGitHubApiForksUrl(params), FETCH_CONFIG);
- const fork = (await response.json())[0];
- return {
- owner: fork.owner.login,
- repo: fork.name,
- };
-};
-
-const getGitHubMeta = async (params: RepoParams) => {
- const url = getGitHubRepoUrl(params);
- const isBaseRepo = isRepoBaseRepo(params);
-
- const [
- isForkedFromBase,
- behindBy,
- ] = await Promise.all([
- getIsRepoForkedFromBase(params),
- isBaseRepo && params.commit
- ? getGitHubCommitsBehindFromCommit(params)
- : getGitHubCommitsBehindFromRepo(params),
- ]);
-
- const isBehind = behindBy === undefined
- ? undefined
- : behindBy > 0;
-
- const label = isBehind === undefined
- ? FALLBACK_TEXT
- : isBehind
- ? `${behindBy} Behind`
- : 'Synced';
-
- const description = isBehind === undefined
- ? FALLBACK_TEXT
- : isBehind
- ? `This fork is ${behindBy} commit${behindBy === 1 ? '' : 's'} behind.`
- : isBaseRepo
- ? 'This build is up to date.'
- : 'This fork is up to date.';
-
- return {
- url,
- isForkedFromBase,
- isBaseRepo,
- behindBy,
- isBehind,
- label,
- description,
- didError: false,
- };
-};
-
-export const getGitHubMetaWithFallback = (params: RepoParams) =>
- getGitHubMeta(params)
- .catch(e => {
- console.error('Error retrieving GitHub meta', { params, error: e });
- return {
- url: undefined,
- isForkedFromBase: false,
- isBaseRepo: undefined,
- behindBy: undefined,
- isBehind: undefined,
- label: FALLBACK_TEXT,
- description: 'Could not connect to GitHub.',
- didError: true,
- };
- });
diff --git a/src/admin/insights/AdminAppInsights.tsx b/src/admin/insights/AdminAppInsights.tsx
new file mode 100644
index 00000000..a5cbb379
--- /dev/null
+++ b/src/admin/insights/AdminAppInsights.tsx
@@ -0,0 +1,96 @@
+import {
+ getPhotosMeta,
+ getUniqueCameras,
+ getUniqueFilmSimulations,
+ getUniqueLenses,
+ getUniqueTags,
+} from '@/photo/db/query';
+import AdminAppInsightsClient from './AdminAppInsightsClient';
+import {
+ APP_CONFIGURATION,
+ GRID_HOMEPAGE_ENABLED,
+ HAS_STATIC_OPTIMIZATION,
+ IS_DEVELOPMENT,
+ IS_PRODUCTION,
+ IS_VERCEL_GIT_PROVIDER_GITHUB,
+ MATTE_PHOTOS,
+ VERCEL_GIT_BRANCH,
+ VERCEL_GIT_COMMIT_SHA,
+ VERCEL_GIT_REPO_OWNER,
+ VERCEL_GIT_REPO_SLUG,
+} from '@/app-core/config';
+import { getGitHubMeta } from '../../platforms/github';
+import { OUTDATED_THRESHOLD } from '@/photo';
+
+const BASIC_PHOTO_INSTALLATION_COUNT = 32;
+
+const owner = VERCEL_GIT_REPO_OWNER;
+const repo = VERCEL_GIT_REPO_SLUG;
+const branch = VERCEL_GIT_BRANCH;
+const commit = VERCEL_GIT_COMMIT_SHA;
+
+export default async function AdminAppInsights() {
+ const [
+ { count: photosCount, dateRange },
+ { count: photosCountHidden },
+ { count: photosCountOutdated },
+ { count: photosCountPortrait },
+ tags,
+ cameras,
+ filmSimulations,
+ lenses,
+ codeMeta,
+ ] = await Promise.all([
+ getPhotosMeta({ hidden: 'include' }),
+ getPhotosMeta({ hidden: 'only' }),
+ getPhotosMeta({ hidden: 'include', updatedBefore: OUTDATED_THRESHOLD }),
+ getPhotosMeta({ maximumAspectRatio: 0.9 }),
+ getUniqueTags(),
+ getUniqueCameras(),
+ getUniqueFilmSimulations(),
+ getUniqueLenses(),
+ IS_VERCEL_GIT_PROVIDER_GITHUB || IS_DEVELOPMENT
+ ? getGitHubMeta({
+ owner,
+ repo,
+ branch,
+ commit,
+ })
+ : undefined,
+ ]);
+
+ const {
+ isAiTextGenerationEnabled,
+ hasVercelBlobStorage,
+ } = APP_CONFIGURATION;
+
+ return (
+ 0 && !MATTE_PHOTOS,
+ gridFirst: (
+ photosCount >= BASIC_PHOTO_INSTALLATION_COUNT &&
+ !GRID_HOMEPAGE_ENABLED
+ ),
+ noStaticOptimization: !HAS_STATIC_OPTIMIZATION,
+ }}
+ photoStats={{
+ photosCount,
+ photosCountHidden,
+ photosCountOutdated,
+ tagsCount: tags.length,
+ camerasCount: cameras.length,
+ filmSimulationsCount: filmSimulations.length,
+ lensesCount: lenses.length,
+ dateRange,
+ }}
+ debug={!IS_PRODUCTION}
+ />
+ );
+}
diff --git a/src/admin/insights/AdminAppInsightsClient.tsx b/src/admin/insights/AdminAppInsightsClient.tsx
new file mode 100644
index 00000000..7d49248f
--- /dev/null
+++ b/src/admin/insights/AdminAppInsightsClient.tsx
@@ -0,0 +1,349 @@
+'use client';
+
+import ScoreCard from '@/components/ScoreCard';
+import ScoreCardRow from '@/components/ScoreCardRow';
+import { dateRangeForPhotos } from '@/photo';
+import PhotoFilmSimulationIcon from '@/simulation/PhotoFilmSimulationIcon';
+import { FaCamera } from 'react-icons/fa';
+import { FaTag } from 'react-icons/fa';
+import { FaCircleInfo, FaRegCalendar } from 'react-icons/fa6';
+import { HiOutlinePhotograph } from 'react-icons/hi';
+import { MdAspectRatio } from 'react-icons/md';
+import { PiWarningBold } from 'react-icons/pi';
+import { TbCone, TbSparkles } from 'react-icons/tb';
+import { getGitHubMeta } from '../../platforms/github';
+import { BiGitBranch, BiGitCommit, BiLogoGithub } from 'react-icons/bi';
+import {
+ TEMPLATE_REPO_BRANCH,
+ TEMPLATE_REPO_OWNER,
+ TEMPLATE_REPO_NAME,
+ VERCEL_GIT_COMMIT_SHA_SHORT,
+ VERCEL_GIT_COMMIT_MESSAGE,
+ TEMPLATE_REPO_URL_FORK,
+ TEMPLATE_REPO_URL_README,
+} from '@/app-core/config';
+import { AdminAppInsights, hasTemplateRecommendations, PhotoStats } from '.';
+import EnvVar from '@/components/EnvVar';
+import { IoSyncCircle } from 'react-icons/io5';
+import clsx from 'clsx/lite';
+import { PATH_ADMIN_OUTDATED } from '@/app-core/paths';
+import { LiaBroomSolid } from 'react-icons/lia';
+import { IoMdGrid } from 'react-icons/io';
+import { RiSpeedMiniLine } from 'react-icons/ri';
+import AdminLink from '../AdminLink';
+import AdminEmptyState from '../AdminEmptyState';
+
+const DEBUG_COMMIT_SHA = '4cd29ed';
+const DEBUG_COMMIT_MESSAGE = 'Long commit message for debugging purposes';
+const DEBUG_BEHIND_BY = 9;
+const DEBUG_PHOTOS_COUNT_OUTDATED = 7;
+
+const readmeAnchor = (anchor: string) =>
+
+ README/{anchor}
+ ;
+
+const renderLabeledEnvVar = (label: string, envVar: string, value = '1') =>
+
+
+ {label}
+
+
+
;
+
+export default function AdminAppInsightsClient({
+ codeMeta,
+ insights,
+ photoStats: {
+ photosCount,
+ photosCountHidden,
+ photosCountOutdated,
+ tagsCount,
+ camerasCount,
+ filmSimulationsCount,
+ lensesCount,
+ dateRange,
+ },
+ debug,
+}: {
+ codeMeta?: Awaited>
+ insights: AdminAppInsights
+ photoStats: PhotoStats
+ debug?: boolean
+}) {
+ const {
+ noFork,
+ forkBehind,
+ noAi,
+ noAiRateLimiting,
+ outdatedPhotos,
+ photoMatting,
+ gridFirst,
+ noStaticOptimization,
+ } = insights;
+
+ const { descriptionWithSpaces } = dateRangeForPhotos(undefined, dateRange);
+
+ const branchLink =
+ {codeMeta?.branch ?? TEMPLATE_REPO_BRANCH}
+ ;
+
+ return (
+
+ {(codeMeta || debug) && <>
+
+ {(noFork || debug) &&
+ }
+ content="This template is not forked"
+ expandContent={<>
+
+ Fork original template
+
+ {' '}
+ to receive the latest fixes and features.
+ {' '}
+ Additional instructions in
+ {' '}
+ {readmeAnchor('receiving-updates')}.
+ >}
+ />}
+ {(forkBehind || debug) && }
+ content={<>
+ This fork is
+ {' '}
+
+ {codeMeta?.behindBy ?? DEBUG_BEHIND_BY}
+ {' '}
+ {(codeMeta?.behindBy ?? DEBUG_BEHIND_BY) === 1
+ ? 'commit'
+ : 'commits'}
+
+ {' '}
+ behind
+ >}
+ expandContent={<>
+
+ Sync your fork
+
+ {' '}
+ to receive the latest fixes and features.
+ >}
+ />}
+ }
+ content={}
+ />
+ }
+ content={branchLink}
+ />
+ }
+ content={
+
+ {VERCEL_GIT_COMMIT_SHA_SHORT ?? DEBUG_COMMIT_SHA}
+
+
+ {VERCEL_GIT_COMMIT_MESSAGE ?? DEBUG_COMMIT_MESSAGE}
+
+ }
+ />
+
+ >}
+
+ {(hasTemplateRecommendations(insights) || debug)
+ ? <>
+ {(noAiRateLimiting || debug) && }
+ content="AI enabled without rate limiting"
+ expandContent={<>
+ Create Vercel KV store and link to this project
+ in order prevent abuse by to enabling rate limiting.
+ >}
+ />}
+ {(noStaticOptimization || debug) && }
+ content="Speed up page load times"
+ expandContent={<>
+ Improve load times by enabling static optimization
+ {' '}
+ on:
+
+ {renderLabeledEnvVar(
+ 'Photo pages',
+ 'NEXT_PUBLIC_STATICALLY_OPTIMIZE_PHOTOS',
+ )}
+ {renderLabeledEnvVar(
+ 'Photo OG images',
+ 'NEXT_PUBLIC_STATICALLY_OPTIMIZE_PHOTO_OG_IMAGES',
+ )}
+ {renderLabeledEnvVar(
+ 'Category pages (tags, cameras, etc.)',
+ 'NEXT_PUBLIC_STATICALLY_OPTIMIZE_PHOTO_CATEGORIES',
+ )}
+ {renderLabeledEnvVar(
+ 'Category OG images',
+ 'NEXT_PUBLIC_STATICALLY_OPTIMIZE_PHOTO_CATEGORY_OG_IMAGES',
+ )}
+
+ See {readmeAnchor('performance')} for cost implications.
+
+
+ >}
+ />}
+ {(noAi || debug) && }
+ content="Improve SEO + accessibility with AI"
+ expandContent={<>
+ Enable automatic AI text generation
+ {' '}
+ by setting .
+ {' '}
+ Further instruction and cost considerations in
+ {' '}
+ {readmeAnchor('ai-text-generation')}.
+ >}
+ />}
+ {(photoMatting || debug) && }
+ content="Vertical photos may benefit from matting"
+ expandContent={<>
+ Enable photo matting to make
+ {' '}
+ portrait and landscape photos appear more consistent
+ {' '}
+ .
+ >}
+ />}
+ {(gridFirst || debug) && }
+ content="Grid homepage"
+ expandContent={<>
+ Now that you have enough photos, consider switching your
+ {' '}
+ default view to grid by setting
+ {' '}
+ {/* eslint-disable-next-line max-len */}
+ .
+ >}
+ />}
+ >
+ :
+ Nothing to report!
+ }
+
+
+ {(outdatedPhotos || debug) && }
+ // eslint-disable-next-line max-len
+ content={`${photosCountOutdated || DEBUG_PHOTOS_COUNT_OUTDATED} outdated ${(photosCountOutdated || DEBUG_PHOTOS_COUNT_OUTDATED) === 1 ? 'photo' : 'photos'}`}
+ expandPath={PATH_ADMIN_OUTDATED}
+ />}
+ }
+ content={<>
+ {photosCount} photos
+ {photosCountHidden > 0 &&
+ ` (${photosCountHidden} hidden)`}
+ >}
+ />
+ }
+ content={`${tagsCount} tags`}
+ />
+ }
+ content={`${camerasCount} cameras`}
+ />
+ {filmSimulationsCount &&
+
+
+ }
+ content={`${filmSimulationsCount} film simulations`}
+ />}
+ }
+ content={`${lensesCount} lenses`}
+ />
+ }
+ content={descriptionWithSpaces}
+ />
+
+
+ );
+}
diff --git a/src/admin/insights/AdminAppInsightsIcon.tsx b/src/admin/insights/AdminAppInsightsIcon.tsx
new file mode 100644
index 00000000..dab0effb
--- /dev/null
+++ b/src/admin/insights/AdminAppInsightsIcon.tsx
@@ -0,0 +1,27 @@
+import clsx from 'clsx/lite';
+import { LuLightbulb } from 'react-icons/lu';
+
+export default function AdminAppInsightsIcon({
+ indicator,
+}: {
+ indicator?: 'blue' | 'yellow'
+}) {
+ return (
+
+
+ {indicator && }
+
+ );
+}
diff --git a/src/admin/insights/index.ts b/src/admin/insights/index.ts
new file mode 100644
index 00000000..66fdffe9
--- /dev/null
+++ b/src/admin/insights/index.ts
@@ -0,0 +1,44 @@
+import { PhotoDateRange } from '@/photo';
+
+export type AdminAppInsight =
+ 'noFork' |
+ 'forkBehind' |
+ 'noAi' |
+ 'noAiRateLimiting' |
+ 'outdatedPhotos' |
+ 'photoMatting' |
+ 'gridFirst' |
+ 'noStaticOptimization';
+
+const RECOMMENDATIONS: AdminAppInsight[] = [
+ 'noAi',
+ 'noAiRateLimiting',
+ 'photoMatting',
+ 'gridFirst',
+ 'noStaticOptimization',
+];
+
+export type AdminAppInsights = Record
+
+export const hasTemplateRecommendations = (insights: AdminAppInsights) =>
+ RECOMMENDATIONS.some(insight => insights[insight]);
+
+export interface PhotoStats {
+ photosCount: number
+ photosCountHidden: number
+ photosCountOutdated: number
+ tagsCount: number
+ camerasCount: number
+ filmSimulationsCount: number
+ lensesCount: number
+ dateRange?: PhotoDateRange
+}
+
+export const getInsightIndicator = ({
+ forkBehind,
+ noAiRateLimiting,
+ outdatedPhotos,
+}: AdminAppInsights) =>
+ forkBehind ||
+ noAiRateLimiting ||
+ outdatedPhotos;
diff --git a/src/site/CommandK.tsx b/src/app-core/CommandK.tsx
similarity index 97%
rename from src/site/CommandK.tsx
rename to src/app-core/CommandK.tsx
index b47c2903..0df5f312 100644
--- a/src/site/CommandK.tsx
+++ b/src/app-core/CommandK.tsx
@@ -18,7 +18,7 @@ import { formatCount, formatCountDescriptive } from '@/utility/string';
import PhotoFilmSimulationIcon from '@/simulation/PhotoFilmSimulationIcon';
import { IoMdCamera } from 'react-icons/io';
import { ADMIN_DEBUG_TOOLS_ENABLED, SHOW_FILM_SIMULATIONS } from './config';
-import { labelForFilmSimulation } from '@/vendors/fujifilm';
+import { labelForFilmSimulation } from '@/platforms/fujifilm';
import { getUniqueFocalLengths } from '@/photo/db/query';
import { formatFocalLength } from '@/focal';
import { TbCone } from 'react-icons/tb';
diff --git a/src/site/Footer.tsx b/src/app-core/Footer.tsx
similarity index 95%
rename from src/site/Footer.tsx
rename to src/app-core/Footer.tsx
index 12bcfe30..2fab4de5 100644
--- a/src/site/Footer.tsx
+++ b/src/app-core/Footer.tsx
@@ -2,9 +2,9 @@
import { clsx } from 'clsx/lite';
import SiteGrid from '../components/SiteGrid';
-import ThemeSwitcher from '@/site/ThemeSwitcher';
+import ThemeSwitcher from '@/app-core/ThemeSwitcher';
import Link from 'next/link';
-import { SHOW_REPO_LINK } from '@/site/config';
+import { SHOW_REPO_LINK } from '@/app-core/config';
import RepoLink from '../components/RepoLink';
import { usePathname } from 'next/navigation';
import { PATH_ADMIN_PHOTOS, isPathAdmin, isPathSignIn } from './paths';
diff --git a/src/site/IconFeed.tsx b/src/app-core/IconFeed.tsx
similarity index 100%
rename from src/site/IconFeed.tsx
rename to src/app-core/IconFeed.tsx
diff --git a/src/site/IconGrSync.tsx b/src/app-core/IconGrSync.tsx
similarity index 100%
rename from src/site/IconGrSync.tsx
rename to src/app-core/IconGrSync.tsx
diff --git a/src/site/IconGrid.tsx b/src/app-core/IconGrid.tsx
similarity index 100%
rename from src/site/IconGrid.tsx
rename to src/app-core/IconGrid.tsx
diff --git a/src/site/IconSearch.tsx b/src/app-core/IconSearch.tsx
similarity index 100%
rename from src/site/IconSearch.tsx
rename to src/app-core/IconSearch.tsx
diff --git a/src/site/Nav.tsx b/src/app-core/Nav.tsx
similarity index 96%
rename from src/site/Nav.tsx
rename to src/app-core/Nav.tsx
index 6f3bf2e0..07715a4c 100644
--- a/src/site/Nav.tsx
+++ b/src/app-core/Nav.tsx
@@ -4,7 +4,7 @@ import { clsx } from 'clsx/lite';
import { usePathname } from 'next/navigation';
import Link from 'next/link';
import SiteGrid from '../components/SiteGrid';
-import ViewSwitcher, { SwitcherSelection } from '@/site/ViewSwitcher';
+import ViewSwitcher, { SwitcherSelection } from '@/app-core/ViewSwitcher';
import {
PATH_ROOT,
isPathAdmin,
@@ -12,7 +12,7 @@ import {
isPathGrid,
isPathProtected,
isPathSignIn,
-} from '@/site/paths';
+} from '@/app-core/paths';
import AnimateItems from '../components/AnimateItems';
import { useAppState } from '@/state/AppState';
import {
diff --git a/src/site/SecretGenerator.tsx b/src/app-core/SecretGenerator.tsx
similarity index 93%
rename from src/site/SecretGenerator.tsx
rename to src/app-core/SecretGenerator.tsx
index c85452d6..4f74c706 100644
--- a/src/site/SecretGenerator.tsx
+++ b/src/app-core/SecretGenerator.tsx
@@ -30,9 +30,7 @@ export default function SecretGenerator() {
'flex flex-nowrap items-center gap-2 leading-none -mx-1',
)}>
{secret ? {secret} : }
-
diff --git a/src/site/ThemeSwitcher.tsx b/src/app-core/ThemeSwitcher.tsx
similarity index 100%
rename from src/site/ThemeSwitcher.tsx
rename to src/app-core/ThemeSwitcher.tsx
diff --git a/src/site/ViewSwitcher.tsx b/src/app-core/ViewSwitcher.tsx
similarity index 93%
rename from src/site/ViewSwitcher.tsx
rename to src/app-core/ViewSwitcher.tsx
index 3d5a2baf..ab39720e 100644
--- a/src/site/ViewSwitcher.tsx
+++ b/src/app-core/ViewSwitcher.tsx
@@ -1,12 +1,12 @@
import Switcher from '@/components/Switcher';
import SwitcherItem from '@/components/SwitcherItem';
-import IconFeed from '@/site/IconFeed';
-import IconGrid from '@/site/IconGrid';
+import IconFeed from '@/app-core/IconFeed';
+import IconGrid from '@/app-core/IconGrid';
import {
PATH_ADMIN_PHOTOS,
PATH_FEED_INFERRED,
PATH_GRID_INFERRED,
-} from '@/site/paths';
+} from '@/app-core/paths';
import { BiLockAlt } from 'react-icons/bi';
import IconSearch from './IconSearch';
import { useAppState } from '@/state/AppState';
diff --git a/src/site/api.ts b/src/app-core/api.ts
similarity index 100%
rename from src/site/api.ts
rename to src/app-core/api.ts
diff --git a/src/site/config.ts b/src/app-core/config.ts
similarity index 90%
rename from src/site/config.ts
rename to src/app-core/config.ts
index bbcba05f..7dbdf6c9 100644
--- a/src/site/config.ts
+++ b/src/app-core/config.ts
@@ -6,17 +6,24 @@ import type { StorageType } from '@/services/storage';
import { makeUrlAbsolute, shortenUrl } from '@/utility/url';
// HARD-CODED GLOBAL CONFIGURATION
+
export const SHOULD_PREFETCH_ALL_LINKS: boolean | undefined = undefined;
-// META / SOURCE / DOMAINS
-export const SITE_TITLE =
- process.env.NEXT_PUBLIC_SITE_TITLE ||
- 'Photo Blog';
+// TEMPLATE META
-// SOURCE
-export const TEMPLATE_BASE_OWNER = 'sambecker';
-export const TEMPLATE_BASE_REPO = 'exif-photo-blog';
-export const TEMPLATE_BASE_BRANCH = 'main';
+export const TEMPLATE_TITLE = 'Photo Blog';
+export const TEMPLATE_DESCRIPTION = 'Store photos with original camera data';
+
+// SOURCE CODE
+
+export const TEMPLATE_REPO_OWNER = 'sambecker';
+export const TEMPLATE_REPO_NAME = 'exif-photo-blog';
+export const TEMPLATE_REPO_BRANCH = 'main';
+// eslint-disable-next-line max-len
+export const TEMPLATE_REPO_URL = `https://github.com/${TEMPLATE_REPO_OWNER}/${TEMPLATE_REPO_NAME}`;
+export const TEMPLATE_REPO_URL_FORK = `${TEMPLATE_REPO_URL}/fork`;
+// eslint-disable-next-line max-len
+export const TEMPLATE_REPO_URL_README = `${TEMPLATE_REPO_URL}?tab=readme-ov-file`;
export const VERCEL_GIT_PROVIDER =
process.env.NEXT_PUBLIC_VERCEL_GIT_PROVIDER;
@@ -59,6 +66,12 @@ export const IS_PREVIEW = VERCEL_ENV === 'preview';
export const VERCEL_BYPASS_KEY = 'x-vercel-protection-bypass';
export const VERCEL_BYPASS_SECRET = process.env.VERCEL_AUTOMATION_BYPASS_SECRET;
+// SITE META
+
+export const SITE_TITLE =
+ process.env.NEXT_PUBLIC_SITE_TITLE ||
+ TEMPLATE_TITLE;
+
// User-facing domain, potential site title
const SITE_DOMAIN =
process.env.NEXT_PUBLIC_SITE_DOMAIN ||
@@ -90,11 +103,14 @@ export const SITE_ABOUT = process.env.NEXT_PUBLIC_SITE_ABOUT;
export const HAS_DEFINED_SITE_DESCRIPTION =
Boolean(process.env.NEXT_PUBLIC_SITE_DESCRIPTION);
+// STORAGE
+
// STORAGE: DATABASE
export const HAS_DATABASE =
Boolean(process.env.POSTGRES_URL);
export const POSTGRES_SSL_ENABLED =
process.env.DISABLE_POSTGRES_SSL === '1' ? false : true;
+
// STORAGE: VERCEL KV
export const HAS_VERCEL_KV =
Boolean(process.env.KV_URL);
@@ -162,6 +178,11 @@ export const STATICALLY_OPTIMIZED_PHOTO_CATEGORIES =
process.env.NEXT_PUBLIC_STATICALLY_OPTIMIZE_PHOTO_CATEGORIES === '1';
export const STATICALLY_OPTIMIZED_PHOTO_CATEGORY_OG_IMAGES =
process.env.NEXT_PUBLIC_STATICALLY_OPTIMIZE_PHOTO_CATEGORY_OG_IMAGES === '1';
+export const HAS_STATIC_OPTIMIZATION =
+ STATICALLY_OPTIMIZED_PHOTOS ||
+ STATICALLY_OPTIMIZED_PHOTO_OG_IMAGES ||
+ STATICALLY_OPTIMIZED_PHOTO_CATEGORIES ||
+ STATICALLY_OPTIMIZED_PHOTO_CATEGORY_OG_IMAGES;
export const PRESERVE_ORIGINAL_UPLOADS =
process.env.NEXT_PUBLIC_PRESERVE_ORIGINAL_UPLOADS === '1' ||
// Legacy environment variable name
@@ -232,7 +253,7 @@ export const ADMIN_DEBUG_TOOLS_ENABLED = process.env.ADMIN_DEBUG_TOOLS === '1';
export const ADMIN_DB_OPTIMIZE_ENABLED = process.env.ADMIN_DB_OPTIMIZE === '1';
export const ADMIN_SQL_DEBUG_ENABLED = process.env.ADMIN_SQL_DEBUG === '1';
-export const CONFIG_CHECKLIST_STATUS = {
+export const APP_CONFIGURATION = {
// Storage
hasDatabase: HAS_DATABASE,
isPostgresSslEnabled: POSTGRES_SSL_ENABLED,
@@ -272,11 +293,7 @@ export const CONFIG_CHECKLIST_STATUS = {
hasAiTextAutoGeneratedFields:
Boolean(process.env.AI_TEXT_AUTO_GENERATED_FIELDS),
// Performance
- isStaticallyOptimized: (
- STATICALLY_OPTIMIZED_PHOTOS ||
- STATICALLY_OPTIMIZED_PHOTO_OG_IMAGES ||
- STATICALLY_OPTIMIZED_PHOTO_CATEGORIES
- ),
+ isStaticallyOptimized: HAS_STATIC_OPTIMIZATION,
arePhotosStaticallyOptimized: STATICALLY_OPTIMIZED_PHOTOS,
arePhotoOGImagesStaticallyOptimized: STATICALLY_OPTIMIZED_PHOTO_OG_IMAGES,
arePhotoCategoriesStaticallyOptimized: STATICALLY_OPTIMIZED_PHOTO_CATEGORIES,
@@ -301,7 +318,7 @@ export const CONFIG_CHECKLIST_STATUS = {
isGridHomepageEnabled: GRID_HOMEPAGE_ENABLED,
gridAspectRatio: GRID_ASPECT_RATIO,
hasGridAspectRatio: Boolean(process.env.NEXT_PUBLIC_GRID_ASPECT_RATIO),
- gridDensity: HIGH_DENSITY_GRID,
+ hasHighGridDensity: HIGH_DENSITY_GRID,
hasGridDensityPreference:
Boolean(process.env.NEXT_PUBLIC_SHOW_LARGE_THUMBNAILS),
// Settings
@@ -326,11 +343,11 @@ export const CONFIG_CHECKLIST_STATUS = {
commitUrl: VERCEL_GIT_COMMIT_URL,
};
-export type ConfigChecklistStatus = typeof CONFIG_CHECKLIST_STATUS;
+export type ConfigChecklistStatus = typeof APP_CONFIGURATION;
export const IS_SITE_READY =
- CONFIG_CHECKLIST_STATUS.hasDatabase &&
- CONFIG_CHECKLIST_STATUS.hasStorageProvider &&
- CONFIG_CHECKLIST_STATUS.hasAuthSecret &&
- CONFIG_CHECKLIST_STATUS.hasAdminUser;
+ APP_CONFIGURATION.hasDatabase &&
+ APP_CONFIGURATION.hasStorageProvider &&
+ APP_CONFIGURATION.hasAuthSecret &&
+ APP_CONFIGURATION.hasAdminUser;
\ No newline at end of file
diff --git a/src/app-core/font.ts b/src/app-core/font.ts
new file mode 100644
index 00000000..9f0f3b78
--- /dev/null
+++ b/src/app-core/font.ts
@@ -0,0 +1,20 @@
+import fs from 'fs';
+import path from 'path';
+import { cwd } from 'process';
+
+const FONT_IBM_PLEX_MONO_FAMILY = 'IBMPlexMono';
+const FONT_IBM_PLEX_MONO_PATH = '/public/fonts/IBMPlexMono-Medium.ttf';
+
+const getFontData = async () =>
+ fs.readFileSync(path.join(cwd(), FONT_IBM_PLEX_MONO_PATH));
+
+export const getIBMPlexMonoMedium = () => getFontData()
+ .then(data => ({
+ fontFamily: FONT_IBM_PLEX_MONO_FAMILY,
+ fonts: [{
+ name: FONT_IBM_PLEX_MONO_FAMILY,
+ data,
+ weight: 500,
+ style: 'normal',
+ } as const],
+ }));
diff --git a/src/site/paths.ts b/src/app-core/paths.ts
similarity index 98%
rename from src/site/paths.ts
rename to src/app-core/paths.ts
index 8556b115..50834d5e 100644
--- a/src/site/paths.ts
+++ b/src/app-core/paths.ts
@@ -39,6 +39,7 @@ export const PATH_ADMIN_OUTDATED = `${PATH_ADMIN}/outdated`;
export const PATH_ADMIN_UPLOADS = `${PATH_ADMIN}/uploads`;
export const PATH_ADMIN_TAGS = `${PATH_ADMIN}/tags`;
export const PATH_ADMIN_CONFIGURATION = `${PATH_ADMIN}/configuration`;
+export const PATH_ADMIN_INSIGHTS = `${PATH_ADMIN}/insights`;
export const PATH_ADMIN_BASELINE = `${PATH_ADMIN}/baseline`;
// Debug paths
@@ -213,6 +214,9 @@ export const isPathTopLevelAdmin = (pathname?: string) =>
export const isPathAdminConfiguration = (pathname?: string) =>
checkPathPrefix(pathname, PATH_ADMIN_CONFIGURATION);
+export const isPathAdminInsights = (pathname?: string) =>
+ checkPathPrefix(pathname, PATH_ADMIN_INSIGHTS);
+
export const isPathProtected = (pathname?: string) =>
checkPathPrefix(pathname, PATH_ADMIN) ||
checkPathPrefix(pathname, pathForTag(TAG_HIDDEN)) ||
diff --git a/src/app/admin/configuration/page.tsx b/src/app/admin/configuration/page.tsx
index ecb28453..59d27f49 100644
--- a/src/app/admin/configuration/page.tsx
+++ b/src/app/admin/configuration/page.tsx
@@ -1,27 +1,14 @@
import ClearCacheButton from '@/admin/ClearCacheButton';
-import GitHubForkStatusBadge from '@/admin/github/GitHubForkStatusBadge';
-import Container from '@/components/Container';
-import SiteGrid from '@/components/SiteGrid';
-import { IS_DEVELOPMENT, IS_VERCEL_GIT_PROVIDER_GITHUB } from '@/site/config';
-import SiteChecklist from '@/site/SiteChecklist';
+import AdminAppConfiguration from '@/admin/AdminAppConfiguration';
+import AdminInfoPage from '@/admin/AdminInfoPage';
-export default async function AdminConfigurationPage() {
+export default function AdminAppConfigurationPage() {
return (
-
-
-
- App Configuration
-
- {(IS_VERCEL_GIT_PROVIDER_GITHUB || IS_DEVELOPMENT) &&
-
}
-
-
-
-
-
- }
- />
+
}
+ >
+
+
);
}
diff --git a/src/app/admin/insights/page.tsx b/src/app/admin/insights/page.tsx
new file mode 100644
index 00000000..0bf5efa4
--- /dev/null
+++ b/src/app/admin/insights/page.tsx
@@ -0,0 +1,8 @@
+import AdminAppInsights from '@/admin/insights/AdminAppInsights';
+import AdminInfoPage from '@/admin/AdminInfoPage';
+
+export default async function AdminInsightsPage() {
+ return
+
+ ;
+}
diff --git a/src/app/admin/outdated/page.tsx b/src/app/admin/outdated/page.tsx
index adb47cfe..ef945ffc 100644
--- a/src/app/admin/outdated/page.tsx
+++ b/src/app/admin/outdated/page.tsx
@@ -1,7 +1,7 @@
import { getPhotos } from '@/photo/db/query';
import { OUTDATED_THRESHOLD } from '@/photo';
import AdminOutdatedClient from '@/admin/AdminOutdatedClient';
-import { AI_TEXT_GENERATION_ENABLED } from '@/site/config';
+import { AI_TEXT_GENERATION_ENABLED } from '@/app-core/config';
export const maxDuration = 60;
diff --git a/src/app/admin/photos/[photoId]/edit/page.tsx b/src/app/admin/photos/[photoId]/edit/page.tsx
index 863695cb..25076704 100644
--- a/src/app/admin/photos/[photoId]/edit/page.tsx
+++ b/src/app/admin/photos/[photoId]/edit/page.tsx
@@ -1,12 +1,12 @@
import { redirect } from 'next/navigation';
import { getPhotoNoStore, getUniqueTagsCached } from '@/photo/cache';
-import { PATH_ADMIN } from '@/site/paths';
+import { PATH_ADMIN } from '@/app-core/paths';
import PhotoEditPageClient from '@/photo/PhotoEditPageClient';
import {
AI_TEXT_GENERATION_ENABLED,
BLUR_ENABLED,
IS_PREVIEW,
-} from '@/site/config';
+} from '@/app-core/config';
import { blurImageFromUrl, resizeImageFromUrl } from '@/photo/server';
import { getNextImageUrlForManipulation } from '@/services/next-image';
diff --git a/src/app/admin/tags/[tag]/edit/page.tsx b/src/app/admin/tags/[tag]/edit/page.tsx
index e8458867..48c71ab4 100644
--- a/src/app/admin/tags/[tag]/edit/page.tsx
+++ b/src/app/admin/tags/[tag]/edit/page.tsx
@@ -2,7 +2,7 @@ import AdminChildPage from '@/components/AdminChildPage';
import { redirect } from 'next/navigation';
import { getPhotosCached } from '@/photo/cache';
import TagForm from '@/tag/TagForm';
-import { PATH_ADMIN, PATH_ADMIN_TAGS, pathForTag } from '@/site/paths';
+import { PATH_ADMIN, PATH_ADMIN_TAGS, pathForTag } from '@/app-core/paths';
import PhotoLightbox from '@/photo/PhotoLightbox';
import { getPhotosMeta } from '@/photo/db/query';
import AdminTagBadge from '@/admin/AdminTagBadge';
diff --git a/src/app/admin/uploads/[uploadPath]/page.tsx b/src/app/admin/uploads/[uploadPath]/page.tsx
index 2481ddbb..595d332a 100644
--- a/src/app/admin/uploads/[uploadPath]/page.tsx
+++ b/src/app/admin/uploads/[uploadPath]/page.tsx
@@ -1,4 +1,4 @@
-import { PATH_ADMIN } from '@/site/paths';
+import { PATH_ADMIN } from '@/app-core/paths';
import { extractImageDataFromBlobPath } from '@/photo/server';
import { redirect } from 'next/navigation';
import { getUniqueTagsCached } from '@/photo/cache';
@@ -7,7 +7,7 @@ import {
AI_TEXT_AUTO_GENERATED_FIELDS,
AI_TEXT_GENERATION_ENABLED,
BLUR_ENABLED,
-} from '@/site/config';
+} from '@/app-core/config';
import ErrorNote from '@/components/ErrorNote';
export const maxDuration = 60;
diff --git a/src/app/api/route.ts b/src/app/api/route.ts
index a3c74dc5..e1a61c62 100644
--- a/src/app/api/route.ts
+++ b/src/app/api/route.ts
@@ -1,10 +1,10 @@
import { getPhotosCached } from '@/photo/cache';
-import { API_PHOTO_REQUEST_LIMIT, formatPhotoForApi } from '@/site/api';
+import { API_PHOTO_REQUEST_LIMIT, formatPhotoForApi } from '@/app-core/api';
import {
BASE_URL,
PUBLIC_API_ENABLED,
SITE_TITLE,
-} from '@/site/config';
+} from '@/app-core/config';
export const dynamic = 'force-dynamic';
diff --git a/src/app/api/storage/presigned-url/[key]/route.ts b/src/app/api/storage/presigned-url/[key]/route.ts
index 9eb0db38..0406b95c 100644
--- a/src/app/api/storage/presigned-url/[key]/route.ts
+++ b/src/app/api/storage/presigned-url/[key]/route.ts
@@ -7,7 +7,7 @@ import {
cloudflareR2Client,
cloudflareR2PutObjectCommandForKey,
} from '@/services/storage/cloudflare-r2';
-import { CURRENT_STORAGE } from '@/site/config';
+import { CURRENT_STORAGE } from '@/app-core/config';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
export async function GET(
diff --git a/src/app/film-demo/animate/page.tsx b/src/app/film-demo/animate/page.tsx
index 92cb1f8a..11612df7 100644
--- a/src/app/film-demo/animate/page.tsx
+++ b/src/app/film-demo/animate/page.tsx
@@ -2,7 +2,7 @@
import SiteGrid from '@/components/SiteGrid';
import { clsx } from 'clsx/lite';
-import { FILM_SIMULATION_FORM_INPUT_OPTIONS } from '@/vendors/fujifilm';
+import { FILM_SIMULATION_FORM_INPUT_OPTIONS } from '@/platforms/fujifilm';
import PhotoFilmSimulation from
'@/simulation/PhotoFilmSimulation';
import { useEffect, useState } from 'react';
diff --git a/src/app/film-demo/page.tsx b/src/app/film-demo/page.tsx
index cd6a94be..a5e7467c 100644
--- a/src/app/film-demo/page.tsx
+++ b/src/app/film-demo/page.tsx
@@ -1,4 +1,4 @@
-import { FILM_SIMULATION_FORM_INPUT_OPTIONS } from '@/vendors/fujifilm';
+import { FILM_SIMULATION_FORM_INPUT_OPTIONS } from '@/platforms/fujifilm';
import PhotoFilmSimulation from
'@/simulation/PhotoFilmSimulation';
diff --git a/src/app/film/[simulation]/[photoId]/page.tsx b/src/app/film/[simulation]/[photoId]/page.tsx
index aaee4411..2171b016 100644
--- a/src/app/film/[simulation]/[photoId]/page.tsx
+++ b/src/app/film/[simulation]/[photoId]/page.tsx
@@ -9,7 +9,7 @@ import {
PATH_ROOT,
absolutePathForPhoto,
absolutePathForPhotoImage,
-} from '@/site/paths';
+} from '@/app-core/paths';
import PhotoDetailPage from '@/photo/PhotoDetailPage';
import { FilmSimulation } from '@/simulation';
import {
diff --git a/src/app/film/[simulation]/image/route.tsx b/src/app/film/[simulation]/image/route.tsx
index 62cb4996..b71f1569 100644
--- a/src/app/film/[simulation]/image/route.tsx
+++ b/src/app/film/[simulation]/image/route.tsx
@@ -6,7 +6,7 @@ import {
import FilmSimulationImageResponse from
'@/image-response/FilmSimulationImageResponse';
import { FilmSimulation } from '@/simulation';
-import { getIBMPlexMonoMedium } from '@/site/font';
+import { getIBMPlexMonoMedium } from '@/app-core/font';
import { ImageResponse } from 'next/og';
import { getImageResponseCacheControlHeaders } from '@/image-response/cache';
import { GENERATE_STATIC_PARAMS_LIMIT } from '@/photo/db';
@@ -14,7 +14,7 @@ import { getUniqueFilmSimulations } from '@/photo/db/query';
import {
STATICALLY_OPTIMIZED_PHOTO_CATEGORY_OG_IMAGES,
IS_PRODUCTION,
-} from '@/site/config';
+} from '@/app-core/config';
export let generateStaticParams:
(() => Promise<{ simulation: FilmSimulation }[]>) | undefined = undefined;
diff --git a/src/app/film/[simulation]/page.tsx b/src/app/film/[simulation]/page.tsx
index 0199457c..262d78fd 100644
--- a/src/app/film/[simulation]/page.tsx
+++ b/src/app/film/[simulation]/page.tsx
@@ -2,9 +2,9 @@ import { INFINITE_SCROLL_GRID_INITIAL } from '@/photo';
import { getUniqueFilmSimulations } from '@/photo/db/query';
import { FilmSimulation, generateMetaForFilmSimulation } from '@/simulation';
import FilmSimulationOverview from '@/simulation/FilmSimulationOverview';
-import { IS_PRODUCTION } from '@/site/config';
+import { IS_PRODUCTION } from '@/app-core/config';
import { getPhotosFilmSimulationDataCached } from '@/simulation/data';
-import { STATICALLY_OPTIMIZED_PHOTO_CATEGORIES } from '@/site/config';
+import { STATICALLY_OPTIMIZED_PHOTO_CATEGORIES } from '@/app-core/config';
import { Metadata } from 'next/types';
import { cache } from 'react';
diff --git a/src/app/focal/[focal]/[photoId]/page.tsx b/src/app/focal/[focal]/[photoId]/page.tsx
index 1ca40aa1..85c4dc06 100644
--- a/src/app/focal/[focal]/[photoId]/page.tsx
+++ b/src/app/focal/[focal]/[photoId]/page.tsx
@@ -9,7 +9,7 @@ import {
PATH_ROOT,
absolutePathForPhoto,
absolutePathForPhotoImage,
-} from '@/site/paths';
+} from '@/app-core/paths';
import PhotoDetailPage from '@/photo/PhotoDetailPage';
import { getPhotosNearIdCached } from '@/photo/cache';
import { cache } from 'react';
diff --git a/src/app/focal/[focal]/image/route.tsx b/src/app/focal/[focal]/image/route.tsx
index 44cc1558..79682cb0 100644
--- a/src/app/focal/[focal]/image/route.tsx
+++ b/src/app/focal/[focal]/image/route.tsx
@@ -3,7 +3,7 @@ import {
IMAGE_OG_DIMENSION_SMALL,
MAX_PHOTOS_TO_SHOW_PER_TAG,
} from '@/image-response';
-import { getIBMPlexMonoMedium } from '@/site/font';
+import { getIBMPlexMonoMedium } from '@/app-core/font';
import { ImageResponse } from 'next/og';
import { getImageResponseCacheControlHeaders } from '@/image-response/cache';
import FocalLengthImageResponse from
@@ -14,7 +14,7 @@ import { getUniqueFocalLengths } from '@/photo/db/query';
import {
STATICALLY_OPTIMIZED_PHOTO_CATEGORY_OG_IMAGES,
IS_PRODUCTION,
-} from '@/site/config';
+} from '@/app-core/config';
export let generateStaticParams:
(() => Promise<{ focal: string }[]>) | undefined = undefined;
diff --git a/src/app/focal/[focal]/page.tsx b/src/app/focal/[focal]/page.tsx
index ed8d78a6..2d01680a 100644
--- a/src/app/focal/[focal]/page.tsx
+++ b/src/app/focal/[focal]/page.tsx
@@ -2,10 +2,10 @@ import { generateMetaForFocalLength, getFocalLengthFromString } from '@/focal';
import FocalLengthOverview from '@/focal/FocalLengthOverview';
import { getPhotosFocalLengthDataCached } from '@/focal/data';
import { INFINITE_SCROLL_GRID_INITIAL } from '@/photo';
-import { IS_PRODUCTION } from '@/site/config';
+import { IS_PRODUCTION } from '@/app-core/config';
import { getUniqueFocalLengths } from '@/photo/db/query';
-import { STATICALLY_OPTIMIZED_PHOTO_CATEGORIES } from '@/site/config';
-import { PATH_ROOT } from '@/site/paths';
+import { STATICALLY_OPTIMIZED_PHOTO_CATEGORIES } from '@/app-core/config';
+import { PATH_ROOT } from '@/app-core/paths';
import type { Metadata } from 'next';
import { redirect } from 'next/navigation';
import { cache } from 'react';
diff --git a/src/app/home-image/route.tsx b/src/app/home-image/route.tsx
index 7cb7acf7..fa820f94 100644
--- a/src/app/home-image/route.tsx
+++ b/src/app/home-image/route.tsx
@@ -4,7 +4,7 @@ import {
MAX_PHOTOS_TO_SHOW_OG,
} from '@/image-response';
import HomeImageResponse from '@/image-response/HomeImageResponse';
-import { getIBMPlexMonoMedium } from '@/site/font';
+import { getIBMPlexMonoMedium } from '@/app-core/font';
import { ImageResponse } from 'next/og';
import { getImageResponseCacheControlHeaders } from '@/image-response/cache';
import { isNextImageReadyBasedOnPhotos } from '@/photo';
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index fbebb074..26171370 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -7,15 +7,15 @@ import {
SITE_DESCRIPTION,
SITE_DOMAIN_OR_TITLE,
SITE_TITLE,
-} from '@/site/config';
+} from '@/app-core/config';
import AppStateProvider from '@/state/AppStateProvider';
import ToasterWithThemes from '@/toast/ToasterWithThemes';
import PhotoEscapeHandler from '@/photo/PhotoEscapeHandler';
import { Metadata } from 'next/types';
import { ThemeProvider } from 'next-themes';
-import Nav from '@/site/Nav';
-import Footer from '@/site/Footer';
-import CommandK from '@/site/CommandK';
+import Nav from '@/app-core/Nav';
+import Footer from '@/app-core/Footer';
+import CommandK from '@/app-core/CommandK';
import SwrConfigClient from '../state/SwrConfigClient';
import AdminBatchEditPanel from '@/admin/AdminBatchEditPanel';
import ShareModals from '@/share/ShareModals';
@@ -72,11 +72,8 @@ export default function RootLayout({
>
-
-
+
+
-
-
-
-
-
-
+
+
+
+
+
+