diff --git a/src/app/admin/tags/[tag]/edit/page.tsx b/src/app/admin/tags/[tag]/edit/page.tsx index ed45870a..2cfb7f5e 100644 --- a/src/app/admin/tags/[tag]/edit/page.tsx +++ b/src/app/admin/tags/[tag]/edit/page.tsx @@ -15,7 +15,11 @@ interface Props { params: { tag: string } } -export default async function PhotoPageEdit({ params: { tag } }: Props) { +export default async function PhotoPageEdit({ + params: { tag: tagFromParams } }: Props +) { + const tag = decodeURIComponent(tagFromParams); + const [ count, photos, diff --git a/src/app/tag/[tag]/page.tsx b/src/app/tag/[tag]/page.tsx index b006195d..bdb72d70 100644 --- a/src/app/tag/[tag]/page.tsx +++ b/src/app/tag/[tag]/page.tsx @@ -6,15 +6,17 @@ import { getPhotosTagDataCached, getPhotosTagDataCachedWithPagination, } from '@/tag/data'; -import { Metadata } from 'next/types'; +import type { Metadata } from 'next'; interface TagProps { params: { tag: string } } export async function generateMetadata({ - params: { tag }, + params: { tag: tagFromParams }, }: TagProps): Promise { + const tag = decodeURIComponent(tagFromParams); + const [ photos, count, @@ -49,9 +51,11 @@ export async function generateMetadata({ } export default async function TagPage({ - params: { tag }, + params: { tag: tagFromParams }, searchParams, }:TagProps & PaginationParams) { + const tag = decodeURIComponent(tagFromParams); + const { photos, count, diff --git a/src/app/tag/[tag]/share/page.tsx b/src/app/tag/[tag]/share/page.tsx index 603b9cef..7b6085ca 100644 --- a/src/app/tag/[tag]/share/page.tsx +++ b/src/app/tag/[tag]/share/page.tsx @@ -7,15 +7,17 @@ import { getPhotosTagDataCached, getPhotosTagDataCachedWithPagination, } from '@/tag/data'; -import { Metadata } from 'next/types'; +import type { Metadata } from 'next'; interface TagProps { params: { tag: string } } export async function generateMetadata({ - params: { tag }, + params: { tag: tagFromParams }, }: TagProps): Promise { + const tag = decodeURIComponent(tagFromParams); + const [ photos, count, @@ -50,9 +52,11 @@ export async function generateMetadata({ } export default async function Share({ - params: { tag }, + params: { tag: tagFromParams }, searchParams, }: TagProps & PaginationParams) { + const tag = decodeURIComponent(tagFromParams); + const { photos, count, diff --git a/src/camera/CameraOverview.tsx b/src/camera/CameraOverview.tsx index 8128747d..0c058030 100644 --- a/src/camera/CameraOverview.tsx +++ b/src/camera/CameraOverview.tsx @@ -16,7 +16,7 @@ export default function CameraOverview({ camera: Camera, photos: Photo[], count: number, - dateRange: PhotoDateRange, + dateRange?: PhotoDateRange, showMorePath?: string, animateOnFirstLoadOnly?: boolean, }) { diff --git a/src/camera/CameraShareModal.tsx b/src/camera/CameraShareModal.tsx index 1dd33484..0bdb0884 100644 --- a/src/camera/CameraShareModal.tsx +++ b/src/camera/CameraShareModal.tsx @@ -13,7 +13,7 @@ export default function CameraShareModal({ camera: Camera photos: Photo[] count: number - dateRange: PhotoDateRange, + dateRange?: PhotoDateRange, }) { return ( - parameterize(`${make}-${model}`); + parameterize(`${make}-${model}`, true); // Assumes no makes ('Fujifilm,' 'Apple,' 'Canon', etc.) have dashes export const getCameraFromKey = (cameraKey: string): Camera => { diff --git a/src/components/TagInput.tsx b/src/components/TagInput.tsx index b5f6bc1d..92cfc680 100644 --- a/src/components/TagInput.tsx +++ b/src/components/TagInput.tsx @@ -72,11 +72,11 @@ export default function TagInput({ onChange?.([ ...selectedOptions, option.startsWith(CREATE_LABEL) - ? option.slice(CREATE_LABEL.length, -1) + ? option.match(new RegExp(`^${CREATE_LABEL} "(.+)"$`))?.[1] ?? option : option, ] .filter(Boolean) - .map(parameterize) + .map(item => parameterize(item)) .join(',')); } setSelectedOptionIndex(undefined); diff --git a/src/photo/actions.ts b/src/photo/actions.ts index a7aaba88..3a2c42cf 100644 --- a/src/photo/actions.ts +++ b/src/photo/actions.ts @@ -22,6 +22,7 @@ import { revalidateAdminPaths, revalidateAllKeysAndPaths, revalidatePhotosKey, + revalidateTagsKey, } from '@/photo/cache'; import { PATH_ADMIN_PHOTOS, PATH_ADMIN_TAGS, PATH_ROOT } from '@/site/paths'; import { extractExifDataFromBlobPath } from './server'; @@ -105,6 +106,7 @@ export async function renamePhotoTagGloballyAction(formData: FormData) { if (tag && updatedTag && tag !== updatedTag) { await sqlRenamePhotoTagGlobally(tag, updatedTag); revalidatePhotosKey(); + revalidateTagsKey(); redirect(PATH_ADMIN_TAGS); } } diff --git a/src/services/vercel-postgres.ts b/src/services/vercel-postgres.ts index 7544de0d..c143b52c 100644 --- a/src/services/vercel-postgres.ts +++ b/src/services/vercel-postgres.ts @@ -174,8 +174,8 @@ const sqlGetPhotosTagCount = async (tag: string) => sql` const sqlGetPhotosCameraCount = async (camera: Camera) => sql` SELECT COUNT(*) FROM photos WHERE - LOWER(make)=${parameterize(camera.make)} AND - LOWER(REPLACE(model, ' ', '-'))=${parameterize(camera.model)} AND + LOWER(make)=${parameterize(camera.make, true)} AND + LOWER(REPLACE(model, ' ', '-'))=${parameterize(camera.model, true)} AND hidden IS NOT TRUE `.then(({ rows }) => parseInt(rows[0].count, 10)); @@ -191,23 +191,29 @@ const sqlGetPhotosDateRange = async () => sql` SELECT MIN(taken_at_naive) as start, MAX(taken_at_naive) as end FROM photos WHERE hidden IS NOT TRUE -`.then(({ rows }) => rows[0] as PhotoDateRange); +`.then(({ rows }) => rows[0]?.start && rows[0]?.end + ? rows[0] as PhotoDateRange + : undefined); const sqlGetPhotosTagDateRange = async (tag: string) => sql` SELECT MIN(taken_at_naive) as start, MAX(taken_at_naive) as end FROM photos WHERE ${tag}=ANY(tags) AND hidden IS NOT TRUE -`.then(({ rows }) => rows[0] as PhotoDateRange); +`.then(({ rows }) => rows[0]?.start && rows[0]?.end + ? rows[0] as PhotoDateRange + : undefined); const sqlGetPhotosCameraDateRange = async (camera: Camera) => sql` SELECT MIN(taken_at_naive) as start, MAX(taken_at_naive) as end FROM photos WHERE - LOWER(make)=${parameterize(camera.make)} AND - LOWER(REPLACE(model, ' ', '-'))=${parameterize(camera.model)} AND + LOWER(make)=${parameterize(camera.make, true)} AND + LOWER(REPLACE(model, ' ', '-'))=${parameterize(camera.model, true)} AND hidden IS NOT TRUE -`.then(({ rows }) => rows[0] as PhotoDateRange); +`.then(({ rows }) => rows[0]?.start && rows[0]?.end + ? rows[0] as PhotoDateRange + : undefined); const sqlGetPhotosFilmSimulationDateRange = async ( simulation: FilmSimulation, @@ -216,7 +222,9 @@ const sqlGetPhotosFilmSimulationDateRange = async ( FROM photos WHERE film_simulation=${simulation} AND hidden IS NOT TRUE -`.then(({ rows }) => rows[0] as PhotoDateRange); +`.then(({ rows }) => rows[0]?.start && rows[0]?.end + ? rows[0] as PhotoDateRange + : undefined); const sqlGetUniqueTags = async () => sql` SELECT DISTINCT unnest(tags) as tag, COUNT(*) @@ -349,8 +357,8 @@ export const getPhotos = async (options: GetPhotosOptions = {}) => { if (camera) { wheres.push(`LOWER(make)=$${valueIndex++}`); wheres.push(`LOWER(REPLACE(model, ' ', '-'))=$${valueIndex++}`); - values.push(parameterize(camera.make)); - values.push(parameterize(camera.model)); + values.push(parameterize(camera.make, true)); + values.push(parameterize(camera.model, true)); } if (simulation) { wheres.push(`film_simulation=$${valueIndex++}`); diff --git a/src/simulation/FilmSimulationOverview.tsx b/src/simulation/FilmSimulationOverview.tsx index acb32584..bd1cbb9f 100644 --- a/src/simulation/FilmSimulationOverview.tsx +++ b/src/simulation/FilmSimulationOverview.tsx @@ -16,7 +16,7 @@ export default function FilmSimulationOverview({ simulation: FilmSimulation, photos: Photo[], count: number, - dateRange: PhotoDateRange, + dateRange?: PhotoDateRange, showMorePath?: string, animateOnFirstLoadOnly?: boolean, }) { diff --git a/src/tag/TagOverview.tsx b/src/tag/TagOverview.tsx index e81111cb..505ef098 100644 --- a/src/tag/TagOverview.tsx +++ b/src/tag/TagOverview.tsx @@ -15,7 +15,7 @@ export default function TagOverview({ tag: string, photos: Photo[], count: number, - dateRange: PhotoDateRange, + dateRange?: PhotoDateRange, showMorePath?: string, animateOnFirstLoadOnly?: boolean, }) { diff --git a/src/utility/string.ts b/src/utility/string.ts index 64bb94bd..5c597cde 100644 --- a/src/utility/string.ts +++ b/src/utility/string.ts @@ -2,9 +2,9 @@ export const convertStringToArray = ( string?: string, shouldParameterize = true, ) => string - ? string.split(',').map(tag => shouldParameterize - ? parameterize(tag) - : tag.trim()) + ? string.split(',').map(item => shouldParameterize + ? parameterize(item) + : item.trim()) : undefined; export const capitalize = (string: string) => @@ -16,14 +16,22 @@ export const capitalizeWords = (string = '') => .map(capitalize) .join(' '); -export const parameterize = (string: string) => +export const parameterize = ( + string: string, + shouldRemoveNonAlphanumeric?: boolean, +) => string .trim() // Replaces spaces, underscores, and dashes with dashes .replaceAll(/[\s_–—]/gi, '-') // Removes all non-alphanumeric characters - .replaceAll(/([^a-z0-9-])/gi, '') - .toLowerCase(); + .replaceAll( + shouldRemoveNonAlphanumeric + ? /([^a-z0-9-])/gi + : /''/gi, + '', + ) + .toLocaleLowerCase(); export const formatCount = (count: number) => `× ${count}`;