From 93b565df21fd123cea35e1ee2bf6b36669261548 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Mon, 23 Oct 2023 00:32:15 -0500 Subject: [PATCH] Add counts on hover to tags, cameras --- src/app/(auth-state)/admin/tags/page.tsx | 4 +- .../(static)/tag/[tag]/[photoId]/layout.tsx | 2 +- .../tag/[tag]/[photoId]/share/page.tsx | 2 +- src/cache/index.ts | 6 +- src/camera/PhotoCamera.tsx | 55 +++++++++------- src/camera/index.ts | 1 + src/photo/PhotoGridSidebar.tsx | 9 ++- src/services/postgres.ts | 65 +++++++++++-------- src/tag/PhotoTag.tsx | 47 ++++++++------ src/tag/index.ts | 5 ++ 10 files changed, 116 insertions(+), 80 deletions(-) diff --git a/src/app/(auth-state)/admin/tags/page.tsx b/src/app/(auth-state)/admin/tags/page.tsx index 34603124..4a11fb6e 100644 --- a/src/app/(auth-state)/admin/tags/page.tsx +++ b/src/app/(auth-state)/admin/tags/page.tsx @@ -5,7 +5,7 @@ import AdminGrid from '@/admin/AdminGrid'; import { Fragment } from 'react'; import DeleteButton from '@/admin/DeleteButton'; import { photoQuantityText } from '@/photo'; -import { getUniqueTagsWithCountCached } from '@/cache'; +import { getUniqueTagsHiddenCached } from '@/cache'; import PhotoTag from '@/tag/PhotoTag'; import { formatTag } from '@/tag'; import EditButton from '@/admin/EditButton'; @@ -14,7 +14,7 @@ import { pathForAdminTagEdit } from '@/site/paths'; export const runtime = 'edge'; export default async function AdminPhotosPage() { - const tags = await getUniqueTagsWithCountCached(); + const tags = await getUniqueTagsHiddenCached(); return ( { + tags.forEach(async ({ tag }) => { const photos = await getPhotos({ tag }); params.push(...photos.map(photo => ({ params: { photoId: photo.id, tag }, diff --git a/src/app/(static)/tag/[tag]/[photoId]/share/page.tsx b/src/app/(static)/tag/[tag]/[photoId]/share/page.tsx index 33ac07d1..85500bcd 100644 --- a/src/app/(static)/tag/[tag]/[photoId]/share/page.tsx +++ b/src/app/(static)/tag/[tag]/[photoId]/share/page.tsx @@ -12,7 +12,7 @@ export async function generateStaticParams() { const params: PhotoTagProps[] = []; const tags = await getUniqueTags(); - tags.forEach(async tag => { + tags.forEach(async ({ tag }) => { const photos = await getPhotos({ tag }); params.push(...photos.map(photo => ({ params: { photoId: photo.id, tag }, diff --git a/src/cache/index.ts b/src/cache/index.ts index 1eb3644b..a34a2a98 100644 --- a/src/cache/index.ts +++ b/src/cache/index.ts @@ -11,7 +11,7 @@ import { getUniqueTags, getPhotosTagDateRange, getPhotosCameraDateRange, - getUniqueTagsWithCount, + getUniqueTagsHidden, } from '@/services/postgres'; import { parseCachedPhotosDates, parseCachedPhotoDates } from '@/photo'; import { getBlobPhotoUrls, getBlobUploadUrls } from '@/services/blob'; @@ -192,9 +192,9 @@ export const getUniqueTagsCached: typeof getUniqueTags = (...args) => )(); // eslint-disable-next-line max-len -export const getUniqueTagsWithCountCached: typeof getUniqueTagsWithCount = (...args) => +export const getUniqueTagsHiddenCached: typeof getUniqueTagsHidden = (...args) => unstable_cache( - () => getUniqueTagsWithCount(...args), + () => getUniqueTagsHidden(...args), [KEY_PHOTOS, KEY_TAGS], { tags: [KEY_PHOTOS, KEY_TAGS], } diff --git a/src/camera/PhotoCamera.tsx b/src/camera/PhotoCamera.tsx index 6d41b648..7af41ffb 100644 --- a/src/camera/PhotoCamera.tsx +++ b/src/camera/PhotoCamera.tsx @@ -9,36 +9,45 @@ export default function PhotoCamera({ camera, showIcon = true, hideApple = true, + countOnHover, }: { camera: Camera showIcon?: boolean hideApple?: boolean + countOnHover?: number }) { return ( - - {showIcon && <> - -   - } - {!(hideApple && camera.make?.toLowerCase() === 'apple') && - <> - {camera.make?.toLowerCase() === 'apple' - ? - : camera.make} + + + {showIcon && <> +   } - {camera.model} - + {!(hideApple && camera.make?.toLowerCase() === 'apple') && + <> + {camera.make?.toLowerCase() === 'apple' + ? + : camera.make} +   + } + {camera.model} + + {countOnHover !== undefined && + + {' '} + {countOnHover} + } + ); } diff --git a/src/camera/index.ts b/src/camera/index.ts index 87c0a4a2..1f8bdca5 100644 --- a/src/camera/index.ts +++ b/src/camera/index.ts @@ -11,6 +11,7 @@ export type Camera = { export type Cameras = { cameraKey: string camera: Camera + count: number }[]; export const createCameraKey = ({ make, model }: Camera) => diff --git a/src/photo/PhotoGridSidebar.tsx b/src/photo/PhotoGridSidebar.tsx index f2f7ff16..e9bd7a26 100644 --- a/src/photo/PhotoGridSidebar.tsx +++ b/src/photo/PhotoGridSidebar.tsx @@ -5,13 +5,14 @@ import PhotoTag from '@/tag/PhotoTag'; import { FaTag } from 'react-icons/fa'; import { IoMdCamera } from 'react-icons/io'; import { photoQuantityText } from '.'; +import { Tags } from '@/tag'; export default function PhotoGridSidebar({ tags, cameras, photosCount, }: { - tags: string[] + tags: Tags cameras: Cameras photosCount: number }) { @@ -20,21 +21,23 @@ export default function PhotoGridSidebar({ {tags.length > 0 && } - items={tags.map(tag => + items={tags.map(({ tag, count }) => )} />} {cameras.length > 0 && } - items={cameras.map(({ cameraKey, camera }) => + items={cameras.map(({ cameraKey, camera, count }) => )} />} diff --git a/src/services/postgres.ts b/src/services/postgres.ts index 8a9ac2a3..e666423c 100644 --- a/src/services/postgres.ts +++ b/src/services/postgres.ts @@ -9,6 +9,7 @@ import { } from '@/photo'; import { Camera, Cameras, createCameraKey } from '@/camera'; import { parameterize } from '@/utility/string'; +import { Tags } from '@/tag'; const PHOTO_DEFAULT_LIMIT = 100; @@ -285,28 +286,36 @@ const sqlGetPhotosCameraDateRange = async (camera: Camera) => sql` `.then(({ rows }) => rows[0] as PhotoDateRange); const sqlGetUniqueTags = async () => sql` - SELECT DISTINCT unnest(tags) as tag FROM photos + SELECT DISTINCT unnest(tags) as tag, COUNT(*) + FROM photos WHERE hidden IS NOT TRUE - ORDER BY tag ASC -`.then(({ rows }) => rows.map(row => row.tag as string)); - -// Include hidden photos for admin usage -const sqlGetUniqueTagsWithCount = async () => sql` - SELECT DISTINCT unnest(tags) as tag, count(distinct id) as count FROM photos GROUP BY tag ORDER BY count DESC -`.then(({ rows }) => rows.map(row => ({ - tag: row.tag as string, - count: parseInt(row.count, 10), +`.then(({ rows }): Tags => rows.map(({ tag, count }) => ({ + tag: tag as string, + count: parseInt(count, 10), + }))); + +const sqlGetUniqueTagsHidden = async () => sql` + SELECT DISTINCT unnest(tags) as tag, COUNT(*) + FROM photos + GROUP BY tag + ORDER BY count DESC +`.then(({ rows }): Tags => rows.map(({ tag, count }) => ({ + tag: tag as string, + count: parseInt(count, 10), }))); const sqlGetUniqueCameras = async () => sql` - SELECT DISTINCT make||' '||model as camera, make, model FROM photos + SELECT DISTINCT make||' '||model as camera, make, model, COUNT(*) + FROM photos WHERE hidden IS NOT TRUE - ORDER BY camera ASC -`.then(({ rows }): Cameras => rows.map(({ make, model }) => ({ + GROUP BY make, model + ORDER BY camera DESC +`.then(({ rows }): Cameras => rows.map(({ make, model, count }) => ({ cameraKey: createCameraKey({ make, model }), camera: { make, model }, + count: parseInt(count, 10), }))); export type GetPhotosOptions = { @@ -348,6 +357,7 @@ const safelyQueryPhotos = async (callback: () => Promise): Promise => { return result; }; +// PHOTOS export const getPhotos = async (options: GetPhotosOptions = {}) => { const { sortBy = 'takenAt', @@ -382,7 +392,6 @@ export const getPhotos = async (options: GetPhotosOptions = {}) => { return safelyQueryPhotos(getPhotosSql) .then(({ rows }) => rows.map(parsePhotoFromDb)); }; - export const getPhoto = async (id: string): Promise => { // Check for photo id forwarding // and convert short ids to uuids @@ -391,25 +400,25 @@ export const getPhoto = async (id: string): Promise => { .then(({ rows }) => rows.map(parsePhotoFromDb)) .then(photos => photos.length > 0 ? photos[0] : undefined); }; - export const getPhotosCount = () => safelyQueryPhotos(sqlGetPhotosCount); -export const getPhotosTagCount = (tag: string) => - safelyQueryPhotos(() => sqlGetPhotosTagCount(tag)); -export const getPhotosCameraCount = (camera: Camera) => - safelyQueryPhotos(() => sqlGetPhotosCameraCount(camera)); - -export const getPhotosTagDateRange = (tag: string) => - safelyQueryPhotos(() => sqlGetPhotosTagDateRange(tag)); -export const getPhotosCameraDateRange = (camera: Camera) => - safelyQueryPhotos(() => sqlGetPhotosCameraDateRange(camera)); - export const getPhotosCountIncludingHidden = () => safelyQueryPhotos(sqlGetPhotosCountIncludingHidden); +// TAGS export const getUniqueTags = () => safelyQueryPhotos(sqlGetUniqueTags); -export const getUniqueTagsWithCount = () => - safelyQueryPhotos(sqlGetUniqueTagsWithCount); +export const getUniqueTagsHidden = () => + safelyQueryPhotos(sqlGetUniqueTagsHidden); +export const getPhotosTagDateRange = (tag: string) => + safelyQueryPhotos(() => sqlGetPhotosTagDateRange(tag)); +export const getPhotosTagCount = (tag: string) => + safelyQueryPhotos(() => sqlGetPhotosTagCount(tag)); -export const getUniqueCameras = () => safelyQueryPhotos(sqlGetUniqueCameras); +// CAMERAS +export const getUniqueCameras = () => + safelyQueryPhotos(sqlGetUniqueCameras); +export const getPhotosCameraDateRange = (camera: Camera) => + safelyQueryPhotos(() => sqlGetPhotosCameraDateRange(camera)); +export const getPhotosCameraCount = (camera: Camera) => + safelyQueryPhotos(() => sqlGetPhotosCameraCount(camera)); diff --git a/src/tag/PhotoTag.tsx b/src/tag/PhotoTag.tsx index 6526ab05..20a24548 100644 --- a/src/tag/PhotoTag.tsx +++ b/src/tag/PhotoTag.tsx @@ -7,29 +7,38 @@ import { formatTag } from '.'; export default function PhotoTag({ tag, showIcon = true, + countOnHover, }: { tag: string showIcon?: boolean + countOnHover?: number }) { return ( - - {showIcon && - } - - {formatTag(tag)} - - + + + {showIcon && + } + + {formatTag(tag)} + + + {countOnHover !== undefined && + + {' '} + {countOnHover} + } + ); } diff --git a/src/tag/index.ts b/src/tag/index.ts index 8b2d7322..a7325185 100644 --- a/src/tag/index.ts +++ b/src/tag/index.ts @@ -7,6 +7,11 @@ import { import { absolutePathForTag, absolutePathForTagImage } from '@/site/paths'; import { capitalizeWords } from '@/utility/string'; +export type Tags = { + tag: string + count: number +}[] + export const formatTag = (tag: string) => capitalizeWords(tag.replaceAll('-', ' '));