diff --git a/src/components/FieldSetWithStatus.tsx b/src/components/FieldSetWithStatus.tsx index cdd8d15e..97aababc 100644 --- a/src/components/FieldSetWithStatus.tsx +++ b/src/components/FieldSetWithStatus.tsx @@ -6,6 +6,7 @@ import { experimental_useFormStatus as useFormStatus } from 'react-dom'; export default function FieldSetWithStatus({ id, label, + note, value, onChange, required, @@ -15,6 +16,7 @@ export default function FieldSetWithStatus({ }: { id: string label: string + note?: string value: string onChange?: (value: string) => void required?: boolean @@ -31,9 +33,13 @@ export default function FieldSetWithStatus({ htmlFor={id} > {label} + {note && + + ({note}) + } {required && - (Required) + Required } {FORM_METADATA_ENTRIES.map(([ key, - { label, required, readOnly, hideIfEmpty }, + { label, note, required, readOnly, hideIfEmpty }, ]) => (!hideIfEmpty || formData[key]) && setFormData({ ...formData, [key]: value })} required={required} diff --git a/src/photo/PhotoLarge.tsx b/src/photo/PhotoLarge.tsx index 6ca27b9c..148d4737 100644 --- a/src/photo/PhotoLarge.tsx +++ b/src/photo/PhotoLarge.tsx @@ -5,6 +5,7 @@ import { cc } from '@/utility/css'; import Link from 'next/link'; import { routeForPhoto } from '@/site/routes'; import SharePhotoButton from './SharePhotoButton'; +import { FaTag } from 'react-icons/fa'; export default function PhotoLarge({ photo, @@ -52,6 +53,17 @@ export default function PhotoLarge({ > {titleForPhoto(photo)} + {photo.tags.length > 0 && + + {photo.tags.map(tag => + + + {tag} + )} + } {photo.make} {photo.model} diff --git a/src/photo/form.ts b/src/photo/form.ts index cba347ba..3ca7d40e 100644 --- a/src/photo/form.ts +++ b/src/photo/form.ts @@ -6,19 +6,22 @@ import { } from '@/utility/date'; import { getOffsetFromExif } from '@/utility/exif'; import { toFixedNumber } from '@/utility/number'; +import { convertStringToArray } from '@/utility/string'; export type PhotoFormData = Record; type FormMeta = { - label: string, - required?: boolean, - readOnly?: boolean, - hideIfEmpty?: boolean, - hideTemporarily?: boolean, + label: string + note?: string + required?: boolean + readOnly?: boolean + hideIfEmpty?: boolean + hideTemporarily?: boolean }; const FORM_METADATA: Record = { title: { label: 'title' }, + tags: { label: 'tags', note: 'comma-separated values' }, id: { label: 'id', readOnly: true, hideIfEmpty: true }, idShort: { label: 'short id', readOnly: true, hideIfEmpty: true }, url: { label: 'url', readOnly: true }, @@ -51,6 +54,8 @@ export const convertPhotoToFormData = ( ): PhotoFormData => { const valueForKey = (key: keyof Photo, value: any) => { switch (key) { + case 'tags': + return value?.join ? value.join(', ') : value; case 'takenAt': return value?.toISOString ? value.toISOString() : value; default: @@ -106,6 +111,8 @@ export const convertFormDataToPhoto = ( return { ...photoForm, + // convert form strings to arrays + tags: convertStringToArray(photoForm.tags), // Convert form strings to numbers aspectRatio: toFixedNumber(parseFloat(photoForm.aspectRatio), 6), focalLength: photoForm.focalLength diff --git a/src/photo/index.ts b/src/photo/index.ts index c550f054..419b7d5b 100644 --- a/src/photo/index.ts +++ b/src/photo/index.ts @@ -42,15 +42,17 @@ export interface PhotoDbInsert extends PhotoExif { extension: string blurData: string title?: string + tags?: string[] locationName?: string priorityOrder?: number } // Raw db response -export interface PhotoDb extends Omit { +export interface PhotoDb extends Omit { updatedAt: Date createdAt: Date takenAt: Date + tags: string[] } // Parsed db response @@ -72,6 +74,7 @@ export const parsePhotoFromDb = (photoDbRaw: PhotoDb): Photo => { ...photoDb, idShort: translator.fromUUID(photoDb.id), + tags: photoDb.tags ?? [], focalLengthFormatted: formatFocalLength(photoDb.focalLength), focalLengthIn35MmFormatFormatted: diff --git a/src/services/postgres.ts b/src/services/postgres.ts index aabd7eb9..665c1e5c 100644 --- a/src/services/postgres.ts +++ b/src/services/postgres.ts @@ -10,6 +10,10 @@ import { isValidUUID } from '@/utility/string'; const PHOTO_DEFAULT_LIMIT = 100; +export const convertArrayToPostgresString = (array?: string[]) => array + ? `{${array.join(',')}}` + : null; + const sqlCreatePhotosTable = () => sql` CREATE TABLE IF NOT EXISTS photos ( @@ -19,6 +23,7 @@ const sqlCreatePhotosTable = () => aspect_ratio REAL DEFAULT 1.5, blur_data TEXT, title VARCHAR(255), + tags VARCHAR(255)[], make VARCHAR(255), model VARCHAR(255), focal_length SMALLINT, @@ -47,6 +52,7 @@ export const sqlInsertPhotoIntoDb = (photo: PhotoDbInsert) => { aspect_ratio, blur_data, title, + tags, make, model, focal_length, @@ -69,6 +75,7 @@ export const sqlInsertPhotoIntoDb = (photo: PhotoDbInsert) => { ${photo.aspectRatio}, ${photo.blurData}, ${photo.title}, + ${convertArrayToPostgresString(photo.tags)}, ${photo.make}, ${photo.model}, ${photo.focalLength}, @@ -96,6 +103,7 @@ export const sqlUpdatePhotoInDb = (photo: PhotoDbInsert) => aspect_ratio=${photo.aspectRatio}, blur_data=${photo.blurData}, title=${photo.title}, + tags=${convertArrayToPostgresString(photo.tags)}, make=${photo.make}, model=${photo.model}, focal_length=${photo.focalLength}, diff --git a/src/utility/string.ts b/src/utility/string.ts index efd8fd76..81db7216 100644 --- a/src/utility/string.ts +++ b/src/utility/string.ts @@ -1,2 +1,11 @@ export const isValidUUID = (id: string): boolean => - /^[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}$/i.test(id); \ No newline at end of file + /^[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}$/i.test(id); + +export const convertStringToArray = ( + string?: string, + parameterize = true, +) => string + ? string.split(',').map(tag => parameterize + ? tag.trim().replaceAll(' ', '-').toLowerCase() + : tag.trim()) + : undefined;