diff --git a/package.json b/package.json
index 3b67206a..ce562cab 100644
--- a/package.json
+++ b/package.json
@@ -39,8 +39,8 @@
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"nanoid": "^5.0.6",
- "next": "14.2.0-canary.23",
- "next-auth": "5.0.0-beta.13",
+ "next": "14.2.0-canary.31",
+ "next-auth": "5.0.0-beta.15",
"next-themes": "^0.3.0",
"postcss": "8.4.35",
"react": "18.2.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index ec78c9b3..8d0186e0 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -49,7 +49,7 @@ dependencies:
version: 7.2.0(eslint@8.57.0)(typescript@5.4.2)
'@vercel/analytics':
specifier: ^1.2.2
- version: 1.2.2(next@14.2.0-canary.23)(react@18.2.0)
+ version: 1.2.2(next@14.2.0-canary.31)(react@18.2.0)
'@vercel/blob':
specifier: ^0.22.1
version: 0.22.1
@@ -58,7 +58,7 @@ dependencies:
version: 0.7.2
'@vercel/speed-insights':
specifier: ^1.0.10
- version: 1.0.10(next@14.2.0-canary.23)(react@18.2.0)
+ version: 1.0.10(next@14.2.0-canary.31)(react@18.2.0)
autoprefixer:
specifier: 10.4.18
version: 10.4.18(postcss@8.4.35)
@@ -96,11 +96,11 @@ dependencies:
specifier: ^5.0.6
version: 5.0.6
next:
- specifier: 14.2.0-canary.23
- version: 14.2.0-canary.23(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0)
+ specifier: 14.2.0-canary.31
+ version: 14.2.0-canary.31(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0)
next-auth:
- specifier: 5.0.0-beta.13
- version: 5.0.0-beta.13(next@14.2.0-canary.23)(react@18.2.0)
+ specifier: 5.0.0-beta.15
+ version: 5.0.0-beta.15(next@14.2.0-canary.31)(react@18.2.0)
next-themes:
specifier: ^0.3.0
version: 0.3.0(react-dom@18.2.0)(react@18.2.0)
@@ -156,8 +156,8 @@ packages:
'@jridgewell/trace-mapping': 0.3.22
dev: false
- /@auth/core@0.27.0:
- resolution: {integrity: sha512-3bydnRJIM/Al6mkYmb53MsC+6G8ojw3lLPzwgVnX4dCo6N2lrib6Wq6r0vxZIhuHGjLObqqtUfpeaEj5aeTHFg==}
+ /@auth/core@0.28.0:
+ resolution: {integrity: sha512-/fh/tb/L4NMSYcyPoo4Imn8vN6MskcVfgESF8/ndgtI4fhD/7u7i5fTVzWgNRZ4ebIEGHNDbWFRxaTu1NtQgvA==}
peerDependencies:
'@simplewebauthn/browser': ^9.0.1
'@simplewebauthn/server': ^9.0.2
@@ -1551,8 +1551,8 @@ packages:
- utf-8-validate
dev: false
- /@next/env@14.2.0-canary.23:
- resolution: {integrity: sha512-cBlFB8Y/iE3K2NX/Km4tP4RZGLsv0D72KI9KxmZepKSkaQBSbtHM0YeHnZ51CFe9UQKzQ/1mPnCY89BjiyIQtQ==}
+ /@next/env@14.2.0-canary.31:
+ resolution: {integrity: sha512-xdUjSv8c5e1QPiB010TcyW1zPL3bK7FySHQDu6NjzZuUkYwm8W9c9NGIdJLB2UQv0rfpaFBKfWNlGbakicrE+g==}
dev: false
/@next/eslint-plugin-next@14.1.3:
@@ -1561,8 +1561,8 @@ packages:
glob: 10.3.10
dev: false
- /@next/swc-darwin-arm64@14.2.0-canary.23:
- resolution: {integrity: sha512-K1f7A/0ljZO7IX+M+phguFP8lxdqMgEv1x1+gC+UmyZ1c8Na1PgirGgUwdLKxuNjyxlRqY5lkQ/FDk2FOYBSLA==}
+ /@next/swc-darwin-arm64@14.2.0-canary.31:
+ resolution: {integrity: sha512-9NRPNOWxY/ecv1hxZej9nVvggIemdCqkwlgmkVv+M2TkAJzff5bwY4IzTuyPxQmioxHs43gPyKh/wpVsH4cuog==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
@@ -1570,8 +1570,8 @@ packages:
dev: false
optional: true
- /@next/swc-darwin-x64@14.2.0-canary.23:
- resolution: {integrity: sha512-3Zg0aZZV9Z0+4QCVNMH/LLW1dRD2mVNtuFBoTI6/7rWUiGrm/9+58sKyjjg+cE9S/zAKvJoFZP7Ask9vnrk4tg==}
+ /@next/swc-darwin-x64@14.2.0-canary.31:
+ resolution: {integrity: sha512-8xbOircQJzJx39GZ4iNd/6PIyOI/qZ8TpjJ9qzA1FpVZUDAMXZIDnQIHIsnxvn0HkP85PByw3tcKZLLKcw5k4g==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
@@ -1579,8 +1579,8 @@ packages:
dev: false
optional: true
- /@next/swc-linux-arm64-gnu@14.2.0-canary.23:
- resolution: {integrity: sha512-OMt5uTXtEZNKaeSvviJQVXzARr3Jyk1BKUQVD10hKUa4edWBcmnrpdqiDVoDCGt9kMOdKdGqJHOJUk/jV/G15w==}
+ /@next/swc-linux-arm64-gnu@14.2.0-canary.31:
+ resolution: {integrity: sha512-zpA19+op2KxnIutpoZbvQiplmoJHsUUPIhynC+PpZtSqA1IWBZoem+T4IhnmHr+F0DQZRqO9lmZZkHdEpRAzbQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
@@ -1588,8 +1588,8 @@ packages:
dev: false
optional: true
- /@next/swc-linux-arm64-musl@14.2.0-canary.23:
- resolution: {integrity: sha512-vllciUQ4U99LCOBnsFt9QGf9AyE8yhBtNdNxbij5QsdQ/F53SamxrbYOgG7RisvRqFmWSQMfHdpGZOE0EUUsvQ==}
+ /@next/swc-linux-arm64-musl@14.2.0-canary.31:
+ resolution: {integrity: sha512-cZ1GuBi6YOHdEw8/lhuxZsObJVpry7irgf1PCsOLigpIqCAyMhbAcQ32FkTA3N5MKWpOkrmS9xQ2B6K1ZaQzCg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
@@ -1597,8 +1597,8 @@ packages:
dev: false
optional: true
- /@next/swc-linux-x64-gnu@14.2.0-canary.23:
- resolution: {integrity: sha512-soKCxTCi0m0hOBSEchH2YTluvQzAEb8HIoQXxligU8C1fmFDX25pwQ4iSWmdvA6xDJn96PG2R64NyytTTMg9TQ==}
+ /@next/swc-linux-x64-gnu@14.2.0-canary.31:
+ resolution: {integrity: sha512-YxWC/fipzs/3cTeGcsSSU3GXEAJ16TI3yo4jgwofWko2jSF2/kpCOZSnRYbqX3eNyBCD4K9/g/9v6rAx2zHiew==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
@@ -1606,8 +1606,8 @@ packages:
dev: false
optional: true
- /@next/swc-linux-x64-musl@14.2.0-canary.23:
- resolution: {integrity: sha512-Qr+4ySSYEh1hSSmUJ50oHtKopkqwo3RFb2CXpzcMqp+6U8WOMYBX+JTSCFdA3lSlUJqScIgoRoMDk9I3TZM71g==}
+ /@next/swc-linux-x64-musl@14.2.0-canary.31:
+ resolution: {integrity: sha512-CoZVZyx08myeDXmoSEuk+eXrwMYenevXP03Rz/+6+BT6zOrq+1s1rFKUZVd6/3AnD9Q7vNCuTGtaL5jaF5koGw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
@@ -1615,8 +1615,8 @@ packages:
dev: false
optional: true
- /@next/swc-win32-arm64-msvc@14.2.0-canary.23:
- resolution: {integrity: sha512-94HGrBqz9s7z6d6hlukbkF4LiU4Bw+a+C3i+J7pBA+3BHtIyffuqmnrP3HY92trP3M328GBN01H7/zqahcvwPQ==}
+ /@next/swc-win32-arm64-msvc@14.2.0-canary.31:
+ resolution: {integrity: sha512-m1nuZmu8DOJKJvVrrz7KxMH1K3IU1UC7av1jD55cFf3ZM5ur06Mx2PvtbKSnSLCjK7Ga8LHMYXBXQWAbkD6Bcg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
@@ -1624,8 +1624,8 @@ packages:
dev: false
optional: true
- /@next/swc-win32-ia32-msvc@14.2.0-canary.23:
- resolution: {integrity: sha512-lrvn6ekxPyrBidQxA0kE3xyys8fCLlbhi1tl2FA0QUPHvgPbu2127Q7+UFnl/NGC9veZfCbw6b7TVFyvPseRsQ==}
+ /@next/swc-win32-ia32-msvc@14.2.0-canary.31:
+ resolution: {integrity: sha512-OFmjN8wK6eSViHUqsh7VLzI8H8d3G6esjn3zOHoSirg821MJLyWVGAXMBjykDz4kTP6VmGdCkohBP6nf/uy94Q==}
engines: {node: '>= 10'}
cpu: [ia32]
os: [win32]
@@ -1633,8 +1633,8 @@ packages:
dev: false
optional: true
- /@next/swc-win32-x64-msvc@14.2.0-canary.23:
- resolution: {integrity: sha512-T8zZcK8M5GYWzIKOlxcothb0sixdFSowUztZTNb/kmUK8Vz1Z+TFBxU00WvBDx9ymrGP2f4EKL9YQqu2d+BmcA==}
+ /@next/swc-win32-x64-msvc@14.2.0-canary.31:
+ resolution: {integrity: sha512-Dv+FC2zYh8aEKsFUpq6815grRS0dcRw9uJ9hxULAZ9EuFcw0iu5zKbEPWZ+klxDrAWA8bw7WYMZfkvEH7bSLOA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
@@ -3088,7 +3088,7 @@ packages:
resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==}
dev: false
- /@vercel/analytics@1.2.2(next@14.2.0-canary.23)(react@18.2.0):
+ /@vercel/analytics@1.2.2(next@14.2.0-canary.31)(react@18.2.0):
resolution: {integrity: sha512-X0rctVWkQV1e5Y300ehVNqpOfSOufo7ieA5PIdna8yX/U7Vjz0GFsGf4qvAhxV02uQ2CVt7GYcrFfddXXK2Y4A==}
peerDependencies:
next: '>= 13'
@@ -3099,7 +3099,7 @@ packages:
react:
optional: true
dependencies:
- next: 14.2.0-canary.23(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0)
+ next: 14.2.0-canary.31(@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
@@ -3124,7 +3124,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.23)(react@18.2.0):
+ /@vercel/speed-insights@1.0.10(next@14.2.0-canary.31)(react@18.2.0):
resolution: {integrity: sha512-4uzdKB0RW6Ff2FkzshzjZ+RlJfLPxgm/00i0XXgxfMPhwnnsk92YgtqsxT9OcPLdJUyVU1DqFlSWWjIQMPkh0g==}
requiresBuild: true
peerDependencies:
@@ -3148,7 +3148,7 @@ packages:
vue-router:
optional: true
dependencies:
- next: 14.2.0-canary.23(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0)
+ next: 14.2.0-canary.31(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
dev: false
@@ -6057,8 +6057,8 @@ packages:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
dev: false
- /next-auth@5.0.0-beta.13(next@14.2.0-canary.23)(react@18.2.0):
- resolution: {integrity: sha512-2m2Gq69WQ0YXcHCCpHn2y5z1bxSlqD/XOuAgrdtz49/VIAdTFFeYZz97RYqf6xMF8VGmoG32VUnJ6LzaHk6Fwg==}
+ /next-auth@5.0.0-beta.15(next@14.2.0-canary.31)(react@18.2.0):
+ resolution: {integrity: sha512-UQggNq8CDu3/w8CYkihKLLnRPNXel98K0j7mtjj9a6XTNYo4Hni8xg/2h1YhElW6vXE8mgtvmH11rU8NKw86jQ==}
peerDependencies:
'@simplewebauthn/browser': ^9.0.1
'@simplewebauthn/server': ^9.0.2
@@ -6073,8 +6073,8 @@ packages:
nodemailer:
optional: true
dependencies:
- '@auth/core': 0.27.0
- next: 14.2.0-canary.23(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0)
+ '@auth/core': 0.28.0
+ next: 14.2.0-canary.31(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
dev: false
@@ -6088,8 +6088,8 @@ packages:
react-dom: 18.2.0(react@18.2.0)
dev: false
- /next@14.2.0-canary.23(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0):
- resolution: {integrity: sha512-HuQYIeSlmBmTueVIxn0Zjxx5I4MD6vG3p6AsFySTF7/hMSF5qFUCHGIj3YJRWsDcUw/LQhuGPNLaGUg119YegQ==}
+ /next@14.2.0-canary.31(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0):
+ resolution: {integrity: sha512-aKls3+4raMbu8ex6YDuQFa8U4ajKojXpn8GvdlFtgNUyBHJ7IrVuaFJw6rU9s9OibfgpFnAbsmvKzYQmAAjmlg==}
engines: {node: '>=18.17.0'}
hasBin: true
peerDependencies:
@@ -6103,7 +6103,7 @@ packages:
sass:
optional: true
dependencies:
- '@next/env': 14.2.0-canary.23
+ '@next/env': 14.2.0-canary.31
'@swc/helpers': 0.5.5
busboy: 1.6.0
caniuse-lite: 1.0.30001591
@@ -6113,15 +6113,15 @@ packages:
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.23
- '@next/swc-darwin-x64': 14.2.0-canary.23
- '@next/swc-linux-arm64-gnu': 14.2.0-canary.23
- '@next/swc-linux-arm64-musl': 14.2.0-canary.23
- '@next/swc-linux-x64-gnu': 14.2.0-canary.23
- '@next/swc-linux-x64-musl': 14.2.0-canary.23
- '@next/swc-win32-arm64-msvc': 14.2.0-canary.23
- '@next/swc-win32-ia32-msvc': 14.2.0-canary.23
- '@next/swc-win32-x64-msvc': 14.2.0-canary.23
+ '@next/swc-darwin-arm64': 14.2.0-canary.31
+ '@next/swc-darwin-x64': 14.2.0-canary.31
+ '@next/swc-linux-arm64-gnu': 14.2.0-canary.31
+ '@next/swc-linux-arm64-musl': 14.2.0-canary.31
+ '@next/swc-linux-x64-gnu': 14.2.0-canary.31
+ '@next/swc-linux-x64-musl': 14.2.0-canary.31
+ '@next/swc-win32-arm64-msvc': 14.2.0-canary.31
+ '@next/swc-win32-ia32-msvc': 14.2.0-canary.31
+ '@next/swc-win32-x64-msvc': 14.2.0-canary.31
transitivePeerDependencies:
- '@babel/core'
- babel-plugin-macros
diff --git a/src/app/grid/page.tsx b/src/app/grid/page.tsx
index 6a139fc8..48c141f3 100644
--- a/src/app/grid/page.tsx
+++ b/src/app/grid/page.tsx
@@ -45,7 +45,7 @@ export default async function GridPage() {
/>
}
- contentSide={
+ contentSide={
{
- const photos = await getPhotosCached({ limit: MAX_PHOTOS_TO_SHOW_OG });
- return generateOgImageMetaForPhotos(photos);
-}
-
-export default async function SetsPage() {
- const [
- photosCount,
- tags,
- cameras,
- simulations,
- ] = await Promise.all(getPhotoSidebarDataCached());
-
- return (
-
-
-
- }
- />
- );
-}
diff --git a/src/auth/index.ts b/src/auth/index.ts
index 53337314..4ea60774 100644
--- a/src/auth/index.ts
+++ b/src/auth/index.ts
@@ -44,6 +44,17 @@ export const {
},
});
+export const safelyRunServerAdminAction = async (
+ callback: () => T,
+): Promise => {
+ const session = await auth();
+ if (session?.user) {
+ return callback();
+ } else {
+ throw new Error('Unauthorized server action request');
+ }
+};
+
export const generateAuthSecret = () => fetch(
'https://generate-secret.vercel.app/32',
{ cache: 'no-cache' },
diff --git a/src/camera/PhotoCamera.tsx b/src/camera/PhotoCamera.tsx
index 1724087b..ef2d9faa 100644
--- a/src/camera/PhotoCamera.tsx
+++ b/src/camera/PhotoCamera.tsx
@@ -3,7 +3,6 @@ import { pathForCamera } from '@/site/paths';
import { IoMdCamera } from 'react-icons/io';
import { Camera, formatCameraText } from '.';
import EntityLink, { EntityLinkExternalProps } from '@/components/EntityLink';
-import { clsx } from 'clsx/lite';
export default function PhotoCamera({
camera,
@@ -28,18 +27,12 @@ export default function PhotoCamera({
icon={showAppleIcon
?
: }
type={showAppleIcon && isCameraApple ? 'icon-first' : type}
badged={badged}
diff --git a/src/components/EntityLink.tsx b/src/components/EntityLink.tsx
index 806b12b6..ae2f0955 100644
--- a/src/components/EntityLink.tsx
+++ b/src/components/EntityLink.tsx
@@ -18,8 +18,8 @@ export default function EntityLink({
title,
type = 'icon-first',
badged,
- contrast,
prefetch,
+ contrast = 'high',
hoverEntity,
}: {
label: ReactNode
@@ -38,15 +38,26 @@ export default function EntityLink({
>;
+ const classForContrast = () => {
+ switch (contrast) {
+ case 'low':
+ return 'text-dim';
+ case 'high':
+ return 'text-main';
+ default:
+ return 'text-medium';
+ }
+ };
+
return (
-
+
@@ -70,7 +81,9 @@ export default function EntityLink({
diff --git a/src/photo/PhotoGridSidebar.tsx b/src/photo/PhotoGridSidebar.tsx
index 93a40327..09b1a6c0 100644
--- a/src/photo/PhotoGridSidebar.tsx
+++ b/src/photo/PhotoGridSidebar.tsx
@@ -37,6 +37,7 @@ export default function PhotoGridSidebar({
countOnHover={count}
type="icon-last"
prefetch={false}
+ contrast="low"
badged
/>
: )}
/>}
@@ -63,6 +65,7 @@ export default function PhotoGridSidebar({
type="text-only"
countOnHover={count}
prefetch={false}
+ contrast="low"
hideAppleIcon
badged
/>)}
diff --git a/src/photo/PhotoLarge.tsx b/src/photo/PhotoLarge.tsx
index 432ae85d..b5f215c5 100644
--- a/src/photo/PhotoLarge.tsx
+++ b/src/photo/PhotoLarge.tsx
@@ -46,16 +46,10 @@ export default function PhotoLarge({
const tags = sortTags(photo.tags, primaryTag);
const camera = cameraFromPhoto(photo);
-
- const renderMiniGrid = (children: JSX.Element, rightPadding = true) =>
- *]:sm:flex-grow',
- rightPadding && 'pr-2',
- )}>
- {children}
-
;
+
+ const showCameraContent = showCamera && shouldShowCameraDataForPhoto(photo);
+ const showTagsContent = tags.length > 0;
+ const showExifContent = shouldShowExifDataForPhoto(photo);
return (
}
contentSide={
- {renderMiniGrid(<>
-
-
-
-
- {titleForPhoto(photo)}
-
-
-
-
-
+ {/* Meta */}
+
+
+
+
+ {titleForPhoto(photo)}
+
- {tags.length > 0 &&
-
}
+
+
+
- {showCamera && shouldShowCameraDataForPhoto(photo) &&
-
-
+
+ {photo.caption &&
+
+ {photo.caption}
+
}
+ {(showCameraContent || showTagsContent) &&
+
+ {showCameraContent &&
+
}
+ {showTagsContent &&
+
}
+
}
+
+
+ {/* EXIF Data */}
+
+ {showExifContent &&
+ <>
+
+ -
+ {photo.focalLengthFormatted}
+ {photo.focalLengthIn35MmFormatFormatted &&
+ <>
+ {' '}
+
+ {photo.focalLengthIn35MmFormatFormatted}
+
+ >}
+
+ - {photo.fNumberFormatted}
+ - {photo.exposureTimeFormatted}
+ - {photo.isoFormatted}
+ - {photo.exposureCompensationFormatted ?? '0ev'}
+
{showSimulation && photo.filmSimulation &&
-
}
-
}
- >)}
- {renderMiniGrid(<>
- {shouldShowExifDataForPhoto(photo) &&
-
- -
- {photo.focalLengthFormatted}
- {photo.focalLengthIn35MmFormatFormatted &&
- <>
- {' '}
-
- {photo.focalLengthIn35MmFormatFormatted}
-
- >}
-
- - {photo.fNumberFormatted}
- - {photo.exposureTimeFormatted}
- - {photo.isoFormatted}
- - {photo.exposureCompensationFormatted ?? '—'}
-
}
+
}
+ >}
{photo.takenAtNaiveFormatted}
@@ -166,7 +159,7 @@ export default function PhotoLarge({
shouldScroll={shouldScrollOnShare}
/>
- >, false)}
+
}
/>
);
diff --git a/src/photo/actions.tsx b/src/photo/actions.tsx
index ebae1d2f..9bf682f6 100644
--- a/src/photo/actions.tsx
+++ b/src/photo/actions.tsx
@@ -34,51 +34,58 @@ import {
} from '@/site/paths';
import { extractExifDataFromBlobPath } from './server';
import { TAG_FAVS, isTagFavs } from '@/tag';
-import { convertPhotoToPhotoDbInsert, titleForPhoto } from '.';
import { TbPhoto } from 'react-icons/tb';
import PhotoTiny from './PhotoTiny';
import { formatDate } from '@/utility/date';
+import { convertPhotoToPhotoDbInsert, titleForPhoto } from '.';
+import { safelyRunServerAdminAction } from '@/auth';
export async function createPhotoAction(formData: FormData) {
- const photo = convertFormDataToPhotoDbInsert(formData, true);
+ return safelyRunServerAdminAction(async () => {
+ const photo = convertFormDataToPhotoDbInsert(formData, true);
- const updatedUrl = await convertUploadToPhoto(photo.url, photo.id);
-
- if (updatedUrl) { photo.url = updatedUrl; }
-
- await sqlInsertPhoto(photo);
-
- revalidateAllKeysAndPaths();
-
- redirect(PATH_ADMIN_PHOTOS);
+ const updatedUrl = await convertUploadToPhoto(photo.url, photo.id);
+
+ if (updatedUrl) { photo.url = updatedUrl; }
+
+ await sqlInsertPhoto(photo);
+
+ revalidateAllKeysAndPaths();
+
+ redirect(PATH_ADMIN_PHOTOS);
+ });
}
export async function updatePhotoAction(formData: FormData) {
- const photo = convertFormDataToPhotoDbInsert(formData);
+ return safelyRunServerAdminAction(async () => {
+ const photo = convertFormDataToPhotoDbInsert(formData);
- await sqlUpdatePhoto(photo);
+ await sqlUpdatePhoto(photo);
- revalidatePhoto(photo.id);
+ revalidatePhoto(photo.id);
- redirect(PATH_ADMIN_PHOTOS);
+ redirect(PATH_ADMIN_PHOTOS);
+ });
}
export async function toggleFavoritePhotoAction(
photoId: string,
shouldRedirect?: boolean,
) {
- const photo = await getPhoto(photoId);
- if (photo) {
- const { tags } = photo;
- photo.tags = tags.some(tag => tag === TAG_FAVS)
- ? tags.filter(tag => !isTagFavs(tag))
- : [...tags, TAG_FAVS];
- await sqlUpdatePhoto(convertPhotoToPhotoDbInsert(photo));
- revalidateAllKeysAndPaths();
- if (shouldRedirect) {
- redirect(pathForPhoto(photoId));
+ return safelyRunServerAdminAction(async () => {
+ const photo = await getPhoto(photoId);
+ if (photo) {
+ const { tags } = photo;
+ photo.tags = tags.some(tag => tag === TAG_FAVS)
+ ? tags.filter(tag => !isTagFavs(tag))
+ : [...tags, TAG_FAVS];
+ await sqlUpdatePhoto(convertPhotoToPhotoDbInsert(photo));
+ revalidateAllKeysAndPaths();
+ if (shouldRedirect) {
+ redirect(pathForPhoto(photoId));
+ }
}
- }
+ });
}
export async function deletePhotoAction(
@@ -86,84 +93,98 @@ export async function deletePhotoAction(
photoUrl: string,
shouldRedirect?: boolean,
) {
- await sqlDeletePhoto(photoId).then(() => deleteStorageUrl(photoUrl));
- revalidateAllKeysAndPaths();
- if (shouldRedirect) {
- redirect(PATH_ROOT);
- }
+ return safelyRunServerAdminAction(async () => {
+ await sqlDeletePhoto(photoId).then(() => deleteStorageUrl(photoUrl));
+ revalidateAllKeysAndPaths();
+ if (shouldRedirect) {
+ redirect(PATH_ROOT);
+ }
+ });
};
export async function deletePhotoFormAction(formData: FormData) {
- return deletePhotoAction(
- formData.get('id') as string,
- formData.get('url') as string,
+ return safelyRunServerAdminAction(async () =>
+ deletePhotoAction(
+ formData.get('id') as string,
+ formData.get('url') as string,
+ )
);
};
export async function deletePhotoTagGloballyAction(formData: FormData) {
- const tag = formData.get('tag') as string;
+ return safelyRunServerAdminAction(async () => {
+ const tag = formData.get('tag') as string;
- await sqlDeletePhotoTagGlobally(tag);
+ await sqlDeletePhotoTagGlobally(tag);
- revalidatePhotosKey();
- revalidateAdminPaths();
+ revalidatePhotosKey();
+ revalidateAdminPaths();
+ });
}
export async function renamePhotoTagGloballyAction(formData: FormData) {
- const tag = formData.get('tag') as string;
- const updatedTag = formData.get('updatedTag') as string;
+ return safelyRunServerAdminAction(async () => {
+ const tag = formData.get('tag') as string;
+ const updatedTag = formData.get('updatedTag') as string;
- if (tag && updatedTag && tag !== updatedTag) {
- await sqlRenamePhotoTagGlobally(tag, updatedTag);
- revalidatePhotosKey();
- revalidateTagsKey();
- redirect(PATH_ADMIN_TAGS);
- }
+ if (tag && updatedTag && tag !== updatedTag) {
+ await sqlRenamePhotoTagGlobally(tag, updatedTag);
+ revalidatePhotosKey();
+ revalidateTagsKey();
+ redirect(PATH_ADMIN_TAGS);
+ }
+ });
}
export async function deleteBlobPhotoAction(formData: FormData) {
- await deleteStorageUrl(formData.get('url') as string);
+ return safelyRunServerAdminAction(async () => {
+ await deleteStorageUrl(formData.get('url') as string);
- revalidateAdminPaths();
+ revalidateAdminPaths();
- if (formData.get('redirectToPhotos') === 'true') {
- redirect(PATH_ADMIN_PHOTOS);
- }
+ if (formData.get('redirectToPhotos') === 'true') {
+ redirect(PATH_ADMIN_PHOTOS);
+ }
+ });
}
export async function getExifDataAction(
photoFormPrevious: Partial
,
): Promise> {
- const { url } = photoFormPrevious;
- if (url) {
- const { photoFormExif } = await extractExifDataFromBlobPath(url);
- if (photoFormExif) {
- return photoFormExif;
+ return safelyRunServerAdminAction(async () => {
+ const { url } = photoFormPrevious;
+ if (url) {
+ const { photoFormExif } = await extractExifDataFromBlobPath(url);
+ if (photoFormExif) {
+ return photoFormExif;
+ }
}
- }
- return {};
+ return {};
+ });
}
export async function syncPhotoExifDataAction(formData: FormData) {
- const photoId = formData.get('id') as string;
- if (photoId) {
- const photo = await getPhoto(photoId);
- if (photo) {
- const { photoFormExif } = await extractExifDataFromBlobPath(photo.url);
- if (photoFormExif) {
- const photoFormDbInsert = convertFormDataToPhotoDbInsert({
- ...convertPhotoToFormData(photo),
- ...photoFormExif,
- });
- await sqlUpdatePhoto(photoFormDbInsert);
- revalidatePhotosKey();
+ return safelyRunServerAdminAction(async () => {
+ const photoId = formData.get('id') as string;
+ if (photoId) {
+ const photo = await getPhoto(photoId);
+ if (photo) {
+ const { photoFormExif } = await extractExifDataFromBlobPath(photo.url);
+ if (photoFormExif) {
+ const photoFormDbInsert = convertFormDataToPhotoDbInsert({
+ ...convertPhotoToFormData(photo),
+ ...photoFormExif,
+ });
+ await sqlUpdatePhoto(photoFormDbInsert);
+ revalidatePhotosKey();
+ }
}
}
- }
+ });
}
export async function syncCacheAction() {
- revalidateAllKeysAndPaths();
+ return safelyRunServerAdminAction(revalidateAllKeysAndPaths);
}
export async function getPhotoItemsAction(query: string) {
diff --git a/src/photo/form/PhotoForm.tsx b/src/photo/form/PhotoForm.tsx
index 96f928be..37752781 100644
--- a/src/photo/form/PhotoForm.tsx
+++ b/src/photo/form/PhotoForm.tsx
@@ -168,15 +168,16 @@ export default function PhotoForm({
tagOptions,
readOnly,
validate,
+ validateStringMaxLength,
capitalize,
hideIfEmpty,
- hideBasedOnCamera,
+ shouldHide,
loadingMessage,
type,
}]) =>
(
(!hideIfEmpty || formData[key]) &&
- !hideBasedOnCamera?.(formData.make)
+ !shouldHide?.(formData)
) &&
validateStringMaxLength
+ ? `${validateStringMaxLength} characters or less`
+ : undefined,
+ });
}
if (key === 'title') {
onTitleChange?.(value.trim());
diff --git a/src/photo/form/index.ts b/src/photo/form/index.ts
index c1404a4a..078e36c6 100644
--- a/src/photo/form/index.ts
+++ b/src/photo/form/index.ts
@@ -39,10 +39,11 @@ type FormMeta = {
virtual?: boolean
readOnly?: boolean
validate?: (value?: string) => string | undefined
+ validateStringMaxLength?: number
capitalize?: boolean
hide?: boolean
hideIfEmpty?: boolean
- hideBasedOnCamera?: (make?: string, mode?: string) => boolean
+ shouldHide?: (formData: Partial) => boolean
loadingMessage?: string
type?: FieldSetType
selectOptions?: { value: string, label: string }[]
@@ -50,10 +51,29 @@ type FormMeta = {
tagOptions?: AnnotatedTag[]
};
+const STRING_MAX_LENGTH_SHORT = 255;
+const STRING_MAX_LENGTH_LONG = 1000;
+
const FORM_METADATA = (
tagOptions?: AnnotatedTag[]
): Record => ({
- title: { label: 'title', capitalize: true },
+ title: {
+ label: 'title',
+ capitalize: true,
+ validateStringMaxLength: STRING_MAX_LENGTH_SHORT,
+ },
+ caption: {
+ label: 'caption',
+ capitalize: true,
+ validateStringMaxLength: STRING_MAX_LENGTH_LONG,
+ shouldHide: ({ title, caption }) => !title && !caption,
+ },
+ semanticDescription: {
+ label: 'semantic description',
+ capitalize: true,
+ validateStringMaxLength: STRING_MAX_LENGTH_LONG,
+ hide: true,
+ },
tags: {
label: 'tags',
tagOptions,
@@ -78,7 +98,7 @@ const FORM_METADATA = (
label: 'fujifilm simulation',
selectOptions: FILM_SIMULATION_FORM_INPUT_OPTIONS,
selectOptionsDefaultLabel: 'Unknown',
- hideBasedOnCamera: make => make !== MAKE_FUJIFILM,
+ shouldHide: ({ make }) => make !== MAKE_FUJIFILM,
},
focalLength: { label: 'focal length' },
focalLengthIn35MmFormat: { label: 'focal length 35mm-equivalent' },
@@ -116,9 +136,12 @@ export const getFormErrors = (
export const isFormValid = (formData: Partial) =>
FORM_METADATA_ENTRIES().every(
- ([key, { required, validate }]) =>
+ ([key, { required, validate, validateStringMaxLength }]) =>
(!required || Boolean(formData[key])) &&
- (validate?.(formData[key]) === undefined)
+ (validate?.(formData[key]) === undefined) &&
+ // eslint-disable-next-line max-len
+ (!validateStringMaxLength || (formData[key]?.length ?? 0) <= validateStringMaxLength) &&
+ (key !== 'tags' || !doesTagsStringIncludeFavs(formData.tags ?? ''))
);
// CREATE FORM DATA: FROM PHOTO
diff --git a/src/photo/index.ts b/src/photo/index.ts
index 680221cc..a420b71d 100644
--- a/src/photo/index.ts
+++ b/src/photo/index.ts
@@ -55,6 +55,8 @@ export interface PhotoDbInsert extends PhotoExif {
extension: string
blurData?: string
title?: string
+ caption?: string
+ semanticDescription?: string
tags?: string[]
locationName?: string
priorityOrder?: number
@@ -237,16 +239,16 @@ export const dateRangeForPhotos = (
};
const photoHasCameraData = (photo: Photo) =>
- photo.make &&
- photo.model;
+ Boolean(photo.make) &&
+ Boolean(photo.model);
const photoHasExifData = (photo: Photo) =>
- photo.focalLength ||
- photo.focalLengthIn35MmFormat ||
- photo.fNumberFormatted ||
- photo.isoFormatted ||
- photo.exposureTimeFormatted ||
- photo.exposureCompensationFormatted;
+ Boolean(photo.focalLength) ||
+ Boolean(photo.focalLengthIn35MmFormat) ||
+ Boolean(photo.fNumberFormatted) ||
+ Boolean(photo.isoFormatted) ||
+ Boolean(photo.exposureTimeFormatted) ||
+ Boolean(photo.exposureCompensationFormatted);
export const shouldShowCameraDataForPhoto = (photo: Photo) =>
SHOW_EXIF_DATA && photoHasCameraData(photo);
diff --git a/src/services/vercel-postgres.ts b/src/services/vercel-postgres.ts
index 69f592e3..97edd055 100644
--- a/src/services/vercel-postgres.ts
+++ b/src/services/vercel-postgres.ts
@@ -29,6 +29,8 @@ const sqlCreatePhotosTable = () =>
aspect_ratio REAL DEFAULT 1.5,
blur_data TEXT,
title VARCHAR(255),
+ caption TEXT,
+ semantic_description TEXT,
tags VARCHAR(255)[],
make VARCHAR(255),
model VARCHAR(255),
@@ -51,9 +53,18 @@ const sqlCreatePhotosTable = () =>
)
`;
+// Migration 01
+const MIGRATION_FIELDS_01 = ['caption', 'semantic_description'];
+const sqlRunMigration01 = () =>
+ sql`
+ ALTER TABLE photos
+ ADD COLUMN IF NOT EXISTS caption TEXT,
+ ADD COLUMN IF NOT EXISTS semantic_description TEXT
+ `;
+
// Must provide id as 8-character nanoid
-export const sqlInsertPhoto = (photo: PhotoDbInsert) => {
- return sql`
+export const sqlInsertPhoto = (photo: PhotoDbInsert) =>
+ safelyQueryPhotos(() => sql`
INSERT INTO photos (
id,
url,
@@ -61,6 +72,8 @@ export const sqlInsertPhoto = (photo: PhotoDbInsert) => {
aspect_ratio,
blur_data,
title,
+ caption,
+ semantic_description,
tags,
make,
model,
@@ -86,6 +99,8 @@ export const sqlInsertPhoto = (photo: PhotoDbInsert) => {
${photo.aspectRatio},
${photo.blurData},
${photo.title},
+ ${photo.caption},
+ ${photo.semanticDescription},
${convertArrayToPostgresString(photo.tags)},
${photo.make},
${photo.model},
@@ -104,17 +119,18 @@ export const sqlInsertPhoto = (photo: PhotoDbInsert) => {
${photo.takenAt},
${photo.takenAtNaive}
)
- `;
-};
+ `, 'sqlInsertPhoto');
export const sqlUpdatePhoto = (photo: PhotoDbInsert) =>
- sql`
+ safelyQueryPhotos(() => sql`
UPDATE photos SET
url=${photo.url},
extension=${photo.extension},
aspect_ratio=${photo.aspectRatio},
blur_data=${photo.blurData},
title=${photo.title},
+ caption=${photo.caption},
+ semantic_description=${photo.semanticDescription},
tags=${convertArrayToPostgresString(photo.tags)},
make=${photo.make},
model=${photo.model},
@@ -134,27 +150,33 @@ export const sqlUpdatePhoto = (photo: PhotoDbInsert) =>
taken_at_naive=${photo.takenAtNaive},
updated_at=${(new Date()).toISOString()}
WHERE id=${photo.id}
- `;
+ `, 'sqlUpdatePhoto');
export const sqlDeletePhotoTagGlobally = (tag: string) =>
- sql`
+ safelyQueryPhotos(() => sql`
UPDATE photos
SET tags=ARRAY_REMOVE(tags, ${tag})
WHERE ${tag}=ANY(tags)
- `;
+ `, 'sqlDeletePhotoTagGlobally');
export const sqlRenamePhotoTagGlobally = (tag: string, updatedTag: string) =>
- sql`
+ safelyQueryPhotos(() => sql`
UPDATE photos
SET tags=ARRAY_REPLACE(tags, ${tag}, ${updatedTag})
WHERE ${tag}=ANY(tags)
- `;
+ `, 'sqlRenamePhotoTagGlobally');
export const sqlDeletePhoto = (id: string) =>
- sql`DELETE FROM photos WHERE id=${id}`;
+ safelyQueryPhotos(
+ () => sql`DELETE FROM photos WHERE id=${id}`,
+ 'sqlDeletePhoto',
+ );
const sqlGetPhoto = (id: string) =>
- sql`SELECT * FROM photos WHERE id=${id} LIMIT 1`;
+ safelyQueryPhotos(
+ () => sql`SELECT * FROM photos WHERE id=${id} LIMIT 1`,
+ 'sqlGetPhoto',
+ );
const sqlGetPhotosCount = async () => sql`
SELECT COUNT(*) FROM photos
@@ -287,8 +309,16 @@ const safelyQueryPhotos = async (
result = await callback();
} catch (e: any) {
screenForPPR(e, undefined, 'neon postgres');
- if (/relation "photos" does not exist/i.test(e.message)) {
- console.log('Creating table "photos" because it did not exist');
+ if (MIGRATION_FIELDS_01.some(field => new RegExp(
+ `column "${field}" of relation "photos" does not exist`,
+ 'i',
+ ).test(e.message))) {
+ console.log('Running migration 01 ...');
+ await sqlRunMigration01();
+ result = await callback();
+ } else if (/relation "photos" does not exist/i.test(e.message)) {
+ // If the table does not exist, create it
+ console.log('Creating photos table ...');
await sqlCreatePhotosTable();
result = await callback();
} else if (/endpoint is in transition/i.test(e.message)) {
diff --git a/src/tag/PhotoTag.tsx b/src/tag/PhotoTag.tsx
index df8b5349..585b849c 100644
--- a/src/tag/PhotoTag.tsx
+++ b/src/tag/PhotoTag.tsx
@@ -20,7 +20,7 @@ export default function PhotoTag({
href={pathForTag(tag)}
icon={}
type={type}
badged={badged}
diff --git a/src/tag/PhotoTags.tsx b/src/tag/PhotoTags.tsx
index dc2cbe54..ef48121e 100644
--- a/src/tag/PhotoTags.tsx
+++ b/src/tag/PhotoTags.tsx
@@ -1,21 +1,21 @@
import PhotoTag from '@/tag/PhotoTag';
import { isTagFavs } from '.';
import FavsTag from './FavsTag';
+import { EntityLinkExternalProps } from '@/components/EntityLink';
export default function PhotoTags({
tags,
- prefetch,
+ contrast,
}: {
tags: string[]
- prefetch?: boolean
-}) {
+} & EntityLinkExternalProps) {
return (
{tags.map(tag =>
{isTagFavs(tag)
- ?
- :
}
+ ?
+ :
}
)}
);