Merge branch 'main' into static
This commit is contained in:
commit
351b68f3e5
@ -1,14 +1,30 @@
|
||||
import { Camera, formatCameraText } from '@/camera';
|
||||
|
||||
const APPLE : Camera = { make: 'Apple', model: 'iPhone 11 Pro' };
|
||||
const APPLE_01 : Camera = { make: 'Apple', model: 'iPhone 11' };
|
||||
const APPLE_02 : Camera = { make: 'Apple', model: 'iPhone 15 Pro Max' };
|
||||
const FUJIFILM : Camera = { make: 'Fujifilm', model: 'X-T5' };
|
||||
const CANON : Camera = { make: 'Canon', model: 'Canon EOS 800D' };
|
||||
const NIKON : Camera = {
|
||||
make: 'Nikon Corporation',
|
||||
model: 'Nikon D7000',
|
||||
};
|
||||
|
||||
describe('Camera', () => {
|
||||
it('labels correctly', () => {
|
||||
const apple: Camera = { make: 'Apple', model: 'iPhone 11 Pro' };
|
||||
expect(formatCameraText(apple, true)).toBe('Apple iPhone 11 Pro');
|
||||
expect(formatCameraText(apple, false)).toBe('iPhone 11 Pro');
|
||||
const fujifilm: Camera = { make: 'Fujifilm', model: 'X-T5' };
|
||||
expect(formatCameraText(fujifilm)).toBe('Fujifilm X-T5');
|
||||
const canon: Camera = { make: 'Canon', model: 'Canon EOS 800D' };
|
||||
expect(formatCameraText(canon)).toBe('Canon EOS 800D');
|
||||
it('labels full text correctly', () => {
|
||||
expect(formatCameraText(APPLE)).toBe('iPhone 11 Pro');
|
||||
expect(formatCameraText(APPLE, 'always')).toBe('Apple iPhone 11 Pro');
|
||||
expect(formatCameraText(APPLE, 'if-not-apple')).toBe('iPhone 11 Pro');
|
||||
expect(formatCameraText(APPLE, 'never')).toBe('iPhone 11 Pro');
|
||||
expect(formatCameraText(FUJIFILM)).toBe('Fujifilm X-T5');
|
||||
expect(formatCameraText(CANON)).toBe('Canon EOS 800D');
|
||||
expect(formatCameraText(NIKON)).toBe('Nikon D7000');
|
||||
});
|
||||
it('labels models correctly', () => {
|
||||
expect(formatCameraText(APPLE, 'never')).toBe('iPhone 11 Pro');
|
||||
expect(formatCameraText(APPLE, 'never', true)).toBe('11 Pro');
|
||||
expect(formatCameraText(APPLE_01, 'never', true)).toBe('iPhone 11');
|
||||
expect(formatCameraText(APPLE_02, 'never', true)).toBe('15 Pro Max');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -18,12 +18,12 @@ import {
|
||||
isPathTagPhotoShare,
|
||||
isPathTagShare,
|
||||
} from '@/site/paths';
|
||||
import { getCameraFromKey } from '@/camera';
|
||||
|
||||
const PHOTO_ID = 'UsKSGcbt';
|
||||
const TAG = 'tag-name';
|
||||
const CAMERA = 'fujifilm-x-t1';
|
||||
const CAMERA_OBJECT = getCameraFromKey(CAMERA);
|
||||
const CAMERA_MAKE = 'fujifilm';
|
||||
const CAMERA_MODEL = 'x-t1';
|
||||
const CAMERA_OBJECT = { make: CAMERA_MAKE, model: CAMERA_MODEL };
|
||||
const FILM_SIMULATION = 'acros';
|
||||
const SHARE = 'share';
|
||||
|
||||
@ -39,7 +39,7 @@ const PATH_TAG_SHARE = `${PATH_TAG}/${SHARE}`;
|
||||
const PATH_TAG_PHOTO = `${PATH_TAG}/${PHOTO_ID}`;
|
||||
const PATH_TAG_PHOTO_SHARE = `${PATH_TAG_PHOTO}/${SHARE}`;
|
||||
|
||||
const PATH_CAMERA = `/shot-on/${CAMERA}`;
|
||||
const PATH_CAMERA = `/shot-on/${CAMERA_MAKE}/${CAMERA_MODEL}`;
|
||||
const PATH_CAMERA_SHARE = `${PATH_CAMERA}/${SHARE}`;
|
||||
const PATH_CAMERA_PHOTO = `${PATH_CAMERA}/${PHOTO_ID}`;
|
||||
const PATH_CAMERA_PHOTO_SHARE = `${PATH_CAMERA_PHOTO}/${SHARE}`;
|
||||
|
||||
@ -42,7 +42,7 @@
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"nanoid": "^5.0.6",
|
||||
"next": "14.2.0-canary.39",
|
||||
"next": "14.2.0-canary.49",
|
||||
"next-auth": "5.0.0-beta.15",
|
||||
"next-themes": "^0.3.0",
|
||||
"openai": "^4.29.2",
|
||||
|
||||
108
pnpm-lock.yaml
generated
108
pnpm-lock.yaml
generated
@ -52,7 +52,7 @@ dependencies:
|
||||
version: 1.0.1
|
||||
'@vercel/analytics':
|
||||
specifier: ^1.2.2
|
||||
version: 1.2.2(next@14.2.0-canary.39)(react@18.2.0)
|
||||
version: 1.2.2(next@14.2.0-canary.49)(react@18.2.0)
|
||||
'@vercel/blob':
|
||||
specifier: ^0.22.1
|
||||
version: 0.22.1
|
||||
@ -64,7 +64,7 @@ dependencies:
|
||||
version: 0.7.2
|
||||
'@vercel/speed-insights':
|
||||
specifier: ^1.0.10
|
||||
version: 1.0.10(next@14.2.0-canary.39)(react@18.2.0)(svelte@4.2.12)(vue@3.4.21)
|
||||
version: 1.0.10(next@14.2.0-canary.49)(react@18.2.0)(svelte@4.2.12)(vue@3.4.21)
|
||||
ai:
|
||||
specifier: ^3.0.13
|
||||
version: 3.0.13(react@18.2.0)(solid-js@1.8.15)(svelte@4.2.12)(vue@3.4.21)(zod@3.22.4)
|
||||
@ -105,11 +105,11 @@ dependencies:
|
||||
specifier: ^5.0.6
|
||||
version: 5.0.6
|
||||
next:
|
||||
specifier: 14.2.0-canary.39
|
||||
version: 14.2.0-canary.39(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0)
|
||||
specifier: 14.2.0-canary.49
|
||||
version: 14.2.0-canary.49(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0)
|
||||
next-auth:
|
||||
specifier: 5.0.0-beta.15
|
||||
version: 5.0.0-beta.15(next@14.2.0-canary.39)(react@18.2.0)
|
||||
version: 5.0.0-beta.15(next@14.2.0-canary.49)(react@18.2.0)
|
||||
next-themes:
|
||||
specifier: ^0.3.0
|
||||
version: 0.3.0(react-dom@18.2.0)(react@18.2.0)
|
||||
@ -1563,8 +1563,8 @@ packages:
|
||||
- utf-8-validate
|
||||
dev: false
|
||||
|
||||
/@next/env@14.2.0-canary.39:
|
||||
resolution: {integrity: sha512-ROeqwq9mybhzfdzNDbz9/0e3fFB6gtC25NZNC/rhZzvgkTvUuYXUbJOJSvvtsoUjQolTCFOhZqKmopX+QgwYwQ==}
|
||||
/@next/env@14.2.0-canary.49:
|
||||
resolution: {integrity: sha512-rQaBRv0PRO3+4lx90zB9eBL0xk230G+6avgCyBL272hckH4XsGgXY6adtBBmZJF1QuDI+pS+DqppXSJvfexsdw==}
|
||||
dev: false
|
||||
|
||||
/@next/eslint-plugin-next@14.1.4:
|
||||
@ -1573,8 +1573,8 @@ packages:
|
||||
glob: 10.3.10
|
||||
dev: false
|
||||
|
||||
/@next/swc-darwin-arm64@14.2.0-canary.39:
|
||||
resolution: {integrity: sha512-ImAEFQBac/jYFCQYAEOxLZlzZfoa0GnbmXlGruzyNXl7RG3gJ3OBXx6G/puySAdytp54tArmr+0h+xoEXbop2Q==}
|
||||
/@next/swc-darwin-arm64@14.2.0-canary.49:
|
||||
resolution: {integrity: sha512-tFFCgRJOk28rIiEGjz2bafqp3G5lV7hXyYjZ7d+gt/MjpLRrtTwu+lRBv/W1VFdTkPv8+k2hvXZNNTHO1n57Ow==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
@ -1582,8 +1582,8 @@ packages:
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@next/swc-darwin-x64@14.2.0-canary.39:
|
||||
resolution: {integrity: sha512-2q0F3L261vYPOrn7KXLX5SzfMe8yPRs0plnExpV2MwQjikt5OhlUdGwRRyEFT0DS0S0cyaKw00nENxBuDi7VyA==}
|
||||
/@next/swc-darwin-x64@14.2.0-canary.49:
|
||||
resolution: {integrity: sha512-NR4Meb67q8M2pNP5a8Tp3Zfar2Ao8ChHWcD3wEBgICcgJ4ZyCQCWXdM+VBsf8a3yuAoXmu1/cwOwWu1KXVC96A==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
@ -1591,8 +1591,8 @@ packages:
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@next/swc-linux-arm64-gnu@14.2.0-canary.39:
|
||||
resolution: {integrity: sha512-efraDAfAjQosfUdW8ZMjnrH3/mveQQxs055BdGfh+L0+hlTf05ECUH07tg3AKqihhnk+sgJUqigR5ZSsUYrqsw==}
|
||||
/@next/swc-linux-arm64-gnu@14.2.0-canary.49:
|
||||
resolution: {integrity: sha512-2bFQUNYnz6L7xOAzvejMj09iqmWwkjFyguGEfmNiFN0kPgJ4viSCKZvoiuG/MPh3VoDSz5N2qx1tehSCy7KbFA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
@ -1600,8 +1600,8 @@ packages:
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@next/swc-linux-arm64-musl@14.2.0-canary.39:
|
||||
resolution: {integrity: sha512-Eb6+d3XkhwaEd69OoTOa4/scqQJtCUiZrmWjR0sVbW3QJ0wWu2o5gz8mInYsLeLwxN+HDy1aDuQSl3hp2PwBbQ==}
|
||||
/@next/swc-linux-arm64-musl@14.2.0-canary.49:
|
||||
resolution: {integrity: sha512-68PjCGC1JghA2tuznu+ExeSP+L6qpf6afblB4wFhDRniP+0hRrZB+1E3jJ3PmBgHtitJJMaplTFeKYQ8xbF8xw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
@ -1609,8 +1609,8 @@ packages:
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@next/swc-linux-x64-gnu@14.2.0-canary.39:
|
||||
resolution: {integrity: sha512-HKkx1WCMsycDFOp76avVMCIGm/E0jw3yugfyIc/g1vRIh6fTOZ9iyLd1Uannu4MorTxGWS4g1ZRr1C5/9Ve8kg==}
|
||||
/@next/swc-linux-x64-gnu@14.2.0-canary.49:
|
||||
resolution: {integrity: sha512-eiDvo0bnYCI59UhaZrNV1k7wZPFHyQ2uJ7/MUH9yvZZcSKBxRDtNc3FmCAZjKiNx/SclMFRAtENLOlDzceRp5g==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
@ -1618,8 +1618,8 @@ packages:
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@next/swc-linux-x64-musl@14.2.0-canary.39:
|
||||
resolution: {integrity: sha512-9W/UTFugvG0fYhNK5IqahiwldH3JSXmF2iCzQMbGMpyhjvOn1UirEZPwkMXz6tdSGXVHwxvvsuhZhZgBIt8csw==}
|
||||
/@next/swc-linux-x64-musl@14.2.0-canary.49:
|
||||
resolution: {integrity: sha512-XgwiLB/WkRjuhWoKZmlRsZl1b8C7dsYlRD3zqHPkrgWhERyyn3AoeRjIa/eHR6nxj7oTu2KHET1oSJoYobH70g==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
@ -1627,8 +1627,8 @@ packages:
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@next/swc-win32-arm64-msvc@14.2.0-canary.39:
|
||||
resolution: {integrity: sha512-rtG2wYP3Sa67F2AqaX2qISefZbc/KN0fj5gPx3ReFIuK8/p6tR/L063xvyNmBZs22DZuc07EaFCQ9Px7EB0C2Q==}
|
||||
/@next/swc-win32-arm64-msvc@14.2.0-canary.49:
|
||||
resolution: {integrity: sha512-jqC5vhFOAewsGdWriuQqR2aalQ8dHJ1WkSl1psluTxpo5UgICBk+H0wQ93a0CEfD0Rj+8QjUFh+U1oYTqE4YIg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
@ -1636,8 +1636,8 @@ packages:
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@next/swc-win32-ia32-msvc@14.2.0-canary.39:
|
||||
resolution: {integrity: sha512-Qh3vNCQQqghFuX4XhKuBhlleaRNIVFTspFMMKdQKFoATVVZh5n/PEeGEIgwjZjsjwfLPI82fkIvxhZkPujcAgg==}
|
||||
/@next/swc-win32-ia32-msvc@14.2.0-canary.49:
|
||||
resolution: {integrity: sha512-Zcfe1+FuFtMCtG0L7F9yh0yRhmLM2gGAUHW41FYN+Rtbi/JFS8qhs/M7pOPkqhEWWKqo3at64q7z8KQh+21VsQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
@ -1645,8 +1645,8 @@ packages:
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@next/swc-win32-x64-msvc@14.2.0-canary.39:
|
||||
resolution: {integrity: sha512-CPFzgcPYamtJpHtrHr55LsZ9g95l9vnm85OckaDQCK+359z4sgWk5Jp2ortPN/ZorDk+KjiixrE8x1Ix07Mk9g==}
|
||||
/@next/swc-win32-x64-msvc@14.2.0-canary.49:
|
||||
resolution: {integrity: sha512-yeCjnmqMmI9aNbRk3DTrKvCuImUWXU+Kl0XC9KFo8iLpOztpCQrMA+pf5s3GRqv1HRzbRoHsj+1VCPXzTmZrLA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
@ -3147,7 +3147,7 @@ packages:
|
||||
crypto-js: 4.2.0
|
||||
dev: false
|
||||
|
||||
/@vercel/analytics@1.2.2(next@14.2.0-canary.39)(react@18.2.0):
|
||||
/@vercel/analytics@1.2.2(next@14.2.0-canary.49)(react@18.2.0):
|
||||
resolution: {integrity: sha512-X0rctVWkQV1e5Y300ehVNqpOfSOufo7ieA5PIdna8yX/U7Vjz0GFsGf4qvAhxV02uQ2CVt7GYcrFfddXXK2Y4A==}
|
||||
peerDependencies:
|
||||
next: '>= 13'
|
||||
@ -3158,7 +3158,7 @@ packages:
|
||||
react:
|
||||
optional: true
|
||||
dependencies:
|
||||
next: 14.2.0-canary.39(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0)
|
||||
next: 14.2.0-canary.49(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0)
|
||||
react: 18.2.0
|
||||
server-only: 0.0.1
|
||||
dev: false
|
||||
@ -3190,7 +3190,7 @@ packages:
|
||||
ws: 8.14.2(bufferutil@4.0.8)(utf-8-validate@6.0.3)
|
||||
dev: false
|
||||
|
||||
/@vercel/speed-insights@1.0.10(next@14.2.0-canary.39)(react@18.2.0)(svelte@4.2.12)(vue@3.4.21):
|
||||
/@vercel/speed-insights@1.0.10(next@14.2.0-canary.49)(react@18.2.0)(svelte@4.2.12)(vue@3.4.21):
|
||||
resolution: {integrity: sha512-4uzdKB0RW6Ff2FkzshzjZ+RlJfLPxgm/00i0XXgxfMPhwnnsk92YgtqsxT9OcPLdJUyVU1DqFlSWWjIQMPkh0g==}
|
||||
requiresBuild: true
|
||||
peerDependencies:
|
||||
@ -3214,7 +3214,7 @@ packages:
|
||||
vue-router:
|
||||
optional: true
|
||||
dependencies:
|
||||
next: 14.2.0-canary.39(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0)
|
||||
next: 14.2.0-canary.49(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0)
|
||||
react: 18.2.0
|
||||
svelte: 4.2.12
|
||||
vue: 3.4.21(typescript@5.4.3)
|
||||
@ -3227,7 +3227,7 @@ packages:
|
||||
'@vue/shared': 3.4.21
|
||||
entities: 4.5.0
|
||||
estree-walker: 2.0.2
|
||||
source-map-js: 1.0.2
|
||||
source-map-js: 1.2.0
|
||||
dev: false
|
||||
|
||||
/@vue/compiler-dom@3.4.21:
|
||||
@ -3248,7 +3248,7 @@ packages:
|
||||
estree-walker: 2.0.2
|
||||
magic-string: 0.30.8
|
||||
postcss: 8.4.38
|
||||
source-map-js: 1.0.2
|
||||
source-map-js: 1.2.0
|
||||
dev: false
|
||||
|
||||
/@vue/compiler-ssr@3.4.21:
|
||||
@ -3822,10 +3822,6 @@ packages:
|
||||
engines: {node: '>=16'}
|
||||
dev: false
|
||||
|
||||
/caniuse-lite@1.0.30001591:
|
||||
resolution: {integrity: sha512-PCzRMei/vXjJyL5mJtzNiUCKP59dm8Apqc3PH8gJkMnMXZGox93RbE76jHsmLwmIo6/3nsYIpJtx0O7u5PqFuQ==}
|
||||
dev: false
|
||||
|
||||
/caniuse-lite@1.0.30001600:
|
||||
resolution: {integrity: sha512-+2S9/2JFhYmYaDpZvo0lKkfvuKIglrx68MwOBqMGHhQsNkLjB5xtc/TGoEPs+MxjSyN/72qer2g97nzR641mOQ==}
|
||||
dev: false
|
||||
@ -4037,7 +4033,7 @@ packages:
|
||||
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
|
||||
dependencies:
|
||||
mdn-data: 2.0.30
|
||||
source-map-js: 1.0.2
|
||||
source-map-js: 1.2.0
|
||||
dev: false
|
||||
|
||||
/css.escape@1.5.1:
|
||||
@ -6381,7 +6377,7 @@ packages:
|
||||
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
||||
dev: false
|
||||
|
||||
/next-auth@5.0.0-beta.15(next@14.2.0-canary.39)(react@18.2.0):
|
||||
/next-auth@5.0.0-beta.15(next@14.2.0-canary.49)(react@18.2.0):
|
||||
resolution: {integrity: sha512-UQggNq8CDu3/w8CYkihKLLnRPNXel98K0j7mtjj9a6XTNYo4Hni8xg/2h1YhElW6vXE8mgtvmH11rU8NKw86jQ==}
|
||||
peerDependencies:
|
||||
'@simplewebauthn/browser': ^9.0.1
|
||||
@ -6398,7 +6394,7 @@ packages:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@auth/core': 0.28.0
|
||||
next: 14.2.0-canary.39(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0)
|
||||
next: 14.2.0-canary.49(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0)
|
||||
react: 18.2.0
|
||||
dev: false
|
||||
|
||||
@ -6412,40 +6408,43 @@ packages:
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
dev: false
|
||||
|
||||
/next@14.2.0-canary.39(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-sTAsUnf7ihBdvN0XwiPKe6kfqxUeEZJaHVOR5RIt2LJ2OnI1mVAp875hjKNxDeOxg2TjpxQCWiEEeKE8IV/tvw==}
|
||||
/next@14.2.0-canary.49(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-sfryWP84xmqUOYAilbiojczTpTGCRTMch3w+EVppzPj0DS6gOWv9vPUHp/6uMWWZ+YX+n3GkYhwRK80Q+FG+kg==}
|
||||
engines: {node: '>=18.17.0'}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@opentelemetry/api': ^1.1.0
|
||||
'@playwright/test': ^1.41.2
|
||||
react: ^18.2.0
|
||||
react-dom: ^18.2.0
|
||||
sass: ^1.3.0
|
||||
peerDependenciesMeta:
|
||||
'@opentelemetry/api':
|
||||
optional: true
|
||||
'@playwright/test':
|
||||
optional: true
|
||||
sass:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@next/env': 14.2.0-canary.39
|
||||
'@next/env': 14.2.0-canary.49
|
||||
'@swc/helpers': 0.5.5
|
||||
busboy: 1.6.0
|
||||
caniuse-lite: 1.0.30001591
|
||||
caniuse-lite: 1.0.30001600
|
||||
graceful-fs: 4.2.11
|
||||
postcss: 8.4.31
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
styled-jsx: 5.1.1(@babel/core@7.23.9)(react@18.2.0)
|
||||
optionalDependencies:
|
||||
'@next/swc-darwin-arm64': 14.2.0-canary.39
|
||||
'@next/swc-darwin-x64': 14.2.0-canary.39
|
||||
'@next/swc-linux-arm64-gnu': 14.2.0-canary.39
|
||||
'@next/swc-linux-arm64-musl': 14.2.0-canary.39
|
||||
'@next/swc-linux-x64-gnu': 14.2.0-canary.39
|
||||
'@next/swc-linux-x64-musl': 14.2.0-canary.39
|
||||
'@next/swc-win32-arm64-msvc': 14.2.0-canary.39
|
||||
'@next/swc-win32-ia32-msvc': 14.2.0-canary.39
|
||||
'@next/swc-win32-x64-msvc': 14.2.0-canary.39
|
||||
'@next/swc-darwin-arm64': 14.2.0-canary.49
|
||||
'@next/swc-darwin-x64': 14.2.0-canary.49
|
||||
'@next/swc-linux-arm64-gnu': 14.2.0-canary.49
|
||||
'@next/swc-linux-arm64-musl': 14.2.0-canary.49
|
||||
'@next/swc-linux-x64-gnu': 14.2.0-canary.49
|
||||
'@next/swc-linux-x64-musl': 14.2.0-canary.49
|
||||
'@next/swc-win32-arm64-msvc': 14.2.0-canary.49
|
||||
'@next/swc-win32-ia32-msvc': 14.2.0-canary.49
|
||||
'@next/swc-win32-x64-msvc': 14.2.0-canary.49
|
||||
transitivePeerDependencies:
|
||||
- '@babel/core'
|
||||
- babel-plugin-macros
|
||||
@ -6843,7 +6842,7 @@ packages:
|
||||
dependencies:
|
||||
nanoid: 3.3.7
|
||||
picocolors: 1.0.0
|
||||
source-map-js: 1.0.2
|
||||
source-map-js: 1.2.0
|
||||
dev: false
|
||||
|
||||
/postcss@8.4.38:
|
||||
@ -7341,11 +7340,6 @@ packages:
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
dev: false
|
||||
|
||||
/source-map-js@1.0.2:
|
||||
resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: false
|
||||
|
||||
/source-map-js@1.2.0:
|
||||
resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
194
src/app/admin/baseline/page.tsx
Normal file
194
src/app/admin/baseline/page.tsx
Normal file
@ -0,0 +1,194 @@
|
||||
'use client';
|
||||
|
||||
import DivDebugBaselineGrid from '@/components/DivDebugBaselineGrid';
|
||||
import FieldSetWithStatus from '@/components/FieldSetWithStatus';
|
||||
import SiteGrid from '@/components/SiteGrid';
|
||||
import EntityLink from '@/components/primitives/EntityLink';
|
||||
import LabeledIcon from '@/components/primitives/LabeledIcon';
|
||||
import PhotoFilmSimulationIcon from '@/simulation/PhotoFilmSimulationIcon';
|
||||
import { useAppState } from '@/state';
|
||||
import { clsx } from 'clsx/lite';
|
||||
import { useState } from 'react';
|
||||
import { FaCamera, FaHandSparkles, FaUserAltSlash } from 'react-icons/fa';
|
||||
import { IoMdCamera } from 'react-icons/io';
|
||||
import { IoImageSharp } from 'react-icons/io5';
|
||||
|
||||
const DEBUG_LINES = new Array(22).fill(null);
|
||||
|
||||
export default function ComponentsPage() {
|
||||
const {
|
||||
shouldShowBaselineGrid,
|
||||
setShouldShowBaselineGrid,
|
||||
} = useAppState();
|
||||
|
||||
const [debugComponents, setDebugComponents] = useState(false);
|
||||
|
||||
return (
|
||||
<SiteGrid
|
||||
contentMain={<>
|
||||
<h1 className="flex mb-6">
|
||||
<div className="grow">
|
||||
<span>Baseline Grid: </span>
|
||||
<span className="text-dim">
|
||||
<span className="md:hidden">13.5px / 19px</span>
|
||||
<span className="hidden md:inline-block">14px / 20px</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className={clsx(
|
||||
'flex gap-1',
|
||||
'[&>*]:inline-flex [&>*]:gap-1 [&_input]:-translate-y-0.5',
|
||||
)}>
|
||||
<FieldSetWithStatus
|
||||
id="grid"
|
||||
label="Grid"
|
||||
type="checkbox"
|
||||
value={shouldShowBaselineGrid ? 'true' : 'false'}
|
||||
onChange={e => setShouldShowBaselineGrid?.(e === 'true')}
|
||||
/>
|
||||
<FieldSetWithStatus
|
||||
id="components"
|
||||
label="Components"
|
||||
type="checkbox"
|
||||
value={debugComponents ? 'true' : 'false'}
|
||||
onChange={e => setDebugComponents(e === 'true')}
|
||||
/>
|
||||
</div>
|
||||
</h1>
|
||||
<DivDebugBaselineGrid className="flex gap-8">
|
||||
<div className="[&>*]:flex">
|
||||
<div>
|
||||
<LabeledIcon
|
||||
icon={<FaCamera size={12} />}
|
||||
debug={debugComponents}
|
||||
>
|
||||
Camera<br />Line two
|
||||
</LabeledIcon>
|
||||
</div>
|
||||
<div>
|
||||
<LabeledIcon icon={<IoImageSharp />} debug={debugComponents}>
|
||||
Image
|
||||
</LabeledIcon>
|
||||
</div>
|
||||
<div>
|
||||
<LabeledIcon icon={<FaUserAltSlash />} debug={debugComponents}>
|
||||
Image
|
||||
</LabeledIcon>
|
||||
</div>
|
||||
<div>
|
||||
<LabeledIcon icon={<FaUserAltSlash />} debug={debugComponents}>
|
||||
Image
|
||||
</LabeledIcon>
|
||||
</div>
|
||||
<div>
|
||||
<LabeledIcon icon={<FaUserAltSlash />} debug={debugComponents}>
|
||||
Image
|
||||
</LabeledIcon>
|
||||
</div>
|
||||
<div>
|
||||
<LabeledIcon icon={<FaUserAltSlash />} debug={debugComponents}>
|
||||
Image
|
||||
</LabeledIcon>
|
||||
</div>
|
||||
<div>
|
||||
<LabeledIcon icon={<FaUserAltSlash />} debug={debugComponents}>
|
||||
Image
|
||||
</LabeledIcon>
|
||||
</div>
|
||||
<div>
|
||||
<LabeledIcon icon={<FaUserAltSlash />} debug={debugComponents}>
|
||||
Image
|
||||
</LabeledIcon>
|
||||
</div>
|
||||
<div>
|
||||
<LabeledIcon icon={<FaUserAltSlash />} debug={debugComponents}>
|
||||
Image
|
||||
</LabeledIcon>
|
||||
</div>
|
||||
<div>
|
||||
<LabeledIcon icon={<FaUserAltSlash />} debug={debugComponents}>
|
||||
Image
|
||||
</LabeledIcon>
|
||||
</div>
|
||||
<div>
|
||||
<LabeledIcon icon={<FaUserAltSlash />} debug={debugComponents}>
|
||||
Image
|
||||
</LabeledIcon>
|
||||
</div>
|
||||
<div>
|
||||
<EntityLink
|
||||
icon={<FaHandSparkles />}
|
||||
label="Image"
|
||||
debug={debugComponents}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<EntityLink
|
||||
icon={<FaHandSparkles />}
|
||||
label="Image"
|
||||
badged
|
||||
debug={debugComponents}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<LabeledIcon
|
||||
icon={<IoMdCamera size={12} />}
|
||||
debug={debugComponents}
|
||||
>
|
||||
Canon Mark III
|
||||
</LabeledIcon>
|
||||
</div>
|
||||
<div>
|
||||
<EntityLink
|
||||
icon={<PhotoFilmSimulationIcon simulation="astia" />}
|
||||
label="Astia/Soft"
|
||||
type="icon-last"
|
||||
iconWide
|
||||
badged
|
||||
debug={debugComponents}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<LabeledIcon icon={<FaUserAltSlash />} debug={debugComponents}>
|
||||
Image
|
||||
</LabeledIcon>
|
||||
</div>
|
||||
<div>
|
||||
<LabeledIcon icon={<FaUserAltSlash />} debug={debugComponents}>
|
||||
Image
|
||||
</LabeledIcon>
|
||||
</div>
|
||||
<div>
|
||||
<EntityLink
|
||||
icon={<PhotoFilmSimulationIcon simulation="astia" />}
|
||||
label="Astia/Soft"
|
||||
type="icon-last"
|
||||
badged
|
||||
debug={debugComponents}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<LabeledIcon icon={<FaUserAltSlash />} debug={debugComponents}>
|
||||
Image
|
||||
</LabeledIcon>
|
||||
</div>
|
||||
<div>
|
||||
<LabeledIcon icon={<FaUserAltSlash />} debug={debugComponents}>
|
||||
Image
|
||||
</LabeledIcon>
|
||||
</div>
|
||||
</div>
|
||||
<div className={clsx(
|
||||
debugComponents && '[&>*]:bg-gray-800',
|
||||
'[&>*]:flex',
|
||||
)}>
|
||||
{DEBUG_LINES.map((_, i) =>
|
||||
<div key={i}>
|
||||
Line {(i + 1).toString().padStart(2, '0')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DivDebugBaselineGrid>
|
||||
</>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -32,6 +32,7 @@ import { getStoragePhotoUrlsNoStore } from '@/services/storage/cache';
|
||||
import MoreComponentsFromSearchParams from
|
||||
'@/components/MoreComponentsFromSearchParams';
|
||||
import { getPhotos } from '@/services/vercel-postgres';
|
||||
import PhotoDate from '@/photo/PhotoDate';
|
||||
|
||||
const DEBUG_PHOTO_BLOBS = false;
|
||||
|
||||
@ -103,7 +104,7 @@ export default async function AdminPhotosPage({
|
||||
'lg:w-[50%] uppercase',
|
||||
'text-dim',
|
||||
)}>
|
||||
{photo.takenAtNaive}
|
||||
<PhotoDate {...{ photo }} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={clsx(
|
||||
|
||||
@ -11,19 +11,15 @@ import {
|
||||
} from '@/site/paths';
|
||||
import PhotoDetailPage from '@/photo/PhotoDetailPage';
|
||||
import { getPhotoCached } from '@/photo/cache';
|
||||
import { cameraFromPhoto } from '@/camera';
|
||||
import { PhotoCameraProps, cameraFromPhoto } from '@/camera';
|
||||
import { getPhotosCameraDataCached } from '@/camera/data';
|
||||
import { ReactNode, cache } from 'react';
|
||||
|
||||
const getPhotoCachedCached =
|
||||
cache((photoId: string) => getPhotoCached(photoId));
|
||||
|
||||
interface PhotoCameraProps {
|
||||
params: { photoId: string, camera: string }
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params: { photoId, camera },
|
||||
params: { photoId, make, model },
|
||||
}: PhotoCameraProps): Promise<Metadata> {
|
||||
const photo = await getPhotoCachedCached(photoId);
|
||||
|
||||
@ -35,7 +31,7 @@ export async function generateMetadata({
|
||||
const url = absolutePathForPhoto(
|
||||
photo,
|
||||
undefined,
|
||||
cameraFromPhoto(photo, camera),
|
||||
cameraFromPhoto(photo, { make, model }),
|
||||
);
|
||||
|
||||
return {
|
||||
@ -57,14 +53,14 @@ export async function generateMetadata({
|
||||
}
|
||||
|
||||
export default async function PhotoCameraPage({
|
||||
params: { photoId, camera: cameraProp },
|
||||
params: { photoId, make, model },
|
||||
children,
|
||||
}: PhotoCameraProps & { children: ReactNode }) {
|
||||
const photo = await getPhotoCachedCached(photoId);
|
||||
|
||||
if (!photo) { redirect(PATH_ROOT); }
|
||||
|
||||
const camera = cameraFromPhoto(photo, cameraProp);
|
||||
const camera = cameraFromPhoto(photo, { make, model });
|
||||
|
||||
const [
|
||||
photos,
|
||||
@ -1,19 +1,17 @@
|
||||
import { getPhotoCached } from '@/photo/cache';
|
||||
import { cameraFromPhoto } from '@/camera';
|
||||
import { PhotoCameraProps, cameraFromPhoto } from '@/camera';
|
||||
import PhotoShareModal from '@/photo/PhotoShareModal';
|
||||
import { PATH_ROOT } from '@/site/paths';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
export default async function Share({
|
||||
params: { photoId, camera: cameraProp },
|
||||
}: {
|
||||
params: { photoId: string, camera: string }
|
||||
}) {
|
||||
params: { photoId, make, model },
|
||||
}: PhotoCameraProps) {
|
||||
const photo = await getPhotoCached(photoId);
|
||||
|
||||
if (!photo) { return redirect(PATH_ROOT); }
|
||||
|
||||
const camera = cameraFromPhoto(photo, cameraProp);
|
||||
const camera = cameraFromPhoto(photo, { make, model });
|
||||
|
||||
return <PhotoShareModal {...{ photo, camera }} />;
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
import { getPhotosCached } from '@/photo/cache';
|
||||
import { getCameraFromKey } from '@/camera';
|
||||
import { CameraProps, getCameraFromParams } from '@/camera';
|
||||
import {
|
||||
IMAGE_OG_DIMENSION_SMALL,
|
||||
MAX_PHOTOS_TO_SHOW_PER_TAG,
|
||||
@ -13,9 +13,9 @@ export const runtime = 'edge';
|
||||
|
||||
export async function GET(
|
||||
_: Request,
|
||||
context: { params: { camera: string } },
|
||||
context: CameraProps,
|
||||
) {
|
||||
const camera = getCameraFromKey(context.params.camera);
|
||||
const camera = getCameraFromParams(context.params);
|
||||
|
||||
const [
|
||||
photos,
|
||||
@ -1,5 +1,5 @@
|
||||
import { getCameraFromKey } from '@/camera';
|
||||
import { Metadata } from 'next/types';
|
||||
import { CameraProps, getCameraFromParams } from '@/camera';
|
||||
import { generateMetaForCamera } from '@/camera/meta';
|
||||
import { GRID_THUMBNAILS_TO_SHOW_MAX } from '@/photo';
|
||||
import { PaginationParams } from '@/site/pagination';
|
||||
@ -9,14 +9,10 @@ import {
|
||||
} from '@/camera/data';
|
||||
import CameraOverview from '@/camera/CameraOverview';
|
||||
|
||||
interface CameraProps {
|
||||
params: { camera: string },
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: CameraProps): Promise<Metadata> {
|
||||
const camera = getCameraFromKey(params.camera);
|
||||
const camera = getCameraFromParams(params);
|
||||
|
||||
const [
|
||||
photos,
|
||||
@ -54,7 +50,7 @@ export default async function CameraPage({
|
||||
params,
|
||||
searchParams,
|
||||
}: CameraProps & PaginationParams) {
|
||||
const camera = getCameraFromKey(params.camera);
|
||||
const camera = getCameraFromParams(params);
|
||||
|
||||
const {
|
||||
photos,
|
||||
@ -1,4 +1,8 @@
|
||||
import { cameraFromPhoto, getCameraFromKey } from '@/camera';
|
||||
import {
|
||||
CameraProps,
|
||||
cameraFromPhoto,
|
||||
getCameraFromParams,
|
||||
} from '@/camera';
|
||||
import CameraShareModal from '@/camera/CameraShareModal';
|
||||
import { generateMetaForCamera } from '@/camera/meta';
|
||||
import { Metadata } from 'next/types';
|
||||
@ -10,14 +14,10 @@ import {
|
||||
} from '@/camera/data';
|
||||
import CameraOverview from '@/camera/CameraOverview';
|
||||
|
||||
interface CameraProps {
|
||||
params: { camera: string }
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: CameraProps): Promise<Metadata> {
|
||||
const camera = getCameraFromKey(params.camera);
|
||||
const camera = getCameraFromParams(params);
|
||||
|
||||
const [
|
||||
photos,
|
||||
@ -55,7 +55,7 @@ export default async function Share({
|
||||
params,
|
||||
searchParams,
|
||||
}: CameraProps & PaginationParams) {
|
||||
const cameraFromParams = getCameraFromKey(params.camera);
|
||||
const cameraFromParams = getCameraFromParams(params);
|
||||
|
||||
const {
|
||||
photos,
|
||||
@ -21,7 +21,7 @@ export default function CameraHeader({
|
||||
const camera = cameraFromPhoto(photos[0], cameraProp);
|
||||
return (
|
||||
<PhotoSetHeader
|
||||
entity={<PhotoCamera {...{ camera }} hideAppleIcon />}
|
||||
entity={<PhotoCamera {...{ camera }} contrast="high" hideAppleIcon />}
|
||||
entityVerb="Photo"
|
||||
entityDescription={
|
||||
descriptionForCameraPhotos(photos, undefined, count, dateRange)}
|
||||
|
||||
@ -2,7 +2,9 @@ import { AiFillApple } from 'react-icons/ai';
|
||||
import { pathForCamera } from '@/site/paths';
|
||||
import { IoMdCamera } from 'react-icons/io';
|
||||
import { Camera, formatCameraText } from '.';
|
||||
import EntityLink, { EntityLinkExternalProps } from '@/components/EntityLink';
|
||||
import EntityLink, {
|
||||
EntityLinkExternalProps,
|
||||
} from '@/components/primitives/EntityLink';
|
||||
|
||||
export default function PhotoCamera({
|
||||
camera,
|
||||
@ -27,12 +29,12 @@ export default function PhotoCamera({
|
||||
icon={showAppleIcon
|
||||
? <AiFillApple
|
||||
title="Apple"
|
||||
className="translate-x-[-2.5px] translate-y-[2px]"
|
||||
className="translate-x-[-0.5px]"
|
||||
size={15}
|
||||
/>
|
||||
: <IoMdCamera
|
||||
size={12}
|
||||
className="translate-x-[-1px] translate-y-[3.5px]"
|
||||
className="translate-x-[-1px]"
|
||||
/>}
|
||||
type={showAppleIcon && isCameraApple ? 'icon-first' : type}
|
||||
badged={badged}
|
||||
|
||||
@ -8,6 +8,14 @@ export type Camera = {
|
||||
model: string
|
||||
};
|
||||
|
||||
export interface CameraProps {
|
||||
params: Camera
|
||||
}
|
||||
|
||||
export interface PhotoCameraProps {
|
||||
params: Camera & { photoId: string }
|
||||
}
|
||||
|
||||
export type CameraWithCount = {
|
||||
cameraKey: string
|
||||
camera: Camera
|
||||
@ -19,11 +27,16 @@ export type Cameras = CameraWithCount[];
|
||||
export const createCameraKey = ({ make, model }: Camera) =>
|
||||
parameterize(`${make}-${model}`, true);
|
||||
|
||||
// Assumes no makes ('Fujifilm,' 'Apple,' 'Canon', etc.) have dashes
|
||||
export const getCameraFromKey = (cameraKey: string): Camera => {
|
||||
const [make, model] = cameraKey.toLowerCase().split(/[-| ](.*)/s);
|
||||
return { make, model };
|
||||
};
|
||||
export const getCameraFromParams = ({
|
||||
make,
|
||||
model,
|
||||
}: {
|
||||
make: string,
|
||||
model: string,
|
||||
}): Camera => ({
|
||||
make: parameterize(make, true),
|
||||
model: parameterize(model, true),
|
||||
});
|
||||
|
||||
export const sortCamerasWithCount = (
|
||||
a: CameraWithCount,
|
||||
@ -36,36 +49,34 @@ export const sortCamerasWithCount = (
|
||||
|
||||
export const cameraFromPhoto = (
|
||||
photo: Photo | undefined,
|
||||
fallback?: Camera | string,
|
||||
fallback?: Camera,
|
||||
): Camera =>
|
||||
photo?.make && photo?.model
|
||||
? { make: photo.make, model: photo.model }
|
||||
: typeof fallback === 'string'
|
||||
? getCameraFromKey(fallback)
|
||||
: fallback ?? CAMERA_PLACEHOLDER;
|
||||
|
||||
export const formatCameraText = (
|
||||
{ make, model: modelRaw }: Camera,
|
||||
includeMakeApple?: boolean,
|
||||
{ make: makeRaw, model: modelRaw }: Camera,
|
||||
includeMake: 'always' | 'never' | 'if-not-apple' = 'if-not-apple',
|
||||
removeIPhoneOnLongModels?: boolean
|
||||
) => {
|
||||
// Remove 'Corporation' from makes like 'Nikon Corporation'
|
||||
const make = makeRaw.replace(/ Corporation/i, '');
|
||||
// Remove potential duplicate make from model
|
||||
const model = modelRaw.replace(`${make} `, '');
|
||||
return make === 'Apple' && !includeMakeApple
|
||||
? model
|
||||
let model = modelRaw.replace(`${make} `, '');
|
||||
if (
|
||||
removeIPhoneOnLongModels &&
|
||||
model.includes('iPhone') &&
|
||||
model.length > 9
|
||||
) {
|
||||
model = model.replace(/iPhone\s*/i, '');
|
||||
}
|
||||
return (
|
||||
includeMake === 'never' ||
|
||||
includeMake === 'if-not-apple' && make === 'Apple'
|
||||
) ? model
|
||||
: `${make} ${model}`;
|
||||
};
|
||||
|
||||
export const formatCameraModelText = (
|
||||
{ make, model: modelRaw }: Camera,
|
||||
) => {
|
||||
// Remove potential duplicate make from model
|
||||
const model = modelRaw.replace(`${make} `, '');
|
||||
const textLength = model?.length ?? 0;
|
||||
if (textLength > 0 && textLength <= 8) {
|
||||
return model;
|
||||
} else if (model?.includes('iPhone')) {
|
||||
return model.split('iPhone')[1];
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
export const formatCameraModelTextShort = (camera: Camera) =>
|
||||
formatCameraText(camera, 'never', true);
|
||||
|
||||
@ -27,8 +27,9 @@ export default function Badge({
|
||||
);
|
||||
case 'small':
|
||||
return clsx(
|
||||
'px-[0.3rem] py-1 rounded-[0.25rem]',
|
||||
'text-[0.7rem] font-medium',
|
||||
'h-max-baseline',
|
||||
'px-[5px] py-[2.75px]',
|
||||
'text-[0.7rem] font-medium rounded-[0.25rem]',
|
||||
highContrast
|
||||
? 'text-invert bg-invert'
|
||||
: 'text-medium bg-gray-300/30 dark:bg-gray-700/50',
|
||||
|
||||
@ -19,6 +19,7 @@ import { BiDesktop, BiMoon, BiSun } from 'react-icons/bi';
|
||||
import { IoInvertModeSharp } from 'react-icons/io5';
|
||||
import { useAppState } from '@/state/AppState';
|
||||
import { getPhotoItemsAction } from '@/photo/actions';
|
||||
import { RiToolsFill } from 'react-icons/ri';
|
||||
|
||||
const LISTENER_KEYDOWN = 'keydown';
|
||||
const MINIMUM_QUERY_LENGTH = 2;
|
||||
@ -38,16 +39,19 @@ export type CommandKSection = {
|
||||
}
|
||||
|
||||
export default function CommandKClient({
|
||||
sections = [],
|
||||
serverSections = [],
|
||||
showDebugTools,
|
||||
footer,
|
||||
}: {
|
||||
sections?: CommandKSection[]
|
||||
serverSections?: CommandKSection[]
|
||||
showDebugTools?: boolean
|
||||
footer?: string
|
||||
}) {
|
||||
const {
|
||||
isCommandKOpen: isOpen,
|
||||
setIsCommandKOpen: setIsOpen,
|
||||
setShouldRespondToKeyboardCommands,
|
||||
setShouldShowBaselineGrid,
|
||||
} = useAppState();
|
||||
|
||||
const isOpenRef = useRef(isOpen);
|
||||
@ -131,7 +135,7 @@ export default function CommandKClient({
|
||||
}
|
||||
}, [isOpen, setShouldRespondToKeyboardCommands]);
|
||||
|
||||
const sectionTheme: CommandKSection = {
|
||||
const clientSections: CommandKSection[] = [{
|
||||
heading: 'Theme',
|
||||
accessory: <IoInvertModeSharp
|
||||
size={14}
|
||||
@ -150,7 +154,18 @@ export default function CommandKClient({
|
||||
annotation: <BiMoon className="translate-x-[1px]" />,
|
||||
action: () => setTheme('dark'),
|
||||
}],
|
||||
};
|
||||
}];
|
||||
|
||||
if (showDebugTools) {
|
||||
clientSections.push({
|
||||
heading: 'Debug Tools',
|
||||
accessory: <RiToolsFill size={16} className="translate-x-[-1px]" />,
|
||||
items: [{
|
||||
label: 'Toggle Baseline Grid',
|
||||
action: () => setShouldShowBaselineGrid?.(prev => !prev),
|
||||
}],
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Command.Dialog
|
||||
@ -203,8 +218,8 @@ export default function CommandKClient({
|
||||
{isLoading ? 'Searching ...' : 'No results found'}
|
||||
</Command.Empty>
|
||||
{queriedSections
|
||||
.concat(sections)
|
||||
.concat(sectionTheme)
|
||||
.concat(serverSections)
|
||||
.concat(clientSections)
|
||||
.filter(({ items }) => items.length > 0)
|
||||
.map(({ heading, accessory, items }) =>
|
||||
<Command.Group
|
||||
|
||||
24
src/components/DivDebugBaselineGrid.tsx
Normal file
24
src/components/DivDebugBaselineGrid.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
'use client';
|
||||
|
||||
import { useAppState } from '@/state/AppState';
|
||||
import { clsx } from 'clsx/lite';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
export default function DivDebugBaselineGrid({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children: ReactNode
|
||||
className?: string
|
||||
}) {
|
||||
const { shouldShowBaselineGrid } = useAppState();
|
||||
|
||||
return (
|
||||
<div className={clsx(
|
||||
className,
|
||||
shouldShowBaselineGrid && 'bg-baseline-grid',
|
||||
)}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,100 +0,0 @@
|
||||
import Link from 'next/link';
|
||||
import { ReactNode } from 'react';
|
||||
import Badge from './Badge';
|
||||
import { clsx } from 'clsx/lite';
|
||||
|
||||
export interface EntityLinkExternalProps {
|
||||
type?: 'icon-last' | 'icon-first' | 'icon-only' | 'text-only'
|
||||
badged?: boolean
|
||||
contrast?: 'low' | 'medium' | 'high'
|
||||
prefetch?: boolean
|
||||
}
|
||||
|
||||
export default function EntityLink({
|
||||
label,
|
||||
labelSmall,
|
||||
href,
|
||||
icon,
|
||||
title,
|
||||
type = 'icon-first',
|
||||
badged,
|
||||
prefetch,
|
||||
contrast = 'high',
|
||||
hoverEntity,
|
||||
}: {
|
||||
label: ReactNode
|
||||
labelSmall?: ReactNode
|
||||
href: string
|
||||
icon?: ReactNode
|
||||
title?: string
|
||||
hoverEntity?: ReactNode
|
||||
} & EntityLinkExternalProps) {
|
||||
const renderLabel = () => <>
|
||||
<span className="xs:hidden">
|
||||
{labelSmall ?? label}
|
||||
</span>
|
||||
<span className="hidden xs:inline-block">
|
||||
{label}
|
||||
</span>
|
||||
</>;
|
||||
|
||||
const classForContrast = () => {
|
||||
switch (contrast) {
|
||||
case 'low':
|
||||
return 'text-dim';
|
||||
case 'high':
|
||||
return 'text-main';
|
||||
default:
|
||||
return 'text-medium';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<span className="group inline-flex items-center gap-2 h-5">
|
||||
<Link
|
||||
href={href}
|
||||
title={title}
|
||||
className={clsx(
|
||||
'inline-flex gap-[0.23rem]',
|
||||
!badged && 'text-main hover:text-gray-900 dark:hover:text-gray-100',
|
||||
classForContrast(),
|
||||
)}
|
||||
prefetch={prefetch}
|
||||
>
|
||||
{type !== 'icon-only' && <>
|
||||
{badged
|
||||
? <span className="h-6 inline-flex items-center">
|
||||
<Badge
|
||||
type="small"
|
||||
highContrast={contrast === 'high'}
|
||||
uppercase
|
||||
interactive
|
||||
>
|
||||
{renderLabel()}
|
||||
</Badge>
|
||||
</span>
|
||||
: <span className="uppercase">
|
||||
{renderLabel()}
|
||||
</span>}
|
||||
</>}
|
||||
{icon && type !== 'text-only' &&
|
||||
<span className={clsx(
|
||||
'flex-shrink-0',
|
||||
'inline-flex min-w-[0.9rem]',
|
||||
contrast === 'high'
|
||||
? 'text-icon'
|
||||
: classForContrast(),
|
||||
type === 'icon-first' && 'order-first',
|
||||
badged && 'translate-y-[4px]',
|
||||
hoverEntity !== undefined && 'group-hover:hidden',
|
||||
)}>
|
||||
{icon}
|
||||
</span>}
|
||||
</Link>
|
||||
{hoverEntity !== undefined &&
|
||||
<span className="hidden group-hover:inline">
|
||||
{hoverEntity}
|
||||
</span>}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@ -17,7 +17,7 @@ export default function HeaderList({
|
||||
<AnimateItems
|
||||
className={clsx(
|
||||
className,
|
||||
'space-y-0.5',
|
||||
'space-y-1',
|
||||
)}
|
||||
scaleOffset={0.95}
|
||||
duration={0.5}
|
||||
|
||||
@ -1,11 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { formatDate } from '@/utility/date';
|
||||
|
||||
export default function LocalDate({ date }: { date: Date }) {
|
||||
return (
|
||||
<>
|
||||
{formatDate(date)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -110,9 +110,8 @@ export default function OGTile({
|
||||
/>}
|
||||
</div>
|
||||
<div className={clsx(
|
||||
'md:text-lg',
|
||||
'font-sans leading-tight',
|
||||
'flex flex-col gap-1 p-3',
|
||||
'font-sans leading-none',
|
||||
'bg-gray-50 dark:bg-gray-900/50',
|
||||
'group-active:bg-gray-50 group-active:dark:bg-gray-900/50',
|
||||
'group-hover:bg-gray-100 group-hover:dark:bg-gray-900/70',
|
||||
|
||||
20
src/components/ResponsiveDate.tsx
Normal file
20
src/components/ResponsiveDate.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import { formatDate } from '@/utility/date';
|
||||
|
||||
export default function ResponsiveDate({
|
||||
date,
|
||||
}: {
|
||||
date: Date
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
{/* Mobile */}
|
||||
<span className="inline-block sm:hidden">
|
||||
{formatDate(date, true)}
|
||||
</span>
|
||||
{/* Desktop */}
|
||||
<span className="hidden sm:inline-block">
|
||||
{formatDate(date)}
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -24,7 +24,7 @@ export default function ShareModal({
|
||||
<div className="space-y-3 md:space-y-4 w-full">
|
||||
<div className={clsx(
|
||||
'flex items-center gap-x-3',
|
||||
'text-xl md:text-3xl leading-snug',
|
||||
'text-2xl leading-snug',
|
||||
)}>
|
||||
<TbPhotoShare size={22} className="hidden xs:block" />
|
||||
<div className="flex-grow">
|
||||
|
||||
89
src/components/primitives/EntityLink.tsx
Normal file
89
src/components/primitives/EntityLink.tsx
Normal file
@ -0,0 +1,89 @@
|
||||
import { ReactNode } from 'react';
|
||||
import LabeledIcon, { LabeledIconType } from './LabeledIcon';
|
||||
import Badge from '../Badge';
|
||||
import { clsx } from 'clsx/lite';
|
||||
|
||||
export interface EntityLinkExternalProps {
|
||||
type?: LabeledIconType
|
||||
badged?: boolean
|
||||
contrast?: 'low' | 'medium' | 'high'
|
||||
}
|
||||
|
||||
export default function EntityLink({
|
||||
icon,
|
||||
label,
|
||||
labelSmall,
|
||||
iconWide,
|
||||
type,
|
||||
badged,
|
||||
contrast = 'medium',
|
||||
href,
|
||||
prefetch,
|
||||
title,
|
||||
hoverEntity,
|
||||
debug,
|
||||
}: {
|
||||
icon: ReactNode
|
||||
label: ReactNode
|
||||
labelSmall?: ReactNode
|
||||
iconWide?: boolean
|
||||
href?: string
|
||||
prefetch?: boolean
|
||||
title?: string
|
||||
hoverEntity?: ReactNode
|
||||
debug?: boolean
|
||||
} & EntityLinkExternalProps) {
|
||||
const classForContrast = () => {
|
||||
switch (contrast) {
|
||||
case 'low':
|
||||
return 'text-dim';
|
||||
case 'high':
|
||||
return 'text-main';
|
||||
default:
|
||||
return 'text-medium';
|
||||
}
|
||||
};
|
||||
|
||||
const renderLabel = () => <>
|
||||
<span className="xs:hidden">
|
||||
{labelSmall ?? label}
|
||||
</span>
|
||||
<span className="hidden xs:inline-block">
|
||||
{label}
|
||||
</span>
|
||||
</>;
|
||||
|
||||
return (
|
||||
<span className="group inline-flex gap-2">
|
||||
<LabeledIcon {...{
|
||||
icon,
|
||||
iconWide,
|
||||
href,
|
||||
prefetch,
|
||||
title,
|
||||
type,
|
||||
className: clsx(
|
||||
classForContrast(),
|
||||
href && !badged && 'hover:text-gray-900 dark:hover:text-gray-100',
|
||||
),
|
||||
debug,
|
||||
}}>
|
||||
{badged
|
||||
? <Badge
|
||||
type="small"
|
||||
highContrast={contrast === 'high'}
|
||||
className='translate-y-[-0.5px]'
|
||||
uppercase
|
||||
interactive
|
||||
>
|
||||
{renderLabel()}
|
||||
</Badge>
|
||||
: renderLabel()}
|
||||
</LabeledIcon>
|
||||
{hoverEntity !== undefined &&
|
||||
<span className="hidden group-hover:inline">
|
||||
{hoverEntity}
|
||||
</span>}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
35
src/components/primitives/Icon.tsx
Normal file
35
src/components/primitives/Icon.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { clsx } from 'clsx/lite';
|
||||
import Spinner from '../Spinner';
|
||||
|
||||
export default function Icon({
|
||||
children,
|
||||
className,
|
||||
iconClassName,
|
||||
wide,
|
||||
loading,
|
||||
debug,
|
||||
}: {
|
||||
children: ReactNode
|
||||
className?: string
|
||||
iconClassName?: string
|
||||
wide?: boolean
|
||||
loading?: boolean
|
||||
debug?: boolean,
|
||||
}) {
|
||||
return (
|
||||
<span className={clsx(
|
||||
'h-[18px] md:h-[20px]',
|
||||
wide ? 'w-[28px]' : 'w-[14px]',
|
||||
'inline-flex items-center justify-center',
|
||||
debug && 'bg-gray-700',
|
||||
className,
|
||||
)}>
|
||||
{loading
|
||||
? <Spinner />
|
||||
: <span className={iconClassName}>
|
||||
{children}
|
||||
</span>}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
60
src/components/primitives/LabeledIcon.tsx
Normal file
60
src/components/primitives/LabeledIcon.tsx
Normal file
@ -0,0 +1,60 @@
|
||||
import { ComponentProps, ReactNode } from 'react';
|
||||
import Icon from './Icon';
|
||||
import { clsx } from 'clsx/lite';
|
||||
import Link from 'next/link';
|
||||
|
||||
export type LabeledIconType =
|
||||
'icon-first' |
|
||||
'icon-last' |
|
||||
'icon-only' |
|
||||
'text-only';
|
||||
|
||||
export default function LabeledIcon({
|
||||
icon,
|
||||
type = 'icon-first',
|
||||
className: classNameProp,
|
||||
children,
|
||||
iconWide,
|
||||
href,
|
||||
prefetch,
|
||||
debug,
|
||||
}: {
|
||||
icon?: ReactNode,
|
||||
type?: LabeledIconType,
|
||||
className?: string,
|
||||
children: ReactNode,
|
||||
iconWide?:boolean,
|
||||
debug?: boolean,
|
||||
} & Partial<ComponentProps<typeof Link>>) {
|
||||
const className = clsx(
|
||||
'inline-flex gap-x-1 md:gap-x-1.5',
|
||||
classNameProp,
|
||||
debug && 'border border-green-500 m-[-1px]',
|
||||
);
|
||||
|
||||
const renderContent = () => <>
|
||||
{icon && type !== 'text-only' &&
|
||||
<Icon {...{
|
||||
className: clsx(type === 'icon-last' && 'order-1'),
|
||||
wide: iconWide,
|
||||
debug,
|
||||
}}>
|
||||
{icon}
|
||||
</Icon>}
|
||||
{children && type !== 'icon-only' &&
|
||||
<span className={clsx(
|
||||
'uppercase',
|
||||
debug && 'bg-gray-700'
|
||||
)}>
|
||||
{children}
|
||||
</span>}
|
||||
</>;
|
||||
|
||||
return href
|
||||
? <Link {...{ href, prefetch, className }}>
|
||||
{renderContent()}
|
||||
</Link>
|
||||
: <div {...{ className }}>
|
||||
{renderContent()}
|
||||
</div>;
|
||||
}
|
||||
@ -5,7 +5,7 @@ import ImagePhotoGrid from './components/ImagePhotoGrid';
|
||||
import ImageContainer from './components/ImageContainer';
|
||||
import { OG_TEXT_BOTTOM_ALIGNMENT } from '@/site/config';
|
||||
import { NextImageSize } from '@/services/next-image';
|
||||
import { cameraFromPhoto, formatCameraModelText } from '@/camera';
|
||||
import { cameraFromPhoto, formatCameraModelTextShort } from '@/camera';
|
||||
|
||||
export default function PhotoImageResponse({
|
||||
photo,
|
||||
@ -19,7 +19,7 @@ export default function PhotoImageResponse({
|
||||
fontFamily: string
|
||||
}) {
|
||||
const model = photo.model
|
||||
? formatCameraModelText(cameraFromPhoto(photo))
|
||||
? formatCameraModelTextShort(cameraFromPhoto(photo))
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
|
||||
17
src/photo/PhotoDate.tsx
Normal file
17
src/photo/PhotoDate.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import ResponsiveDate from '@/components/ResponsiveDate';
|
||||
import { Photo } from '.';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export default function PhotoDate({
|
||||
photo: { takenAtNaive },
|
||||
}: {
|
||||
photo: Photo
|
||||
}) {
|
||||
const date = useMemo(() => {
|
||||
const date = new Date(takenAtNaive);
|
||||
return isNaN(date.getTime()) ? new Date() : date;
|
||||
}, [takenAtNaive]);
|
||||
return (
|
||||
<ResponsiveDate {...{ date }} />
|
||||
);
|
||||
}
|
||||
@ -18,6 +18,7 @@ import PhotoFilmSimulation from '@/simulation/PhotoFilmSimulation';
|
||||
import { sortTags } from '@/tag';
|
||||
import AdminPhotoMenu from '@/admin/AdminPhotoMenu';
|
||||
import { Suspense } from 'react';
|
||||
import DivDebugBaselineGrid from '@/components/DivDebugBaselineGrid';
|
||||
|
||||
export default function PhotoLarge({
|
||||
photo,
|
||||
@ -70,12 +71,11 @@ export default function PhotoLarge({
|
||||
/>
|
||||
</Link>}
|
||||
contentSide={
|
||||
<div className={clsx(
|
||||
<DivDebugBaselineGrid className={clsx(
|
||||
'relative',
|
||||
'leading-snug',
|
||||
'sticky top-4 self-start -translate-y-1',
|
||||
'grid grid-cols-2 md:grid-cols-1',
|
||||
'gap-x-0.5 sm:gap-x-1 gap-y-4',
|
||||
'gap-x-0.5 sm:gap-x-1 gap-y-baseline',
|
||||
'pb-6',
|
||||
)}>
|
||||
{/* Meta */}
|
||||
@ -95,7 +95,7 @@ export default function PhotoLarge({
|
||||
</div>
|
||||
</Suspense>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-baseline">
|
||||
{photo.caption &&
|
||||
<div className="uppercase">
|
||||
{photo.caption}
|
||||
@ -113,7 +113,7 @@ export default function PhotoLarge({
|
||||
</div>
|
||||
</div>
|
||||
{/* EXIF Data */}
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-baseline">
|
||||
{showExifContent &&
|
||||
<>
|
||||
<ul className="text-medium">
|
||||
@ -141,8 +141,8 @@ export default function PhotoLarge({
|
||||
/>}
|
||||
</>}
|
||||
<div className={clsx(
|
||||
'flex gap-2',
|
||||
'md:flex-col md:gap-4 md:justify-normal',
|
||||
'flex gap-x-1.5 gap-y-baseline',
|
||||
'md:flex-col md:justify-normal',
|
||||
)}>
|
||||
<div className={clsx(
|
||||
'text-medium uppercase pr-1',
|
||||
@ -161,7 +161,7 @@ export default function PhotoLarge({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>}
|
||||
</DivDebugBaselineGrid>}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -4,6 +4,7 @@ import ShareButton from '@/components/ShareButton';
|
||||
import AnimateItems from '@/components/AnimateItems';
|
||||
import { ReactNode } from 'react';
|
||||
import { HIGH_DENSITY_GRID } from '@/site/config';
|
||||
import DivDebugBaselineGrid from '@/components/DivDebugBaselineGrid';
|
||||
|
||||
export default function PhotoSetHeader({
|
||||
entity,
|
||||
@ -35,7 +36,7 @@ export default function PhotoSetHeader({
|
||||
type="bottom"
|
||||
distanceOffset={10}
|
||||
animateOnFirstLoadOnly
|
||||
items={[<div
|
||||
items={[<DivDebugBaselineGrid
|
||||
key="PhotosHeader"
|
||||
className={clsx(
|
||||
'grid gap-0.5 sm:gap-1 items-start',
|
||||
@ -74,7 +75,7 @@ export default function PhotoSetHeader({
|
||||
? start
|
||||
: <>{end}<br />– {start}</>}
|
||||
</span>
|
||||
</div>]}
|
||||
</DivDebugBaselineGrid>]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -36,7 +36,6 @@ import { extractExifDataFromBlobPath } from './server';
|
||||
import { TAG_FAVS, isTagFavs } from '@/tag';
|
||||
import { TbPhoto } from 'react-icons/tb';
|
||||
import PhotoTiny from './PhotoTiny';
|
||||
import { formatDate } from '@/utility/date';
|
||||
import {
|
||||
convertPhotoToPhotoDbInsert,
|
||||
getKeywordsForPhoto,
|
||||
@ -45,6 +44,7 @@ import {
|
||||
import { safelyRunAdminServerAction } from '@/auth';
|
||||
import { AI_IMAGE_QUERIES, AiImageQuery } from './ai';
|
||||
import { streamOpenAiImageQuery } from '@/services/openai';
|
||||
import PhotoDate from './PhotoDate';
|
||||
|
||||
export async function createPhotoAction(formData: FormData) {
|
||||
return safelyRunAdminServerAction(async () => {
|
||||
@ -211,14 +211,7 @@ export async function getPhotoItemsAction(query: string) {
|
||||
items: photos.map(photo => ({
|
||||
label: titleForPhoto(photo),
|
||||
keywords: getKeywordsForPhoto(photo),
|
||||
annotation: <>
|
||||
<span className="hidden sm:inline-block">
|
||||
{formatDate(photo.takenAt)}
|
||||
</span>
|
||||
<span className="inline-block sm:hidden">
|
||||
{formatDate(photo.takenAt, true)}
|
||||
</span>
|
||||
</>,
|
||||
annotation: <PhotoDate {...{ photo }} />,
|
||||
accessory: <PhotoTiny photo={photo} />,
|
||||
path: pathForPhoto(photo),
|
||||
})),
|
||||
|
||||
@ -41,6 +41,7 @@ export default function AiButton({
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={clsx(
|
||||
'flex min-w-[3.25rem] min-h-9 justify-center',
|
||||
className,
|
||||
|
||||
@ -211,7 +211,7 @@ const sqlGetPhotosCameraMeta = async (camera: Camera) => sql`
|
||||
SELECT COUNT(*), MIN(taken_at_naive) as start, MAX(taken_at_naive) as end
|
||||
FROM photos
|
||||
WHERE
|
||||
LOWER(make)=${parameterize(camera.make, true)} AND
|
||||
LOWER(REPLACE(make, ' ', '-'))=${parameterize(camera.make, true)} AND
|
||||
LOWER(REPLACE(model, ' ', '-'))=${parameterize(camera.model, true)} AND
|
||||
hidden IS NOT TRUE
|
||||
`.then(({ rows }) => ({
|
||||
@ -381,7 +381,7 @@ export const getPhotos = async (options: GetPhotosOptions = {}) => {
|
||||
values.push(tag);
|
||||
}
|
||||
if (camera) {
|
||||
wheres.push(`LOWER(make)=$${valueIndex++}`);
|
||||
wheres.push(`LOWER(REPLACE(make, ' ', '-'))=$${valueIndex++}`);
|
||||
wheres.push(`LOWER(REPLACE(model, ' ', '-'))=$${valueIndex++}`);
|
||||
values.push(parameterize(camera.make, true));
|
||||
values.push(parameterize(camera.model, true));
|
||||
|
||||
@ -2,7 +2,9 @@ import { labelForFilmSimulation } from '@/vendors/fujifilm';
|
||||
import PhotoFilmSimulationIcon from './PhotoFilmSimulationIcon';
|
||||
import { pathForFilmSimulation } from '@/site/paths';
|
||||
import { FilmSimulation } from '.';
|
||||
import EntityLink, { EntityLinkExternalProps } from '@/components/EntityLink';
|
||||
import EntityLink, {
|
||||
EntityLinkExternalProps,
|
||||
} from '@/components/primitives/EntityLink';
|
||||
|
||||
export default function PhotoFilmSimulation({
|
||||
simulation,
|
||||
@ -22,16 +24,14 @@ export default function PhotoFilmSimulation({
|
||||
label={medium}
|
||||
labelSmall={small}
|
||||
href={pathForFilmSimulation(simulation)}
|
||||
icon={<PhotoFilmSimulationIcon
|
||||
simulation={simulation}
|
||||
className="translate-y-[-1px]"
|
||||
/>}
|
||||
icon={<PhotoFilmSimulationIcon simulation={simulation} />}
|
||||
title={`Film Simulation: ${large}`}
|
||||
type={type}
|
||||
badged={badged}
|
||||
contrast={contrast}
|
||||
prefetch={prefetch}
|
||||
hoverEntity={countOnHover}
|
||||
iconWide
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ import {
|
||||
getUniqueTagsCached,
|
||||
} from '@/photo/cache';
|
||||
import {
|
||||
PATH_ADMIN_BASELINE,
|
||||
PATH_ADMIN_CONFIGURATION,
|
||||
PATH_ADMIN_PHOTOS,
|
||||
PATH_ADMIN_TAGS,
|
||||
@ -26,6 +27,7 @@ import { FaTag } from 'react-icons/fa';
|
||||
import { IoMdCamera } from 'react-icons/io';
|
||||
import { HiDocumentText } from 'react-icons/hi';
|
||||
import { signOutAction } from '@/auth/actions';
|
||||
import { ADMIN_DEBUG_TOOLS_ENABLED } from './config';
|
||||
|
||||
export default async function CommandK() {
|
||||
const [
|
||||
@ -124,14 +126,22 @@ export default async function CommandK() {
|
||||
}],
|
||||
};
|
||||
|
||||
if (isAdminLoggedIn && ADMIN_DEBUG_TOOLS_ENABLED) {
|
||||
SECTION_ADMIN.items.push({
|
||||
label: 'Baseline Overview',
|
||||
path: PATH_ADMIN_BASELINE,
|
||||
});
|
||||
}
|
||||
|
||||
return <CommandKClient
|
||||
sections={[
|
||||
serverSections={[
|
||||
SECTION_TAGS,
|
||||
SECTION_CAMERAS,
|
||||
SECTION_FILM,
|
||||
SECTION_PAGES,
|
||||
SECTION_ADMIN,
|
||||
]}
|
||||
showDebugTools={isAdminLoggedIn && ADMIN_DEBUG_TOOLS_ENABLED}
|
||||
footer={photoQuantityText(count, false)}
|
||||
/>;
|
||||
}
|
||||
|
||||
@ -38,10 +38,7 @@ export default function FooterClient({
|
||||
'flex items-center',
|
||||
'text-dim min-h-10',
|
||||
)}>
|
||||
<div className={clsx(
|
||||
'flex items-center flex-grow flex-wrap h-10',
|
||||
'gap-x-4 min-w-0',
|
||||
)}>
|
||||
<div className="flex gap-x-4 gap-y-0.5 flex-grow flex-wrap">
|
||||
{isPathAdmin(pathname)
|
||||
? <>
|
||||
{userEmail === undefined &&
|
||||
|
||||
@ -57,7 +57,6 @@ export default function NavClient({
|
||||
className={clsx(
|
||||
'flex items-center',
|
||||
'w-full min-h-[4rem]',
|
||||
'leading-none',
|
||||
)}>
|
||||
<div className="flex-grow">
|
||||
<ViewSwitcher
|
||||
|
||||
@ -139,7 +139,7 @@ export default function SiteChecklistClient({
|
||||
</div>;
|
||||
|
||||
return (
|
||||
<div className="text-sm max-w-xl space-y-6 w-full">
|
||||
<div className="max-w-xl space-y-6 w-full">
|
||||
<Checklist
|
||||
title="Storage"
|
||||
icon={<BiData size={16} />}
|
||||
|
||||
@ -117,8 +117,8 @@ export const 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';
|
||||
(process.env.NEXT_PUBLIC_OG_TEXT_ALIGNMENT ?? '').toUpperCase() === 'BOTTOM';
|
||||
export const ADMIN_DEBUG_TOOLS_ENABLED = process.env.ADMIN_DEBUG_TOOLS === '1';
|
||||
|
||||
export const HIGH_DENSITY_GRID = GRID_ASPECT_RATIO <= 1;
|
||||
|
||||
|
||||
@ -116,7 +116,7 @@
|
||||
hover:text-gray-600
|
||||
hover:dark:text-gray-400
|
||||
}
|
||||
/* Common Utilities: Text */
|
||||
/* Utilities: Text */
|
||||
.text-main {
|
||||
@apply
|
||||
text-gray-900 dark:text-gray-100
|
||||
@ -145,7 +145,7 @@
|
||||
@apply
|
||||
text-red-500 dark:text-red-400
|
||||
}
|
||||
/* Common Utilities: Background */
|
||||
/* Utilities: Background */
|
||||
.bg-main {
|
||||
@apply
|
||||
bg-white dark:bg-black
|
||||
@ -159,4 +159,28 @@
|
||||
@apply
|
||||
bg-gray-900 dark:bg-gray-100
|
||||
}
|
||||
/* Utilities: Baseline Grid */
|
||||
.space-y-baseline {
|
||||
@apply
|
||||
space-y-[1.1875rem] md:space-y-[1.25rem]
|
||||
}
|
||||
.gap-y-baseline {
|
||||
@apply
|
||||
gap-y-[1.1875rem] md:gap-y-[1.25rem]
|
||||
}
|
||||
.gap-baseline {
|
||||
@apply
|
||||
gap-[1.1875rem] md:gap-[1.25rem]
|
||||
}
|
||||
.max-h-baseline {
|
||||
@apply
|
||||
max-h-[1.1875rem] md:max-h-[1.25rem]
|
||||
}
|
||||
.bg-baseline-grid {
|
||||
@apply
|
||||
bg-[repeating-linear-gradient(to_bottom,#eee,#eee_1px,transparent_1px,transparent_1.1875rem)]
|
||||
md:bg-[repeating-linear-gradient(to_bottom,#eee,#eee_1px,transparent_1px,transparent_1.25rem)]
|
||||
dark:bg-[repeating-linear-gradient(to_bottom,#222,#222_1px,transparent_1px,transparent_1.1875rem)]
|
||||
dark:md:bg-[repeating-linear-gradient(to_bottom,#222,#222_1px,transparent_1px,transparent_1.25rem)]
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,11 +1,8 @@
|
||||
import { Photo } from '@/photo';
|
||||
import { BASE_URL } from './config';
|
||||
import {
|
||||
Camera,
|
||||
createCameraKey,
|
||||
getCameraFromKey,
|
||||
} from '@/camera';
|
||||
import { Camera } from '@/camera';
|
||||
import { FilmSimulation } from '@/simulation';
|
||||
import { parameterize } from '@/utility/string';
|
||||
|
||||
// Core paths
|
||||
export const PATH_ROOT = '/';
|
||||
@ -24,7 +21,7 @@ export const PREFIX_FILM_SIMULATION = '/film';
|
||||
// Dynamic paths
|
||||
const PATH_PHOTO_DYNAMIC = `${PREFIX_PHOTO}/[photoId]`;
|
||||
const PATH_TAG_DYNAMIC = `${PREFIX_TAG}/[tag]`;
|
||||
const PATH_CAMERA_DYNAMIC = `${PREFIX_CAMERA}/[camera]`;
|
||||
const PATH_CAMERA_DYNAMIC = `${PREFIX_CAMERA}/[make]/[model]`;
|
||||
const PATH_FILM_SIMULATION_DYNAMIC = `${PREFIX_FILM_SIMULATION}/[simulation]`;
|
||||
|
||||
// Admin paths
|
||||
@ -32,6 +29,7 @@ export const PATH_ADMIN_PHOTOS = `${PATH_ADMIN}/photos`;
|
||||
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_BASELINE = `${PATH_ADMIN}/baseline`;
|
||||
|
||||
// API paths
|
||||
export const PATH_API_STORAGE = `${PATH_API}/storage`;
|
||||
@ -125,8 +123,11 @@ export const pathForTag = (tag: string, next?: number) =>
|
||||
export const pathForTagShare = (tag: string) =>
|
||||
`${pathForTag(tag)}/${SHARE}`;
|
||||
|
||||
export const pathForCamera = (camera: Camera, next?: number) =>
|
||||
pathWithNext(`${PREFIX_CAMERA}/${createCameraKey(camera)}`, next);
|
||||
export const pathForCamera = ({ make, model }: Camera, next?: number) =>
|
||||
pathWithNext(
|
||||
`${PREFIX_CAMERA}/${parameterize(make, true)}/${parameterize(model, true)}`,
|
||||
next,
|
||||
);
|
||||
|
||||
export const pathForCameraShare = (camera: Camera) =>
|
||||
`${pathForCamera(camera)}/${SHARE}`;
|
||||
@ -195,22 +196,22 @@ export const isPathTagPhoto = (pathname = '') =>
|
||||
export const isPathTagPhotoShare = (pathname = '') =>
|
||||
new RegExp(`^${PREFIX_TAG}/[^/]+/[^/]+/${SHARE}/?$`).test(pathname);
|
||||
|
||||
// shot-on/[camera]
|
||||
// shot-on/[make]/[model]
|
||||
export const isPathCamera = (pathname = '') =>
|
||||
new RegExp(`^${PREFIX_CAMERA}/[^/]+/?$`).test(pathname);
|
||||
|
||||
// shot-on/[camera]/share
|
||||
export const isPathCameraShare = (pathname = '') =>
|
||||
new RegExp(`^${PREFIX_CAMERA}/[^/]+/${SHARE}/?$`).test(pathname);
|
||||
|
||||
// shot-on/[camera]/[photoId]
|
||||
export const isPathCameraPhoto = (pathname = '') =>
|
||||
new RegExp(`^${PREFIX_CAMERA}/[^/]+/[^/]+/?$`).test(pathname);
|
||||
|
||||
// shot-on/[camera]/[photoId]/share
|
||||
export const isPathCameraPhotoShare = (pathname = '') =>
|
||||
// shot-on/[make]/[model]/share
|
||||
export const isPathCameraShare = (pathname = '') =>
|
||||
new RegExp(`^${PREFIX_CAMERA}/[^/]+/[^/]+/${SHARE}/?$`).test(pathname);
|
||||
|
||||
// shot-on/[make]/[model]/[photoId]
|
||||
export const isPathCameraPhoto = (pathname = '') =>
|
||||
new RegExp(`^${PREFIX_CAMERA}/[^/]+/[^/]+/[^/]+/?$`).test(pathname);
|
||||
|
||||
// shot-on/[make]/[model]/[photoId]/share
|
||||
export const isPathCameraPhotoShare = (pathname = '') =>
|
||||
new RegExp(`^${PREFIX_CAMERA}/[^/]+/[^/]+/[^/]+/${SHARE}/?$`).test(pathname);
|
||||
|
||||
// film/[simulation]
|
||||
export const isPathFilmSimulation = (pathname = '') =>
|
||||
new RegExp(`^${PREFIX_FILM_SIMULATION}/[^/]+/?$`).test(pathname);
|
||||
@ -257,18 +258,20 @@ export const getPathComponents = (pathname = ''): {
|
||||
const photoIdFromTag = pathname.match(
|
||||
new RegExp(`^${PREFIX_TAG}/[^/]+/((?!${SHARE})[^/]+)`))?.[1];
|
||||
const photoIdFromCamera = pathname.match(
|
||||
new RegExp(`^${PREFIX_CAMERA}/[^/]+/((?!${SHARE})[^/]+)`))?.[1];
|
||||
new RegExp(`^${PREFIX_CAMERA}/[^/]+/[^/]+/((?!${SHARE})[^/]+)`))?.[1];
|
||||
const photoIdFromFilmSimulation = pathname.match(
|
||||
new RegExp(`^${PREFIX_FILM_SIMULATION}/[^/]+/((?!${SHARE})[^/]+)`))?.[1];
|
||||
const tag = pathname.match(
|
||||
new RegExp(`^${PREFIX_TAG}/([^/]+)`))?.[1];
|
||||
const cameraString = pathname.match(
|
||||
const cameraMake = pathname.match(
|
||||
new RegExp(`^${PREFIX_CAMERA}/([^/]+)`))?.[1];
|
||||
const cameraModel = pathname.match(
|
||||
new RegExp(`^${PREFIX_CAMERA}/[^/]+/([^/]+)`))?.[1];
|
||||
const simulation = pathname.match(
|
||||
new RegExp(`^${PREFIX_FILM_SIMULATION}/([^/]+)`))?.[1] as FilmSimulation;
|
||||
|
||||
const camera = cameraString
|
||||
? getCameraFromKey(cameraString)
|
||||
const camera = cameraMake && cameraModel
|
||||
? { make: cameraMake, model: cameraModel }
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
|
||||
@ -11,6 +11,8 @@ export interface AppStateContext {
|
||||
setShouldRespondToKeyboardCommands?: Dispatch<SetStateAction<boolean>>
|
||||
isCommandKOpen?: boolean
|
||||
setIsCommandKOpen?: Dispatch<SetStateAction<boolean>>
|
||||
shouldShowBaselineGrid?: boolean
|
||||
setShouldShowBaselineGrid?: Dispatch<SetStateAction<boolean>>
|
||||
clearNextPhotoAnimation?: () => void
|
||||
}
|
||||
|
||||
|
||||
@ -12,15 +12,16 @@ export default function AppStateProvider({
|
||||
}) {
|
||||
const { previousPathname } = usePathnames();
|
||||
|
||||
const [hasLoaded, setHasLoaded] = useState(false);
|
||||
|
||||
const [hasLoaded, setHasLoaded] =
|
||||
useState(false);
|
||||
const [nextPhotoAnimation, setNextPhotoAnimation] =
|
||||
useState<AnimationConfig>();
|
||||
|
||||
const [shouldRespondToKeyboardCommands, setShouldRespondToKeyboardCommands] =
|
||||
useState(true);
|
||||
|
||||
const [isCommandKOpen, setIsCommandKOpen] = useState(false);
|
||||
const [isCommandKOpen, setIsCommandKOpen] =
|
||||
useState(false);
|
||||
const [shouldShowBaselineGrid, setShouldShowBaselineGrid] =
|
||||
useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setHasLoaded?.(true);
|
||||
@ -38,6 +39,8 @@ export default function AppStateProvider({
|
||||
setShouldRespondToKeyboardCommands,
|
||||
isCommandKOpen,
|
||||
setIsCommandKOpen,
|
||||
shouldShowBaselineGrid,
|
||||
setShouldShowBaselineGrid,
|
||||
clearNextPhotoAnimation: () => setNextPhotoAnimation?.(undefined),
|
||||
}}
|
||||
>
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
import { FaStar } from 'react-icons/fa';
|
||||
import EntityLink, { EntityLinkExternalProps } from '@/components/EntityLink';
|
||||
import { TAG_FAVS } from '.';
|
||||
import { pathForTag } from '@/site/paths';
|
||||
import { clsx } from 'clsx/lite';
|
||||
import EntityLink, {
|
||||
EntityLinkExternalProps,
|
||||
} from '@/components/primitives/EntityLink';
|
||||
|
||||
export default function FavsTag({
|
||||
type,
|
||||
@ -31,7 +33,7 @@ export default function FavsTag({
|
||||
size={12}
|
||||
className={clsx(
|
||||
'text-amber-500',
|
||||
'translate-x-[-1px] translate-y-[3.5px]',
|
||||
'translate-x-[-1px] translate-y-[-0.5px]',
|
||||
)}
|
||||
/>}
|
||||
type={type}
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import { pathForTag } from '@/site/paths';
|
||||
import { FaTag } from 'react-icons/fa';
|
||||
import { formatTag } from '.';
|
||||
import EntityLink, { EntityLinkExternalProps } from '@/components/EntityLink';
|
||||
import EntityLink, {
|
||||
EntityLinkExternalProps,
|
||||
} from '@/components/primitives/EntityLink';
|
||||
|
||||
export default function PhotoTag({
|
||||
tag,
|
||||
@ -20,7 +22,7 @@ export default function PhotoTag({
|
||||
href={pathForTag(tag)}
|
||||
icon={<FaTag
|
||||
size={11}
|
||||
className="translate-y-[5px]"
|
||||
className="translate-y-[1px]"
|
||||
/>}
|
||||
type={type}
|
||||
badged={badged}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import PhotoTag from '@/tag/PhotoTag';
|
||||
import { isTagFavs } from '.';
|
||||
import FavsTag from './FavsTag';
|
||||
import { EntityLinkExternalProps } from '@/components/EntityLink';
|
||||
import { EntityLinkExternalProps } from '@/components/primitives/EntityLink';
|
||||
|
||||
export default function PhotoTags({
|
||||
tags,
|
||||
@ -10,13 +10,13 @@ export default function PhotoTags({
|
||||
tags: string[]
|
||||
} & EntityLinkExternalProps) {
|
||||
return (
|
||||
<div className="-space-y-0.5">
|
||||
<div className="flex flex-col">
|
||||
{tags.map(tag =>
|
||||
<div key={tag}>
|
||||
<>
|
||||
{isTagFavs(tag)
|
||||
? <FavsTag {...{ contrast }} />
|
||||
: <PhotoTag {...{ tag, contrast }} />}
|
||||
</div>)}
|
||||
? <FavsTag key={tag} {...{ contrast }} />
|
||||
: <PhotoTag key={tag} {...{ tag, contrast }} />}
|
||||
</>)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -22,7 +22,7 @@ export default function TagHeader({
|
||||
<PhotoSetHeader
|
||||
entity={isTagFavs(tag)
|
||||
? <FavsTag />
|
||||
: <PhotoTag tag={tag} />}
|
||||
: <PhotoTag tag={tag} contrast="high" />}
|
||||
entityVerb="Tagged"
|
||||
entityDescription={descriptionForTaggedPhotos(photos, undefined, count)}
|
||||
photos={photos}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { format, parseISO, parse } from 'date-fns';
|
||||
|
||||
const DATE_STRING_FORMAT_SHORT = 'dd MMM yyyy';
|
||||
const DATE_STRING_FORMAT = 'd MMM yyyy h:mma';
|
||||
const DATE_STRING_FORMAT = 'dd MMM yyyy h:mma';
|
||||
const DATE_STRING_FORMAT_POSTGRES = 'yyyy-MM-dd HH:mm:ss';
|
||||
|
||||
type AmbiguousTimestamp = number | string;
|
||||
|
||||
@ -12,13 +12,13 @@ module.exports = {
|
||||
...defaultTheme.screens,
|
||||
},
|
||||
fontSize: {
|
||||
'xs': '0.75rem',
|
||||
'sm': ['0.825rem', '1.15rem'],
|
||||
'base': ['0.875rem', '1.275rem'],
|
||||
'lg': ['0.925rem', '1.05rem'],
|
||||
'xl': '1rem',
|
||||
'2xl': '1.1rem',
|
||||
'3xl': ['1.3rem', '1.7rem'],
|
||||
'xs': ['0.75rem', '1rem'], // 12px on 16px
|
||||
'sm': ['0.84375rem', '1.1875rem'], // 13.5px on 19px [Default: mobile]
|
||||
'base': ['0.875rem', '1.25rem'], // 14px on 20px [Default: desktop]
|
||||
'lg': ['1rem', '1.25rem'], // 16px on 20px
|
||||
'xl': ['1.125rem', '1.25rem'], // 18px on 20px
|
||||
'2xl': ['1.25rem', '1.25rem'], // 20px on 20px
|
||||
'3xl': ['1.5rem', '1.5rem'], // 24px on 24px
|
||||
},
|
||||
extend: {
|
||||
fontFamily: {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user