Merge pull request #192 from sambecker/admin-info
Introduce Admin Insights ✨
This commit is contained in:
commit
d63710cc98
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@ -12,7 +12,7 @@ import {
|
||||
isPathProtected,
|
||||
isPathTag,
|
||||
isPathTagPhoto,
|
||||
} from '@/site/paths';
|
||||
} from '@/app-core/paths';
|
||||
import { TAG_HIDDEN } from '@/tag';
|
||||
|
||||
const PHOTO_ID = 'UsKSGcbt';
|
||||
|
||||
22
package.json
22
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"
|
||||
}
|
||||
|
||||
345
pnpm-lock.yaml
generated
345
pnpm-lock.yaml
generated
@ -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: {}
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
20
src/admin/AdminAppConfiguration.tsx
Normal file
20
src/admin/AdminAppConfiguration.tsx
Normal file
@ -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 (
|
||||
<Suspense fallback={<AdminAppConfigurationClient {...{
|
||||
...APP_CONFIGURATION,
|
||||
isAnalyzingConfiguration: true,
|
||||
simplifiedView,
|
||||
}} /> }>
|
||||
<AdminAppConfigurationServer {...{ simplifiedView }} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
@ -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) =>
|
||||
<>
|
||||
<a {...{
|
||||
href,
|
||||
...external && { target: '_blank', rel: 'noopener noreferrer' },
|
||||
className: clsx(
|
||||
'underline hover:no-underline',
|
||||
),
|
||||
}}>
|
||||
{text}
|
||||
</a>
|
||||
{external &&
|
||||
<>
|
||||
|
||||
<FiExternalLink
|
||||
size={14}
|
||||
className='inline translate-y-[-1.5px]'
|
||||
/>
|
||||
</>}
|
||||
</>;
|
||||
|
||||
const renderEnvVar = (
|
||||
variable: string,
|
||||
minimal?: boolean,
|
||||
) =>
|
||||
<div
|
||||
key={variable}
|
||||
className={clsx(
|
||||
'overflow-x-auto overflow-y-hidden',
|
||||
minimal && 'inline-flex',
|
||||
)}
|
||||
>
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<span className={clsx(
|
||||
'text-[11px] font-medium tracking-wider',
|
||||
'px-0.5 py-[0.5px]',
|
||||
'rounded-[5px]',
|
||||
'bg-gray-100 dark:bg-gray-800',
|
||||
)}>
|
||||
`{variable}`
|
||||
</span>
|
||||
{!minimal && <CopyButton label={variable} text={variable} subtle />}
|
||||
</span>
|
||||
</div>;
|
||||
|
||||
const renderEnvVars = (variables: string[]) =>
|
||||
<div className="pt-1 space-y-1">
|
||||
{variables.map(envVar => renderEnvVar(envVar))}
|
||||
<div className="pt-1 flex flex-col gap-1">
|
||||
{variables.map(envVar =>
|
||||
<EnvVar key={envVar} variable={envVar} />)}
|
||||
</div>;
|
||||
|
||||
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({
|
||||
</WarningNote>;
|
||||
|
||||
return (
|
||||
<div className="max-w-xl w-full">
|
||||
<>
|
||||
<div className="space-y-3 -mt-3">
|
||||
<Checklist
|
||||
<ChecklistGroup
|
||||
title="Storage"
|
||||
icon={<BiData size={16} />}
|
||||
>
|
||||
@ -234,11 +185,13 @@ export default function SiteChecklistClient({
|
||||
: renderSubStatus('optional', <>
|
||||
Vercel Postgres:
|
||||
{' '}
|
||||
{renderLink(
|
||||
<AdminLink
|
||||
// eslint-disable-next-line max-len
|
||||
'https://vercel.com/docs/storage/vercel-postgres/quickstart#create-a-postgres-database',
|
||||
'create store',
|
||||
)}
|
||||
href="https://vercel.com/docs/storage/vercel-postgres/quickstart#create-a-postgres-database"
|
||||
externalIcon
|
||||
>
|
||||
create store
|
||||
</AdminLink>
|
||||
{' '}
|
||||
and connect to project
|
||||
</>)}
|
||||
@ -270,11 +223,13 @@ export default function SiteChecklistClient({
|
||||
: renderSubStatus('optional', <>
|
||||
{labelForStorage('vercel-blob')}:
|
||||
{' '}
|
||||
{renderLink(
|
||||
<AdminLink
|
||||
// eslint-disable-next-line max-len
|
||||
'https://vercel.com/docs/storage/vercel-blob/quickstart#create-a-blob-store',
|
||||
'create store',
|
||||
)}
|
||||
href="https://vercel.com/docs/storage/vercel-blob/quickstart#create-a-blob-store"
|
||||
externalIcon
|
||||
>
|
||||
create store
|
||||
</AdminLink>
|
||||
{' '}
|
||||
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',
|
||||
)}
|
||||
<AdminLink
|
||||
// eslint-disable-next-line max-len
|
||||
href="https://github.com/sambecker/exif-photo-blog#cloudflare-r2"
|
||||
externalIcon
|
||||
>
|
||||
create/configure bucket
|
||||
</AdminLink>
|
||||
</>)}
|
||||
{hasAwsS3Storage
|
||||
? renderSubStatus('checked', 'AWS S3: connected')
|
||||
: renderSubStatus('optional', <>
|
||||
{labelForStorage('aws-s3')}:
|
||||
{' '}
|
||||
{renderLink(
|
||||
'https://github.com/sambecker/exif-photo-blog#aws-s3',
|
||||
'create/configure bucket',
|
||||
)}
|
||||
<AdminLink
|
||||
href="https://github.com/sambecker/exif-photo-blog#aws-s3"
|
||||
externalIcon
|
||||
>
|
||||
create/configure bucket
|
||||
</AdminLink>
|
||||
</>)}
|
||||
</ChecklistRow>
|
||||
</Checklist>
|
||||
<Checklist
|
||||
</ChecklistGroup>
|
||||
<ChecklistGroup
|
||||
title="Authentication"
|
||||
icon={<BiLockAlt size={16} />}
|
||||
>
|
||||
@ -331,8 +291,8 @@ export default function SiteChecklistClient({
|
||||
'ADMIN_PASSWORD',
|
||||
])}
|
||||
</ChecklistRow>
|
||||
</Checklist>
|
||||
<Checklist
|
||||
</ChecklistGroup>
|
||||
<ChecklistGroup
|
||||
title="Content"
|
||||
icon={<BiPencil size={16} />}
|
||||
>
|
||||
@ -373,9 +333,9 @@ export default function SiteChecklistClient({
|
||||
Store in environment variable (seen in grid sidebar):
|
||||
{renderEnvVars(['NEXT_PUBLIC_SITE_ABOUT'])}
|
||||
</ChecklistRow>
|
||||
</Checklist>
|
||||
</ChecklistGroup>
|
||||
{!simplifiedView && <>
|
||||
<Checklist
|
||||
<ChecklistGroup
|
||||
title="AI text generation"
|
||||
titleShort="AI"
|
||||
icon={<HiSparkles />}
|
||||
@ -408,11 +368,13 @@ export default function SiteChecklistClient({
|
||||
{kvError && renderError({
|
||||
connection: { provider: 'Vercel KV', error: kvError},
|
||||
})}
|
||||
{renderLink(
|
||||
<AdminLink
|
||||
// eslint-disable-next-line max-len
|
||||
'https://vercel.com/docs/storage/vercel-kv/quickstart#create-a-kv-database',
|
||||
'Create Vercel KV store',
|
||||
)}
|
||||
href="https://vercel.com/docs/storage/vercel-kv/quickstart#create-a-kv-database"
|
||||
externalIcon
|
||||
>
|
||||
Create Vercel KV store
|
||||
</AdminLink>
|
||||
{' '}
|
||||
and connect to project in order to enable rate limiting
|
||||
</ChecklistRow>
|
||||
@ -429,8 +391,8 @@ export default function SiteChecklistClient({
|
||||
(default: {'"title, tags, semantic"'}):
|
||||
{renderEnvVars(['AI_TEXT_AUTO_GENERATED_FIELDS'])}
|
||||
</ChecklistRow>
|
||||
</Checklist>
|
||||
<Checklist
|
||||
</ChecklistGroup>
|
||||
<ChecklistGroup
|
||||
title="Performance"
|
||||
icon={<RiSpeedMiniLine size={18} />}
|
||||
optional
|
||||
@ -490,8 +452,8 @@ export default function SiteChecklistClient({
|
||||
image blur data being stored and displayed:
|
||||
{renderEnvVars(['NEXT_PUBLIC_BLUR_DISABLED'])}
|
||||
</ChecklistRow>
|
||||
</Checklist>
|
||||
<Checklist
|
||||
</ChecklistGroup>
|
||||
<ChecklistGroup
|
||||
title="Visual"
|
||||
icon={<PiPaintBrushHousehold size={19} />}
|
||||
optional
|
||||
@ -518,8 +480,8 @@ export default function SiteChecklistClient({
|
||||
of each photo, and display a surrounding border:
|
||||
{renderEnvVars(['NEXT_PUBLIC_MATTE_PHOTOS'])}
|
||||
</ChecklistRow>
|
||||
</Checklist>
|
||||
<Checklist
|
||||
</ChecklistGroup>
|
||||
<ChecklistGroup
|
||||
title="Display"
|
||||
icon={<BiHide size={18} />}
|
||||
optional
|
||||
@ -578,8 +540,8 @@ export default function SiteChecklistClient({
|
||||
Set environment variable to {'"1"'} to hide footer link:
|
||||
{renderEnvVars(['NEXT_PUBLIC_HIDE_REPO_LINK'])}
|
||||
</ChecklistRow>
|
||||
</Checklist>
|
||||
<Checklist
|
||||
</ChecklistGroup>
|
||||
<ChecklistGroup
|
||||
title="Grid"
|
||||
icon={<IoMdGrid size={17} />}
|
||||
optional
|
||||
@ -604,7 +566,7 @@ export default function SiteChecklistClient({
|
||||
{renderEnvVars(['NEXT_PUBLIC_GRID_ASPECT_RATIO'])}
|
||||
</ChecklistRow>
|
||||
<ChecklistRow
|
||||
title={`Grid density: ${gridDensity ? 'low' : 'high'}`}
|
||||
title={`Grid density: ${hasHighGridDensity ? 'high' : 'low'}`}
|
||||
status={hasGridDensityPreference}
|
||||
optional
|
||||
>
|
||||
@ -613,10 +575,10 @@ export default function SiteChecklistClient({
|
||||
aspect ratio):
|
||||
{renderEnvVars(['NEXT_PUBLIC_SHOW_LARGE_THUMBNAILS'])}
|
||||
</ChecklistRow>
|
||||
</Checklist>
|
||||
<Checklist
|
||||
</ChecklistGroup>
|
||||
<ChecklistGroup
|
||||
title="Settings"
|
||||
icon={<BiCog size={16} />}
|
||||
icon={<HiOutlineCog size={17} className="translate-y-[0.5px]" />}
|
||||
optional
|
||||
>
|
||||
<ChecklistRow
|
||||
@ -664,9 +626,9 @@ export default function SiteChecklistClient({
|
||||
keep OG image text bottom aligned (default is {'"top"'}):
|
||||
{renderEnvVars(['NEXT_PUBLIC_OG_TEXT_ALIGNMENT'])}
|
||||
</ChecklistRow>
|
||||
</Checklist>
|
||||
</ChecklistGroup>
|
||||
{areInternalToolsEnabled &&
|
||||
<Checklist
|
||||
<ChecklistGroup
|
||||
title="Internal"
|
||||
icon={<CgDebug size={16} />}
|
||||
optional
|
||||
@ -698,7 +660,7 @@ export default function SiteChecklistClient({
|
||||
console output for all sql queries:
|
||||
{renderEnvVars(['ADMIN_SQL_DEBUG'])}
|
||||
</ChecklistRow>
|
||||
</Checklist>}
|
||||
</ChecklistGroup>}
|
||||
</>}
|
||||
</div>
|
||||
<div className="pl-11 pr-2 sm:pr-11 mt-4 md:mt-7">
|
||||
@ -715,23 +677,8 @@ export default function SiteChecklistClient({
|
||||
{baseUrl || 'Not Defined'}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-bold">Commit</span>
|
||||
|
||||
{commitSha
|
||||
? commitUrl
|
||||
? <Link
|
||||
title={commitMessage}
|
||||
href={commitUrl}
|
||||
target="_blank"
|
||||
>
|
||||
{commitSha}
|
||||
</Link>
|
||||
: <span title={commitMessage}>{commitSha}</span>
|
||||
: 'Not Found'}
|
||||
</div>
|
||||
</div>}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -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 (
|
||||
<SiteChecklistClient {...{
|
||||
...CONFIG_CHECKLIST_STATUS,
|
||||
<AdminAppConfigurationClient {...{
|
||||
...APP_CONFIGURATION,
|
||||
...connectionErrors,
|
||||
simplifiedView,
|
||||
}} />
|
||||
@ -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 (
|
||||
<MoreMenu
|
||||
items={[{
|
||||
label: 'App Config',
|
||||
icon: <BiCog className="text-[17px]" />,
|
||||
label: 'Insights',
|
||||
icon: <span className="scale-90 translate-y-[-2px]">
|
||||
<AdminAppInsightsIcon />
|
||||
</span>,
|
||||
href: PATH_ADMIN_INSIGHTS,
|
||||
}, {
|
||||
label: 'Configuration',
|
||||
icon: <LuCog
|
||||
className="text-[16px] translate-x-[0.5px]"
|
||||
/>,
|
||||
href: PATH_ADMIN_CONFIGURATION,
|
||||
}, {
|
||||
label: isSelecting
|
||||
? 'Exit Select'
|
||||
: 'Select Multiple',
|
||||
: 'Select',
|
||||
icon: isSelecting
|
||||
? <IoCloseSharp
|
||||
className="text-[18px] translate-y-[-0.5px]"
|
||||
/>
|
||||
: <ImCheckboxUnchecked
|
||||
className="text-[0.75rem]"
|
||||
className="text-[0.75rem] translate-x-[0.5px]"
|
||||
/>,
|
||||
href: PATH_GRID_INFERRED,
|
||||
action: () => {
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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';
|
||||
|
||||
29
src/admin/AdminEmptyState.tsx
Normal file
29
src/admin/AdminEmptyState.tsx
Normal file
@ -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 (
|
||||
<div className={clsx(
|
||||
'flex flex-col gap-4 justify-center items-center p-8',
|
||||
includeContainer &&'component-surface shadow-xs',
|
||||
)}>
|
||||
<div className={clsx(
|
||||
'size-14 flex justify-center items-center',
|
||||
'text-[1.75rem] text-medium',
|
||||
'border border-main rounded-xl shadow-xs',
|
||||
)}>
|
||||
{icon ?? <IoInformationCircleOutline />}
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
32
src/admin/AdminInfoPage.tsx
Normal file
32
src/admin/AdminInfoPage.tsx
Normal file
@ -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 (
|
||||
<SiteGrid
|
||||
contentMain={
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-4 min-h-9">
|
||||
<div className="grow">
|
||||
{title}
|
||||
</div>
|
||||
{accessory}
|
||||
</div>
|
||||
<Container spaceChildren={false}>
|
||||
<div className="max-w-xl w-full">
|
||||
{children}
|
||||
</div>
|
||||
</Container>
|
||||
</div>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
38
src/admin/AdminLink.tsx
Normal file
38
src/admin/AdminLink.tsx
Normal file
@ -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<typeof Link> & {
|
||||
externalIcon?: boolean
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<Link
|
||||
{...props}
|
||||
href={href}
|
||||
target="blank"
|
||||
className={clsx(
|
||||
'underline underline-offset-4',
|
||||
'decoration-gray-300 dark:decoration-gray-700',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
{externalIcon &&
|
||||
<>
|
||||
|
||||
<FiExternalLink
|
||||
size={14}
|
||||
className="inline translate-y-[-1.5px]"
|
||||
/>
|
||||
</>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -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 (
|
||||
<AdminNavClient {...{ items, mostRecentPhotoUpdateTime }} />
|
||||
<AdminNavClient {...{
|
||||
items,
|
||||
mostRecentPhotoUpdateTime,
|
||||
includeInsights,
|
||||
}} />
|
||||
);
|
||||
}
|
||||
|
||||
@ -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({
|
||||
<span>({count})</span>}
|
||||
</LinkWithStatus>)}
|
||||
</div>
|
||||
<LinkWithLoader
|
||||
href={PATH_ADMIN_CONFIGURATION}
|
||||
className={isPathAdminConfiguration(pathname)
|
||||
? 'font-bold'
|
||||
: 'text-dim'}
|
||||
loader={<Spinner />}
|
||||
>
|
||||
<BiCog
|
||||
size={18}
|
||||
className="inline-flex translate-y-0.5"
|
||||
aria-label="App Configuration"
|
||||
/>
|
||||
</LinkWithLoader>
|
||||
<div className="flex gap-3">
|
||||
{includeInsights &&
|
||||
<LinkWithLoader
|
||||
href={PATH_ADMIN_INSIGHTS}
|
||||
className={clsx(
|
||||
'translate-y-[-2px]',
|
||||
isPathAdminInsights(pathname)
|
||||
? 'font-bold'
|
||||
: 'text-dim')}
|
||||
loader={<Spinner className="translate-y-[1px]" />}
|
||||
>
|
||||
<AdminAppInsightsIcon />
|
||||
</LinkWithLoader>}
|
||||
<LinkWithLoader
|
||||
href={PATH_ADMIN_CONFIGURATION}
|
||||
className={isPathAdminConfiguration(pathname)
|
||||
? 'font-bold'
|
||||
: 'text-dim'}
|
||||
loader={<Spinner className="translate-y-[-0.75px]" />}
|
||||
>
|
||||
<LuCog
|
||||
size={20}
|
||||
className="inline-flex translate-y-[1px]"
|
||||
aria-label="App Configuration"
|
||||
/>
|
||||
</LinkWithLoader>
|
||||
</div>
|
||||
</div>
|
||||
{shouldShowBanner &&
|
||||
<Note icon={<FaRegClock className="shrink-0" />}>
|
||||
|
||||
@ -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({
|
||||
<div className="space-y-6">
|
||||
<Note>
|
||||
<div className="space-y-1.5">
|
||||
<div className="font-bold">
|
||||
Outdated photos found
|
||||
</div>
|
||||
{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
|
||||
</div>
|
||||
</Note>
|
||||
<div className="space-y-4">
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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';
|
||||
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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';
|
||||
|
||||
|
||||
@ -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';
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
? <GitHubForkStatusBadgeClient
|
||||
label="Local"
|
||||
tooltip="GitHub status unknown when running locally."
|
||||
/>
|
||||
: <Suspense>
|
||||
<GitHubForkStatusBadgeServer />
|
||||
</Suspense>;
|
||||
}
|
||||
@ -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 (
|
||||
<Tooltip content={tooltip}>
|
||||
<div className={clsx(
|
||||
'opacity-0 transition-opacity animate-fade-in',
|
||||
'inline-flex items-center gap-2',
|
||||
'border transition-colors',
|
||||
'select-none',
|
||||
'pl-[4.5px] pr-2.5 py-[3px]',
|
||||
'rounded-full shadow-xs',
|
||||
classNameForStyle(),
|
||||
)}>
|
||||
{!label
|
||||
? <Spinner
|
||||
color="text"
|
||||
className="translate-x-[3px]"
|
||||
/>
|
||||
: <BiLogoGithub size={17} />}
|
||||
{label ?? 'Checking'}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
@ -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) =>
|
||||
<a
|
||||
href={getGitHubRepoUrl({ owner, repo })}
|
||||
target="_blank"
|
||||
className="underline hover:no-underline hover:text-main"
|
||||
>
|
||||
{text}
|
||||
</a>;
|
||||
|
||||
const isBehindContent = <>
|
||||
{' '}
|
||||
{repoLink('Sync on GitHub')} for latest updates.
|
||||
</>;
|
||||
|
||||
const didErrorContent = <>
|
||||
{' '}
|
||||
Could not connect to {repoLink('GitHub')}.
|
||||
</>;
|
||||
|
||||
return isForkedFromBase || isBaseRepo
|
||||
? <GitHubForkStatusBadgeClient {...{
|
||||
url,
|
||||
label,
|
||||
tooltip: <>
|
||||
{description}
|
||||
{didError
|
||||
? didErrorContent
|
||||
: isBehind
|
||||
? isBehindContent
|
||||
: null}
|
||||
</>,
|
||||
style: didError || isBehind === undefined || isBehind
|
||||
? 'warning'
|
||||
: 'mono',
|
||||
}} />
|
||||
: 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<RepoParams> => {
|
||||
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,
|
||||
};
|
||||
});
|
||||
96
src/admin/insights/AdminAppInsights.tsx
Normal file
96
src/admin/insights/AdminAppInsights.tsx
Normal file
@ -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 (
|
||||
<AdminAppInsightsClient
|
||||
codeMeta={codeMeta}
|
||||
insights={{
|
||||
noFork: !codeMeta?.isForkedFromBase && !codeMeta?.isBaseRepo,
|
||||
forkBehind: Boolean(codeMeta?.isBehind),
|
||||
noAi: !isAiTextGenerationEnabled,
|
||||
noAiRateLimiting: isAiTextGenerationEnabled && !hasVercelBlobStorage,
|
||||
outdatedPhotos: Boolean(photosCountOutdated),
|
||||
photoMatting: photosCountPortrait > 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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
349
src/admin/insights/AdminAppInsightsClient.tsx
Normal file
349
src/admin/insights/AdminAppInsightsClient.tsx
Normal file
@ -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) =>
|
||||
<AdminLink href={`${TEMPLATE_REPO_URL_README}#${anchor}`}>
|
||||
README/{anchor}
|
||||
</AdminLink>;
|
||||
|
||||
const renderLabeledEnvVar = (label: string, envVar: string, value = '1') =>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<span className="text-xs uppercase font-medium tracking-wider">
|
||||
{label}
|
||||
</span>
|
||||
<EnvVar variable={envVar} value={value} />
|
||||
</div>;
|
||||
|
||||
export default function AdminAppInsightsClient({
|
||||
codeMeta,
|
||||
insights,
|
||||
photoStats: {
|
||||
photosCount,
|
||||
photosCountHidden,
|
||||
photosCountOutdated,
|
||||
tagsCount,
|
||||
camerasCount,
|
||||
filmSimulationsCount,
|
||||
lensesCount,
|
||||
dateRange,
|
||||
},
|
||||
debug,
|
||||
}: {
|
||||
codeMeta?: Awaited<ReturnType<typeof getGitHubMeta>>
|
||||
insights: AdminAppInsights
|
||||
photoStats: PhotoStats
|
||||
debug?: boolean
|
||||
}) {
|
||||
const {
|
||||
noFork,
|
||||
forkBehind,
|
||||
noAi,
|
||||
noAiRateLimiting,
|
||||
outdatedPhotos,
|
||||
photoMatting,
|
||||
gridFirst,
|
||||
noStaticOptimization,
|
||||
} = insights;
|
||||
|
||||
const { descriptionWithSpaces } = dateRangeForPhotos(undefined, dateRange);
|
||||
|
||||
const branchLink = <a
|
||||
className="truncate"
|
||||
href={codeMeta?.urlBranch}
|
||||
target="blank"
|
||||
>
|
||||
{codeMeta?.branch ?? TEMPLATE_REPO_BRANCH}
|
||||
</a>;
|
||||
|
||||
return (
|
||||
<div className="space-y-6 md:space-y-8">
|
||||
{(codeMeta || debug) && <>
|
||||
<ScoreCard title="Source code">
|
||||
{(noFork || debug) &&
|
||||
<ScoreCardRow
|
||||
icon={<FaCircleInfo
|
||||
size={15}
|
||||
className="text-blue-500 translate-y-[1px]"
|
||||
/>}
|
||||
content="This template is not forked"
|
||||
expandContent={<>
|
||||
<AdminLink href={TEMPLATE_REPO_URL_FORK}>
|
||||
Fork original template
|
||||
</AdminLink>
|
||||
{' '}
|
||||
to receive the latest fixes and features.
|
||||
{' '}
|
||||
Additional instructions in
|
||||
{' '}
|
||||
{readmeAnchor('receiving-updates')}.
|
||||
</>}
|
||||
/>}
|
||||
{(forkBehind || debug) && <ScoreCardRow
|
||||
icon={<IoSyncCircle
|
||||
size={18}
|
||||
className="text-blue-500"
|
||||
/>}
|
||||
content={<>
|
||||
This fork is
|
||||
{' '}
|
||||
<span className={clsx(
|
||||
'text-blue-600 bg-blue-100/60',
|
||||
'dark:text-blue-400 dark:bg-blue-900/50',
|
||||
'px-1.5 pt-[1px] pb-0.5 rounded-md',
|
||||
)}>
|
||||
{codeMeta?.behindBy ?? DEBUG_BEHIND_BY}
|
||||
{' '}
|
||||
{(codeMeta?.behindBy ?? DEBUG_BEHIND_BY) === 1
|
||||
? 'commit'
|
||||
: 'commits'}
|
||||
</span>
|
||||
{' '}
|
||||
behind
|
||||
</>}
|
||||
expandContent={<>
|
||||
<AdminLink href={codeMeta?.urlRepo ?? ''}>
|
||||
Sync your fork
|
||||
</AdminLink>
|
||||
{' '}
|
||||
to receive the latest fixes and features.
|
||||
</>}
|
||||
/>}
|
||||
<ScoreCardRow
|
||||
icon={<BiLogoGithub size={17} />}
|
||||
content={<div
|
||||
className="flex flex-wrap gap-x-4 gap-y-1 overflow-auto"
|
||||
>
|
||||
<div className="flex items-center gap-1 *:whitespace-nowrap">
|
||||
<a
|
||||
href={codeMeta?.urlOwner}
|
||||
target="blank"
|
||||
>
|
||||
{codeMeta?.owner ?? TEMPLATE_REPO_OWNER}
|
||||
</a>
|
||||
<div>/</div>
|
||||
<a
|
||||
href={codeMeta?.urlRepo}
|
||||
target="blank"
|
||||
>
|
||||
{codeMeta?.repo ?? TEMPLATE_REPO_NAME}
|
||||
</a>
|
||||
</div>
|
||||
<div className="hidden sm:flex items-center gap-1 min-w-0">
|
||||
<BiGitBranch size={17} />
|
||||
{branchLink}
|
||||
</div>
|
||||
</div>}
|
||||
/>
|
||||
<ScoreCardRow
|
||||
className="sm:hidden"
|
||||
icon={<BiGitBranch size={17} />}
|
||||
content={branchLink}
|
||||
/>
|
||||
<ScoreCardRow
|
||||
icon={<BiGitCommit
|
||||
size={18}
|
||||
className="translate-y-[-0.5px]"
|
||||
/>}
|
||||
content={<a
|
||||
href={codeMeta?.urlCommit}
|
||||
target="blank"
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<span className="text-medium hidden sm:inline-block">
|
||||
{VERCEL_GIT_COMMIT_SHA_SHORT ?? DEBUG_COMMIT_SHA}
|
||||
</span>
|
||||
<span className="truncate">
|
||||
{VERCEL_GIT_COMMIT_MESSAGE ?? DEBUG_COMMIT_MESSAGE}
|
||||
</span>
|
||||
</a>}
|
||||
/>
|
||||
</ScoreCard>
|
||||
</>}
|
||||
<ScoreCard title="Template recommendations">
|
||||
{(hasTemplateRecommendations(insights) || debug)
|
||||
? <>
|
||||
{(noAiRateLimiting || debug) && <ScoreCardRow
|
||||
icon={<PiWarningBold
|
||||
size={17}
|
||||
className="translate-x-[0.5px] text-amber-600"
|
||||
/>}
|
||||
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) && <ScoreCardRow
|
||||
icon={<RiSpeedMiniLine
|
||||
size={19}
|
||||
className="translate-x-[1px] translate-y-[-1.5px]"
|
||||
/>}
|
||||
content="Speed up page load times"
|
||||
expandContent={<>
|
||||
Improve load times by enabling static optimization
|
||||
{' '}
|
||||
on:
|
||||
<div className="flex flex-col gap-y-4 mt-3">
|
||||
{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',
|
||||
)}
|
||||
<span>
|
||||
See {readmeAnchor('performance')} for cost implications.
|
||||
</span>
|
||||
</div>
|
||||
</>}
|
||||
/>}
|
||||
{(noAi || debug) && <ScoreCardRow
|
||||
icon={<TbSparkles size={17} />}
|
||||
content="Improve SEO + accessibility with AI"
|
||||
expandContent={<>
|
||||
Enable automatic AI text generation
|
||||
{' '}
|
||||
by setting <EnvVar variable="OPENAI_SECRET_KEY" />.
|
||||
{' '}
|
||||
Further instruction and cost considerations in
|
||||
{' '}
|
||||
{readmeAnchor('ai-text-generation')}.
|
||||
</>}
|
||||
/>}
|
||||
{(photoMatting || debug) && <ScoreCardRow
|
||||
icon={<MdAspectRatio
|
||||
size={17}
|
||||
className="rotate-90 translate-x-[-1px]"
|
||||
/>}
|
||||
content="Vertical photos may benefit from matting"
|
||||
expandContent={<>
|
||||
Enable photo matting to make
|
||||
{' '}
|
||||
portrait and landscape photos appear more consistent
|
||||
{' '}
|
||||
<EnvVar variable="NEXT_PUBLIC_MATTE_PHOTOS" value="1" />.
|
||||
</>}
|
||||
/>}
|
||||
{(gridFirst || debug) && <ScoreCardRow
|
||||
icon={<IoMdGrid size={18} className="translate-y-[-1px]" />}
|
||||
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 */}
|
||||
<EnvVar variable="NEXT_PUBLIC_GRID_HOMEPAGE_ENABLED" value="1" />.
|
||||
</>}
|
||||
/>}
|
||||
</>
|
||||
: <AdminEmptyState includeContainer={false}>
|
||||
Nothing to report!
|
||||
</AdminEmptyState>}
|
||||
</ScoreCard>
|
||||
<ScoreCard title="Library Stats">
|
||||
{(outdatedPhotos || debug) && <ScoreCardRow
|
||||
icon={<LiaBroomSolid
|
||||
size={19}
|
||||
className="translate-y-[-2px] text-amber-600"
|
||||
/>}
|
||||
// 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}
|
||||
/>}
|
||||
<ScoreCardRow
|
||||
icon={<HiOutlinePhotograph
|
||||
size={17}
|
||||
className="translate-y-[0.5px]"
|
||||
/>}
|
||||
content={<>
|
||||
{photosCount} photos
|
||||
{photosCountHidden > 0 &&
|
||||
` (${photosCountHidden} hidden)`}
|
||||
</>}
|
||||
/>
|
||||
<ScoreCardRow
|
||||
icon={<FaTag
|
||||
size={12}
|
||||
className="translate-y-[3px]"
|
||||
/>}
|
||||
content={`${tagsCount} tags`}
|
||||
/>
|
||||
<ScoreCardRow
|
||||
icon={<FaCamera
|
||||
size={13}
|
||||
className="translate-y-[2px]"
|
||||
/>}
|
||||
content={`${camerasCount} cameras`}
|
||||
/>
|
||||
{filmSimulationsCount &&
|
||||
<ScoreCardRow
|
||||
icon={<span className="inline-flex w-3">
|
||||
<PhotoFilmSimulationIcon
|
||||
className="shrink-0 translate-x-[-1px] translate-y-[-0.5px]"
|
||||
height={18}
|
||||
/>
|
||||
</span>}
|
||||
content={`${filmSimulationsCount} film simulations`}
|
||||
/>}
|
||||
<ScoreCardRow
|
||||
icon={<TbCone className="rotate-[270deg] translate-x-[-2px]" />}
|
||||
content={`${lensesCount} lenses`}
|
||||
/>
|
||||
<ScoreCardRow
|
||||
icon={<FaRegCalendar
|
||||
size={13}
|
||||
className="translate-y-[1.5px] translate-x-[-2px]"
|
||||
/>}
|
||||
content={descriptionWithSpaces}
|
||||
/>
|
||||
</ScoreCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
27
src/admin/insights/AdminAppInsightsIcon.tsx
Normal file
27
src/admin/insights/AdminAppInsightsIcon.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import clsx from 'clsx/lite';
|
||||
import { LuLightbulb } from 'react-icons/lu';
|
||||
|
||||
export default function AdminAppInsightsIcon({
|
||||
indicator,
|
||||
}: {
|
||||
indicator?: 'blue' | 'yellow'
|
||||
}) {
|
||||
return (
|
||||
<span className="inline-flex relative">
|
||||
<LuLightbulb
|
||||
size={19}
|
||||
className="translate-y-[3px]"
|
||||
/>
|
||||
{indicator && <span className={clsx(
|
||||
'absolute',
|
||||
'top-[2px] right-[0.5px]',
|
||||
'size-2 rounded-full',
|
||||
indicator === 'yellow'
|
||||
? 'bg-amber-500'
|
||||
: indicator === 'blue'
|
||||
? 'bg-blue-500'
|
||||
: undefined,
|
||||
)} />}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
44
src/admin/insights/index.ts
Normal file
44
src/admin/insights/index.ts
Normal file
@ -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<AdminAppInsight, boolean>
|
||||
|
||||
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;
|
||||
@ -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';
|
||||
@ -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';
|
||||
@ -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 {
|
||||
@ -30,9 +30,7 @@ export default function SecretGenerator() {
|
||||
'flex flex-nowrap items-center gap-2 leading-none -mx-1',
|
||||
)}>
|
||||
{secret ? <span>{secret}</span> : <Spinner />}
|
||||
<div
|
||||
className="flex items-center gap-0.5 translate-y-[-2px]"
|
||||
>
|
||||
<div className="flex items-center gap-0.5 translate-y-[-0.5px]">
|
||||
<CopyButton label="Secret" text={secret} />
|
||||
</div>
|
||||
</div>
|
||||
@ -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';
|
||||
@ -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;
|
||||
|
||||
20
src/app-core/font.ts
Normal file
20
src/app-core/font.ts
Normal file
@ -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],
|
||||
}));
|
||||
@ -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)) ||
|
||||
@ -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 (
|
||||
<SiteGrid
|
||||
contentMain={
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="grow">
|
||||
App Configuration
|
||||
</div>
|
||||
{(IS_VERCEL_GIT_PROVIDER_GITHUB || IS_DEVELOPMENT) &&
|
||||
<GitHubForkStatusBadge />}
|
||||
<ClearCacheButton />
|
||||
</div>
|
||||
<Container spaceChildren={false}>
|
||||
<SiteChecklist />
|
||||
</Container>
|
||||
</div>}
|
||||
/>
|
||||
<AdminInfoPage
|
||||
title="App Configuration"
|
||||
accessory={<ClearCacheButton />}
|
||||
>
|
||||
<AdminAppConfiguration />
|
||||
</AdminInfoPage>
|
||||
);
|
||||
}
|
||||
|
||||
8
src/app/admin/insights/page.tsx
Normal file
8
src/app/admin/insights/page.tsx
Normal file
@ -0,0 +1,8 @@
|
||||
import AdminAppInsights from '@/admin/insights/AdminAppInsights';
|
||||
import AdminInfoPage from '@/admin/AdminInfoPage';
|
||||
|
||||
export default async function AdminInsightsPage() {
|
||||
return <AdminInfoPage title="App Insights">
|
||||
<AdminAppInsights />
|
||||
</AdminInfoPage>;
|
||||
}
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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';
|
||||
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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';
|
||||
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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';
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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';
|
||||
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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({
|
||||
>
|
||||
<body>
|
||||
<AppStateProvider>
|
||||
<SwrConfigClient>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme={DEFAULT_THEME}
|
||||
>
|
||||
<ThemeProvider attribute="class" defaultTheme={DEFAULT_THEME}>
|
||||
<SwrConfigClient>
|
||||
<main className={clsx(
|
||||
'mx-3 mb-3',
|
||||
'lg:mx-6 lg:mb-6',
|
||||
@ -96,12 +93,12 @@ export default function RootLayout({
|
||||
<Footer />
|
||||
</main>
|
||||
<CommandK />
|
||||
</ThemeProvider>
|
||||
</SwrConfigClient>
|
||||
<Analytics debug={false} />
|
||||
<SpeedInsights debug={false} />
|
||||
<PhotoEscapeHandler />
|
||||
<ToasterWithThemes />
|
||||
</SwrConfigClient>
|
||||
<Analytics debug={false} />
|
||||
<SpeedInsights debug={false} />
|
||||
<PhotoEscapeHandler />
|
||||
<ToasterWithThemes />
|
||||
</ThemeProvider>
|
||||
</AppStateProvider>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
import { getPhotoCached } from '@/photo/cache';
|
||||
import { IMAGE_OG_DIMENSION } from '@/image-response';
|
||||
import PhotoImageResponse from '@/image-response/PhotoImageResponse';
|
||||
import { getIBMPlexMonoMedium } from '@/site/font';
|
||||
import { getIBMPlexMonoMedium } from '@/app-core/font';
|
||||
import { ImageResponse } from 'next/og';
|
||||
import { getImageResponseCacheControlHeaders } from '@/image-response/cache';
|
||||
import {
|
||||
IS_PRODUCTION,
|
||||
STATICALLY_OPTIMIZED_PHOTO_OG_IMAGES,
|
||||
} from '@/site/config';
|
||||
} from '@/app-core/config';
|
||||
import { getPhotoIds } from '@/photo/db/query';
|
||||
import { GENERATE_STATIC_PARAMS_LIMIT } from '@/photo/db';
|
||||
import { isNextImageReadyBasedOnPhotos } from '@/photo';
|
||||
|
||||
@ -9,10 +9,10 @@ import {
|
||||
PATH_ROOT,
|
||||
absolutePathForPhoto,
|
||||
absolutePathForPhotoImage,
|
||||
} from '@/site/paths';
|
||||
} from '@/app-core/paths';
|
||||
import PhotoDetailPage from '@/photo/PhotoDetailPage';
|
||||
import { getPhotosNearIdCached } from '@/photo/cache';
|
||||
import { IS_PRODUCTION, STATICALLY_OPTIMIZED_PHOTOS } from '@/site/config';
|
||||
import { IS_PRODUCTION, STATICALLY_OPTIMIZED_PHOTOS } from '@/app-core/config';
|
||||
import { getPhotoIds } from '@/photo/db/query';
|
||||
import { GENERATE_STATIC_PARAMS_LIMIT } from '@/photo/db';
|
||||
import { cache } from 'react';
|
||||
|
||||
@ -7,7 +7,7 @@ import PhotosEmptyState from '@/photo/PhotosEmptyState';
|
||||
import { Metadata } from 'next/types';
|
||||
import { cache } from 'react';
|
||||
import { getPhotos, getPhotosMeta } from '@/photo/db/query';
|
||||
import { GRID_HOMEPAGE_ENABLED } from '@/site/config';
|
||||
import { GRID_HOMEPAGE_ENABLED } from '@/app-core/config';
|
||||
import { getPhotoSidebarData } from '@/photo/data';
|
||||
import PhotoGridPage from '@/photo/PhotoGridPage';
|
||||
import PhotoFeedPage from '@/photo/PhotoFeedPage';
|
||||
|
||||
@ -9,7 +9,7 @@ import {
|
||||
PATH_ROOT,
|
||||
absolutePathForPhoto,
|
||||
absolutePathForPhotoImage,
|
||||
} from '@/site/paths';
|
||||
} from '@/app-core/paths';
|
||||
import PhotoDetailPage from '@/photo/PhotoDetailPage';
|
||||
import {
|
||||
getPhotosMetaCached,
|
||||
|
||||
@ -5,7 +5,7 @@ import {
|
||||
MAX_PHOTOS_TO_SHOW_PER_TAG,
|
||||
} from '@/image-response';
|
||||
import CameraImageResponse from '@/image-response/CameraImageResponse';
|
||||
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';
|
||||
@ -13,7 +13,7 @@ import { getUniqueCameras } from '@/photo/db/query';
|
||||
import {
|
||||
STATICALLY_OPTIMIZED_PHOTO_CATEGORY_OG_IMAGES,
|
||||
IS_PRODUCTION,
|
||||
} from '@/site/config';
|
||||
} from '@/app-core/config';
|
||||
|
||||
export let generateStaticParams:
|
||||
(() => Promise<{ camera: Camera }[]>) | undefined = undefined;
|
||||
|
||||
@ -5,8 +5,8 @@ import { INFINITE_SCROLL_GRID_INITIAL } from '@/photo';
|
||||
import { getPhotosCameraDataCached } from '@/camera/data';
|
||||
import CameraOverview from '@/camera/CameraOverview';
|
||||
import { cache } from 'react';
|
||||
import { STATICALLY_OPTIMIZED_PHOTO_CATEGORIES } from '@/site/config';
|
||||
import { IS_PRODUCTION } from '@/site/config';
|
||||
import { STATICALLY_OPTIMIZED_PHOTO_CATEGORIES } from '@/app-core/config';
|
||||
import { IS_PRODUCTION } from '@/app-core/config';
|
||||
import { getUniqueCameras } from '@/photo/db/query';
|
||||
|
||||
const getPhotosCameraDataCachedCached = cache((
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { auth } from '@/auth';
|
||||
import SignInForm from '@/auth/SignInForm';
|
||||
import { PATH_ADMIN } from '@/site/paths';
|
||||
import { PATH_ADMIN } from '@/app-core/paths';
|
||||
import { clsx } from 'clsx/lite';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -4,7 +4,7 @@ import {
|
||||
MAX_PHOTOS_TO_SHOW_PER_TAG,
|
||||
} from '@/image-response';
|
||||
import TagImageResponse from '@/image-response/TagImageResponse';
|
||||
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';
|
||||
@ -12,7 +12,7 @@ import { getUniqueTags } from '@/photo/db/query';
|
||||
import {
|
||||
STATICALLY_OPTIMIZED_PHOTO_CATEGORY_OG_IMAGES,
|
||||
IS_PRODUCTION,
|
||||
} from '@/site/config';
|
||||
} from '@/app-core/config';
|
||||
|
||||
export let generateStaticParams:
|
||||
(() => Promise<{ tag: string }[]>) | undefined = undefined;
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { INFINITE_SCROLL_GRID_INITIAL } from '@/photo';
|
||||
import { getUniqueTags } from '@/photo/db/query';
|
||||
import { IS_PRODUCTION } from '@/site/config';
|
||||
import { STATICALLY_OPTIMIZED_PHOTO_CATEGORIES } from '@/site/config';
|
||||
import { PATH_ROOT } from '@/site/paths';
|
||||
import { IS_PRODUCTION } from '@/app-core/config';
|
||||
import { STATICALLY_OPTIMIZED_PHOTO_CATEGORIES } from '@/app-core/config';
|
||||
import { PATH_ROOT } from '@/app-core/paths';
|
||||
import { generateMetaForTag } from '@/tag';
|
||||
import TagOverview from '@/tag/TagOverview';
|
||||
import { getPhotosTagDataCached } from '@/tag/data';
|
||||
|
||||
@ -8,7 +8,7 @@ import {
|
||||
getPhotosNearIdCached,
|
||||
} from '@/photo/cache';
|
||||
import { getPhotosMeta } from '@/photo/db/query';
|
||||
import { PATH_ROOT, absolutePathForPhoto } from '@/site/paths';
|
||||
import { PATH_ROOT, absolutePathForPhoto } from '@/app-core/paths';
|
||||
import { TAG_HIDDEN } from '@/tag';
|
||||
import { Metadata } from 'next';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
@ -4,7 +4,7 @@ import SiteGrid from '@/components/SiteGrid';
|
||||
import PhotoGrid from '@/photo/PhotoGrid';
|
||||
import { getPhotosNoStore } from '@/photo/cache';
|
||||
import { getPhotosMeta } from '@/photo/db/query';
|
||||
import { absolutePathForTag } from '@/site/paths';
|
||||
import { absolutePathForTag } from '@/app-core/paths';
|
||||
import { TAG_HIDDEN, descriptionForTaggedPhotos, titleForTag } from '@/tag';
|
||||
import HiddenHeader from '@/tag/HiddenHeader';
|
||||
import { Metadata } from 'next';
|
||||
|
||||
@ -5,7 +5,7 @@ import {
|
||||
} from '@/image-response';
|
||||
import TemplateImageResponse from
|
||||
'@/image-response/TemplateImageResponse';
|
||||
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';
|
||||
|
||||
@ -5,7 +5,7 @@ import {
|
||||
} from '@/image-response';
|
||||
import TemplateImageResponse from
|
||||
'@/image-response/TemplateImageResponse';
|
||||
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';
|
||||
|
||||
@ -1,24 +1,25 @@
|
||||
/* eslint-disable max-len */
|
||||
import {
|
||||
TEMPLATE_REPO_OWNER,
|
||||
TEMPLATE_REPO_NAME,
|
||||
TEMPLATE_DESCRIPTION,
|
||||
TEMPLATE_TITLE,
|
||||
} from '@/app-core/config';
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
const REQUIRE_ENV_VARS = false;
|
||||
|
||||
const TITLE = 'Photo Blog';
|
||||
const DESCRIPTION = 'Store photos with original camera data';
|
||||
const REPO_TEAM = 'sambecker';
|
||||
const REPO_NAME = 'exif-photo-blog';
|
||||
|
||||
export function GET() {
|
||||
const url = new URL('https://vercel.com/new/clone');
|
||||
|
||||
url.searchParams.set('demo-title', TITLE);
|
||||
url.searchParams.set('demo-description', DESCRIPTION);
|
||||
url.searchParams.set('demo-title', TEMPLATE_TITLE);
|
||||
url.searchParams.set('demo-description', TEMPLATE_DESCRIPTION);
|
||||
url.searchParams.set('demo-url', 'https://photos.sambecker.com');
|
||||
url.searchParams.set('demo-description', DESCRIPTION);
|
||||
url.searchParams.set('demo-description', TEMPLATE_DESCRIPTION);
|
||||
url.searchParams.set('demo-image', 'https://photos.sambecker.com/template-image-tight');
|
||||
url.searchParams.set('project-name', TITLE);
|
||||
url.searchParams.set('repository-name', REPO_NAME);
|
||||
url.searchParams.set('repository-url', `https://github.com/${REPO_TEAM}/${REPO_NAME}`);
|
||||
url.searchParams.set('project-name', TEMPLATE_TITLE);
|
||||
url.searchParams.set('repository-name', TEMPLATE_REPO_NAME);
|
||||
url.searchParams.set('repository-url', `https://github.com/${TEMPLATE_REPO_OWNER}/${TEMPLATE_REPO_NAME}`);
|
||||
url.searchParams.set('from', 'templates');
|
||||
url.searchParams.set('skippable-integrations', '1');
|
||||
if (REQUIRE_ENV_VARS) {
|
||||
|
||||
@ -10,7 +10,7 @@ import {
|
||||
signIn,
|
||||
signOut,
|
||||
} from '@/auth';
|
||||
import { PATH_ADMIN_PHOTOS, PATH_ROOT } from '@/site/paths';
|
||||
import { PATH_ADMIN_PHOTOS, PATH_ROOT } from '@/app-core/paths';
|
||||
import type { Session } from 'next-auth';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { isPathProtected } from '@/site/paths';
|
||||
import { isPathProtected } from '@/app-core/paths';
|
||||
import NextAuth, { User } from 'next-auth';
|
||||
import Credentials from 'next-auth/providers/credentials';
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { Photo, PhotoDateRange } from '@/photo';
|
||||
import { absolutePathForCameraImage, pathForCamera } from '@/site/paths';
|
||||
import { absolutePathForCameraImage, pathForCamera } from '@/app-core/paths';
|
||||
import OGTile from '@/components/OGTile';
|
||||
import { Camera } from '.';
|
||||
import { descriptionForCameraPhotos, titleForCamera } from './meta';
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { absolutePathForCamera } from '@/site/paths';
|
||||
import { absolutePathForCamera } from '@/app-core/paths';
|
||||
import { PhotoSetAttributes } from '../photo';
|
||||
import ShareModal from '@/share/ShareModal';
|
||||
import CameraOGTile from './CameraOGTile';
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { AiFillApple } from 'react-icons/ai';
|
||||
import { pathForCamera } from '@/site/paths';
|
||||
import { pathForCamera } from '@/app-core/paths';
|
||||
import { IoMdCamera } from 'react-icons/io';
|
||||
import { Camera, formatCameraText, isCameraApple } from '.';
|
||||
import EntityLink, {
|
||||
|
||||
@ -8,7 +8,7 @@ import { Camera, cameraFromPhoto, formatCameraText } from '.';
|
||||
import {
|
||||
absolutePathForCamera,
|
||||
absolutePathForCameraImage,
|
||||
} from '@/site/paths';
|
||||
} from '@/app-core/paths';
|
||||
|
||||
// Meta functions moved to separate file to avoid
|
||||
// dependencies (camelcase-keys) found in photo/index.ts
|
||||
|
||||
@ -5,7 +5,7 @@ import Badge from './Badge';
|
||||
import ResponsiveText from './primitives/ResponsiveText';
|
||||
import { parameterize } from '@/utility/string';
|
||||
|
||||
export default function Checklist({
|
||||
export default function ChecklistGroup({
|
||||
title,
|
||||
titleShort,
|
||||
icon,
|
||||
@ -42,7 +42,7 @@ export default function ChecklistRow({
|
||||
{experimental &&
|
||||
<ExperimentalBadge className="translate-y-[-0.5px]" />}
|
||||
</div>
|
||||
<div>
|
||||
<div className="leading-relaxed">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -25,13 +25,13 @@ export default function Container({
|
||||
switch (color) {
|
||||
case 'gray': return [
|
||||
'text-medium',
|
||||
'bg-gray-50 border-gray-200',
|
||||
'dark:bg-gray-900/40 dark:border-gray-800',
|
||||
'bg-gray-50 dark:bg-gray-900/40',
|
||||
'border-gray-200 dark:border-gray-800',
|
||||
];
|
||||
case 'blue': return [
|
||||
'text-main',
|
||||
'bg-blue-50/50 border-blue-200',
|
||||
'dark:bg-blue-950/30 dark:border-blue-600/50',
|
||||
'text-blue-900 dark:text-blue-300',
|
||||
'bg-blue-50/50 dark:bg-blue-950/30',
|
||||
'border-blue-200 dark:border-blue-600/30',
|
||||
];
|
||||
case 'red': return [
|
||||
'text-red-600 dark:text-red-500/90',
|
||||
@ -41,7 +41,7 @@ export default function Container({
|
||||
case 'yellow': return [
|
||||
'text-amber-700 dark:text-amber-500/90',
|
||||
'bg-amber-50/50 dark:bg-amber-950/30',
|
||||
'border-amber-200/80 dark:border-amber-800/30',
|
||||
'border-amber-600/30 dark:border-amber-800/30',
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
@ -7,17 +7,19 @@ export default function CopyButton({
|
||||
label,
|
||||
text,
|
||||
subtle,
|
||||
className,
|
||||
}: {
|
||||
label: string
|
||||
text?: string,
|
||||
subtle?: boolean
|
||||
className?: string
|
||||
}) {
|
||||
return (
|
||||
<LoaderButton
|
||||
icon={<BiCopy size={15} />}
|
||||
className={clsx(
|
||||
'translate-y-[2px]',
|
||||
subtle && 'text-gray-300 dark:text-gray-700',
|
||||
className,
|
||||
)}
|
||||
onClick={text
|
||||
? () => {
|
||||
|
||||
40
src/components/EnvVar.tsx
Normal file
40
src/components/EnvVar.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
import clsx from 'clsx/lite';
|
||||
import CopyButton from './CopyButton';
|
||||
|
||||
export default function EnvVar({
|
||||
variable,
|
||||
value,
|
||||
includeCopyButton = true,
|
||||
}: {
|
||||
variable: string,
|
||||
value?: string,
|
||||
includeCopyButton?: boolean,
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'inline-flex',
|
||||
'overflow-x-auto overflow-y-hidden',
|
||||
)}
|
||||
>
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<span className={clsx(
|
||||
'px-1.5 rounded-md',
|
||||
'text-[11px] font-medium tracking-wider',
|
||||
'text-gray-600 dark:text-gray-300',
|
||||
'bg-gray-100 dark:bg-gray-800',
|
||||
'whitespace-nowrap',
|
||||
)}>
|
||||
{variable}{value && ` = ${value}`}
|
||||
</span>
|
||||
{includeCopyButton &&
|
||||
<CopyButton
|
||||
className="translate-y-[0.5px]"
|
||||
label={variable}
|
||||
text={variable}
|
||||
subtle
|
||||
/>}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
import { ReactNode } from 'react';
|
||||
import SiteGrid from './SiteGrid';
|
||||
import { clsx } from 'clsx/lite';
|
||||
import { PATH_ROOT } from '@/site/paths';
|
||||
import { PATH_ROOT } from '@/app-core/paths';
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function HttpStatusPage({
|
||||
|
||||
@ -6,9 +6,11 @@ import Link from 'next/link';
|
||||
export default function LinkWithLoader({
|
||||
loader,
|
||||
children,
|
||||
debugLoading,
|
||||
...props
|
||||
}: ComponentProps<typeof Link> & {
|
||||
loader: ReactNode
|
||||
debugLoading?: boolean
|
||||
}) {
|
||||
return (
|
||||
<LinkWithStatus {...props}>
|
||||
@ -19,7 +21,7 @@ export default function LinkWithLoader({
|
||||
)}>
|
||||
{children}
|
||||
</span>
|
||||
{isLoading && <span className={clsx(
|
||||
{(isLoading || debugLoading) && <span className={clsx(
|
||||
'absolute inset-0',
|
||||
'flex items-center justify-center',
|
||||
)}>
|
||||
|
||||
@ -89,7 +89,7 @@ export default function LinkWithStatus({
|
||||
{...props }
|
||||
href={href}
|
||||
className={clsx(
|
||||
'relative flex transition-[colors,opacity]',
|
||||
'relative inline-flex transition-[colors,opacity]',
|
||||
(loadingClassName || isControlled)
|
||||
? 'opacity-100'
|
||||
: isLoading ? 'opacity-50' : 'opacity-100',
|
||||
|
||||
@ -6,9 +6,9 @@ import { clsx } from 'clsx/lite';
|
||||
import useClickInsideOutside from '@/utility/useClickInsideOutside';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import AnimateItems from './AnimateItems';
|
||||
import { PATH_ROOT } from '@/site/paths';
|
||||
import { PATH_ROOT } from '@/app-core/paths';
|
||||
import usePrefersReducedMotion from '@/utility/usePrefersReducedMotion';
|
||||
import useMetaThemeColor from '@/site/useMetaThemeColor';
|
||||
import useMetaThemeColor from '@/utility/useMetaThemeColor';
|
||||
import useEscapeHandler from '@/utility/useEscapeHandler';
|
||||
|
||||
export default function Modal({
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { TEMPLATE_REPO_NAME, TEMPLATE_REPO_URL } from '@/app-core/config';
|
||||
import { clsx } from 'clsx/lite';
|
||||
import Link from 'next/link';
|
||||
import { BiLogoGithub } from 'react-icons/bi';
|
||||
@ -9,7 +10,7 @@ export default function RepoLink() {
|
||||
Made with
|
||||
</span>
|
||||
<Link
|
||||
href="http://github.com/sambecker/exif-photo-blog"
|
||||
href={TEMPLATE_REPO_URL}
|
||||
target="_blank"
|
||||
className={clsx(
|
||||
'flex items-center gap-0.5',
|
||||
@ -21,7 +22,7 @@ export default function RepoLink() {
|
||||
size={16}
|
||||
className="translate-y-[0.5px] hidden xs:inline-block"
|
||||
/>
|
||||
exif-photo-blog
|
||||
{TEMPLATE_REPO_NAME}
|
||||
</Link>
|
||||
</span>
|
||||
);
|
||||
|
||||
31
src/components/ScoreCard.tsx
Normal file
31
src/components/ScoreCard.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import clsx from 'clsx/lite';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
export default function ScoreCard({
|
||||
title,
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
title?: string,
|
||||
children: ReactNode,
|
||||
className?: string,
|
||||
}) {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{title &&
|
||||
<div className={clsx(
|
||||
'pl-[15px]',
|
||||
'uppercase font-medium tracking-wider text-[0.8rem]',
|
||||
'text-medium',
|
||||
)}>
|
||||
{title}
|
||||
</div>}
|
||||
<div className={clsx(
|
||||
'component-surface shadow-xs divide-y divide-medium',
|
||||
className,
|
||||
)}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
77
src/components/ScoreCardRow.tsx
Normal file
77
src/components/ScoreCardRow.tsx
Normal file
@ -0,0 +1,77 @@
|
||||
import { clsx } from 'clsx/lite';
|
||||
import { ReactNode, useState } from 'react';
|
||||
import {
|
||||
LuChevronRight,
|
||||
LuChevronsDownUp,
|
||||
LuChevronsUpDown,
|
||||
} from 'react-icons/lu';
|
||||
import LinkWithStatus from './LinkWithStatus';
|
||||
import Spinner from './Spinner';
|
||||
|
||||
const expandAccessoryClasses = clsx(
|
||||
'flex items-center justify-center',
|
||||
'w-9 h-8',
|
||||
'*:shrink-0',
|
||||
);
|
||||
|
||||
export default function ScoreCardRow({
|
||||
icon,
|
||||
content,
|
||||
expandContent,
|
||||
expandPath,
|
||||
className,
|
||||
}: {
|
||||
icon: ReactNode
|
||||
content: ReactNode
|
||||
expandContent?: ReactNode
|
||||
expandPath?: string
|
||||
className?: string
|
||||
}) {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
return (
|
||||
<div className={clsx(
|
||||
'flex',
|
||||
'py-2 pr-2',
|
||||
className,
|
||||
)}>
|
||||
<div className={clsx(
|
||||
'flex justify-center pt-[8px] w-11 sm:w-14',
|
||||
'shrink-0 text-icon',
|
||||
)}>
|
||||
{icon}
|
||||
</div>
|
||||
<div className="grow space-y-2 py-1.5 w-full overflow-auto">
|
||||
<div className={clsx(
|
||||
'text-main pr-2',
|
||||
// Truncate on small screens unless expanded
|
||||
!isExpanded && 'truncate md:truncate-none',
|
||||
)}>
|
||||
{content}
|
||||
</div>
|
||||
{isExpanded &&
|
||||
<div className="text-medium leading-relaxed">
|
||||
{expandContent}
|
||||
</div>}
|
||||
</div>
|
||||
{expandContent && <button
|
||||
type="button"
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
className={expandAccessoryClasses}
|
||||
>
|
||||
{isExpanded
|
||||
? <LuChevronsDownUp size={16} />
|
||||
: <LuChevronsUpDown size={16} />}
|
||||
</button>}
|
||||
{expandPath && <LinkWithStatus
|
||||
className={clsx('button', expandAccessoryClasses)}
|
||||
href={expandPath}
|
||||
>
|
||||
{({ isLoading }) => <> {isLoading
|
||||
? <Spinner />
|
||||
: <LuChevronRight size={16} />}
|
||||
</>}
|
||||
</LinkWithStatus>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user