Add special "favs" tag

This commit is contained in:
Sam Becker 2023-12-30 23:06:24 -05:00
parent 91e1fb2166
commit 0f632fe236
14 changed files with 129 additions and 61 deletions

View File

@ -11,6 +11,7 @@
"exif",
"exifr",
"exiftool",
"favs",
"ghijklmnopqrstuv",
"hgetall",
"hset",

View File

@ -1,10 +1,4 @@
import {
getPhotosCached,
getPhotosCountCached,
getUniqueCamerasCached,
getUniqueFilmSimulationsCached,
getUniqueTagsCached,
} from '@/cache';
import { getPhotosCached } from '@/cache';
import SiteGrid from '@/components/SiteGrid';
import { generateOgImageMetaForPhotos } from '@/photo';
import PhotoGrid from '@/photo/PhotoGrid';
@ -17,7 +11,7 @@ import {
getPaginationForSearchParams,
} from '@/site/pagination';
import PhotoGridSidebar from '@/photo/PhotoGridSidebar';
import { SHOW_FILM_SIMULATIONS } from '@/site/config';
import { getPhotoSidebarDataCached } from '@/photo/data';
export const runtime = 'edge';
@ -37,10 +31,7 @@ export default async function GridPage({ searchParams }: PaginationParams) {
simulations,
] = await Promise.all([
getPhotosCached({ limit }),
getPhotosCountCached(),
getUniqueTagsCached(),
getUniqueCamerasCached(),
SHOW_FILM_SIMULATIONS ? getUniqueFilmSimulationsCached() : [],
...getPhotoSidebarDataCached(),
]);
const showMorePath = photosCount > photos.length

View File

