Add counts on hover to tags, cameras
This commit is contained in:
parent
a303080cea
commit
93b565df21
@ -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
|
||||
|
||||
@ -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 },
|
||||
|
||||
@ -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
6
src/cache/index.ts
vendored
@ -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],
|
||||
}
|
||||
|
||||
@ -9,36 +9,45 @@ export default function PhotoCamera({
|
||||
camera,
|
||||
showIcon = true,
|
||||
hideApple = true,
|
||||
countOnHover,
|
||||
}: {
|
||||
camera: Camera
|
||||
showIcon?: boolean
|
||||
hideApple?: boolean
|
||||
countOnHover?: number
|
||||
}) {
|
||||
return (
|
||||
<Link
|
||||
href={pathForCamera(camera)}
|
||||
className={cc(
|
||||
'inline-flex items-center self-start',
|
||||
'uppercase',
|
||||
'hover:text-gray-900 dark:hover:text-gray-100',
|
||||
)}
|
||||
>
|
||||
{showIcon && <>
|
||||
<IoMdCamera size={13} className="translate-y-[-0.25px]" />
|
||||
|
||||
</>}
|
||||
{!(hideApple && camera.make?.toLowerCase() === 'apple') &&
|
||||
<>
|
||||
{camera.make?.toLowerCase() === 'apple'
|
||||
? <AiFillApple
|
||||
title="Apple"
|
||||
className="translate-y-[-0.5px]"
|
||||
size={14}
|
||||
/>
|
||||
: camera.make}
|
||||
<span className="group">
|
||||
<Link
|
||||
href={pathForCamera(camera)}
|
||||
className={cc(
|
||||
'inline-flex items-center self-start',
|
||||
'uppercase',
|
||||
'hover:text-gray-900 dark:hover:text-gray-100',
|
||||
)}
|
||||
>
|
||||
{showIcon && <>
|
||||
<IoMdCamera size={13} className="translate-y-[-0.25px]" />
|
||||
|
||||
</>}
|
||||
{camera.model}
|
||||
</Link>
|
||||
{!(hideApple && camera.make?.toLowerCase() === 'apple') &&
|
||||
<>
|
||||
{camera.make?.toLowerCase() === 'apple'
|
||||
? <AiFillApple
|
||||
title="Apple"
|
||||
className="translate-y-[-0.5px]"
|
||||
size={14}
|
||||
/>
|
||||
: camera.make}
|
||||
|
||||
</>}
|
||||
{camera.model}
|
||||
</Link>
|
||||
{countOnHover !== undefined &&
|
||||
<span className="hidden group-hover:inline">
|
||||
{' '}
|
||||
{countOnHover}
|
||||
</span>}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@ -11,6 +11,7 @@ export type Camera = {
|
||||
export type Cameras = {
|
||||
cameraKey: string
|
||||
camera: Camera
|
||||
count: number
|
||||
}[];
|
||||
|
||||
export const createCameraKey = ({ make, model }: Camera) =>
|
||||
|
||||
@ -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
|
||||
/>)}
|
||||
/>}
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -7,29 +7,38 @@ import { formatTag } from '.';
|
||||
export default function PhotoTag({
|
||||
tag,
|
||||
showIcon = true,
|
||||
countOnHover,
|
||||
}: {
|
||||
tag: string
|
||||
showIcon?: boolean
|
||||
countOnHover?: number
|
||||
}) {
|
||||
return (
|
||||
<Link
|
||||
href={pathForTag(tag)}
|
||||
className={cc(
|
||||
'flex items-center gap-x-1.5 self-start',
|
||||
'hover:text-gray-900 dark:hover:text-gray-100',
|
||||
)}
|
||||
>
|
||||
{showIcon &&
|
||||
<FaTag
|
||||
size={11}
|
||||
className={cc(
|
||||
'flex-shrink-0',
|
||||
'text-gray-700 dark:text-gray-300 translate-y-[0.5px]',
|
||||
)}
|
||||
/>}
|
||||
<span className="uppercase">
|
||||
{formatTag(tag)}
|
||||
</span>
|
||||
</Link>
|
||||
<span className="group">
|
||||
<Link
|
||||
href={pathForTag(tag)}
|
||||
className={cc(
|
||||
'inline-flex items-center gap-x-1.5 self-start',
|
||||
'hover:text-gray-900 dark:hover:text-gray-100',
|
||||
)}
|
||||
>
|
||||
{showIcon &&
|
||||
<FaTag
|
||||
size={11}
|
||||
className={cc(
|
||||
'flex-shrink-0',
|
||||
'text-gray-700 dark:text-gray-300 translate-y-[0.5px]',
|
||||
)}
|
||||
/>}
|
||||
<span className="uppercase">
|
||||
{formatTag(tag)}
|
||||
</span>
|
||||
</Link>
|
||||
{countOnHover !== undefined &&
|
||||
<span className="hidden group-hover:inline">
|
||||
{' '}
|
||||
{countOnHover}
|
||||
</span>}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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('-', ' '));
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user