Merge pull request #169 from sambecker/date-validation

Introduce date-time validation + customization
This commit is contained in:
Sam Becker 2025-01-22 19:43:17 -06:00 committed by GitHub
commit 4ee1bd5e5e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 451 additions and 319 deletions

View File

@ -96,31 +96,34 @@ _⚠ READ BEFORE PROCEEDING_
Application behavior can be changed by configuring the following environment variables:
#### Site meta
#### Content
- `NEXT_PUBLIC_SITE_TITLE` (seen in browser tab)
- `NEXT_PUBLIC_SITE_DESCRIPTION` (seen in nav, beneath title)
- `NEXT_PUBLIC_SITE_ABOUT` (seen in grid sidebar—accepts rich formatting tags: `<b>`, `<strong>`, `<i>`, `<em>`, `<u>`, `<br>`)
#### Site performance
#### Performance
> ⚠️ Enabling may result in increased project usage
- `NEXT_PUBLIC_STATICALLY_OPTIMIZE_PHOTOS = 1` enables static optimization for photo pages (`p/[photoId]`), i.e., renders pages at build time
- `NEXT_PUBLIC_STATICALLY_OPTIMIZE_PHOTO_OG_IMAGES = 1` enables static optimization for OG images, i.e., renders images at build time
- `NEXT_PUBLIC_STATICALLY_OPTIMIZE_PHOTO_CATEGORIES = 1` enables static optimization for photo categories (`tag/[tag]`, `shot-on/[make]/[model]`, etc.), i.e., renders pages at build time
- `NEXT_PUBLIC_PRESERVE_ORIGINAL_UPLOADS = 1` prevents photo uploads being compressed before storing
#### Site behavior
#### Display
- `NEXT_PUBLIC_HIDE_EXIF_DATA = 1` hides EXIF data in photo details and OG images (potentially useful for portfolios, which don't focus on photography)
- `NEXT_PUBLIC_HIDE_TAKEN_AT_TIME = 1` hides taken at time from photo meta
- `NEXT_PUBLIC_HIDE_SOCIAL = 1` removes X button from share modal
- `NEXT_PUBLIC_HIDE_FILM_SIMULATIONS = 1` prevents Fujifilm simulations showing up in `/grid` sidebar and CMD-K search results
- `NEXT_PUBLIC_HIDE_REPO_LINK = 1` removes footer link to repo
#### Settings
- `NEXT_PUBLIC_GRID_HOMEPAGE = 1` shows grid layout on homepage
- `NEXT_PUBLIC_DEFAULT_THEME = light | dark` sets preferred initial theme (defaults to `system` when not configured)
- `NEXT_PUBLIC_MATTE_PHOTOS = 1` constrains the size of each photo, and enables a surrounding border (potentially useful for photos with tall aspect ratios)
- `NEXT_PUBLIC_BLUR_DISABLED = 1` prevents image blur data being stored and displayed (potentially useful for limiting Postgres usage)
- `NEXT_PUBLIC_GEO_PRIVACY = 1` disables collection/display of location-based data (⚠️ re-compresses uploaded images in order to remove GPS information)
- `NEXT_PUBLIC_HIDE_REPO_LINK = 1` removes footer link to repo
- `NEXT_PUBLIC_ALLOW_PUBLIC_DOWNLOADS = 1` enables public photo downloads for all visitors (⚠️ may result in increased bandwidth usage)
- `NEXT_PUBLIC_PUBLIC_API = 1` enables public API available at `/api`
- `NEXT_PUBLIC_IGNORE_PRIORITY_ORDER = 1` prevents `priority_order` field affecting photo order
- `NEXT_PUBLIC_HIDE_SOCIAL = 1` removes X button from share modal
- `NEXT_PUBLIC_HIDE_FILM_SIMULATIONS = 1` prevents Fujifilm simulations showing up in `/grid` sidebar and CMD-K search results
- `NEXT_PUBLIC_HIDE_EXIF_DATA = 1` hides EXIF data in photo details and OG images (potentially useful for portfolios, which don't focus on photography)
- `NEXT_PUBLIC_GRID_ASPECT_RATIO = 1.5` sets aspect ratio for grid tiles (defaults to `1`—setting to `0` removes the constraint)
- `NEXT_PUBLIC_SHOW_LARGE_THUMBNAILS = 1` ensures large thumbnails on photo grid views
- `NEXT_PUBLIC_OG_TEXT_ALIGNMENT = BOTTOM` keeps OG image text bottom aligned (default is top)

View File

@ -1,6 +1,9 @@
/* eslint-disable max-len */
import {
convertTimestampToNaivePostgresString,
convertTimestampWithOffsetToPostgresString,
validatePostgresDateString,
validateNaivePostgresDateString,
} from '../src/utility/date';
describe('Date utility', () => {
@ -29,19 +32,34 @@ describe('Date utility', () => {
expect(convertTimestampToNaivePostgresString(timestamp))
.toBe('2023-12-02 16:38:36');
});
it('Malformed date string', () => {
const timestamp = '2024/01a/01 Z';
expect(convertTimestampWithOffsetToPostgresString(timestamp))
.toBe(convertTimestampWithOffsetToPostgresString(
new Date().toISOString(),
));
});
it('Empty string', () => {
const timestamp = ' ';
expect(convertTimestampWithOffsetToPostgresString(timestamp))
.toBe(convertTimestampWithOffsetToPostgresString(
new Date().toISOString(),
));
});
});
it('Malformed date string', () => {
const timestamp = '2024/01a/01 Z';
expect(convertTimestampWithOffsetToPostgresString(timestamp))
.toBe(convertTimestampWithOffsetToPostgresString(
new Date().toISOString(),
));
});
it('Empty string', () => {
const timestamp = ' ';
expect(convertTimestampWithOffsetToPostgresString(timestamp))
.toBe(convertTimestampWithOffsetToPostgresString(
new Date().toISOString(),
));
describe('validates date strings', () => {
it('Correct', () => {
expect(validatePostgresDateString('2025-01-03T21:00:44.000Z')).toBe(true);
expect(validateNaivePostgresDateString('2025-01-03 16:00:44')).toBe(true);
});
it('Incorrect', () => {
expect(validatePostgresDateString('2024-01-01')).toBe(false);
expect(validatePostgresDateString('2025-01-03 16:00:44')).toBe(false);
expect(validateNaivePostgresDateString('2024-01-01')).toBe(false);
expect(validatePostgresDateString('2025-01-03T21:00:44.000')).toBe(false);
expect(validateNaivePostgresDateString('2025-01-03T16:00:44')).toBe(false);
expect(validatePostgresDateString('2025-01-03T21:00:44.000ZZ')).toBe(false);
expect(validateNaivePostgresDateString('2025-01-03 16:00:44Z')).toBe(false);
});
});
});

View File

@ -9,18 +9,18 @@
"analyze": "ANALYZE=true next build"
},
"dependencies": {
"@ai-sdk/openai": "^1.1.0",
"@aws-sdk/client-s3": "3.732.0",
"@aws-sdk/s3-request-presigner": "3.732.0",
"@radix-ui/react-dialog": "^1.1.4",
"@radix-ui/react-dropdown-menu": "^2.1.4",
"@ai-sdk/openai": "^1.1.1",
"@aws-sdk/client-s3": "3.733.0",
"@aws-sdk/s3-request-presigner": "3.733.0",
"@radix-ui/react-dialog": "^1.1.5",
"@radix-ui/react-dropdown-menu": "^2.1.5",
"@radix-ui/react-visually-hidden": "^1.1.1",
"@upstash/ratelimit": "^2.0.5",
"@vercel/analytics": "^1.4.1",
"@vercel/blob": "^0.27.1",
"@vercel/kv": "^3.0.0",
"@vercel/speed-insights": "^1.1.0",
"ai": "^4.1.0",
"ai": "^4.1.1",
"camelcase-keys": "^9.1.3",
"cmdk": "^1.0.4",
"date-fns": "^4.1.0",
@ -28,7 +28,7 @@
"exifr": "^7.1.3",
"framer-motion": "^12.0.1",
"nanoid": "^5.0.9",
"next": "15.1.5",
"next": "15.1.6",
"next-auth": "5.0.0-beta.25",
"next-themes": "^0.4.4",
"pg": "^8.13.1",
@ -43,7 +43,7 @@
"use-debounce": "^10.0.4"
},
"devDependencies": {
"@next/bundle-analyzer": "15.1.5",
"@next/bundle-analyzer": "15.1.6",
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/forms": "^0.5.10",
"@testing-library/jest-dom": "^6.6.3",
@ -57,7 +57,7 @@
"autoprefixer": "10.4.20",
"clsx": "^2.1.1",
"eslint": "9.18.0",
"eslint-config-next": "15.1.5",
"eslint-config-next": "15.1.6",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"postcss": "8.5.1",

274
pnpm-lock.yaml generated
View File

@ -9,20 +9,20 @@ importers:
.:
dependencies:
'@ai-sdk/openai':
specifier: ^1.1.0
version: 1.1.0(zod@3.23.8)
specifier: ^1.1.1
version: 1.1.1(zod@3.23.8)
'@aws-sdk/client-s3':
specifier: 3.732.0
version: 3.732.0
specifier: 3.733.0
version: 3.733.0
'@aws-sdk/s3-request-presigner':
specifier: 3.732.0
version: 3.732.0
specifier: 3.733.0
version: 3.733.0
'@radix-ui/react-dialog':
specifier: ^1.1.4
version: 1.1.4(@types/react-dom@19.0.3(@types/react@19.0.7))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
specifier: ^1.1.5
version: 1.1.5(@types/react-dom@19.0.3(@types/react@19.0.7))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@radix-ui/react-dropdown-menu':
specifier: ^2.1.4
version: 2.1.4(@types/react-dom@19.0.3(@types/react@19.0.7))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
specifier: ^2.1.5
version: 2.1.5(@types/react-dom@19.0.3(@types/react@19.0.7))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@radix-ui/react-visually-hidden':
specifier: ^1.1.1
version: 1.1.1(@types/react-dom@19.0.3(@types/react@19.0.7))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
@ -31,7 +31,7 @@ importers:
version: 2.0.5(@upstash/redis@1.34.3)
'@vercel/analytics':
specifier: ^1.4.1
version: 1.4.1(next@15.1.5(@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))
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))
'@vercel/blob':
specifier: ^0.27.1
version: 0.27.1
@ -40,10 +40,10 @@ importers:
version: 3.0.0
'@vercel/speed-insights':
specifier: ^1.1.0
version: 1.1.0(next@15.1.5(@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))
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))
ai:
specifier: ^4.1.0
version: 4.1.0(react@19.0.0)(zod@3.23.8)
specifier: ^4.1.1
version: 4.1.1(react@19.0.0)(zod@3.23.8)
camelcase-keys:
specifier: ^9.1.3
version: 9.1.3
@ -66,11 +66,11 @@ importers:
specifier: ^5.0.9
version: 5.0.9
next:
specifier: 15.1.5
version: 15.1.5(@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.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)
next-auth:
specifier: 5.0.0-beta.25
version: 5.0.0-beta.25(next@15.1.5(@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.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-themes:
specifier: ^0.4.4
version: 0.4.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
@ -106,8 +106,8 @@ importers:
version: 10.0.4(react@19.0.0)
devDependencies:
'@next/bundle-analyzer':
specifier: 15.1.5
version: 15.1.5
specifier: 15.1.6
version: 15.1.6
'@tailwindcss/container-queries':
specifier: ^0.1.1
version: 0.1.1(tailwindcss@3.4.17)
@ -148,8 +148,8 @@ importers:
specifier: 9.18.0
version: 9.18.0(jiti@1.21.7)
eslint-config-next:
specifier: 15.1.5
version: 15.1.5(eslint@9.18.0(jiti@1.21.7))(typescript@5.7.3)
specifier: 15.1.6
version: 15.1.6(eslint@9.18.0(jiti@1.21.7))(typescript@5.7.3)
jest:
specifier: ^29.7.0
version: 29.7.0(@types/node@22.10.7)
@ -171,14 +171,14 @@ packages:
'@adobe/css-tools@4.4.0':
resolution: {integrity: sha512-Ff9+ksdQQB3rMncgqDK78uLznstjyfIf2Arnh22pW8kBpLs6rpKDwgnZT46hin5Hl1WzazzK64DOrhSwYpS7bQ==}
'@ai-sdk/openai@1.1.0':
resolution: {integrity: sha512-D2DaGMK89yYgO32n4Gr7gBJeJGGGS27gdfzYFMRDXlZmKh7VW1WXBp3FXxDwpmt0CgLoVI4qV8lf+gslah+kWw==}
'@ai-sdk/openai@1.1.1':
resolution: {integrity: sha512-0tUlrjSMWYYQxiC/6d6n5C6nxUYSHzlt/FipJgzKQleMts3Br5+u2cM4nwOVtuS14J2MsBM/SK2DGL0lFctirA==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.0.0
'@ai-sdk/provider-utils@2.1.0':
resolution: {integrity: sha512-rBUabNoyB25PBUjaiMSk86fHNSCqTngNZVvXxv8+6mvw47JX5OexW+ZHRsEw8XKTE8+hqvNFVzctaOrRZ2i9Zw==}
'@ai-sdk/provider-utils@2.1.1':
resolution: {integrity: sha512-+FRXSAdzPJFJN6TpyvyGWLo7WJuoBKI1g66UL+sli1HrxlldXSwxRPeb8tMMmNcyi3VKQogg2VsoJjlt4ort5w==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.0.0
@ -186,12 +186,12 @@ packages:
zod:
optional: true
'@ai-sdk/provider@1.0.4':
resolution: {integrity: sha512-lJi5zwDosvvZER3e/pB8lj1MN3o3S7zJliQq56BRr4e9V3fcRyFtwP0JRxaRS5vHYX3OJ154VezVoQNrk0eaKw==}
'@ai-sdk/provider@1.0.5':
resolution: {integrity: sha512-KATFp9CNXtMEzs8KBwLYK2+rGkkeED6p1+4koQveszyscIavObXIRW7vjr0MoZ9HFIHOUlrcak+3s/Xt3UXmAg==}
engines: {node: '>=18'}
'@ai-sdk/react@1.1.0':
resolution: {integrity: sha512-U5lBbLyf1pw79xsk5dgHSkBv9Jta3xzWlOLpxsmHlxh1X94QOH3e1gm+nioQ/JvTuHLm23j2tz3i4MpMdchwXQ==}
'@ai-sdk/react@1.1.1':
resolution: {integrity: sha512-7LX/YF8sis8UM7p8ftUcu0xySG86/TBddcB42w/+mWOXL6hjYzcuGD8G121TobHsnxVxbsBlF/ykps/GYVvLNg==}
engines: {node: '>=18'}
peerDependencies:
react: ^18 || ^19 || ^19.0.0-rc
@ -202,8 +202,8 @@ packages:
zod:
optional: true
'@ai-sdk/ui-utils@1.1.0':
resolution: {integrity: sha512-ETXwdHaHwzC7NIehbthDFGwsTFk+gNtRL/lm85nR4WDFvvYQptoM/7wTANs0p0H7zumB3Ep5hKzv0Encu8vSRw==}
'@ai-sdk/ui-utils@1.1.1':
resolution: {integrity: sha512-lkTxGoebnEgs8HtKeWut0AglXN7zpWQwYmun4yuhpiup7DxPWTmt3vGiYvqQTBOFAmyoea3uzIKjHwRHuayr2w==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.0.0
@ -256,8 +256,8 @@ packages:
'@aws-crypto/util@5.2.0':
resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==}
'@aws-sdk/client-s3@3.732.0':
resolution: {integrity: sha512-ITPcG40qdiLXZRvNQ8V3u4yB16afcdIabdRBN6Blba31rk0MfeBGWwah0+lLSTFo1ZIrIQvBl6PAQ7mO0mkKLg==}
'@aws-sdk/client-s3@3.733.0':
resolution: {integrity: sha512-LmAbtNxrgbtB+YVt/HPPyKBgJWrvHOv5yNn98Ndlwm1mBgvI1N7+HQlI5ZWIKBCkwJtLtdS8ZVHzPtqnyWO+YA==}
engines: {node: '>=18.0.0'}
'@aws-sdk/client-sso@3.731.0':
@ -324,8 +324,8 @@ packages:
resolution: {integrity: sha512-y6FLASB1iKWuR5tUipMyo77bt0lEl3OnCrrd2xw/H24avq1HhJjjPR0HHhJE6QKJzF/FYXeV88tcyPSMe32VDw==}
engines: {node: '>=18.0.0'}
'@aws-sdk/middleware-sdk-s3@3.731.0':
resolution: {integrity: sha512-J9aKyQaVoec5eWTSDfO4h2sKHNP0wTzN15LFcHnkD+e/d0rdmOi7BTkkbJrIaynma9WShIasmrtM3HNi9GiiTA==}
'@aws-sdk/middleware-sdk-s3@3.733.0':
resolution: {integrity: sha512-XX/sP61LugQZck6W8WQJpYQEeW/h7t0qgxfZEv9Qk9fWBxxdcR1j4zkmSD3Da5vgnBl8dJ3wdmI2k96qw6ONkQ==}
engines: {node: '>=18.0.0'}
'@aws-sdk/middleware-ssec@3.731.0':
@ -344,12 +344,12 @@ packages:
resolution: {integrity: sha512-XlDpRNkDVHF59f07JmkuAidEv//m3hT6/JL85h0l3+zrpaRWhf8n8lVUyAPNq35ZujK8AcorYM+93u7hdWsliQ==}
engines: {node: '>=18.0.0'}
'@aws-sdk/s3-request-presigner@3.732.0':
resolution: {integrity: sha512-aIzl8UDZp1fNS6haLKmyHcLXg1vWhu9Yimz/9W1xElGB3XZc0LsTlp57yVTmx9ROYo3kAh+Z6RhF73bESTOmjA==}
'@aws-sdk/s3-request-presigner@3.733.0':
resolution: {integrity: sha512-EcB4b2cR/2yGUZXS4S7swEDaodImh4b4ndBYOfcKaP0m5A3cJ2evVjncKFOPLZmnIXRlqRXN1r0bW82uKxXnrA==}
engines: {node: '>=18.0.0'}
'@aws-sdk/signature-v4-multi-region@3.731.0':
resolution: {integrity: sha512-1r/b4Os15dR+BCVRRLVQJMF7Krq6xX6IKHxN43kuvODYWz8Nv3XDlaSpeRpAzyJuzW/fTp4JgE+z0+gmJfdEeA==}
'@aws-sdk/signature-v4-multi-region@3.733.0':
resolution: {integrity: sha512-gdN59yEDHSoEZqUJF4vnTl1OoiTfa8fyBWTbu4Ri1cYE20cFvoePHdz+eG6ipe7VZNwKf8j/ZQeOgO40jNbZKQ==}
engines: {node: '>=18.0.0'}
'@aws-sdk/token-providers@3.731.1':
@ -844,59 +844,59 @@ packages:
'@jridgewell/trace-mapping@0.3.25':
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
'@next/bundle-analyzer@15.1.5':
resolution: {integrity: sha512-pCYMPgGRwf+FjEwUXFo3QF14VzBSPPsBHSFuXUpq5ifKcY8LbcmoF2xMVVMa2HoYgA1XuqPSAIfLJr4YXNa9xQ==}
'@next/bundle-analyzer@15.1.6':
resolution: {integrity: sha512-hGzQyDqJzFHcHNCyTqM3o05BpVq5tGnRODccZBVJDBf5Miv/26UJPMB0wh9L9j3ylgHC+0/v8BaBnBBek1rC6Q==}
'@next/env@15.1.5':
resolution: {integrity: sha512-jg8ygVq99W3/XXb9Y6UQsritwhjc+qeiO7QrGZRYOfviyr/HcdnhdBQu4gbp2rBIh2ZyBYTBMWbPw3JSCb0GHw==}
'@next/env@15.1.6':
resolution: {integrity: sha512-d9AFQVPEYNr+aqokIiPLNK/MTyt3DWa/dpKveiAaVccUadFbhFEvY6FXYX2LJO2Hv7PHnLBu2oWwB4uBuHjr/w==}
'@next/eslint-plugin-next@15.1.5':
resolution: {integrity: sha512-3cCrXBybsqe94UxD6DBQCYCCiP9YohBMgZ5IzzPYHmPzj8oqNlhBii5b6o1HDDaRHdz2pVnSsAROCtrczy8O0g==}
'@next/eslint-plugin-next@15.1.6':
resolution: {integrity: sha512-+slMxhTgILUntZDGNgsKEYHUvpn72WP1YTlkmEhS51vnVd7S9jEEy0n9YAMcI21vUG4akTw9voWH02lrClt/yw==}
'@next/swc-darwin-arm64@15.1.5':
resolution: {integrity: sha512-5ttHGE75Nw9/l5S8zR2xEwR8OHEqcpPym3idIMAZ2yo+Edk0W/Vf46jGqPOZDk+m/SJ+vYZDSuztzhVha8rcdA==}
'@next/swc-darwin-arm64@15.1.6':
resolution: {integrity: sha512-u7lg4Mpl9qWpKgy6NzEkz/w0/keEHtOybmIl0ykgItBxEM5mYotS5PmqTpo+Rhg8FiOiWgwr8USxmKQkqLBCrw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
'@next/swc-darwin-x64@15.1.5':
resolution: {integrity: sha512-8YnZn7vDURUUTInfOcU5l0UWplZGBqUlzvqKKUFceM11SzfNEz7E28E1Arn4/FsOf90b1Nopboy7i7ufc4jXag==}
'@next/swc-darwin-x64@15.1.6':
resolution: {integrity: sha512-x1jGpbHbZoZ69nRuogGL2MYPLqohlhnT9OCU6E6QFewwup+z+M6r8oU47BTeJcWsF2sdBahp5cKiAcDbwwK/lg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@next/swc-linux-arm64-gnu@15.1.5':
resolution: {integrity: sha512-rDJC4ctlYbK27tCyFUhgIv8o7miHNlpCjb2XXfTLQszwAUOSbcMN9q2y3urSrrRCyGVOd9ZR9a4S45dRh6JF3A==}
'@next/swc-linux-arm64-gnu@15.1.6':
resolution: {integrity: sha512-jar9sFw0XewXsBzPf9runGzoivajeWJUc/JkfbLTC4it9EhU8v7tCRLH7l5Y1ReTMN6zKJO0kKAGqDk8YSO2bg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@next/swc-linux-arm64-musl@15.1.5':
resolution: {integrity: sha512-FG5RApf4Gu+J+pHUQxXPM81oORZrKBYKUaBTylEIQ6Lz17hKVDsLbSXInfXM0giclvXbyiLXjTv42sQMATmZ0A==}
'@next/swc-linux-arm64-musl@15.1.6':
resolution: {integrity: sha512-+n3u//bfsrIaZch4cgOJ3tXCTbSxz0s6brJtU3SzLOvkJlPQMJ+eHVRi6qM2kKKKLuMY+tcau8XD9CJ1OjeSQQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@next/swc-linux-x64-gnu@15.1.5':
resolution: {integrity: sha512-NX2Ar3BCquAOYpnoYNcKz14eH03XuF7SmSlPzTSSU4PJe7+gelAjxo3Y7F2m8+hLT8ZkkqElawBp7SWBdzwqQw==}
'@next/swc-linux-x64-gnu@15.1.6':
resolution: {integrity: sha512-SpuDEXixM3PycniL4iVCLyUyvcl6Lt0mtv3am08sucskpG0tYkW1KlRhTgj4LI5ehyxriVVcfdoxuuP8csi3kQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@next/swc-linux-x64-musl@15.1.5':
resolution: {integrity: sha512-EQgqMiNu3mrV5eQHOIgeuh6GB5UU57tu17iFnLfBEhYfiOfyK+vleYKh2dkRVkV6ayx3eSqbIYgE7J7na4hhcA==}
'@next/swc-linux-x64-musl@15.1.6':
resolution: {integrity: sha512-L4druWmdFSZIIRhF+G60API5sFB7suTbDRhYWSjiw0RbE+15igQvE2g2+S973pMGvwN3guw7cJUjA/TmbPWTHQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@next/swc-win32-arm64-msvc@15.1.5':
resolution: {integrity: sha512-HPULzqR/VqryQZbZME8HJE3jNFmTGcp+uRMHabFbQl63TtDPm+oCXAz3q8XyGv2AoihwNApVlur9Up7rXWRcjg==}
'@next/swc-win32-arm64-msvc@15.1.6':
resolution: {integrity: sha512-s8w6EeqNmi6gdvM19tqKKWbCyOBvXFbndkGHl+c9YrzsLARRdCHsD9S1fMj8gsXm9v8vhC8s3N8rjuC/XrtkEg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
'@next/swc-win32-x64-msvc@15.1.5':
resolution: {integrity: sha512-n74fUb/Ka1dZSVYfjwQ+nSJ+ifUff7jGurFcTuJNKZmI62FFOxQXUYit/uZXPTj2cirm1rvGWHG2GhbSol5Ikw==}
'@next/swc-win32-x64-msvc@15.1.6':
resolution: {integrity: sha512-6xomMuu54FAFxttYr5PJbEfu96godcxBTRk1OhAvJq0/EnmFU/Ybiax30Snis4vdWZ9LGpf7Roy5fSs7v/5ROQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
@ -983,8 +983,8 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-dialog@1.1.4':
resolution: {integrity: sha512-Ur7EV1IwQGCyaAuyDRiOLA5JIUZxELJljF+MbM/2NC0BYwfuRrbpS30BiQBJrVruscgUkieKkqXYDOoByaxIoA==}
'@radix-ui/react-dialog@1.1.5':
resolution: {integrity: sha512-LaO3e5h/NOEL4OfXjxD43k9Dx+vn+8n+PCFt6uhX/BADFflllyv3WJG6rgvvSVBxpTch938Qq/LGc2MMxipXPw==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
@ -1005,8 +1005,8 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-dismissable-layer@1.1.3':
resolution: {integrity: sha512-onrWn/72lQoEucDmJnr8uczSNTujT0vJnA/X5+3AkChVPowr8n1yvIKIabhWyMQeMvvmdpsvcyDqx3X1LEXCPg==}
'@radix-ui/react-dismissable-layer@1.1.4':
resolution: {integrity: sha512-XDUI0IVYVSwjMXxM6P4Dfti7AH+Y4oS/TB+sglZ/EXc7cqLwGAmp1NlMrcUjj7ks6R5WTZuWKv44FBbLpwU3sA==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
@ -1018,8 +1018,8 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-dropdown-menu@2.1.4':
resolution: {integrity: sha512-iXU1Ab5ecM+yEepGAWK8ZhMyKX4ubFdCNtol4sT9D0OVErG9PNElfx3TQhjw7n7BC5nFVz68/5//clWy+8TXzA==}
'@radix-ui/react-dropdown-menu@2.1.5':
resolution: {integrity: sha512-50ZmEFL1kOuLalPKHrLWvPFMons2fGx9TqQCWlPwDVpbAnaUJ1g4XNcKqFNMQymYU0kKWR4MDDi+9vUQBGFgcQ==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
@ -1062,8 +1062,8 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-menu@2.1.4':
resolution: {integrity: sha512-BnOgVoL6YYdHAG6DtXONaR29Eq4nvbi8rutrV/xlr3RQCMMb3yqP85Qiw/3NReozrSW+4dfLkK+rc1hb4wPU/A==}
'@radix-ui/react-menu@2.1.5':
resolution: {integrity: sha512-uH+3w5heoMJtqVCgYOtYVMECk1TOrkUn0OG0p5MqXC0W2ppcuVeESbou8PTHoqAjbdTEK19AGXBWcEtR5WpEQg==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
@ -1766,8 +1766,8 @@ packages:
resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
engines: {node: '>= 6.0.0'}
ai@4.1.0:
resolution: {integrity: sha512-95nI9hBSSAKPrMnpJbaB3yqvh+G8BS4/EtFz3HR0HgEDJpxC0R6JAlB8+B/BXHd/roNGBrS08Z3Zain/6OFSYA==}
ai@4.1.1:
resolution: {integrity: sha512-oZTzQfrvrXuXnAJhoCsGcLUxSMWWYKkqrk2LfCzcukvB8us7ZUnFBgs9drhXuFP3JkWVbeIZHXjDexZIZcbi8g==}
engines: {node: '>=18'}
peerDependencies:
react: ^18 || ^19 || ^19.0.0-rc
@ -2367,8 +2367,8 @@ packages:
engines: {node: '>=6.0'}
hasBin: true
eslint-config-next@15.1.5:
resolution: {integrity: sha512-Awm7iUJY8toOR+fU8yTxZnA7/LyOGUGOd6cENCuDfJ3gucHOSmLdOSGJ4u+nlrs8p5qXemua42bZmq+uOzxl6Q==}
eslint-config-next@15.1.6:
resolution: {integrity: sha512-Wd1uy6y7nBbXUSg9QAuQ+xYEKli5CgUhLjz1QHW11jLDis5vK5XB3PemL6jEmy7HrdhaRFDz+GTZ/3FoH+EUjg==}
peerDependencies:
eslint: ^7.23.0 || ^8.0.0 || ^9.0.0
typescript: '>=3.3.1'
@ -3331,8 +3331,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.5:
resolution: {integrity: sha512-Cf/TEegnt01hn3Hoywh6N8fvkhbOuChO4wFje24+a86wKOubgVaWkDqxGVgoWlz2Hp9luMJ9zw3epftujdnUOg==}
next@15.1.6:
resolution: {integrity: sha512-Hch4wzbaX0vKQtalpXvUiw5sYivBy4cm5rzUKrBnUB/y436LGrvOUqYvlSeNVCWFO/770gDlltR9gqZH62ct4Q==}
engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
hasBin: true
peerDependencies:
@ -4381,39 +4381,39 @@ snapshots:
'@adobe/css-tools@4.4.0': {}
'@ai-sdk/openai@1.1.0(zod@3.23.8)':
'@ai-sdk/openai@1.1.1(zod@3.23.8)':
dependencies:
'@ai-sdk/provider': 1.0.4
'@ai-sdk/provider-utils': 2.1.0(zod@3.23.8)
'@ai-sdk/provider': 1.0.5
'@ai-sdk/provider-utils': 2.1.1(zod@3.23.8)
zod: 3.23.8
'@ai-sdk/provider-utils@2.1.0(zod@3.23.8)':
'@ai-sdk/provider-utils@2.1.1(zod@3.23.8)':
dependencies:
'@ai-sdk/provider': 1.0.4
'@ai-sdk/provider': 1.0.5
eventsource-parser: 3.0.0
nanoid: 3.3.8
secure-json-parse: 2.7.0
optionalDependencies:
zod: 3.23.8
'@ai-sdk/provider@1.0.4':
'@ai-sdk/provider@1.0.5':
dependencies:
json-schema: 0.4.0
'@ai-sdk/react@1.1.0(react@19.0.0)(zod@3.23.8)':
'@ai-sdk/react@1.1.1(react@19.0.0)(zod@3.23.8)':
dependencies:
'@ai-sdk/provider-utils': 2.1.0(zod@3.23.8)
'@ai-sdk/ui-utils': 1.1.0(zod@3.23.8)
'@ai-sdk/provider-utils': 2.1.1(zod@3.23.8)
'@ai-sdk/ui-utils': 1.1.1(zod@3.23.8)
swr: 2.3.0(react@19.0.0)
throttleit: 2.1.0
optionalDependencies:
react: 19.0.0
zod: 3.23.8
'@ai-sdk/ui-utils@1.1.0(zod@3.23.8)':
'@ai-sdk/ui-utils@1.1.1(zod@3.23.8)':
dependencies:
'@ai-sdk/provider': 1.0.4
'@ai-sdk/provider-utils': 2.1.0(zod@3.23.8)
'@ai-sdk/provider': 1.0.5
'@ai-sdk/provider-utils': 2.1.1(zod@3.23.8)
zod-to-json-schema: 3.24.1(zod@3.23.8)
optionalDependencies:
zod: 3.23.8
@ -4482,7 +4482,7 @@ snapshots:
'@smithy/util-utf8': 2.3.0
tslib: 2.8.1
'@aws-sdk/client-s3@3.732.0':
'@aws-sdk/client-s3@3.733.0':
dependencies:
'@aws-crypto/sha1-browser': 5.2.0
'@aws-crypto/sha256-browser': 5.2.0
@ -4496,11 +4496,11 @@ snapshots:
'@aws-sdk/middleware-location-constraint': 3.731.0
'@aws-sdk/middleware-logger': 3.731.0
'@aws-sdk/middleware-recursion-detection': 3.731.0
'@aws-sdk/middleware-sdk-s3': 3.731.0
'@aws-sdk/middleware-sdk-s3': 3.733.0
'@aws-sdk/middleware-ssec': 3.731.0
'@aws-sdk/middleware-user-agent': 3.731.0
'@aws-sdk/region-config-resolver': 3.731.0
'@aws-sdk/signature-v4-multi-region': 3.731.0
'@aws-sdk/signature-v4-multi-region': 3.733.0
'@aws-sdk/types': 3.731.0
'@aws-sdk/util-endpoints': 3.731.0
'@aws-sdk/util-user-agent-browser': 3.731.0
@ -4748,7 +4748,7 @@ snapshots:
'@smithy/types': 4.1.0
tslib: 2.8.1
'@aws-sdk/middleware-sdk-s3@3.731.0':
'@aws-sdk/middleware-sdk-s3@3.733.0':
dependencies:
'@aws-sdk/core': 3.731.0
'@aws-sdk/types': 3.731.0
@ -4833,9 +4833,9 @@ snapshots:
'@smithy/util-middleware': 4.0.1
tslib: 2.8.1
'@aws-sdk/s3-request-presigner@3.732.0':
'@aws-sdk/s3-request-presigner@3.733.0':
dependencies:
'@aws-sdk/signature-v4-multi-region': 3.731.0
'@aws-sdk/signature-v4-multi-region': 3.733.0
'@aws-sdk/types': 3.731.0
'@aws-sdk/util-format-url': 3.731.0
'@smithy/middleware-endpoint': 4.0.1
@ -4844,9 +4844,9 @@ snapshots:
'@smithy/types': 4.1.0
tslib: 2.8.1
'@aws-sdk/signature-v4-multi-region@3.731.0':
'@aws-sdk/signature-v4-multi-region@3.733.0':
dependencies:
'@aws-sdk/middleware-sdk-s3': 3.731.0
'@aws-sdk/middleware-sdk-s3': 3.733.0
'@aws-sdk/types': 3.731.0
'@smithy/protocol-http': 5.0.1
'@smithy/signature-v4': 5.0.1
@ -5469,41 +5469,41 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.4.15
'@next/bundle-analyzer@15.1.5':
'@next/bundle-analyzer@15.1.6':
dependencies:
webpack-bundle-analyzer: 4.10.1
transitivePeerDependencies:
- bufferutil
- utf-8-validate
'@next/env@15.1.5': {}
'@next/env@15.1.6': {}
'@next/eslint-plugin-next@15.1.5':
'@next/eslint-plugin-next@15.1.6':
dependencies:
fast-glob: 3.3.1
'@next/swc-darwin-arm64@15.1.5':
'@next/swc-darwin-arm64@15.1.6':
optional: true
'@next/swc-darwin-x64@15.1.5':
'@next/swc-darwin-x64@15.1.6':
optional: true
'@next/swc-linux-arm64-gnu@15.1.5':
'@next/swc-linux-arm64-gnu@15.1.6':
optional: true
'@next/swc-linux-arm64-musl@15.1.5':
'@next/swc-linux-arm64-musl@15.1.6':
optional: true
'@next/swc-linux-x64-gnu@15.1.5':
'@next/swc-linux-x64-gnu@15.1.6':
optional: true
'@next/swc-linux-x64-musl@15.1.5':
'@next/swc-linux-x64-musl@15.1.6':
optional: true
'@next/swc-win32-arm64-msvc@15.1.5':
'@next/swc-win32-arm64-msvc@15.1.6':
optional: true
'@next/swc-win32-x64-msvc@15.1.5':
'@next/swc-win32-x64-msvc@15.1.6':
optional: true
'@nodelib/fs.scandir@2.1.5':
@ -5568,12 +5568,12 @@ snapshots:
optionalDependencies:
'@types/react': 19.0.7
'@radix-ui/react-dialog@1.1.4(@types/react-dom@19.0.3(@types/react@19.0.7))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
'@radix-ui/react-dialog@1.1.5(@types/react-dom@19.0.3(@types/react@19.0.7))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
dependencies:
'@radix-ui/primitive': 1.1.1
'@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.7)(react@19.0.0)
'@radix-ui/react-context': 1.1.1(@types/react@19.0.7)(react@19.0.0)
'@radix-ui/react-dismissable-layer': 1.1.3(@types/react-dom@19.0.3(@types/react@19.0.7))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@radix-ui/react-dismissable-layer': 1.1.4(@types/react-dom@19.0.3(@types/react@19.0.7))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@radix-ui/react-focus-guards': 1.1.1(@types/react@19.0.7)(react@19.0.0)
'@radix-ui/react-focus-scope': 1.1.1(@types/react-dom@19.0.3(@types/react@19.0.7))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@radix-ui/react-id': 1.1.0(@types/react@19.0.7)(react@19.0.0)
@ -5596,7 +5596,7 @@ snapshots:
optionalDependencies:
'@types/react': 19.0.7
'@radix-ui/react-dismissable-layer@1.1.3(@types/react-dom@19.0.3(@types/react@19.0.7))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
'@radix-ui/react-dismissable-layer@1.1.4(@types/react-dom@19.0.3(@types/react@19.0.7))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
dependencies:
'@radix-ui/primitive': 1.1.1
'@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.7)(react@19.0.0)
@ -5609,13 +5609,13 @@ snapshots:
'@types/react': 19.0.7
'@types/react-dom': 19.0.3(@types/react@19.0.7)
'@radix-ui/react-dropdown-menu@2.1.4(@types/react-dom@19.0.3(@types/react@19.0.7))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
'@radix-ui/react-dropdown-menu@2.1.5(@types/react-dom@19.0.3(@types/react@19.0.7))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
dependencies:
'@radix-ui/primitive': 1.1.1
'@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.7)(react@19.0.0)
'@radix-ui/react-context': 1.1.1(@types/react@19.0.7)(react@19.0.0)
'@radix-ui/react-id': 1.1.0(@types/react@19.0.7)(react@19.0.0)
'@radix-ui/react-menu': 2.1.4(@types/react-dom@19.0.3(@types/react@19.0.7))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@radix-ui/react-menu': 2.1.5(@types/react-dom@19.0.3(@types/react@19.0.7))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@radix-ui/react-primitive': 2.0.1(@types/react-dom@19.0.3(@types/react@19.0.7))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@radix-ui/react-use-controllable-state': 1.1.0(@types/react@19.0.7)(react@19.0.0)
react: 19.0.0
@ -5648,14 +5648,14 @@ snapshots:
optionalDependencies:
'@types/react': 19.0.7
'@radix-ui/react-menu@2.1.4(@types/react-dom@19.0.3(@types/react@19.0.7))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
'@radix-ui/react-menu@2.1.5(@types/react-dom@19.0.3(@types/react@19.0.7))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
dependencies:
'@radix-ui/primitive': 1.1.1
'@radix-ui/react-collection': 1.1.1(@types/react-dom@19.0.3(@types/react@19.0.7))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.7)(react@19.0.0)
'@radix-ui/react-context': 1.1.1(@types/react@19.0.7)(react@19.0.0)
'@radix-ui/react-direction': 1.1.0(@types/react@19.0.7)(react@19.0.0)
'@radix-ui/react-dismissable-layer': 1.1.3(@types/react-dom@19.0.3(@types/react@19.0.7))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@radix-ui/react-dismissable-layer': 1.1.4(@types/react-dom@19.0.3(@types/react@19.0.7))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@radix-ui/react-focus-guards': 1.1.1(@types/react@19.0.7)(react@19.0.0)
'@radix-ui/react-focus-scope': 1.1.1(@types/react-dom@19.0.3(@types/react@19.0.7))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@radix-ui/react-id': 1.1.0(@types/react@19.0.7)(react@19.0.0)
@ -6389,9 +6389,9 @@ snapshots:
dependencies:
crypto-js: 4.2.0
'@vercel/analytics@1.4.1(next@15.1.5(@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.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))':
optionalDependencies:
next: 15.1.5(@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.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)
@ -6408,9 +6408,9 @@ snapshots:
dependencies:
'@upstash/redis': 1.34.3
'@vercel/speed-insights@1.1.0(next@15.1.5(@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.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))':
optionalDependencies:
next: 15.1.5(@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.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)
@ -6500,12 +6500,12 @@ snapshots:
transitivePeerDependencies:
- supports-color
ai@4.1.0(react@19.0.0)(zod@3.23.8):
ai@4.1.1(react@19.0.0)(zod@3.23.8):
dependencies:
'@ai-sdk/provider': 1.0.4
'@ai-sdk/provider-utils': 2.1.0(zod@3.23.8)
'@ai-sdk/react': 1.1.0(react@19.0.0)(zod@3.23.8)
'@ai-sdk/ui-utils': 1.1.0(zod@3.23.8)
'@ai-sdk/provider': 1.0.5
'@ai-sdk/provider-utils': 2.1.1(zod@3.23.8)
'@ai-sdk/react': 1.1.1(react@19.0.0)(zod@3.23.8)
'@ai-sdk/ui-utils': 1.1.1(zod@3.23.8)
'@opentelemetry/api': 1.9.0
jsondiffpatch: 0.6.0
optionalDependencies:
@ -6837,7 +6837,7 @@ snapshots:
cmdk@1.0.4(@types/react-dom@19.0.3(@types/react@19.0.7))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
dependencies:
'@radix-ui/react-dialog': 1.1.4(@types/react-dom@19.0.3(@types/react@19.0.7))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@radix-ui/react-dialog': 1.1.5(@types/react-dom@19.0.3(@types/react@19.0.7))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@radix-ui/react-id': 1.1.0(@types/react@19.0.7)(react@19.0.0)
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@19.0.3(@types/react@19.0.7))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
react: 19.0.0
@ -7195,9 +7195,9 @@ snapshots:
optionalDependencies:
source-map: 0.6.1
eslint-config-next@15.1.5(eslint@9.18.0(jiti@1.21.7))(typescript@5.7.3):
eslint-config-next@15.1.6(eslint@9.18.0(jiti@1.21.7))(typescript@5.7.3):
dependencies:
'@next/eslint-plugin-next': 15.1.5
'@next/eslint-plugin-next': 15.1.6
'@rushstack/eslint-patch': 1.10.3
'@typescript-eslint/eslint-plugin': 8.19.0(@typescript-eslint/parser@8.19.0(eslint@9.18.0(jiti@1.21.7))(typescript@5.7.3))(eslint@9.18.0(jiti@1.21.7))(typescript@5.7.3)
'@typescript-eslint/parser': 8.19.0(eslint@9.18.0(jiti@1.21.7))(typescript@5.7.3)
@ -8422,10 +8422,10 @@ snapshots:
natural-compare@1.4.0: {}
next-auth@5.0.0-beta.25(next@15.1.5(@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.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):
dependencies:
'@auth/core': 0.37.2
next: 15.1.5(@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.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-themes@0.4.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
@ -8433,9 +8433,9 @@ snapshots:
react: 19.0.0
react-dom: 19.0.0(react@19.0.0)
next@15.1.5(@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.6(@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.5
'@next/env': 15.1.6
'@swc/counter': 0.1.3
'@swc/helpers': 0.5.15
busboy: 1.6.0
@ -8445,14 +8445,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.5
'@next/swc-darwin-x64': 15.1.5
'@next/swc-linux-arm64-gnu': 15.1.5
'@next/swc-linux-arm64-musl': 15.1.5
'@next/swc-linux-x64-gnu': 15.1.5
'@next/swc-linux-x64-musl': 15.1.5
'@next/swc-win32-arm64-msvc': 15.1.5
'@next/swc-win32-x64-msvc': 15.1.5
'@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
'@opentelemetry/api': 1.9.0
sharp: 0.33.5
transitivePeerDependencies:

View File

@ -58,7 +58,7 @@ export default function FieldSetWithStatus({
{!hideLabel && label &&
<label
className={clsx(
'flex gap-2 items-center select-none',
'flex flex-wrap gap-x-2 items-center select-none',
type === 'checkbox' && 'order-2 pt-[3px]',
)}
htmlFor={id}

View File

@ -10,11 +10,13 @@ export default function ResponsiveDate({
className,
titleLabel,
timezone: timezoneFromProps,
hideTime,
}: {
date: Date
className?: string
titleLabel?: string
timezone?: Timezone
hideTime?: boolean,
}) {
const [timezone, setTimezone] = useState(timezoneFromProps);
@ -24,23 +26,30 @@ export default function ResponsiveDate({
}
}, [timezoneFromProps]);
const showPlaceholderContent = timezone === undefined;
const showPlaceholder = timezone === undefined;
const titleDateFormatted = formatDate(date, undefined, timezone)
const titleDateFormatted = formatDate({ date, timezone })
.toLocaleUpperCase();
const title = titleLabel
? `${titleLabel}: ${titleDateFormatted}`
: titleDateFormatted;
const contentClass = showPlaceholderContent && 'opacity-0 select-none';
const contentClass = showPlaceholder && 'opacity-0 select-none';
const formatDateProps = {
date,
timezone,
showPlaceholder,
hideTime,
} as const;
return (
<span
title={showPlaceholderContent ? 'LOADING LOCAL TIME' : title}
title={showPlaceholder ? 'LOADING LOCAL TIME' : title}
className={clsx(
'uppercase rounded-md transition-colors',
showPlaceholderContent && 'bg-dim',
showPlaceholder && 'bg-dim',
className,
)}
>
@ -49,20 +58,20 @@ export default function ResponsiveDate({
className={clsx('xs:hidden', contentClass)}
aria-hidden
>
{formatDate(date, 'short', timezone, showPlaceholderContent)}
{formatDate({ ...formatDateProps, length: 'short' })}
</span>
{/* Medium */}
<span
className={clsx('hidden xs:inline-block sm:hidden', contentClass)}
aria-hidden
>
{formatDate(date, 'medium', timezone,showPlaceholderContent)}
{formatDate({ ...formatDateProps, length: 'medium' })}
</span>
{/* Large */}
<span
className={clsx('hidden sm:inline-block', contentClass)}
>
{formatDate(date, undefined, timezone, showPlaceholderContent)}
{formatDate(formatDateProps)}
</span>
</span>
);

View File

@ -8,11 +8,13 @@ export default function PhotoDate({
className,
dateType = 'takenAt',
timezone,
hideTime,
}: {
photo: Photo
className?: string
dateType?: 'takenAt' | 'createdAt' | 'updatedAt'
timezone: Timezone
hideTime?: boolean
}) {
const date = useMemo(() => {
const date = new Date(dateType === 'takenAt'
@ -45,6 +47,7 @@ export default function PhotoDate({
className,
titleLabel: getTitleLabel(),
timezone,
hideTime,
}} />
);
}

View File

@ -28,6 +28,7 @@ import PhotoLink from './PhotoLink';
import {
SHOULD_PREFETCH_ALL_LINKS,
ALLOW_PUBLIC_DOWNLOADS,
SHOW_TAKEN_AT_TIME,
} from '@/site/config';
import AdminPhotoMenuClient from '@/admin/AdminPhotoMenuClient';
import { RevalidatePhoto } from './InfinitePhotoScroll';
@ -248,8 +249,10 @@ export default function PhotoLarge({
!hasNonDateContent && isUserSignedIn && 'md:pr-7',
)}
// Created at is a naive datetime which
// does not require a timezone
// does not require a timezone and will not
// cause server/client time mismatch
timezone={null}
hideTime={!SHOW_TAKEN_AT_TIME}
/>
<div className={clsx(
'flex gap-1 translate-y-[0.5px]',

View File

@ -5,6 +5,8 @@ import {
convertTimestampWithOffsetToPostgresString,
generateLocalNaivePostgresString,
generateLocalPostgresString,
validationMessageNaivePostgresDateString,
validationMessagePostgresDateString,
} from '@/utility/date';
import {
convertApertureValueToFNumber,
@ -116,8 +118,14 @@ const FORM_METADATA = (
locationName: { label: 'location name', hide: true },
latitude: { label: 'latitude' },
longitude: { label: 'longitude' },
takenAt: { label: 'taken at' },
takenAtNaive: { label: 'taken at (naive)' },
takenAt: {
label: 'taken at',
validate: validationMessagePostgresDateString,
},
takenAtNaive: {
label: 'taken at (naive)',
validate: validationMessageNaivePostgresDateString,
},
priorityOrder: { label: 'priority order' },
favorite: { label: 'favorite', type: 'checkbox', excludeFromInsert: true },
hidden: { label: 'hidden', type: 'checkbox' },

View File

@ -212,7 +212,10 @@ export const titleForPhoto = (
if (photo.title) {
return photo.title;
} else if (preferDateOverUntitled && (photo.takenAt || photo.createdAt)) {
return formatDate(photo.takenAt || photo.createdAt, 'tiny');
return formatDate({
date: photo.takenAt || photo.createdAt,
length: 'tiny',
});
} else {
return 'Untitled';
}

View File

@ -11,6 +11,7 @@ import {
BiCog,
BiCopy,
BiData,
BiHide,
BiLockAlt,
BiPencil,
} from 'react-icons/bi';
@ -29,7 +30,7 @@ import WarningNote from '@/components/WarningNote';
import { RiSpeedMiniLine } from 'react-icons/ri';
export default function SiteChecklistClient({
// Config checklist
// Storage
hasDatabase,
isPostgresSslEnabled,
hasVercelPostgres,
@ -40,38 +41,49 @@ export default function SiteChecklistClient({
hasAwsS3Storage,
hasMultipleStorageProviders,
currentStorage,
// Auth
hasAuthSecret,
hasAdminUser,
// Content
hasDomain,
hasTitle,
hasDescription,
hasAbout,
hasDefaultTheme,
showRepoLink,
showSocial,
showFilmSimulations,
showExifInfo,
// AI
isAiTextGenerationEnabled,
aiTextAutoGeneratedFields,
hasAiTextAutoGeneratedFields,
// Performance
isStaticallyOptimized,
arePhotosStaticallyOptimized,
arePhotoOGImagesStaticallyOptimized,
arePhotoCategoriesStaticallyOptimized,
areOriginalUploadsPreserved,
// Display
showExifInfo,
showTakenAtTimeHidden,
showSocial,
showFilmSimulations,
showRepoLink,
// Settings
isGridHomepageEnabled,
hasDefaultTheme,
defaultTheme,
arePhotosMatted,
isBlurEnabled,
isGeoPrivacyEnabled,
isPriorityOrderEnabled,
isAiTextGenerationEnabled,
aiTextAutoGeneratedFields,
hasAiTextAutoGeneratedFields,
isPublicApiEnabled,
arePublicDownloadsEnabled,
isOgTextBottomAligned,
gridAspectRatio,
hasGridAspectRatio,
gridDensity,
hasGridDensityPreference,
arePublicDownloadsEnabled,
isPublicApiEnabled,
isPriorityOrderEnabled,
isOgTextBottomAligned,
// Misc
baseUrl,
commitSha,
commitMessage,
// Connection status
databaseError,
storageError,
@ -81,9 +93,6 @@ export default function SiteChecklistClient({
simplifiedView,
isTestingConnections,
secret,
baseUrl,
commitSha,
commitMessage,
}: ConfigChecklistStatus &
Partial<Awaited<ReturnType<typeof testConnectionsAction>>> & {
simplifiedView?: boolean
@ -473,6 +482,57 @@ export default function SiteChecklistClient({
{renderEnvVars(['NEXT_PUBLIC_PRESERVE_ORIGINAL_UPLOADS'])}
</ChecklistRow>
</Checklist>
<Checklist
title="Display"
icon={<BiHide size={18} />}
optional
>
<ChecklistRow
title="Show EXIF data"
status={showExifInfo}
optional
>
Set environment variable to {'"1"'} to hide EXIF data:
{renderEnvVars(['NEXT_PUBLIC_HIDE_EXIF_DATA'])}
</ChecklistRow>
<ChecklistRow
title="Show taken at time"
status={showTakenAtTimeHidden}
optional
>
Set environment variable to {'"1"'} to hide
taken at time from photo meta:
{renderEnvVars(['NEXT_PUBLIC_HIDE_TAKEN_AT_TIME'])}
</ChecklistRow>
<ChecklistRow
title="Show social"
status={showSocial}
optional
>
Set environment variable to {'"1"'} to hide
{' '}
X button from share modal:
{renderEnvVars(['NEXT_PUBLIC_HIDE_SOCIAL'])}
</ChecklistRow>
<ChecklistRow
title="Show Fujifilm simulations"
status={showFilmSimulations}
optional
>
Set environment variable to {'"1"'} to prevent
simulations showing up in /grid sidebar and
CMD-K results:
{renderEnvVars(['NEXT_PUBLIC_HIDE_FILM_SIMULATIONS'])}
</ChecklistRow>
<ChecklistRow
title="Show repo link"
status={showRepoLink}
optional
>
Set environment variable to {'"1"'} to hide footer link:
{renderEnvVars(['NEXT_PUBLIC_HIDE_REPO_LINK'])}
</ChecklistRow>
</Checklist>
<Checklist
title="Settings"
icon={<BiCog size={16} />}
@ -528,12 +588,24 @@ export default function SiteChecklistClient({
{renderEnvVars(['NEXT_PUBLIC_GEO_PRIVACY'])}
</ChecklistRow>
<ChecklistRow
title="Show repo link"
status={showRepoLink}
title={`Grid aspect ratio: ${gridAspectRatio}`}
status={hasGridAspectRatio}
optional
>
Set environment variable to {'"1"'} to hide footer link:
{renderEnvVars(['NEXT_PUBLIC_HIDE_REPO_LINK'])}
Set environment variable to any number to enforce aspect ratio
{' '}
(default is {'"1"'}, i.e., square)set to {'"0"'} to disable:
{renderEnvVars(['NEXT_PUBLIC_GRID_ASPECT_RATIO'])}
</ChecklistRow>
<ChecklistRow
title={`Grid density: ${gridDensity ? 'low' : 'high'}`}
status={hasGridDensityPreference}
optional
>
Set environment variable to {'"1"'} to ensure large thumbnails
on photo grid views (if not configured, density is based on
aspect ratio configuration):
{renderEnvVars(['NEXT_PUBLIC_SHOW_LARGE_THUMBNAILS'])}
</ChecklistRow>
<ChecklistRow
title="Public downloads"
@ -562,54 +634,6 @@ export default function SiteChecklistClient({
priority order photo field affecting photo order:
{renderEnvVars(['NEXT_PUBLIC_IGNORE_PRIORITY_ORDER'])}
</ChecklistRow>
<ChecklistRow
title="Show social"
status={showSocial}
optional
>
Set environment variable to {'"1"'} to hide
{' '}
X button from share modal:
{renderEnvVars(['NEXT_PUBLIC_HIDE_SOCIAL'])}
</ChecklistRow>
<ChecklistRow
title="Show Fujifilm simulations"
status={showFilmSimulations}
optional
>
Set environment variable to {'"1"'} to prevent
simulations showing up in /grid sidebar and
CMD-K results:
{renderEnvVars(['NEXT_PUBLIC_HIDE_FILM_SIMULATIONS'])}
</ChecklistRow>
<ChecklistRow
title="Show EXIF data"
status={showExifInfo}
optional
>
Set environment variable to {'"1"'} to hide EXIF data:
{renderEnvVars(['NEXT_PUBLIC_HIDE_EXIF_DATA'])}
</ChecklistRow>
<ChecklistRow
title={`Grid aspect ratio: ${gridAspectRatio}`}
status={hasGridAspectRatio}
optional
>
Set environment variable to any number to enforce aspect ratio
{' '}
(default is {'"1"'}, i.e., square)set to {'"0"'} to disable:
{renderEnvVars(['NEXT_PUBLIC_GRID_ASPECT_RATIO'])}
</ChecklistRow>
<ChecklistRow
title={`Grid density: ${gridDensity ? 'low' : 'high'}`}
status={hasGridDensityPreference}
optional
>
Set environment variable to {'"1"'} to ensure large thumbnails
on photo grid views (if not configured, density is based on
aspect ratio configuration):
{renderEnvVars(['NEXT_PUBLIC_SHOW_LARGE_THUMBNAILS'])}
</ChecklistRow>
<ChecklistRow
title="Legacy OG text alignment"
status={isOgTextBottomAligned}

View File

@ -122,6 +122,13 @@ export const CURRENT_STORAGE: StorageType =
: 'vercel-blob'
);
// AI
export const AI_TEXT_GENERATION_ENABLED =
Boolean(process.env.OPENAI_SECRET_KEY);
export const AI_TEXT_AUTO_GENERATED_FIELDS = parseAiAutoGeneratedFieldsText(
process.env.AI_TEXT_AUTO_GENERATED_FIELDS);
// PERFORMANCE
export const STATICALLY_OPTIMIZED_PHOTOS =
@ -139,6 +146,19 @@ export const PRESERVE_ORIGINAL_UPLOADS =
// Legacy environment variable name
process.env.NEXT_PUBLIC_PRO_MODE === '1';
// DISPLAY
export const SHOW_EXIF_DATA =
process.env.NEXT_PUBLIC_HIDE_EXIF_DATA !== '1';
export const SHOW_TAKEN_AT_TIME =
process.env.NEXT_PUBLIC_HIDE_TAKEN_AT_TIME !== '1';
export const SHOW_SOCIAL =
process.env.NEXT_PUBLIC_HIDE_SOCIAL !== '1';
export const SHOW_FILM_SIMULATIONS =
process.env.NEXT_PUBLIC_HIDE_FILM_SIMULATIONS !== '1';
export const SHOW_REPO_LINK =
process.env.NEXT_PUBLIC_HIDE_REPO_LINK !== '1';
// SETTINGS
export const GRID_HOMEPAGE_ENABLED =
@ -155,39 +175,30 @@ export const BLUR_ENABLED =
process.env.NEXT_PUBLIC_BLUR_DISABLED !== '1';
export const GEO_PRIVACY_ENABLED =
process.env.NEXT_PUBLIC_GEO_PRIVACY === '1';
export const AI_TEXT_GENERATION_ENABLED =
Boolean(process.env.OPENAI_SECRET_KEY);
export const AI_TEXT_AUTO_GENERATED_FIELDS = parseAiAutoGeneratedFieldsText(
process.env.AI_TEXT_AUTO_GENERATED_FIELDS);
export const PRIORITY_ORDER_ENABLED =
process.env.NEXT_PUBLIC_IGNORE_PRIORITY_ORDER !== '1';
export const PUBLIC_API_ENABLED =
process.env.NEXT_PUBLIC_PUBLIC_API === '1';
export const ALLOW_PUBLIC_DOWNLOADS =
process.env.NEXT_PUBLIC_ALLOW_PUBLIC_DOWNLOADS === '1';
export const SHOW_REPO_LINK =
process.env.NEXT_PUBLIC_HIDE_REPO_LINK !== '1';
export const SHOW_SOCIAL =
process.env.NEXT_PUBLIC_HIDE_SOCIAL !== '1';
export const SHOW_FILM_SIMULATIONS =
process.env.NEXT_PUBLIC_HIDE_FILM_SIMULATIONS !== '1';
export const SHOW_EXIF_DATA =
process.env.NEXT_PUBLIC_HIDE_EXIF_DATA !== '1';
export const GRID_ASPECT_RATIO =
process.env.NEXT_PUBLIC_GRID_ASPECT_RATIO
? parseFloat(process.env.NEXT_PUBLIC_GRID_ASPECT_RATIO)
: 1;
export const OG_TEXT_BOTTOM_ALIGNMENT =
(process.env.NEXT_PUBLIC_OG_TEXT_ALIGNMENT ?? '').toUpperCase() === 'BOTTOM';
export const ADMIN_DEBUG_TOOLS_ENABLED = process.env.ADMIN_DEBUG_TOOLS === '1';
export const PREFERS_LOW_DENSITY_GRID =
process.env.NEXT_PUBLIC_SHOW_LARGE_THUMBNAILS === '1';
export const HIGH_DENSITY_GRID =
GRID_ASPECT_RATIO <= 1 &&
!PREFERS_LOW_DENSITY_GRID;
export const ALLOW_PUBLIC_DOWNLOADS =
process.env.NEXT_PUBLIC_ALLOW_PUBLIC_DOWNLOADS === '1';
export const PUBLIC_API_ENABLED =
process.env.NEXT_PUBLIC_PUBLIC_API === '1';
export const PRIORITY_ORDER_ENABLED =
process.env.NEXT_PUBLIC_IGNORE_PRIORITY_ORDER !== '1';
export const OG_TEXT_BOTTOM_ALIGNMENT =
(process.env.NEXT_PUBLIC_OG_TEXT_ALIGNMENT ?? '').toUpperCase() === 'BOTTOM';
// INTERNAL
export const ADMIN_DEBUG_TOOLS_ENABLED = process.env.ADMIN_DEBUG_TOOLS === '1';
export const CONFIG_CHECKLIST_STATUS = {
// STORAGE
hasDatabase: HAS_DATABASE,
isPostgresSslEnabled: POSTGRES_SSL_ENABLED,
hasVercelPostgres: (
@ -205,20 +216,27 @@ export const CONFIG_CHECKLIST_STATUS = {
),
hasMultipleStorageProviders: HAS_MULTIPLE_STORAGE_PROVIDERS,
currentStorage: CURRENT_STORAGE,
// AUTH
hasAuthSecret: Boolean(process.env.AUTH_SECRET),
hasAdminUser: (
Boolean(process.env.ADMIN_EMAIL) &&
Boolean(process.env.ADMIN_PASSWORD)
),
// CONTENT
hasDomain: Boolean(process.env.NEXT_PUBLIC_SITE_DOMAIN),
hasTitle: Boolean(process.env.NEXT_PUBLIC_SITE_TITLE),
hasDescription: HAS_DEFINED_SITE_DESCRIPTION,
hasAbout: Boolean(process.env.NEXT_PUBLIC_SITE_ABOUT),
hasDefaultTheme: Boolean(process.env.NEXT_PUBLIC_DEFAULT_THEME),
showRepoLink: SHOW_REPO_LINK,
showSocial: SHOW_SOCIAL,
showFilmSimulations: SHOW_FILM_SIMULATIONS,
showExifInfo: SHOW_EXIF_DATA,
// AI
isAiTextGenerationEnabled: AI_TEXT_GENERATION_ENABLED,
aiTextAutoGeneratedFields: process.env.AI_TEXT_AUTO_GENERATED_FIELDS
? AI_TEXT_AUTO_GENERATED_FIELDS.length === 0
? ['none']
: AI_TEXT_AUTO_GENERATED_FIELDS
: ['all'],
hasAiTextAutoGeneratedFields:
Boolean(process.env.AI_TEXT_AUTO_GENERATED_FIELDS),
// PERFORMANCE
isStaticallyOptimized: (
STATICALLY_OPTIMIZED_PHOTOS ||
STATICALLY_OPTIMIZED_PHOTO_OG_IMAGES ||
@ -228,28 +246,29 @@ export const CONFIG_CHECKLIST_STATUS = {
arePhotoOGImagesStaticallyOptimized: STATICALLY_OPTIMIZED_PHOTO_OG_IMAGES,
arePhotoCategoriesStaticallyOptimized: STATICALLY_OPTIMIZED_PHOTO_CATEGORIES,
areOriginalUploadsPreserved: PRESERVE_ORIGINAL_UPLOADS,
// DISPLAY
showExifInfo: SHOW_EXIF_DATA,
showTakenAtTimeHidden: SHOW_TAKEN_AT_TIME,
showSocial: SHOW_SOCIAL,
showFilmSimulations: SHOW_FILM_SIMULATIONS,
showRepoLink: SHOW_REPO_LINK,
// SETTINGS
isGridHomepageEnabled: GRID_HOMEPAGE_ENABLED,
hasDefaultTheme: Boolean(process.env.NEXT_PUBLIC_DEFAULT_THEME),
defaultTheme: DEFAULT_THEME,
arePhotosMatted: MATTE_PHOTOS,
isBlurEnabled: BLUR_ENABLED,
isGeoPrivacyEnabled: GEO_PRIVACY_ENABLED,
isAiTextGenerationEnabled: AI_TEXT_GENERATION_ENABLED,
aiTextAutoGeneratedFields: process.env.AI_TEXT_AUTO_GENERATED_FIELDS
? AI_TEXT_AUTO_GENERATED_FIELDS.length === 0
? ['none']
: AI_TEXT_AUTO_GENERATED_FIELDS
: ['all'],
hasAiTextAutoGeneratedFields:
Boolean(process.env.AI_TEXT_AUTO_GENERATED_FIELDS),
isPriorityOrderEnabled: PRIORITY_ORDER_ENABLED,
isPublicApiEnabled: PUBLIC_API_ENABLED,
arePublicDownloadsEnabled: ALLOW_PUBLIC_DOWNLOADS,
isOgTextBottomAligned: OG_TEXT_BOTTOM_ALIGNMENT,
gridAspectRatio: GRID_ASPECT_RATIO,
hasGridAspectRatio: Boolean(process.env.NEXT_PUBLIC_GRID_ASPECT_RATIO),
gridDensity: HIGH_DENSITY_GRID,
hasGridDensityPreference:
Boolean(process.env.NEXT_PUBLIC_SHOW_LARGE_THUMBNAILS),
arePublicDownloadsEnabled: ALLOW_PUBLIC_DOWNLOADS,
isPublicApiEnabled: PUBLIC_API_ENABLED,
isPriorityOrderEnabled: PRIORITY_ORDER_ENABLED,
isOgTextBottomAligned: OG_TEXT_BOTTOM_ALIGNMENT,
// MISC
baseUrl: BASE_URL,
commitSha: VERCEL_COMMIT_SHA ? VERCEL_COMMIT_SHA.slice(0, 7) : undefined,
commitMessage: VERCEL_COMMIT_MESSAGE,

View File

@ -2,56 +2,78 @@ import { parseISO, parse, format } from 'date-fns';
import { formatInTimeZone } from 'date-fns-tz';
import { Timezone } from './timezone';
const DATE_STRING_FORMAT_TINY = 'dd MMM yy';
const DATE_STRING_FORMAT_TINY_PLACEHOLDER = '00 000 00';
const DATE_STRING_FORMAT_TINY = 'dd MMM yy';
const DATE_STRING_FORMAT_TINY_PLACEHOLDER = '00 000 00';
const DATE_STRING_FORMAT_SHORT = 'dd MMM yyyy';
const DATE_STRING_FORMAT_SHORT_PLACEHOLDER = '00 000 0000';
const DATE_STRING_FORMAT_SHORT = 'dd MMM yyyy';
const DATE_STRING_FORMAT_SHORT_PLACEHOLDER = '00 000 0000';
const DATE_STRING_FORMAT_MEDIUM = 'dd MMM yy h:mma';
const DATE_STRING_FORMAT_MEDIUM_PLACEHOLDER = '00 000 00 00:0000';
const DATE_STRING_FORMAT_MEDIUM = 'dd MMM yy h:mma';
const DATE_STRING_FORMAT_MEDIUM_PLACEHOLDER = '00 000 00 00:0000';
const DATE_STRING_FORMAT_LONG = 'dd MMM yyyy h:mma';
const DATE_STRING_FORMAT_LONG_PLACEHOLDER = '00 000 0000 00:0000';
const DATE_STRING_FORMAT_LONG = 'dd MMM yyyy h:mma';
const DATE_STRING_FORMAT_LONG_PLACEHOLDER = '00 000 0000 00:0000';
const DATE_STRING_FORMAT_POSTGRES = 'yyyy-MM-dd HH:mm:ss';
const DATE_STRING_FORMAT_POSTGRES = 'yyyy-MM-dd HH:mm:ss';
export const VALIDATION_EXAMPLE_POSTGRES = '2025-01-03T21:00:44.000Z';
export const VALIDATION_EXAMPLE_POSTGRES_NAIVE = '2025-01-03 16:00:44';
type AmbiguousTimestamp = number | string;
type Length = 'tiny' | 'short' | 'medium' | 'long';
export const formatDate = (
export const formatDate = ({
date,
length = 'long',
timezone,
hideTime,
showPlaceholder,
}: {
date: Date,
length: Length = 'long',
length?: Length,
timezone?: Timezone,
hideTime?: boolean,
showPlaceholder?: boolean,
) => {
switch (length) {
case 'tiny': return showPlaceholder
? DATE_STRING_FORMAT_TINY_PLACEHOLDER
: timezone
? formatInTimeZone(date, timezone, DATE_STRING_FORMAT_TINY)
: format(date, DATE_STRING_FORMAT_TINY);
case 'short': return showPlaceholder
? DATE_STRING_FORMAT_SHORT_PLACEHOLDER
: timezone
? formatInTimeZone(date, timezone, DATE_STRING_FORMAT_SHORT)
: format(date, DATE_STRING_FORMAT_SHORT);
case 'medium': return showPlaceholder
? DATE_STRING_FORMAT_MEDIUM_PLACEHOLDER
: timezone
? formatInTimeZone(date, timezone, DATE_STRING_FORMAT_MEDIUM)
: format(date, DATE_STRING_FORMAT_MEDIUM);
default: return showPlaceholder
}) => {
let formatString = !hideTime
? DATE_STRING_FORMAT_LONG
: DATE_STRING_FORMAT_SHORT;
let placeholderString = !hideTime
? DATE_STRING_FORMAT_LONG_PLACEHOLDER
: timezone
? formatInTimeZone(date, timezone, DATE_STRING_FORMAT_LONG)
: format(date, DATE_STRING_FORMAT_LONG);
: DATE_STRING_FORMAT_SHORT_PLACEHOLDER;
switch (length) {
case 'tiny':
formatString = DATE_STRING_FORMAT_TINY;
placeholderString = DATE_STRING_FORMAT_TINY_PLACEHOLDER;
break;
case 'short':
formatString = DATE_STRING_FORMAT_SHORT;
placeholderString = DATE_STRING_FORMAT_SHORT_PLACEHOLDER;
break;
case 'medium':
formatString = !hideTime
? DATE_STRING_FORMAT_MEDIUM
: DATE_STRING_FORMAT_SHORT;
placeholderString = !hideTime
? DATE_STRING_FORMAT_MEDIUM_PLACEHOLDER
: DATE_STRING_FORMAT_SHORT_PLACEHOLDER;
break;
}
return showPlaceholder
? placeholderString
: timezone
? formatInTimeZone(date, timezone, formatString)
: format(date, formatString);
};
export const formatDateFromPostgresString = (date: string, length?: Length) =>
formatDate(parse(date, DATE_STRING_FORMAT_POSTGRES, new Date()), length);
formatDate({
date: parse(date, DATE_STRING_FORMAT_POSTGRES, new Date()),
length,
});
export const formatDateForPostgres = (date: Date) =>
date.toISOString().replace(
@ -103,3 +125,23 @@ export const generateLocalPostgresString = () =>
export const generateLocalNaivePostgresString = () =>
format(new Date(), DATE_STRING_FORMAT_POSTGRES);
// Form validation to prevent Postgres runtime errors
// POSTGRES: 2025-01-03T21:00:44.000Z
export const validatePostgresDateString = (date = ''): boolean =>
/^(\d{4}-\d{2}-\d{2})T\d{2}:\d{2}:\d{2}(.[\d]+)*Z$/.test(date);
export const validationMessagePostgresDateString = (date = '') =>
validatePostgresDateString(date)
? undefined
: `Invalid format (${VALIDATION_EXAMPLE_POSTGRES})`;
// NAIVE: 2025-01-03 16:00:44
export const validateNaivePostgresDateString = (date = ''): boolean =>
/^(\d{4}-\d{2}-\d{2}) \d{2}:\d{2}:\d{2}$/.test(date);
export const validationMessageNaivePostgresDateString = (date = '') =>
validateNaivePostgresDateString(date)
? undefined
: `Invalid format (${VALIDATION_EXAMPLE_POSTGRES_NAIVE})`;