From 69ec607e37185b8fb915ba77ad5121c2b0b1f320 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Sat, 29 Mar 2025 15:44:53 -0500 Subject: [PATCH] Show category counts on hover --- src/app/config.ts | 4 ++ src/camera/PhotoCamera.tsx | 12 +----- src/category/actions.ts | 42 +++++++++++++++++++ src/category/data.ts | 18 ++++++--- src/category/useCategoryCounts.ts | 49 +++++++++++++++++++++++ src/category/useCategoryCountsForPhoto.ts | 47 ++++++++++++++++++++++ src/components/primitives/EntityLink.tsx | 1 + src/focal/PhotoFocalLength.tsx | 12 +----- src/lens/PhotoLens.tsx | 12 +----- src/photo/PhotoLarge.tsx | 14 +++++++ src/recipe/PhotoRecipe.tsx | 14 ++----- src/simulation/PhotoFilmSimulation.tsx | 6 +-- src/state/AppState.ts | 4 +- src/state/AppStateProvider.tsx | 7 ++++ src/tag/PhotoTag.tsx | 12 +----- src/tag/PhotoTags.tsx | 13 +++++- tailwind.css | 8 ++++ 17 files changed, 211 insertions(+), 64 deletions(-) create mode 100644 src/category/actions.ts create mode 100644 src/category/useCategoryCounts.ts create mode 100644 src/category/useCategoryCountsForPhoto.ts diff --git a/src/app/config.ts b/src/app/config.ts index 084e4581..e899ec7e 100644 --- a/src/app/config.ts +++ b/src/app/config.ts @@ -241,8 +241,12 @@ export const MATTE_COLOR_DARK = export const CATEGORY_VISIBILITY = getOrderedCategoriesFromString( process.env.NEXT_PUBLIC_CATEGORY_VISIBILITY); +export const SHOW_CAMERAS = + CATEGORY_VISIBILITY.includes('cameras'); export const SHOW_LENSES = CATEGORY_VISIBILITY.includes('lenses'); +export const SHOW_TAGS = + CATEGORY_VISIBILITY.includes('tags'); export const SHOW_RECIPES = CATEGORY_VISIBILITY.includes('recipes'); export const SHOW_FILM_SIMULATIONS = diff --git a/src/camera/PhotoCamera.tsx b/src/camera/PhotoCamera.tsx index 4a2516ca..dc7b1eed 100644 --- a/src/camera/PhotoCamera.tsx +++ b/src/camera/PhotoCamera.tsx @@ -10,12 +10,8 @@ import { isCameraApple } from '@/platforms/apple'; export default function PhotoCamera({ camera, hideAppleIcon, - type, - badged, - contrast, - prefetch, countOnHover, - className, + ...props }: { camera: Camera hideAppleIcon?: boolean @@ -26,6 +22,7 @@ export default function PhotoCamera({ return ( } - type={type} - className={className} - badged={badged} - contrast={contrast} - prefetch={prefetch} hoverEntity={countOnHover} /> ); diff --git a/src/category/actions.ts b/src/category/actions.ts new file mode 100644 index 00000000..0d18584c --- /dev/null +++ b/src/category/actions.ts @@ -0,0 +1,42 @@ +'use server'; + +import { createLensKey } from '@/lens'; +import { getDataForCategories } from './data'; + +export const getCountsForCategoriesAction = async () => { + const [ + cameras, + lenses, + tags, + recipes, + filmSimulations, + focalLengths, + ] = await Promise.all(getDataForCategories()); + + return { + cameras: cameras.reduce((acc, camera) => { + acc[camera.cameraKey] = camera.count; + return acc; + }, {} as Record), + lenses: lenses.reduce((acc, lens) => { + acc[createLensKey(lens.lens)] = lens.count; + return acc; + }, {} as Record), + tags: tags.reduce((acc, tag) => { + acc[tag.tag] = tag.count; + return acc; + }, {} as Record), + recipes: recipes.reduce((acc, recipe) => { + acc[recipe.recipe] = recipe.count; + return acc; + }, {} as Record), + filmSimulations: filmSimulations.reduce((acc, filmSimulation) => { + acc[filmSimulation.simulation] = filmSimulation.count; + return acc; + }, {} as Record), + focalLengths: focalLengths.reduce((acc, focalLength) => { + acc[focalLength.focal] = focalLength.count; + return acc; + }, {} as Record), + }; +}; diff --git a/src/category/data.ts b/src/category/data.ts index 9a3e6082..c7087bee 100644 --- a/src/category/data.ts +++ b/src/category/data.ts @@ -11,23 +11,29 @@ import { SHOW_FOCAL_LENGTHS, SHOW_LENSES, SHOW_RECIPES, + SHOW_CAMERAS, + SHOW_TAGS, } from '@/app/config'; import { sortTagsByCount } from '@/tag'; import { sortCategoriesByCount } from '@/category'; import { sortFocalLengths } from '@/focal'; export const getDataForCategories = () => [ - getUniqueCameras() - .then(sortCategoriesByCount) - .catch(() => []), + SHOW_CAMERAS + ? getUniqueCameras() + .then(sortCategoriesByCount) + .catch(() => []) + : [], SHOW_LENSES ? getUniqueLenses() .then(sortCategoriesByCount) .catch(() => []) : [], - getUniqueTags() - .then(sortTagsByCount) - .catch(() => []), + SHOW_TAGS + ? getUniqueTags() + .then(sortTagsByCount) + .catch(() => []) + : [], SHOW_RECIPES ? getUniqueRecipes() .then(sortCategoriesByCount) diff --git a/src/category/useCategoryCounts.ts b/src/category/useCategoryCounts.ts new file mode 100644 index 00000000..75e9bbf6 --- /dev/null +++ b/src/category/useCategoryCounts.ts @@ -0,0 +1,49 @@ +import { createCameraKey } from '@/camera'; +import { createLensKey } from '@/lens'; +import { Camera } from '@/camera'; +import { Lens } from '@/lens'; +import { useAppState } from '@/state/AppState'; +import { useCallback } from 'react'; + +export default function useCategoryCounts() { + const { categoriesWithCounts } = useAppState(); + + const getCameraCount = useCallback((camera: Camera) => { + const cameraCounts = categoriesWithCounts?.cameras ?? {}; + return cameraCounts[createCameraKey(camera)]; + }, [categoriesWithCounts]); + + const getLensCount = useCallback((lens: Lens) => { + const lensCounts = categoriesWithCounts?.lenses ?? {}; + return lensCounts[createLensKey(lens)]; + }, [categoriesWithCounts]); + + const getTagCount = useCallback((tag: string) => { + const tagCounts = categoriesWithCounts?.tags ?? {}; + return tagCounts[tag]; + }, [categoriesWithCounts]); + + const getRecipeCount = useCallback((recipe: string) => { + const recipeCounts = categoriesWithCounts?.recipes ?? {}; + return recipeCounts[recipe]; + }, [categoriesWithCounts]); + + const getFilmSimulationCount = useCallback((simulation: string) => { + const filmSimulationCounts = categoriesWithCounts?.filmSimulations ?? {}; + return filmSimulationCounts[simulation]; + }, [categoriesWithCounts]); + + const getFocalLengthCount = useCallback((focalLength: number) => { + const focalLengthCounts = categoriesWithCounts?.focalLengths ?? {}; + return focalLengthCounts[focalLength]; + }, [categoriesWithCounts]); + + return { + getCameraCount, + getLensCount, + getTagCount, + getRecipeCount, + getFilmSimulationCount, + getFocalLengthCount, + }; +} diff --git a/src/category/useCategoryCountsForPhoto.ts b/src/category/useCategoryCountsForPhoto.ts new file mode 100644 index 00000000..86b50620 --- /dev/null +++ b/src/category/useCategoryCountsForPhoto.ts @@ -0,0 +1,47 @@ +import { Photo } from '@/photo'; +import useCategoryCounts from './useCategoryCounts'; +import { cameraFromPhoto } from '@/camera'; +import { lensFromPhoto } from '@/lens'; +import { useMemo } from 'react'; + +export default function useCategoryCountsForPhoto(photo: Photo) { + const { + getCameraCount, + getLensCount, + getTagCount, + getRecipeCount, + getFilmSimulationCount, + getFocalLengthCount, + } = useCategoryCounts(); + + const camera = cameraFromPhoto(photo); + const lens = lensFromPhoto(photo); + + const categoryCounts = useMemo(() => ({ + cameraCount: getCameraCount(camera), + lensCount: getLensCount(lens), + tagCounts: photo.tags.reduce((acc, tag) => { + acc[tag] = getTagCount(tag); + return acc; + }, {} as Record), + recipeCount: photo.recipeTitle ? getRecipeCount(photo.recipeTitle) : 0, + simulationCount: + photo.filmSimulation ? getFilmSimulationCount(photo.filmSimulation) : 0, + focalCount: photo.focalLength ? getFocalLengthCount(photo.focalLength) : 0, + }), [ + getCameraCount, + getLensCount, + getRecipeCount, + getFilmSimulationCount, + getFocalLengthCount, + getTagCount, + camera, + lens, + photo.tags, + photo.recipeTitle, + photo.filmSimulation, + photo.focalLength, + ]); + + return categoryCounts; +} diff --git a/src/components/primitives/EntityLink.tsx b/src/components/primitives/EntityLink.tsx index 0ea09ff1..f1e77b76 100644 --- a/src/components/primitives/EntityLink.tsx +++ b/src/components/primitives/EntityLink.tsx @@ -12,6 +12,7 @@ export interface EntityLinkExternalProps { type?: LabeledIconType badged?: boolean contrast?: ComponentProps['contrast'] + uppercase?: boolean prefetch?: boolean className?: string } diff --git a/src/focal/PhotoFocalLength.tsx b/src/focal/PhotoFocalLength.tsx index e99c3330..d0b99a3c 100644 --- a/src/focal/PhotoFocalLength.tsx +++ b/src/focal/PhotoFocalLength.tsx @@ -7,26 +7,18 @@ import IconFocalLength from '@/components/icons/IconFocalLength'; export default function PhotoFocalLength({ focal, - type, - badged, - contrast, - prefetch, countOnHover, - className, + ...props }: { focal: number countOnHover?: number } & EntityLinkExternalProps) { return ( } - type={type} - className={className} - badged={badged} - contrast={contrast} - prefetch={prefetch} hoverEntity={countOnHover} /> ); diff --git a/src/lens/PhotoLens.tsx b/src/lens/PhotoLens.tsx index 3cb5ecfa..de9b5723 100644 --- a/src/lens/PhotoLens.tsx +++ b/src/lens/PhotoLens.tsx @@ -7,13 +7,9 @@ import IconLens from '@/components/icons/IconLens'; export default function PhotoLens({ lens, - type, - badged, - contrast, - prefetch, countOnHover, - className, shortText, + ...props }: { lens: Lens countOnHover?: number @@ -21,17 +17,13 @@ export default function PhotoLens({ } & EntityLinkExternalProps) { return ( } - type={type} - className={className} - badged={badged} - contrast={contrast} - prefetch={prefetch} hoverEntity={countOnHover} /> ); diff --git a/src/photo/PhotoLarge.tsx b/src/photo/PhotoLarge.tsx index aed963f6..673f9369 100644 --- a/src/photo/PhotoLarge.tsx +++ b/src/photo/PhotoLarge.tsx @@ -51,6 +51,7 @@ import PhotoRecipe from '@/recipe/PhotoRecipe'; import PhotoLens from '@/lens/PhotoLens'; import { lensFromPhoto } from '@/lens'; import MaskedScroll from '@/components/MaskedScroll'; +import useCategoryCountsForPhoto from '@/category/useCategoryCountsForPhoto'; export default function PhotoLarge({ photo, @@ -114,6 +115,14 @@ export default function PhotoLarge({ isUserSignedIn, } = useAppState(); + const { + cameraCount, + lensCount, + tagCounts, + recipeCount, + simulationCount, + } = useCategoryCountsForPhoto(photo); + const showZoomControls = showZoomControlsProp && areZoomControlsShown; const refRecipe = useRef(null); @@ -306,6 +315,7 @@ export default function PhotoLarge({ camera={camera} contrast="medium" prefetch={prefetchRelatedLinks} + countOnHover={cameraCount} />} {showLensContent && } } {showRecipeContent && recipeTitle && @@ -320,10 +331,12 @@ export default function PhotoLarge({ recipe={recipeTitle} contrast="medium" prefetch={prefetchRelatedLinks} + countOnHover={recipeCount} />} {showTagsContent && } @@ -384,6 +397,7 @@ export default function PhotoLarge({ } {showRecipeButton && diff --git a/src/recipe/PhotoRecipe.tsx b/src/recipe/PhotoRecipe.tsx index 0cf442ac..94af5e47 100644 --- a/src/recipe/PhotoRecipe.tsx +++ b/src/recipe/PhotoRecipe.tsx @@ -9,15 +9,11 @@ import IconRecipe from '@/components/icons/IconRecipe'; export default function PhotoRecipe({ recipe, - type, - badged, - contrast, - prefetch, countOnHover, - className, refButton, isOpen, recipeOnClick, + ...props }: { recipe: string refButton?: RefObject @@ -28,22 +24,18 @@ export default function PhotoRecipe({ return (
} - className={className} - type={type} - badged={badged} - contrast={contrast} - prefetch={prefetch} hoverEntity={countOnHover} /> {recipeOnClick && diff --git a/src/simulation/PhotoFilmSimulation.tsx b/src/simulation/PhotoFilmSimulation.tsx index 98029d79..0d26d680 100644 --- a/src/simulation/PhotoFilmSimulation.tsx +++ b/src/simulation/PhotoFilmSimulation.tsx @@ -13,9 +13,8 @@ export default function PhotoFilmSimulation({ type = 'icon-last', badged = true, contrast = 'low', - prefetch, countOnHover, - className, + ...props }: { simulation: FilmSimulation countOnHover?: number @@ -25,6 +24,7 @@ export default function PhotoFilmSimulation({ return ( } title={`Film Simulation: ${large}`} type={type} - className={className} badged={badged} contrast={contrast} - prefetch={prefetch} hoverEntity={countOnHover} iconWide /> diff --git a/src/state/AppState.ts b/src/state/AppState.ts index 09a79c48..bad3c776 100644 --- a/src/state/AppState.ts +++ b/src/state/AppState.ts @@ -11,7 +11,7 @@ import { InsightsIndicatorStatus } from '@/admin/insights'; import { INITIAL_UPLOAD_STATE, UploadState } from '@/admin/upload'; import { AdminData } from '@/admin/actions'; import { RecipeProps } from '@/recipe'; - +import { getCountsForCategoriesAction } from '@/category/actions'; export type AppStateContext = { // CORE previousPathname?: string @@ -24,6 +24,8 @@ export type AppStateContext = { clearNextPhotoAnimation?: () => void shouldRespondToKeyboardCommands?: boolean setShouldRespondToKeyboardCommands?: Dispatch> + categoriesWithCounts?: + Awaited> // MODAL isCommandKOpen?: boolean setIsCommandKOpen?: Dispatch> diff --git a/src/state/AppStateProvider.tsx b/src/state/AppStateProvider.tsx index af49e2a1..1200d945 100644 --- a/src/state/AppStateProvider.tsx +++ b/src/state/AppStateProvider.tsx @@ -24,6 +24,7 @@ import { useRouter, usePathname } from 'next/navigation'; import { isPathAdmin, PATH_SIGN_IN } from '@/app/paths'; import { INITIAL_UPLOAD_STATE, UploadState } from '@/admin/upload'; import { RecipeProps } from '@/recipe'; +import { getCountsForCategoriesAction } from '@/category/actions'; export default function AppStateProvider({ children, @@ -90,6 +91,11 @@ export default function AppStateProvider({ const invalidateSwr = useCallback(() => setSwrTimestamp(Date.now()), []); + const { data: categoriesWithCounts } = useSWR( + 'getDataForCategories', + getCountsForCategoriesAction, + ); + const { data: auth, error: authError, @@ -167,6 +173,7 @@ export default function AppStateProvider({ clearNextPhotoAnimation: () => setNextPhotoAnimation?.(undefined), shouldRespondToKeyboardCommands, setShouldRespondToKeyboardCommands, + categoriesWithCounts, // MODAL isCommandKOpen, setIsCommandKOpen, diff --git a/src/tag/PhotoTag.tsx b/src/tag/PhotoTag.tsx index 532fcdb5..8e14ddfb 100644 --- a/src/tag/PhotoTag.tsx +++ b/src/tag/PhotoTag.tsx @@ -7,26 +7,18 @@ import IconTag from '@/components/icons/IconTag'; export default function PhotoTag({ tag, - type, - badged, - contrast, - prefetch, countOnHover, - className, + ...props }: { tag: string countOnHover?: number } & EntityLinkExternalProps) { return ( } - type={type} - className={className} - badged={badged} - contrast={contrast} - prefetch={prefetch} hoverEntity={countOnHover} /> ); diff --git a/src/tag/PhotoTags.tsx b/src/tag/PhotoTags.tsx index 802f76ee..b0050146 100644 --- a/src/tag/PhotoTags.tsx +++ b/src/tag/PhotoTags.tsx @@ -6,18 +6,27 @@ import { Fragment } from 'react'; export default function PhotoTags({ tags, + tagCounts = {}, contrast, prefetch, }: { tags: string[] + tagCounts?: Record } & EntityLinkExternalProps) { return (
{tags.map(tag => {isTagFavs(tag) - ? - : } + ? + : } )}
); diff --git a/tailwind.css b/tailwind.css index f2f8f7d5..e7312cb2 100644 --- a/tailwind.css +++ b/tailwind.css @@ -7,6 +7,14 @@ @custom-variant dark (&:where(.dark, .dark *)); +@custom-variant hover { + @media (pointer: fine) { + &:hover { + @slot; + } + } +} + @theme { --font-mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;