From ab8d088df572c59290bb2385dcefa0718f8c40b0 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Tue, 27 Feb 2024 23:32:51 -0600 Subject: [PATCH] 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}`;