From 2ec32cac12bf6f9b1f796963cd25eb746439da16 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Fri, 15 Mar 2024 20:40:06 -0500 Subject: [PATCH 01/13] Add caption, description fields to Photo --- src/photo/form/index.ts | 2 ++ src/photo/index.ts | 2 ++ src/services/vercel-postgres.ts | 54 ++++++++++++++++++++++++--------- 3 files changed, 44 insertions(+), 14 deletions(-) diff --git a/src/photo/form/index.ts b/src/photo/form/index.ts index c1404a4a..5979828b 100644 --- a/src/photo/form/index.ts +++ b/src/photo/form/index.ts @@ -54,6 +54,8 @@ const FORM_METADATA = ( tagOptions?: AnnotatedTag[] ): Record => ({ title: { label: 'title', capitalize: true }, + caption: { label: 'caption', capitalize: true }, + description: { label: 'description', capitalize: true, hide: true}, tags: { label: 'tags', tagOptions, diff --git a/src/photo/index.ts b/src/photo/index.ts index 05608c81..cb4a003a 100644 --- a/src/photo/index.ts +++ b/src/photo/index.ts @@ -47,6 +47,8 @@ export interface PhotoDbInsert extends PhotoExif { extension: string blurData?: string title?: string + caption?: string + description?: string tags?: string[] locationName?: string priorityOrder?: number diff --git a/src/services/vercel-postgres.ts b/src/services/vercel-postgres.ts index 7cc7cc2b..00e41984 100644 --- a/src/services/vercel-postgres.ts +++ b/src/services/vercel-postgres.ts @@ -28,6 +28,8 @@ const sqlCreatePhotosTable = () => aspect_ratio REAL DEFAULT 1.5, blur_data TEXT, title VARCHAR(255), + caption TEXT, + description TEXT, tags VARCHAR(255)[], make VARCHAR(255), model VARCHAR(255), @@ -50,9 +52,18 @@ const sqlCreatePhotosTable = () => ) `; +// MIGRATION 01 +const MIGRATION_FIELDS_01 = ['caption', 'description']; +const sqlRunMigration01 = () => + sql` + ALTER TABLE photos + ADD COLUMN caption TEXT, + ADD COLUMN 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, @@ -60,6 +71,8 @@ export const sqlInsertPhoto = (photo: PhotoDbInsert) => { aspect_ratio, blur_data, title, + caption, + description, tags, make, model, @@ -85,6 +98,8 @@ export const sqlInsertPhoto = (photo: PhotoDbInsert) => { ${photo.aspectRatio}, ${photo.blurData}, ${photo.title}, + ${photo.caption}, + ${photo.description}, ${convertArrayToPostgresString(photo.tags)}, ${photo.make}, ${photo.model}, @@ -103,17 +118,18 @@ export const sqlInsertPhoto = (photo: PhotoDbInsert) => { ${photo.takenAt}, ${photo.takenAtNaive} ) - `; -}; + `); 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}, + description=${photo.description}, tags=${convertArrayToPostgresString(photo.tags)}, make=${photo.make}, model=${photo.model}, @@ -133,27 +149,29 @@ export const sqlUpdatePhoto = (photo: PhotoDbInsert) => taken_at_naive=${photo.takenAtNaive}, updated_at=${(new Date()).toISOString()} WHERE id=${photo.id} - `; + `); export const sqlDeletePhotoTagGlobally = (tag: string) => - sql` + safelyQueryPhotos(() => sql` UPDATE photos SET tags=ARRAY_REMOVE(tags, ${tag}) WHERE ${tag}=ANY(tags) - `; + `); export const sqlRenamePhotoTagGlobally = (tag: string, updatedTag: string) => - sql` + safelyQueryPhotos(() => sql` UPDATE photos SET tags=ARRAY_REPLACE(tags, ${tag}, ${updatedTag}) WHERE ${tag}=ANY(tags) - `; + `); export const sqlDeletePhoto = (id: string) => - sql`DELETE FROM photos WHERE id=${id}`; + safelyQueryPhotos(() => sql`DELETE FROM photos WHERE id=${id}`); const sqlGetPhoto = (id: string) => - sql`SELECT * FROM photos WHERE id=${id} LIMIT 1`; + safelyQueryPhotos(() => + sql`SELECT * FROM photos WHERE id=${id} LIMIT 1` + ); const sqlGetPhotosCount = async () => sql` SELECT COUNT(*) FROM photos @@ -291,8 +309,16 @@ const safelyQueryPhotos = async (callback: () => Promise): Promise => { try { result = await callback(); } catch (e: any) { - 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)) { From fead3d01e5ad1c498de02b49679b515913fa5bc2 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Fri, 15 Mar 2024 21:14:20 -0500 Subject: [PATCH 02/13] Hide caption field for photos without titles --- src/photo/form/PhotoForm.tsx | 4 ++-- src/photo/form/index.ts | 10 +++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/photo/form/PhotoForm.tsx b/src/photo/form/PhotoForm.tsx index 8db158c1..6cf08010 100644 --- a/src/photo/form/PhotoForm.tsx +++ b/src/photo/form/PhotoForm.tsx @@ -170,13 +170,13 @@ export default function PhotoForm({ validate, capitalize, hideIfEmpty, - hideBasedOnCamera, + shouldHide, loadingMessage, type, }]) => ( (!hideIfEmpty || formData[key]) && - !hideBasedOnCamera?.(formData.make) + !shouldHide?.(formData) ) && boolean + shouldHide?: (formData: Partial) => boolean loadingMessage?: string type?: FieldSetType selectOptions?: { value: string, label: string }[] @@ -54,7 +54,11 @@ const FORM_METADATA = ( tagOptions?: AnnotatedTag[] ): Record => ({ title: { label: 'title', capitalize: true }, - caption: { label: 'caption', capitalize: true }, + caption: { + label: 'caption', + capitalize: true, + shouldHide: ({ title, caption }) => !title && !caption, + }, description: { label: 'description', capitalize: true, hide: true}, tags: { label: 'tags', @@ -80,7 +84,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' }, From 5e50d55989030e76e341e00727a45f8293962e59 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Sat, 16 Mar 2024 12:29:30 -0500 Subject: [PATCH 03/13] Only add columns if they don't exist --- src/services/vercel-postgres.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/vercel-postgres.ts b/src/services/vercel-postgres.ts index 00e41984..1562a980 100644 --- a/src/services/vercel-postgres.ts +++ b/src/services/vercel-postgres.ts @@ -57,8 +57,8 @@ const MIGRATION_FIELDS_01 = ['caption', 'description']; const sqlRunMigration01 = () => sql` ALTER TABLE photos - ADD COLUMN caption TEXT, - ADD COLUMN description TEXT + ADD COLUMN IF NOT EXISTS caption TEXT, + ADD COLUMN IF NOT EXISTS description TEXT `; // Must provide id as 8-character nanoid From fccfbe83f5c570374305131c272fa3b558544d6c Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Sat, 16 Mar 2024 13:03:05 -0500 Subject: [PATCH 04/13] Validate photo string lengths --- src/photo/form/PhotoForm.tsx | 8 ++++++++ src/photo/form/index.ts | 25 +++++++++++++++++++++---- src/services/vercel-postgres.ts | 2 +- 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/src/photo/form/PhotoForm.tsx b/src/photo/form/PhotoForm.tsx index 6cf08010..6f4e4b76 100644 --- a/src/photo/form/PhotoForm.tsx +++ b/src/photo/form/PhotoForm.tsx @@ -168,6 +168,7 @@ export default function PhotoForm({ tagOptions, readOnly, validate, + validateStringMaxLength, capitalize, hideIfEmpty, shouldHide, @@ -189,6 +190,13 @@ export default function PhotoForm({ setFormData({ ...formData, [key]: value }); if (validate) { setFormErrors({ ...formErrors, [key]: validate(value) }); + } else if (validateStringMaxLength !== undefined) { + setFormErrors({ + ...formErrors, + [key]: value.length > 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 a1b9c7db..ec10e46a 100644 --- a/src/photo/form/index.ts +++ b/src/photo/form/index.ts @@ -39,6 +39,7 @@ type FormMeta = { virtual?: boolean readOnly?: boolean validate?: (value?: string) => string | undefined + validateStringMaxLength?: number capitalize?: boolean hide?: boolean hideIfEmpty?: boolean @@ -50,16 +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, }, - description: { label: 'description', capitalize: true, hide: true}, + description: { + label: 'description', + capitalize: true, + validateStringMaxLength: STRING_MAX_LENGTH_LONG, + hide: true, + }, tags: { label: 'tags', tagOptions, @@ -122,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/services/vercel-postgres.ts b/src/services/vercel-postgres.ts index 1562a980..4c234b05 100644 --- a/src/services/vercel-postgres.ts +++ b/src/services/vercel-postgres.ts @@ -52,7 +52,7 @@ const sqlCreatePhotosTable = () => ) `; -// MIGRATION 01 +// Migration 01 const MIGRATION_FIELDS_01 = ['caption', 'description']; const sqlRunMigration01 = () => sql` From b2e7b2902254952bd2764dbaef0b83c0c5637eda Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Sat, 16 Mar 2024 20:48:01 -0500 Subject: [PATCH 05/13] Add caption text to photo details --- src/components/EntityLink.tsx | 19 +++++++++++-- src/photo/PhotoGridSidebar.tsx | 3 ++ src/photo/PhotoLarge.tsx | 51 +++++++++++++++++----------------- src/tag/PhotoTag.tsx | 2 +- src/tag/PhotoTags.tsx | 8 ++++-- 5 files changed, 51 insertions(+), 32 deletions(-) diff --git a/src/components/EntityLink.tsx b/src/components/EntityLink.tsx index 64785490..f22ffee3 100644 --- a/src/components/EntityLink.tsx +++ b/src/components/EntityLink.tsx @@ -17,7 +17,7 @@ export default function EntityLink({ title, type = 'icon-first', badged, - contrast, + contrast = 'high', hoverEntity, }: { label: ReactNode @@ -36,6 +36,17 @@ export default function EntityLink({ ; + const classForContrast = () => { + switch (contrast) { + case 'low': + return 'text-dim'; + case 'high': + return 'text-main'; + default: + return 'text-medium'; + } + }; + return ( {type !== 'icon-only' && <> @@ -67,7 +78,9 @@ export default function EntityLink({ : )} />} @@ -60,6 +62,7 @@ export default function PhotoGridSidebar({ camera={camera} type="text-only" countOnHover={count} + contrast="low" hideAppleIcon badged />)} diff --git a/src/photo/PhotoLarge.tsx b/src/photo/PhotoLarge.tsx index ecfe79ac..f37ac43c 100644 --- a/src/photo/PhotoLarge.tsx +++ b/src/photo/PhotoLarge.tsx @@ -44,16 +44,6 @@ 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} -
; return ( - {renderMiniGrid(<> -
+ {/* Meta */} +
+
+ {photo.caption && <> +
+ {photo.caption} +
+ {tags.length > 0 &&
} + } {tags.length > 0 && - } + }
+
+ {/* EXIF: Camera + Film Simulation */} +
{showCamera && shouldShowCameraDataForPhoto(photo) &&
}
} - )} - {renderMiniGrid(<> +
+ {/* EXIF: Details */} +
{shouldShowExifDataForPhoto(photo) &&
  • @@ -132,13 +130,16 @@ export default function PhotoLarge({
  • {photo.isoFormatted}
  • {photo.exposureCompensationFormatted ?? '—'}
} +
+ {/* Date + Share */} +
{photo.takenAtNaiveFormatted}
@@ -153,7 +154,7 @@ export default function PhotoLarge({ shouldScroll={shouldScrollOnShare} />
- , false)} +
} /> ); diff --git a/src/tag/PhotoTag.tsx b/src/tag/PhotoTag.tsx index ecc4efaf..56fab6a6 100644 --- a/src/tag/PhotoTag.tsx +++ b/src/tag/PhotoTag.tsx @@ -19,7 +19,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 4804dc17..ef48121e 100644 --- a/src/tag/PhotoTags.tsx +++ b/src/tag/PhotoTags.tsx @@ -1,19 +1,21 @@ import PhotoTag from '@/tag/PhotoTag'; import { isTagFavs } from '.'; import FavsTag from './FavsTag'; +import { EntityLinkExternalProps } from '@/components/EntityLink'; export default function PhotoTags({ tags, + contrast, }: { tags: string[] -}) { +} & EntityLinkExternalProps) { return (
{tags.map(tag =>
{isTagFavs(tag) - ? - : } + ? + : }
)}
); From 4b4e169febdbec8595dcb6b1e3f68e65803208b8 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Sat, 16 Mar 2024 22:25:00 -0500 Subject: [PATCH 06/13] Refine new content positioning --- src/photo/PhotoLarge.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/photo/PhotoLarge.tsx b/src/photo/PhotoLarge.tsx index f37ac43c..8e0d3275 100644 --- a/src/photo/PhotoLarge.tsx +++ b/src/photo/PhotoLarge.tsx @@ -63,6 +63,7 @@ export default function PhotoLarge({ 'sticky top-4 self-start -translate-y-1', 'grid grid-cols-2 sm:grid-cols-4 md:grid-cols-1', 'gap-y-4', + 'pb-4', )}> {/* Meta */}
@@ -134,12 +135,11 @@ export default function PhotoLarge({ {/* Date + Share */}
{photo.takenAtNaiveFormatted}
From aa52ca42619449c165647a8b02df7e164c9893b3 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Sat, 16 Mar 2024 23:02:24 -0500 Subject: [PATCH 07/13] Refine photo detail --- src/photo/PhotoLarge.tsx | 179 +++++++++++++++++++++------------------ 1 file changed, 95 insertions(+), 84 deletions(-) diff --git a/src/photo/PhotoLarge.tsx b/src/photo/PhotoLarge.tsx index 8e0d3275..3e704dd5 100644 --- a/src/photo/PhotoLarge.tsx +++ b/src/photo/PhotoLarge.tsx @@ -16,7 +16,7 @@ import { cameraFromPhoto } from '@/camera'; import PhotoFilmSimulation from '@/simulation/PhotoFilmSimulation'; import { sortTags } from '@/tag'; import AdminPhotoMenu from '@/admin/AdminPhotoMenu'; -import { Suspense } from 'react'; +import { Suspense, useMemo } from 'react'; export default function PhotoLarge({ photo, @@ -45,6 +45,17 @@ export default function PhotoLarge({ const camera = cameraFromPhoto(photo); + const showCameraRow = showCamera && shouldShowCameraDataForPhoto(photo); + + const showExifRow = shouldShowExifDataForPhoto(photo); + + const rowCount = useMemo(() => { + let count = 1; + if (showCameraRow) { count++; } + if (showExifRow) { count++; } + return count; + }, [showCameraRow, showExifRow]); + return ( *:not(:last-child)]:pr-3', )}> {/* Meta */} -
-
-
-
- - {titleForPhoto(photo)} - -
- -
- -
-
+
+
+
+ + {titleForPhoto(photo)} +
- {photo.caption && <> -
- {photo.caption} + +
+
- {tags.length > 0 &&
} - } - {tags.length > 0 && - } +
+ {photo.caption && <> +
+ {photo.caption} +
+ {tags.length > 0 && +
} + } + {tags.length > 0 && + }
{/* EXIF: Camera + Film Simulation */} -
- {showCamera && shouldShowCameraDataForPhoto(photo) && -
- - {showSimulation && photo.filmSimulation && -
- -
} -
} -
- {/* EXIF: Details */} -
- {shouldShowExifDataForPhoto(photo) && -
    -
  • - {photo.focalLengthFormatted} - {photo.focalLengthIn35MmFormatFormatted && - <> - {' '} - - {photo.focalLengthIn35MmFormatFormatted} - - } -
  • -
  • {photo.fNumberFormatted}
  • -
  • {photo.exposureTimeFormatted}
  • -
  • {photo.isoFormatted}
  • -
  • {photo.exposureCompensationFormatted ?? '—'}
  • -
} -
- {/* Date + Share */} -
-
-
- {photo.takenAtNaiveFormatted} -
- + + {showSimulation && photo.filmSimulation && +
+ +
} +
} + {/* EXIF: Details */} + {showExifRow && +
    +
  • + {photo.focalLengthFormatted} + {photo.focalLengthIn35MmFormatFormatted && + <> + {' '} + + {photo.focalLengthIn35MmFormatFormatted} + + } +
  • +
  • {photo.fNumberFormatted}
  • +
  • {photo.exposureTimeFormatted}
  • +
  • {photo.isoFormatted}
  • +
  • {photo.exposureCompensationFormatted ?? '—'}
  • +
} + {/* Date + Share */} +
+
+ {photo.takenAtNaiveFormatted}
+
} /> From a6ba4f8257cc45d03e89d1d3d750f30898a7e54b Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Sun, 17 Mar 2024 19:39:37 -0500 Subject: [PATCH 08/13] Change db field: description to semantic_description --- src/photo/form/index.ts | 4 ++-- src/photo/index.ts | 2 +- src/services/vercel-postgres.ts | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/photo/form/index.ts b/src/photo/form/index.ts index ec10e46a..078e36c6 100644 --- a/src/photo/form/index.ts +++ b/src/photo/form/index.ts @@ -68,8 +68,8 @@ const FORM_METADATA = ( validateStringMaxLength: STRING_MAX_LENGTH_LONG, shouldHide: ({ title, caption }) => !title && !caption, }, - description: { - label: 'description', + semanticDescription: { + label: 'semantic description', capitalize: true, validateStringMaxLength: STRING_MAX_LENGTH_LONG, hide: true, diff --git a/src/photo/index.ts b/src/photo/index.ts index cb4a003a..5156e4b4 100644 --- a/src/photo/index.ts +++ b/src/photo/index.ts @@ -48,7 +48,7 @@ export interface PhotoDbInsert extends PhotoExif { blurData?: string title?: string caption?: string - description?: string + semanticDescription?: string tags?: string[] locationName?: string priorityOrder?: number diff --git a/src/services/vercel-postgres.ts b/src/services/vercel-postgres.ts index 4c234b05..8516c62e 100644 --- a/src/services/vercel-postgres.ts +++ b/src/services/vercel-postgres.ts @@ -29,7 +29,7 @@ const sqlCreatePhotosTable = () => blur_data TEXT, title VARCHAR(255), caption TEXT, - description TEXT, + semantic_description TEXT, tags VARCHAR(255)[], make VARCHAR(255), model VARCHAR(255), @@ -53,12 +53,12 @@ const sqlCreatePhotosTable = () => `; // Migration 01 -const MIGRATION_FIELDS_01 = ['caption', 'description']; +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 description TEXT + ADD COLUMN IF NOT EXISTS semantic_description TEXT `; // Must provide id as 8-character nanoid @@ -72,7 +72,7 @@ export const sqlInsertPhoto = (photo: PhotoDbInsert) => blur_data, title, caption, - description, + semantic_description, tags, make, model, @@ -99,7 +99,7 @@ export const sqlInsertPhoto = (photo: PhotoDbInsert) => ${photo.blurData}, ${photo.title}, ${photo.caption}, - ${photo.description}, + ${photo.semanticDescription}, ${convertArrayToPostgresString(photo.tags)}, ${photo.make}, ${photo.model}, @@ -129,7 +129,7 @@ export const sqlUpdatePhoto = (photo: PhotoDbInsert) => blur_data=${photo.blurData}, title=${photo.title}, caption=${photo.caption}, - description=${photo.description}, + semantic_description=${photo.semanticDescription}, tags=${convertArrayToPostgresString(photo.tags)}, make=${photo.make}, model=${photo.model}, From 031a1498935ee5dd2e4c3a9d319025b24bba35c8 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Sun, 17 Mar 2024 22:44:26 -0500 Subject: [PATCH 09/13] Refine large photo layout --- src/camera/PhotoCamera.tsx | 13 +-- src/photo/PhotoLarge.tsx | 167 +++++++++++++++++-------------------- 2 files changed, 80 insertions(+), 100 deletions(-) diff --git a/src/camera/PhotoCamera.tsx b/src/camera/PhotoCamera.tsx index 15f58ede..1f9eb84c 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, @@ -27,18 +26,12 @@ export default function PhotoCamera({ icon={showAppleIcon ? : } type={showAppleIcon && isCameraApple ? 'icon-first' : type} badged={badged} diff --git a/src/photo/PhotoLarge.tsx b/src/photo/PhotoLarge.tsx index 3e704dd5..3f999b2e 100644 --- a/src/photo/PhotoLarge.tsx +++ b/src/photo/PhotoLarge.tsx @@ -16,7 +16,7 @@ import { cameraFromPhoto } from '@/camera'; import PhotoFilmSimulation from '@/simulation/PhotoFilmSimulation'; import { sortTags } from '@/tag'; import AdminPhotoMenu from '@/admin/AdminPhotoMenu'; -import { Suspense, useMemo } from 'react'; +import { Suspense } from 'react'; export default function PhotoLarge({ photo, @@ -49,13 +49,6 @@ export default function PhotoLarge({ const showExifRow = shouldShowExifDataForPhoto(photo); - const rowCount = useMemo(() => { - let count = 1; - if (showCameraRow) { count++; } - if (showExifRow) { count++; } - return count; - }, [showCameraRow, showExifRow]); - return ( *:not(:last-child)]:pr-3', )}> {/* Meta */} -
-
-
- - {titleForPhoto(photo)} - -
- -
- +
+
+
+
+ + {titleForPhoto(photo)} +
- -
- {photo.caption && <> -
- {photo.caption} + +
+ +
+
- {tags.length > 0 && -
} - } - {tags.length > 0 && - } + {photo.caption && +
+ {photo.caption} +
} +
+ {(showCameraRow || tags.length > 0) && +
+ {showCameraRow && + } + {tags.length > 0 && + } +
}
- {/* EXIF: Camera + Film Simulation */} - {showCameraRow && -
- - {showSimulation && photo.filmSimulation && -
+ {/* EXIF Data */} +
+ {showExifRow && + <> +
    +
  • + {photo.focalLengthFormatted} + {photo.focalLengthIn35MmFormatFormatted && + <> + {' '} + + {photo.focalLengthIn35MmFormatFormatted} + + } +
  • +
  • {photo.fNumberFormatted}
  • +
  • {photo.exposureTimeFormatted}
  • +
  • {photo.isoFormatted}
  • +
  • {photo.exposureCompensationFormatted ?? '0ev'}
  • +
+ {showSimulation && photo.filmSimulation && -
} -
} - {/* EXIF: Details */} - {showExifRow && -
    -
  • - {photo.focalLengthFormatted} - {photo.focalLengthIn35MmFormatFormatted && - <> - {' '} - - {photo.focalLengthIn35MmFormatFormatted} - - } -
  • -
  • {photo.fNumberFormatted}
  • -
  • {photo.exposureTimeFormatted}
  • -
  • {photo.isoFormatted}
  • -
  • {photo.exposureCompensationFormatted ?? '—'}
  • -
} - {/* Date + Share */} -
+ />} + }
- {photo.takenAtNaiveFormatted} +
+ {photo.takenAtNaiveFormatted} +
+
-
} /> From f940798c4dd8233053ea4d0f3b40149b19b25964 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Sun, 17 Mar 2024 23:41:01 -0500 Subject: [PATCH 10/13] Finalize photo detail page with captions --- src/components/EntityLink.tsx | 2 +- src/photo/PhotoLarge.tsx | 62 +++++++++++++++++------------------ src/photo/index.ts | 16 ++++----- 3 files changed, 40 insertions(+), 40 deletions(-) diff --git a/src/components/EntityLink.tsx b/src/components/EntityLink.tsx index f22ffee3..da271fd6 100644 --- a/src/components/EntityLink.tsx +++ b/src/components/EntityLink.tsx @@ -48,7 +48,7 @@ export default function EntityLink({ }; return ( - + 0; + const showExifContent = shouldShowExifDataForPhoto(photo); return ( } contentSide={
*:not(:last-child)]:pr-3', )}> {/* Meta */} -
-
-
-
- - {titleForPhoto(photo)} - -
- -
- -
-
+
+
+
+ + {titleForPhoto(photo)} +
+ +
+ +
+
+
+
{photo.caption &&
{photo.caption}
} + {(showCameraContent || showTagsContent) && +
+ {showCameraContent && + } + {showTagsContent && + } +
}
- {(showCameraRow || tags.length > 0) && -
- {showCameraRow && - } - {tags.length > 0 && - } -
}
{/* EXIF Data */}
- {showExifRow && + {showExifContent && <>
  • diff --git a/src/photo/index.ts b/src/photo/index.ts index 5156e4b4..89094cff 100644 --- a/src/photo/index.ts +++ b/src/photo/index.ts @@ -231,16 +231,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); From 75fd60bd2eaf3d879b55edabd504678466b1bbc4 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Mon, 18 Mar 2024 09:09:21 -0500 Subject: [PATCH 11/13] Refine /grid sidebar layout --- src/app/grid/page.tsx | 2 +- src/app/sets/page.tsx | 45 ----------------------------------- src/components/HeaderList.tsx | 7 ++++-- 3 files changed, 6 insertions(+), 48 deletions(-) delete mode 100644 src/app/sets/page.tsx diff --git a/src/app/grid/page.tsx b/src/app/grid/page.tsx index d26d83aa..54a2697b 100644 --- a/src/app/grid/page.tsx +++ b/src/app/grid/page.tsx @@ -42,7 +42,7 @@ export default async function GridPage({ searchParams }: PaginationParams) { photos.length > 0 ? } - 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/components/HeaderList.tsx b/src/components/HeaderList.tsx index 8868b487..9abc7f98 100644 --- a/src/components/HeaderList.tsx +++ b/src/components/HeaderList.tsx @@ -15,7 +15,10 @@ export default function HeaderList({ }) { return ( From e36f09185d3ceb3ed04d394bf4fde258099add38 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Mon, 18 Mar 2024 09:14:54 -0500 Subject: [PATCH 12/13] Fix /grid sticky sidebar --- src/app/grid/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/grid/page.tsx b/src/app/grid/page.tsx index 54a2697b..0b896071 100644 --- a/src/app/grid/page.tsx +++ b/src/app/grid/page.tsx @@ -42,7 +42,7 @@ export default async function GridPage({ searchParams }: PaginationParams) { photos.length > 0 ? } - contentSide={
    + contentSide={
    Date: Tue, 19 Mar 2024 12:34:09 -0500 Subject: [PATCH 13/13] Update next-auth --- package.json | 2 +- pnpm-lock.yaml | 14 ++-- src/auth/index.ts | 11 +++ src/photo/actions.ts | 163 ++++++++++++++++++++++++------------------- 4 files changed, 111 insertions(+), 79 deletions(-) diff --git a/package.json b/package.json index 4e81a298..86939297 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "jest-environment-jsdom": "^29.7.0", "nanoid": "^5.0.6", "next": "14.1.3", - "next-auth": "5.0.0-beta.13", + "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 7f56bdd4..5f59b382 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -99,8 +99,8 @@ dependencies: specifier: 14.1.3 version: 14.1.3(@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.1.3)(react@18.2.0) + specifier: 5.0.0-beta.15 + version: 5.0.0-beta.15(next@14.1.3)(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 @@ -6052,8 +6052,8 @@ packages: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} dev: false - /next-auth@5.0.0-beta.13(next@14.1.3)(react@18.2.0): - resolution: {integrity: sha512-2m2Gq69WQ0YXcHCCpHn2y5z1bxSlqD/XOuAgrdtz49/VIAdTFFeYZz97RYqf6xMF8VGmoG32VUnJ6LzaHk6Fwg==} + /next-auth@5.0.0-beta.15(next@14.1.3)(react@18.2.0): + resolution: {integrity: sha512-UQggNq8CDu3/w8CYkihKLLnRPNXel98K0j7mtjj9a6XTNYo4Hni8xg/2h1YhElW6vXE8mgtvmH11rU8NKw86jQ==} peerDependencies: '@simplewebauthn/browser': ^9.0.1 '@simplewebauthn/server': ^9.0.2 @@ -6068,7 +6068,7 @@ packages: nodemailer: optional: true dependencies: - '@auth/core': 0.27.0 + '@auth/core': 0.28.0 next: 14.1.3(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 dev: false 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/photo/actions.ts b/src/photo/actions.ts index fe2bb913..7b2033e7 100644 --- a/src/photo/actions.ts +++ b/src/photo/actions.ts @@ -33,47 +33,54 @@ import { import { extractExifDataFromBlobPath } from './server'; import { TAG_FAVS, isTagFavs } from '@/tag'; import { convertPhotoToPhotoDbInsert } 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); - revalidateAllKeysAndPaths(); + revalidateAllKeysAndPaths(); - 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( @@ -81,82 +88,96 @@ 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); }