-
-
-
- {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}
@@ -153,7 +152,7 @@ export default function PhotoLarge({
shouldScroll={shouldScrollOnShare}
/>
- >, false)}
+
}
/>
);
diff --git a/src/photo/form/PhotoForm.tsx b/src/photo/form/PhotoForm.tsx
index 8db158c1..6f4e4b76 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 05608c81..89094cff 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
+ semanticDescription?: string
tags?: string[]
locationName?: string
priorityOrder?: number
@@ -229,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);
diff --git a/src/services/vercel-postgres.ts b/src/services/vercel-postgres.ts
index 7cc7cc2b..8516c62e 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,
+ semantic_description TEXT,
tags VARCHAR(255)[],
make VARCHAR(255),
model VARCHAR(255),
@@ -50,9 +52,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,
@@ -60,6 +71,8 @@ export const sqlInsertPhoto = (photo: PhotoDbInsert) => {
aspect_ratio,
blur_data,
title,
+ caption,
+ semantic_description,
tags,
make,
model,
@@ -85,6 +98,8 @@ export const sqlInsertPhoto = (photo: PhotoDbInsert) => {
${photo.aspectRatio},
${photo.blurData},
${photo.title},
+ ${photo.caption},
+ ${photo.semanticDescription},
${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},
+ semantic_description=${photo.semanticDescription},
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)) {
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)
- ?
- :
}
+ ?
+ :
}
)}
);