Create tag page
This commit is contained in:
parent
3c78cb2024
commit
a904558730
@ -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('/'); }
|
||||
|
||||
26
src/app/(static)/t/[tag]/page.tsx
Normal file
26
src/app/(static)/t/[tag]/page.tsx
Normal 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
1
src/cache/index.ts
vendored
@ -9,6 +9,7 @@ const PHOTO_PATHS = [
|
||||
'/grid',
|
||||
'/p/[photoId]',
|
||||
'/p/[photoId]/image',
|
||||
'/t/[tag]',
|
||||
'/admin/photos',
|
||||
'/admin/photos/[photoId]',
|
||||
'/admin/photos/[photoId]/edit',
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
20
src/tag/PhotoTag.tsx
Normal 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
17
src/tag/PhotoTags.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user