@ -1,17 +1,11 @@
import {
getPhotosCached,
getPhotosCountCached,
getUniqueCamerasCached,
getUniqueFilmSimulationsCached,
getUniqueTagsCached,
} from '@/cache';
import { getPhotosCached } from '@/cache';
import InfoBlock from '@/components/InfoBlock';
import RedirectOnDesktop from '@/components/RedirectOnDesktop';
import SiteGrid from '@/components/SiteGrid';
import { generateOgImageMetaForPhotos } from '@/photo';
import PhotoGridSidebar from '@/photo/PhotoGridSidebar';
import { getPhotoSidebarDataCached } from '@/photo/data';
import { MAX_PHOTOS_TO_SHOW_OG } from '@/photo/image-response';
import { SHOW_FILM_SIMULATIONS } from '@/site/config';
import { PATH_GRID } from '@/site/paths';
import { Metadata } from 'next';
@ -26,12 +20,7 @@ export default async function SetsPage() {
tags,
cameras,
simulations,
] = await Promise.all([
getPhotosCountCached(),
getUniqueTagsCached(),
getUniqueCamerasCached(),
SHOW_FILM_SIMULATIONS ? getUniqueFilmSimulationsCached() : [],
]);
] = await Promise.all(getPhotoSidebarDataCached());
return (
<SiteGrid

View File

@ -10,7 +10,7 @@ export default function PhotoCamera({
hideAppleIcon,
type = 'icon-first',
badged,
dim,
contrast,
countOnHover,
}: {
camera: Camera
@ -45,7 +45,7 @@ export default function PhotoCamera({
/>}
type={showAppleIcon && isCameraApple ? 'icon-first' : type}
badged={badged}
dim={dim}
contrast={contrast}
hoverEntity={countOnHover}
/>
);

View File

@ -2,30 +2,39 @@ import { clsx } from 'clsx';
export default function Badge({
children,
type = 'primary',
type = 'large',
highContrast,
uppercase,
interactive,
}: {
children: React.ReactNode
type?: 'primary' | 'secondary' | 'text-only'
type?: 'large' | 'small' | 'text-only'
highContrast?: boolean
uppercase?: boolean
interactive?: boolean
}) {
const stylesForType = () => {
switch (type) {
case 'primary': return clsx(
'px-1.5 py-[0.3rem] rounded-md',
'bg-gray-100/80 dark:bg-gray-900/80',
'border border-gray-200/60 dark:border-gray-800/75'
);
case 'secondary': return clsx(
'px-[0.3rem] py-1 rounded-[0.25rem]',
'bg-gray-300/30 dark:bg-gray-700/50',
'text-medium',
'font-medium text-[0.7rem]',
interactive && 'hover:text-gray-900 dark:hover:text-gray-100',
interactive && 'active:bg-gray-200 dark:active:bg-gray-700/60',
);
case 'large':
return clsx(
'px-1.5 py-[0.3rem] rounded-md',
'bg-gray-100/80 dark:bg-gray-900/80',
'border border-gray-200/60 dark:border-gray-800/75'
);
case 'small':
return clsx(
'px-[0.3rem] py-1 rounded-[0.25rem]',
'text-[0.7rem] font-medium',
highContrast
? 'text-invert bg-main'
: 'text-medium bg-gray-300/30 dark:bg-gray-700/50',
interactive && highContrast
? 'hover:opacity-70'
: 'hover:text-gray-900 dark:hover:text-gray-100',
interactive && highContrast
? 'active:opacity-90'
: 'active:bg-gray-200 dark:active:bg-gray-700/60',
);
}
};
return (

View File

@ -6,7 +6,7 @@ import { clsx } from 'clsx';
export interface EntityLinkExternalProps {
type?: 'icon-last' | 'icon-first' | 'icon-only' | 'text-only'
badged?: boolean
dim?: boolean
contrast?: 'low' | 'medium' | 'high'
}
export default function EntityLink({
@ -17,8 +17,8 @@ export default function EntityLink({
title,
type = 'icon-first',
badged,
contrast,
hoverEntity,
dim,
}: {
label: ReactNode
labelSmall?: ReactNode
@ -44,13 +44,18 @@ export default function EntityLink({
className={clsx(
'inline-flex gap-[0.23rem]',
!badged && 'text-main hover:text-gray-900 dark:hover:text-gray-100',
dim && 'text-dim',
contrast === 'low' && 'text-dim',
)}
>
{type !== 'icon-only' && <>
{badged
? <span className="h-6 inline-flex items-center">
<Badge type="secondary" uppercase interactive>
<Badge
type="small"
highContrast={contrast === 'high'}
uppercase
interactive
>
{renderLabel()}
</Badge>
</span>
@ -61,9 +66,11 @@ export default function EntityLink({
{icon && type !== 'text-only' &&
<span className={clsx(
'flex-shrink-0',
'text-dim inline-flex min-w-[0.9rem]',
'inline-flex min-w-[0.9rem]',
contrast === 'low' ? 'text-dim' : 'text-main',
type === 'icon-first' && 'order-first',
badged && 'translate-y-[4px]',
hoverEntity !== undefined && 'group-hover:hidden',
)}>
{icon}
</span>}

View File

@ -5,12 +5,11 @@ import PhotoTag from '@/tag/PhotoTag';
import { FaTag } from 'react-icons/fa';
import { IoMdCamera } from 'react-icons/io';
import { PhotoDateRange, dateRangeForPhotos, photoQuantityText } from '.';
import { Tags } from '@/tag';
import PhotoFilmSimulation from
'@/simulation/PhotoFilmSimulation';
import PhotoFilmSimulationIcon from
'@/simulation/PhotoFilmSimulationIcon';
import { TAG_FAVS, Tags } from '@/tag';
import PhotoFilmSimulation from '@/simulation/PhotoFilmSimulation';
import PhotoFilmSimulationIcon from '@/simulation/PhotoFilmSimulationIcon';
import { FilmSimulations, sortFilmSimulationsWithCount } from '@/simulation';
import FavsTag from '../tag/FavsTag';
export default function PhotoGridSidebar({
tags,
@ -32,8 +31,14 @@ export default function PhotoGridSidebar({
{tags.length > 0 && <HeaderList
title='Tags'
icon={<FaTag size={12} className="text-icon" />}
items={tags.map(({ tag, count }) =>
<PhotoTag
items={tags.map(({ tag, count }) => tag === TAG_FAVS
? <FavsTag
key={TAG_FAVS}
countOnHover={count}
type="icon-last"
badged
/>
: <PhotoTag
key={tag}
tag={tag}
type="text-only"

17
src/photo/data.ts Normal file
View File

@ -0,0 +1,17 @@
import {
getPhotosCountCached,
getUniqueCamerasCached,
getUniqueFilmSimulationsCached,
getUniqueTagsCached,
} from '@/cache';
import { SHOW_FILM_SIMULATIONS } from '@/site/config';
import { TAG_FAVS, Tags } from '@/tag';
export const getPhotoSidebarDataCached = () => [
getPhotosCountCached(),
getUniqueTagsCached().then(tags =>
([tags.find(({ tag }) => tag === TAG_FAVS) ?? []] as Tags)
.concat(tags.filter(({ tag }) => tag !== TAG_FAVS))),
getUniqueCamerasCached(),
SHOW_FILM_SIMULATIONS ? getUniqueFilmSimulationsCached() : [],
] as const;

View File

@ -8,7 +8,7 @@ export default function PhotoFilmSimulation({
simulation,
type = 'icon-last',
badged = true,
dim,
contrast,
countOnHover,
}: {
simulation: FilmSimulation
@ -28,7 +28,7 @@ export default function PhotoFilmSimulation({
title={`Film Simulation: ${large}`}
type={type}
badged={badged}
dim={dim}
contrast={contrast}
hoverEntity={countOnHover}
/>
);

View File

@ -132,4 +132,8 @@
@apply
text-red-500 dark:text-red-400
}
.bg-main {
@apply
bg-gray-900 dark:bg-gray-100
}
}

38
src/tag/FavsTag.tsx Normal file
View File

@ -0,0 +1,38 @@
import { FaStar } from 'react-icons/fa';
import EntityLink, { EntityLinkExternalProps } from '@/components/EntityLink';
import { TAG_FAVS } from '.';
import { pathForTag } from '@/site/paths';
export default function FavsTag({
type,
badged,
contrast,
countOnHover,
}: {
countOnHover?: number
} & EntityLinkExternalProps) {
return (
<EntityLink
label={
badged
? <span className="inline-flex gap-1">
{TAG_FAVS}
<FaStar
size={10}
className="text-amber-500"
/>
</span>
: TAG_FAVS}
href={pathForTag(TAG_FAVS)}
icon={!badged &&
<FaStar
size={12}
className="text-amber-500 translate-y-[4px]"
/>}
type={type}
hoverEntity={countOnHover}
badged={badged}
contrast={contrast}
/>
);
}

View File

@ -7,7 +7,7 @@ export default function PhotoTag({
tag,
type,
badged,
dim,
contrast,
countOnHover,
}: {
tag: string
@ -23,7 +23,7 @@ export default function PhotoTag({
/>}
type={type}
badged={badged}
dim={dim}
contrast={contrast}
hoverEntity={countOnHover}
/>
);

View File

@ -1,8 +1,9 @@
import { Photo, PhotoDateRange } from '@/photo';
import PhotoTag from './PhotoTag';
import { descriptionForTaggedPhotos } from '.';
import { descriptionForTaggedPhotos, isTagFavs } from '.';
import { pathForTagShare } from '@/site/paths';
import PhotoSetHeader from '@/photo/PhotoSetHeader';
import FavsTag from './FavsTag';
export default function TagHeader({
tag,
@ -19,7 +20,9 @@ export default function TagHeader({
}) {
return (
<PhotoSetHeader
entity={<PhotoTag tag={tag} />}
entity={isTagFavs(tag)
? <FavsTag />
: <PhotoTag tag={tag} />}
entityVerb="Tagged"
entityDescription={descriptionForTaggedPhotos(photos, undefined, count)}
photos={photos}

View File

@ -7,6 +7,8 @@ import {
import { absolutePathForTag, absolutePathForTagImage } from '@/site/paths';
import { capitalizeWords } from '@/utility/string';
export const TAG_FAVS = 'favs';
export type Tags = {
tag: string
count: number
@ -50,3 +52,5 @@ export const generateMetaForTag = (
descriptionForTaggedPhotos(photos, true, explicitCount, explicitDateRange),
images: absolutePathForTagImage(tag),
});
export const isTagFavs = (tag: string) => tag === TAG_FAVS;