diff --git a/src/app/(static)/p/[photoId]/layout.tsx b/src/app/(static)/p/[photoId]/layout.tsx index 0743df69..f3aa0075 100644 --- a/src/app/(static)/p/[photoId]/layout.tsx +++ b/src/app/(static)/p/[photoId]/layout.tsx @@ -1,4 +1,3 @@ -import { PropsWithChildren } from 'react'; import AnimateItems from '@/components/AnimateItems'; import PhotoLinks from '@/photo/PhotoLinks'; import SiteGrid from '@/components/SiteGrid'; @@ -23,13 +22,11 @@ const THUMBNAILS_TO_SHOW_MAX = 12; export const runtime = 'edge'; -interface Props extends PropsWithChildren { +export async function generateMetadata({ + params: { photoId }, +}: { params: { photoId: string } -} - -export async function generateMetadata( - { params: { photoId } }: Props -): Promise { +}): Promise { const photo = await getPhoto(photoId); if (!photo) { return {}; } @@ -59,7 +56,10 @@ export async function generateMetadata( export default async function PhotoPage({ params: { photoId }, children, -}: Props) { +}: { + params: { photoId: string } + children: React.ReactNode +}) { const photo = await getPhoto(photoId); if (!photo) { redirect('/'); } diff --git a/src/app/(static)/t/[tag]/page.tsx b/src/app/(static)/t/[tag]/page.tsx new file mode 100644 index 00000000..dd86587c --- /dev/null +++ b/src/app/(static)/t/[tag]/page.tsx @@ -0,0 +1,26 @@ +import SiteGrid from '@/components/SiteGrid'; +import PhotoGrid from '@/photo/PhotoGrid'; +import { getPhotos } from '@/services/postgres'; +import PhotoTag from '@/tag/PhotoTag'; + +export default async function TagPage({ + params: { tag }, +}: { + params: { tag: string } +}) { + const photos = await getPhotos(undefined, undefined, undefined, tag); + + return ( + +
+ + + {photos.length} {photos.length === 1 ? 'photo' : 'photos'} + +
+ + } + /> + ); +} diff --git a/src/cache/index.ts b/src/cache/index.ts index 5a3cddaf..ed1becd8 100644 --- a/src/cache/index.ts +++ b/src/cache/index.ts @@ -9,6 +9,7 @@ const PHOTO_PATHS = [ '/grid', '/p/[photoId]', '/p/[photoId]/image', + '/t/[tag]', '/admin/photos', '/admin/photos/[photoId]', '/admin/photos/[photoId]/edit', diff --git a/src/photo/PhotoLarge.tsx b/src/photo/PhotoLarge.tsx index 148d4737..e5cc9bcc 100644 --- a/src/photo/PhotoLarge.tsx +++ b/src/photo/PhotoLarge.tsx @@ -5,7 +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'; +import PhotoTags from '@/tag/PhotoTags'; export default function PhotoLarge({ photo, @@ -54,16 +54,7 @@ export default function PhotoLarge({ {titleForPhoto(photo)} {photo.tags.length > 0 && -
- {photo.tags.map(tag => -
- - {tag} -
)} -
} + }
{photo.make} {photo.model}
diff --git a/src/services/postgres.ts b/src/services/postgres.ts index 665c1e5c..ae6c64ba 100644 --- a/src/services/postgres.ts +++ b/src/services/postgres.ts @@ -159,6 +159,18 @@ const sqlGetPhotosFromDbSortedByPriority = ( ` .then(({ rows }) => rows.map(parsePhotoFromDb)); +const sqlGetPhotosFromDbByTag = ( + limit = PHOTO_DEFAULT_LIMIT, + offset = 0, + tag: string, +) => + sql` + SELECT * FROM photos WHERE ${tag}=ANY(tags) + ORDER BY taken_at DESC + LIMIT ${limit} OFFSET ${offset} + ` + .then(({ rows }) => rows.map(parsePhotoFromDb)); + const sqlGetPhotoFromDb = (id: string) => sql`SELECT * FROM photos WHERE id=${id} LIMIT 1` .then(({ rows }) => rows.map(parsePhotoFromDb)); @@ -167,29 +179,32 @@ export const getPhotos = async ( sortBy: 'createdAt' | 'takenAt' | 'priority' = 'takenAt', limit?: number, offset?: number, + tag?: string, ) => { let photos; - const getPhotosRequest = sortBy === 'createdAt' - ? sqlGetPhotosFromDbSortedByCreatedAt - : sortBy === 'priority' - ? sqlGetPhotosFromDbSortedByPriority - : sqlGetPhotosFromDb; + const getPhotosRequest = tag + ? () => sqlGetPhotosFromDbByTag(limit, offset, tag) + : sortBy === 'createdAt' + ? () => sqlGetPhotosFromDbSortedByCreatedAt(limit, offset) + : sortBy === 'priority' + ? () => sqlGetPhotosFromDbSortedByPriority(limit, offset) + : () => sqlGetPhotosFromDb(limit, offset); try { - photos = await getPhotosRequest(limit, offset); + photos = await getPhotosRequest(); } catch (e: any) { if (/relation "photos" does not exist/i.test(e.message)) { console.log( 'Creating table "photos" because it did not exist', ); await sqlCreatePhotosTable(); - photos = await getPhotosRequest(limit, offset); + photos = await getPhotosRequest(); } else if (/endpoint is in transition/i.test(e.message)) { // Wait 5 seconds and try again await new Promise(resolve => setTimeout(resolve, 5000)); try { - photos = await getPhotosRequest(limit, offset); + photos = await getPhotosRequest(); } catch (e: any) { console.log(`sql get error on retry (after 5000ms): ${e.message} `); throw e; diff --git a/src/site/routes.ts b/src/site/routes.ts index 5e72f109..72ea13d4 100644 --- a/src/site/routes.ts +++ b/src/site/routes.ts @@ -1,16 +1,20 @@ import { Photo } from '@/photo'; import { BASE_URL } from './config'; -export const ROUTE_ADMIN_UPLOAD = '/admin/uploads'; +const PHOTO_PREFIX = '/p'; +const TAG_PREFIX = '/t'; +export const ROUTE_ADMIN_UPLOAD = '/admin/uploads'; export const ROUTE_ADMIN_UPLOAD_BLOB_HANDLER = '/admin/uploads/blob'; export const ABSOLUTE_ROUTE_FOR_HOME_IMAGE = `${BASE_URL}/home-image`; export const routeForPhoto = (photo: Photo, share?: boolean) => share - ? `/p/${photo.idShort}/share` - : `/p/${photo.idShort}`; + ? `${PHOTO_PREFIX}/${photo.idShort}/share` + : `${PHOTO_PREFIX}/${photo.idShort}`; + +export const routeForTag = (tag: string) => `${TAG_PREFIX}/${tag}`; export const absoluteRouteForPhoto = (photo: Photo) => `${BASE_URL}${routeForPhoto(photo)}`; diff --git a/src/tag/PhotoTag.tsx b/src/tag/PhotoTag.tsx new file mode 100644 index 00000000..ea7fcabd --- /dev/null +++ b/src/tag/PhotoTag.tsx @@ -0,0 +1,20 @@ +import Link from 'next/link'; +import { routeForTag } from '@/site/routes'; +import { FaTag } from 'react-icons/fa'; + +export default function PhotoTag({ + tag, +}: { + tag: string +}) { + return ( + + + {tag} + + ); +} diff --git a/src/tag/PhotoTags.tsx b/src/tag/PhotoTags.tsx new file mode 100644 index 00000000..02864fcc --- /dev/null +++ b/src/tag/PhotoTags.tsx @@ -0,0 +1,17 @@ +import PhotoTag from '@/tag/PhotoTag'; + +export default function PhotoTags({ + tags, +}: { + tags: string[] +}) { + return ( +
+ {tags.map(tag => + )} +
+ ); +}