Create tag page

This commit is contained in:
Sam Becker 2023-09-14 16:07:56 -05:00
parent 3c78cb2024
commit a904558730
8 changed files with 104 additions and 30 deletions

View File

@ -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<Metadata> {
}): Promise<Metadata> {
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('/'); }

View File

@ -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 (
<SiteGrid
contentMain={<div className="space-y-8 mt-4">
<div className="flex items-center gap-2">
<PhotoTag tag={tag} />
<span className="uppercase text-gray-400 dark:text-gray-500">
{photos.length} {photos.length === 1 ? 'photo' : 'photos'}
</span>
</div>
<PhotoGrid photos={photos} />
</div>}
/>
);
}

1
src/cache/index.ts vendored
View File

@ -9,6 +9,7 @@ const PHOTO_PATHS = [
'/grid',
'/p/[photoId]',
'/p/[photoId]/image',
'/t/[tag]',
'/admin/photos',
'/admin/photos/[photoId]',
'/admin/photos/[photoId]/edit',

View File

@ -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)}
</Link>
{photo.tags.length > 0 &&
<div className="uppercase">
{photo.tags.map(tag =>
<div
className="flex items-center gap-x-1.5"
key={tag}
>
<FaTag size={11} />
<span>{tag}</span>
</div>)}
</div>}
<PhotoTags tags={photo.tags} />}
<div className="uppercase">
{photo.make} {photo.model}
</div>

View File

@ -159,6 +159,18 @@ const sqlGetPhotosFromDbSortedByPriority = (
`
.then(({ rows }) => rows.map(parsePhotoFromDb));
const sqlGetPhotosFromDbByTag = (
limit = PHOTO_DEFAULT_LIMIT,
offset = 0,
tag: string,
) =>
sql<PhotoDb>`
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<PhotoDb>`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;

View File

@ -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)}`;

20
src/tag/PhotoTag.tsx Normal file
View File

@ -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 (
<Link
key={tag}
href={routeForTag(tag)}
className="flex items-center gap-x-1.5"
>
<FaTag size={11} />
<span className="uppercase">{tag}</span>
</Link>
);
}

17
src/tag/PhotoTags.tsx Normal file
View File

@ -0,0 +1,17 @@
import PhotoTag from '@/tag/PhotoTag';
export default function PhotoTags({
tags,
}: {
tags: string[]
}) {
return (
<div>
{tags.map(tag =>
<PhotoTag
key={tag}
tag={tag}
/>)}
</div>
);
}