diff --git a/.vscode/settings.json b/.vscode/settings.json index b1ff9f78..7f46718b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -11,6 +11,7 @@ "exif", "exifr", "exiftool", + "favs", "ghijklmnopqrstuv", "hgetall", "hset", diff --git a/src/app/(static)/grid/page.tsx b/src/app/(static)/grid/page.tsx index 04b49373..c9a4d5bc 100644 --- a/src/app/(static)/grid/page.tsx +++ b/src/app/(static)/grid/page.tsx @@ -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 diff --git a/src/app/(static)/sets/page.tsx b/src/app/(static)/sets/page.tsx index 9427e65d..dfcca476 100644 --- a/src/app/(static)/sets/page.tsx +++ b/src/app/(static)/sets/page.tsx @@ -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 ( } type={showAppleIcon && isCameraApple ? 'icon-first' : type} badged={badged} - dim={dim} + contrast={contrast} hoverEntity={countOnHover} /> ); diff --git a/src/components/Badge.tsx b/src/components/Badge.tsx index 541dfb89..c8078b01 100644 --- a/src/components/Badge.tsx +++ b/src/components/Badge.tsx @@ -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 ( diff --git a/src/components/EntityLink.tsx b/src/components/EntityLink.tsx index aad2dd48..220d20fa 100644 --- a/src/components/EntityLink.tsx +++ b/src/components/EntityLink.tsx @@ -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 ? - + {renderLabel()} @@ -61,9 +66,11 @@ export default function EntityLink({ {icon && type !== 'text-only' && {icon} } diff --git a/src/photo/PhotoGridSidebar.tsx b/src/photo/PhotoGridSidebar.tsx index 0eec733c..999c3c25 100644 --- a/src/photo/PhotoGridSidebar.tsx +++ b/src/photo/PhotoGridSidebar.tsx @@ -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 && } - items={tags.map(({ tag, count }) => - tag === TAG_FAVS + ? + : [ + 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; diff --git a/src/simulation/PhotoFilmSimulation.tsx b/src/simulation/PhotoFilmSimulation.tsx index 08bb8a02..7087be84 100644 --- a/src/simulation/PhotoFilmSimulation.tsx +++ b/src/simulation/PhotoFilmSimulation.tsx @@ -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} /> ); diff --git a/src/site/globals.css b/src/site/globals.css index 81c052e0..dc8ba265 100644 --- a/src/site/globals.css +++ b/src/site/globals.css @@ -132,4 +132,8 @@ @apply text-red-500 dark:text-red-400 } + .bg-main { + @apply + bg-gray-900 dark:bg-gray-100 + } } diff --git a/src/tag/FavsTag.tsx b/src/tag/FavsTag.tsx new file mode 100644 index 00000000..fc92975a --- /dev/null +++ b/src/tag/FavsTag.tsx @@ -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 ( + + {TAG_FAVS} + + + : TAG_FAVS} + href={pathForTag(TAG_FAVS)} + icon={!badged && + } + type={type} + hoverEntity={countOnHover} + badged={badged} + contrast={contrast} + /> + ); +} diff --git a/src/tag/PhotoTag.tsx b/src/tag/PhotoTag.tsx index 800833fc..ecc4efaf 100644 --- a/src/tag/PhotoTag.tsx +++ b/src/tag/PhotoTag.tsx @@ -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} /> ); diff --git a/src/tag/TagHeader.tsx b/src/tag/TagHeader.tsx index 7d63ed21..f320de7d 100644 --- a/src/tag/TagHeader.tsx +++ b/src/tag/TagHeader.tsx @@ -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 ( } + entity={isTagFavs(tag) + ? + : } entityVerb="Tagged" entityDescription={descriptionForTaggedPhotos(photos, undefined, count)} photos={photos} diff --git a/src/tag/index.ts b/src/tag/index.ts index e3ab8bb7..1b773b93 100644 --- a/src/tag/index.ts +++ b/src/tag/index.ts @@ -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;