From af7af534016c91bb55ff0040de2a7c4132a0ece1 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Mon, 2 Oct 2023 11:51:04 -0500 Subject: [PATCH] Create device photo set view --- src/app/(static)/d/[device]/page.tsx | 71 ++++++++++++++++++++++++++++ src/app/(static)/grid/page.tsx | 3 +- src/device/DeviceHeader.tsx | 26 ++++++++++ src/device/PhotoDevice.tsx | 45 ++++++++++++++++++ src/device/index.ts | 14 ++++++ src/photo/PhotoDevice.tsx | 32 ------------- src/photo/PhotoHeader.tsx | 54 +++++++++++++++++++++ src/photo/PhotoLarge.tsx | 9 +++- src/photo/index.ts | 12 +++++ src/services/postgres.ts | 4 +- src/tag/TagHeader.tsx | 45 ++++-------------- src/tag/index.ts | 11 ++--- 12 files changed, 247 insertions(+), 79 deletions(-) create mode 100644 src/app/(static)/d/[device]/page.tsx create mode 100644 src/device/DeviceHeader.tsx create mode 100644 src/device/PhotoDevice.tsx create mode 100644 src/device/index.ts delete mode 100644 src/photo/PhotoDevice.tsx create mode 100644 src/photo/PhotoHeader.tsx diff --git a/src/app/(static)/d/[device]/page.tsx b/src/app/(static)/d/[device]/page.tsx new file mode 100644 index 00000000..8c56e93c --- /dev/null +++ b/src/app/(static)/d/[device]/page.tsx @@ -0,0 +1,71 @@ +import { getPhotosCached } from '@/cache'; +import SiteGrid from '@/components/SiteGrid'; +import DeviceHeader from '@/device/DeviceHeader'; +import { getMakeModelFromDevice } from '@/device'; +import PhotoGrid from '@/photo/PhotoGrid'; +import { getUniqueDevices } from '@/services/postgres'; +import { generateMetaForTag } from '@/tag'; +import { Metadata } from 'next'; + +interface TagProps { + params: { device: string } +} + +export async function generateStaticParams() { + const devices = await getUniqueDevices(); + return devices.map(device => ({ + params: { device }, + })); +} + +export async function generateMetadata({ + params: { device }, +}: TagProps): Promise { + const photos = await getPhotosCached({ + device: getMakeModelFromDevice(device), + }); + + const { + url, + title, + description, + images, + } = generateMetaForTag(device, photos); + + return { + title, + openGraph: { + title, + description, + images, + url, + }, + twitter: { + images, + description, + card: 'summary_large_image', + }, + description, + }; +} + +export default async function DevicePage({ params: { device } }:TagProps) { + const photos = await getPhotosCached({ + device: getMakeModelFromDevice(device), + }); + + // Harvest original make/model with proper spaces/slashes + const deviceFormatted = photos.length > 0 + ? `${photos[0].make} ${photos[0].model}` + : device; + + return ( + + + + } + /> + ); +} diff --git a/src/app/(static)/grid/page.tsx b/src/app/(static)/grid/page.tsx index fb2d488c..9abea5ff 100644 --- a/src/app/(static)/grid/page.tsx +++ b/src/app/(static)/grid/page.tsx @@ -8,7 +8,7 @@ import HeaderList from '@/components/HeaderList'; import MorePhotos from '@/components/MorePhotos'; import SiteGrid from '@/components/SiteGrid'; import { generateOgImageMetaForPhotos, getPhotosLimitForQuery } from '@/photo'; -import PhotoDevice from '@/photo/PhotoDevice'; +import PhotoDevice from '@/device/PhotoDevice'; import PhotoGrid from '@/photo/PhotoGrid'; import PhotosEmptyState from '@/photo/PhotosEmptyState'; import { MAX_PHOTOS_TO_SHOW_HOME } from '@/photo/image-response'; @@ -69,6 +69,7 @@ export default async function GridPage({ key={device} make={make} model={model} + showIcon={false} hideApple />)} />} diff --git a/src/device/DeviceHeader.tsx b/src/device/DeviceHeader.tsx new file mode 100644 index 00000000..51a2b452 --- /dev/null +++ b/src/device/DeviceHeader.tsx @@ -0,0 +1,26 @@ +import { Photo } from '@/photo'; +import { pathForTagShare } from '@/site/paths'; +import PhotoHeader from '@/photo/PhotoHeader'; +import { descriptionForDevicePhotos, getMakeModelFromDevice } from '.'; +import PhotoDevice from './PhotoDevice'; + +export default function DeviceHeader({ + device, + photos, + selectedPhoto, +}: { + device: string + photos: Photo[] + selectedPhoto?: Photo +}) { + return ( + } + entityVerb="Device" + entityDescription={descriptionForDevicePhotos(photos)} + photos={photos} + selectedPhoto={selectedPhoto} + sharePath={pathForTagShare(device)} + /> + ); +} diff --git a/src/device/PhotoDevice.tsx b/src/device/PhotoDevice.tsx new file mode 100644 index 00000000..0c0a3fa1 --- /dev/null +++ b/src/device/PhotoDevice.tsx @@ -0,0 +1,45 @@ +import { AiFillApple } from 'react-icons/ai'; +import { cc } from '@/utility/css'; +import Link from 'next/link'; +import { pathForDevice } from '@/site/paths'; +import { IoMdCamera } from 'react-icons/io'; + +export default function PhotoDevice({ + make, + model, + showIcon = true, + hideApple = true, +}: { + make?: string + model?: string + showIcon?: boolean + hideApple?: boolean +}) { + console.log({ make, model, showIcon, hideApple }); + return ( + + {showIcon && <> + +   + } + {!(hideApple && make?.toLowerCase() === 'apple') && + <> + {make?.toLowerCase() === 'apple' + ? + : make} +   + } + {model} + + ); +} diff --git a/src/device/index.ts b/src/device/index.ts new file mode 100644 index 00000000..094323f0 --- /dev/null +++ b/src/device/index.ts @@ -0,0 +1,14 @@ +import { Photo, descriptionForPhotoSet } from '@/photo'; + +// Assumes no device makes ('Fujifilm,' 'Apple,' 'Canon', etc.) +// will have dashes in them +export const getMakeModelFromDevice = (device: string) => { + const [make, model] = device.toLowerCase().split(/[-| ](.*)/s); + return { make, model }; +}; + +export const descriptionForDevicePhotos = ( + photos: Photo[], + dateBased?: boolean, +) => + descriptionForPhotoSet(photos, 'device', dateBased); diff --git a/src/photo/PhotoDevice.tsx b/src/photo/PhotoDevice.tsx deleted file mode 100644 index 78539abc..00000000 --- a/src/photo/PhotoDevice.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { AiFillApple } from 'react-icons/ai'; -import { cc } from '@/utility/css'; - -export default function PhotoDevice({ - make, - model, - hideApple, -}: { - make?: string - model?: string - hideApple?: boolean -}) { - return ( -
- {!(hideApple && make === 'Apple') && - <> - {make === 'Apple' - ? - : make} -   - } - {model} -
- ); -} diff --git a/src/photo/PhotoHeader.tsx b/src/photo/PhotoHeader.tsx new file mode 100644 index 00000000..3e5958bb --- /dev/null +++ b/src/photo/PhotoHeader.tsx @@ -0,0 +1,54 @@ +import { cc } from '@/utility/css'; +import { Photo, dateRangeForPhotos } from '.'; +import ShareButton from '@/components/ShareButton'; + +export default function PhotoHeader({ + entity, + entityVerb, + entityDescription, + photos, + selectedPhoto, + sharePath, +}: { + entity: JSX.Element + entityVerb: string + entityDescription: string + photos: Photo[] + selectedPhoto?: Photo + sharePath: string +}) { + const { start, end } = dateRangeForPhotos(photos); + + const selectedPhotoIndex = selectedPhoto + ? photos.findIndex(photo => photo.id === selectedPhoto.id) + : undefined; + + return ( +
+ {entity} + + {selectedPhotoIndex !== undefined + ? `${entityVerb} ${selectedPhotoIndex + 1} of ${photos.length}` + : entityDescription} + {selectedPhotoIndex === undefined && + } + + + {start === end + ? start + : <>{start}
– {end}} +
+
+ ); +} diff --git a/src/photo/PhotoLarge.tsx b/src/photo/PhotoLarge.tsx index a9e6c67c..91b42e29 100644 --- a/src/photo/PhotoLarge.tsx +++ b/src/photo/PhotoLarge.tsx @@ -6,7 +6,7 @@ import Link from 'next/link'; import { pathForPhoto, pathForPhotoShare } from '@/site/paths'; import PhotoTags from '@/tag/PhotoTags'; import ShareButton from '@/components/ShareButton'; -import PhotoDevice from './PhotoDevice'; +import PhotoDevice from '../device/PhotoDevice'; export default function PhotoLarge({ photo, @@ -64,7 +64,12 @@ export default function PhotoLarge({ {tagsToShow.length > 0 && } - + )} {renderMiniGrid(<>
    export const titleForPhoto = (photo: Photo) => photo.title || 'Untitled'; +export const labelForPhotos = (photos: Photo[]) => + photos.length === 1 ? 'Photo' : 'Photos'; + +export const descriptionForPhotoSet = ( + photos:Photo[], + descriptor: string, + dateBased?: boolean, +) => + dateBased + ? dateRangeForPhotos(photos).description.toUpperCase() + : `${photos.length} ${descriptor} ${labelForPhotos(photos)}`; + export const dateRangeForPhotos = (photos: Photo[]) => { const start = photos[0].takenAtNaiveFormattedShort; const end = photos[photos.length - 1].takenAtNaiveFormattedShort; diff --git a/src/services/postgres.ts b/src/services/postgres.ts index 88a640c3..11d8475d 100644 --- a/src/services/postgres.ts +++ b/src/services/postgres.ts @@ -194,7 +194,9 @@ const sqlGetPhotosByDevice = async ( model: string, ) => sql` SELECT * FROM photos - WHERE make=${make} AND model=${model} + WHERE + LOWER(make)=${make} AND + LOWER(REPLACE(model, ' ', '-'))=${model} ORDER BY taken_at DESC LIMIT ${limit} `; diff --git a/src/tag/TagHeader.tsx b/src/tag/TagHeader.tsx index 0f25aa49..1b6ac981 100644 --- a/src/tag/TagHeader.tsx +++ b/src/tag/TagHeader.tsx @@ -1,9 +1,8 @@ -import { Photo, dateRangeForPhotos } from '@/photo'; -import { cc } from '@/utility/css'; +import { Photo } from '@/photo'; import PhotoTag from './PhotoTag'; import { descriptionForTaggedPhotos } from '.'; -import ShareButton from '@/components/ShareButton'; import { pathForTagShare } from '@/site/paths'; +import PhotoHeader from '@/photo/PhotoHeader'; export default function TagHeader({ tag, @@ -14,38 +13,14 @@ export default function TagHeader({ photos: Photo[] selectedPhoto?: Photo }) { - const { start, end } = dateRangeForPhotos(photos); - - const selectedPhotoIndex = selectedPhoto - ? photos.findIndex(photo => photo.id === selectedPhoto.id) - : undefined; - return ( -
    - - - {selectedPhotoIndex !== undefined - ? `Tagged ${selectedPhotoIndex + 1} of ${photos.length}` - : descriptionForTaggedPhotos(photos)} - {selectedPhotoIndex === undefined && - } - - - {start === end - ? start - : <>{start}
    – {end}} -
    -
    + } + entityVerb="Tagged" + entityDescription={descriptionForTaggedPhotos(photos)} + photos={photos} + selectedPhoto={selectedPhoto} + sharePath={pathForTagShare(tag)} + /> ); } diff --git a/src/tag/index.ts b/src/tag/index.ts index 8b97dc88..f8de8a28 100644 --- a/src/tag/index.ts +++ b/src/tag/index.ts @@ -1,22 +1,17 @@ -import { Photo, dateRangeForPhotos } from '@/photo'; +import { Photo, descriptionForPhotoSet, labelForPhotos } from '@/photo'; import { absolutePathForTag, absolutePathForTagImage } from '@/site/paths'; import { capitalizeWords } from '@/utility/string'; -const labelForPhotos = (photos: Photo[]) => - photos.length === 1 ? 'Photo' : 'Photos'; - export const titleForTag = (tag: string, photos:Photo[]) => [ capitalizeWords(tag.replaceAll('-', ' ')), `(${photos.length} ${labelForPhotos(photos)})`, ].join(' '); export const descriptionForTaggedPhotos = ( - photos:Photo[], + photos: Photo[], dateBased?: boolean, ) => - dateBased - ? dateRangeForPhotos(photos).description.toUpperCase() - : `${photos.length} Tagged ${labelForPhotos(photos)}`; + descriptionForPhotoSet(photos, 'tagged', dateBased); export const generateMetaForTag = (tag: string, photos: Photo[]) => ({ url: absolutePathForTag(tag),