Add counts on hover to tags, cameras

This commit is contained in:
Sam Becker 2023-10-23 00:32:15 -05:00
parent a303080cea
commit 93b565df21
10 changed files with 116 additions and 80 deletions

View File

@ -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 (
<SiteGrid

View File

@ -23,7 +23,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 },

View File

@ -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 },

6
src/cache/index.ts vendored
View File

@ -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],
}

View File

@ -9,12 +9,15 @@ export default function PhotoCamera({
camera,
showIcon = true,
hideApple = true,
countOnHover,
}: {
camera: Camera
showIcon?: boolean
hideApple?: boolean
countOnHover?: number
}) {
return (
<span className="group">
<Link
href={pathForCamera(camera)}
className={cc(
@ -40,5 +43,11 @@ export default function PhotoCamera({
</>}
{camera.model}
</Link>
{countOnHover !== undefined &&
<span className="hidden group-hover:inline">
{' '}
{countOnHover}
</span>}
</span>
);
}

View File

@ -11,6 +11,7 @@ export type Camera = {
export type Cameras = {
cameraKey: string
camera: Camera
count: number
}[];
export const createCameraKey = ({ make, model }: Camera) =>

View File

@ -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 && <HeaderList
title='Tags'
icon={<FaTag size={12} />}
items={tags.map(tag =>
items={tags.map(({ tag, count }) =>
<PhotoTag
key={tag}
tag={tag}
showIcon={false}
countOnHover={count}
/>)}
/>}
{cameras.length > 0 && <HeaderList
title="Cameras"
icon={<IoMdCamera size={13} />}
items={cameras.map(({ cameraKey, camera }) =>
items={cameras.map(({ cameraKey, camera, count }) =>
<PhotoCamera
key={cameraKey}
camera={camera}
showIcon={false}
countOnHover={count}
hideApple
/>)}
/>}

View File

@ -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 <T>(callback: () => Promise<T>): Promise<T> => {
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<Photo | undefined> => {
// Check for photo id forwarding
// and convert short ids to uuids
@ -391,25 +400,25 @@ export const getPhoto = async (id: string): Promise<Photo | undefined> => {
.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));

View File

@ -7,15 +7,18 @@ import { formatTag } from '.';
export default function PhotoTag({
tag,
showIcon = true,
countOnHover,
}: {
tag: string
showIcon?: boolean
countOnHover?: number
}) {
return (
<span className="group">
<Link
href={pathForTag(tag)}
className={cc(
'flex items-center gap-x-1.5 self-start',
'inline-flex items-center gap-x-1.5 self-start',
'hover:text-gray-900 dark:hover:text-gray-100',
)}
>
@ -31,5 +34,11 @@ export default function PhotoTag({
{formatTag(tag)}
</span>
</Link>
{countOnHover !== undefined &&
<span className="hidden group-hover:inline">
{' '}
{countOnHover}
</span>}
</span>
);
}

View File

@ -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('-', ' '));