From ab8d088df572c59290bb2385dcefa0718f8c40b0 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Tue, 27 Feb 2024 23:32:51 -0600 Subject: [PATCH 1/2] Make photo tag text encoding more resilient --- src/app/admin/tags/[tag]/edit/page.tsx | 6 +++++- src/app/tag/[tag]/page.tsx | 10 +++++++--- src/app/tag/[tag]/share/page.tsx | 10 +++++++--- src/camera/index.ts | 2 +- src/components/TagInput.tsx | 4 ++-- src/photo/actions.ts | 2 ++ src/services/vercel-postgres.ts | 12 ++++++------ src/utility/string.ts | 20 ++++++++++++++------ 8 files changed, 44 insertions(+), 22 deletions(-) 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 72f74459..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'; +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 a7f3caba..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'; +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/index.ts b/src/camera/index.ts index c89b4673..8b609f9b 100644 --- a/src/camera/index.ts +++ b/src/camera/index.ts @@ -17,7 +17,7 @@ export type CameraWithCount = { export type Cameras = CameraWithCount[]; export const createCameraKey = ({ make, model }: Camera) => - 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 f8531a3b..82c99011 100644 --- a/src/services/vercel-postgres.ts +++ b/src/services/vercel-postgres.ts @@ -173,8 +173,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)); @@ -203,8 +203,8 @@ 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); @@ -347,8 +347,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/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}`; From 3f0944c10465f966198e799452102783771504cb Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Tue, 27 Feb 2024 23:52:33 -0600 Subject: [PATCH 2/2] Make date range queries fail gracefully --- src/camera/CameraOverview.tsx | 2 +- src/camera/CameraShareModal.tsx | 2 +- src/services/vercel-postgres.ts | 16 ++++++++++++---- src/simulation/FilmSimulationOverview.tsx | 2 +- src/tag/TagOverview.tsx | 2 +- 5 files changed, 16 insertions(+), 8 deletions(-) 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 ( 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 @@ -206,7 +210,9 @@ const sqlGetPhotosCameraDateRange = async (camera: Camera) => sql` 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, @@ -215,7 +221,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(*) 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, }) {