Vercel/src/photo/PhotoGridSidebar.tsx
2025-03-25 14:14:00 -05:00

274 lines
7.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
import PhotoCamera from '@/camera/PhotoCamera';
import HeaderList from '@/components/HeaderList';
import PhotoTag from '@/tag/PhotoTag';
import { PhotoDateRange, dateRangeForPhotos, photoQuantityText } from '.';
import { TAG_FAVS, TAG_HIDDEN, addHiddenToTags } from '@/tag';
import PhotoFilmSimulation from '@/simulation/PhotoFilmSimulation';
import FavsTag from '../tag/FavsTag';
import { useAppState } from '@/state/AppState';
import { useMemo, useRef } from 'react';
import HiddenTag from '@/tag/HiddenTag';
import { CATEGORY_VISIBILITY, PAGE_ABOUT } from '@/app/config';
import {
htmlHasBrParagraphBreaks,
safelyParseFormattedHtml,
} from '@/utility/html';
import { clsx } from 'clsx/lite';
import PhotoRecipe from '@/recipe/PhotoRecipe';
import IconCamera from '@/components/icons/IconCamera';
import IconRecipe from '@/components/icons/IconRecipe';
import IconTag from '@/components/icons/IconTag';
import IconFilmSimulation from '@/components/icons/IconFilmSimulation';
import IconLens from '@/components/icons/IconLens';
import PhotoLens from '@/lens/PhotoLens';
import IconFocalLength from '@/components/icons/IconFocalLength';
import {
getCategoriesWithItemsCount,
PhotoSetCategories,
} from '@/category';
import PhotoFocalLength from '@/focal/PhotoFocalLength';
import useElementHeight from '@/utility/useElementHeight';
const APPROXIMATE_ITEM_HEIGHT = 34;
const ABOUT_HEIGHT_OFFSET = 80;
export default function PhotoGridSidebar({
photosCount,
photosDateRange,
containerHeight,
...categories
}: PhotoSetCategories & {
photosCount: number
photosDateRange?: PhotoDateRange
containerHeight?: number
}) {
const {
cameras,
lenses,
tags,
simulations,
recipes,
focalLengths,
} = categories;
const categoriesCount = getCategoriesWithItemsCount(
CATEGORY_VISIBILITY,
categories,
);
const aboutRef = useRef<HTMLParagraphElement>(null);
const aboutHeight = useElementHeight(aboutRef);
const height = containerHeight
? containerHeight - (aboutHeight ? aboutHeight + ABOUT_HEIGHT_OFFSET : 0)
: undefined;
const maxItemsPerCategory = height
? Math.max(
Math.floor(height / categoriesCount / APPROXIMATE_ITEM_HEIGHT),
// Always show at least 2 items
2,
)
: undefined;
const { start, end } = dateRangeForPhotos(undefined, photosDateRange);
const { photosCountHidden } = useAppState();
const tagsIncludingHidden = useMemo(() =>
addHiddenToTags(tags, photosCountHidden)
, [tags, photosCountHidden]);
const camerasContent = cameras.length > 0
? <HeaderList
key="cameras"
title="Cameras"
icon={<IconCamera
size={15}
className="translate-x-[0.5px]"
/>}
maxItems={maxItemsPerCategory}
items={cameras
.map(({ cameraKey, camera, count }) =>
<PhotoCamera
key={cameraKey}
camera={camera}
type="text-only"
countOnHover={count}
prefetch={false}
contrast="low"
hideAppleIcon
badged
/>)}
/>
: null;
const lensesContent = lenses.length > 0
? <HeaderList
key="lenses"
title="Lenses"
icon={<IconLens size={15} />}
maxItems={maxItemsPerCategory}
items={lenses
.map(({ lensKey, lens, count }) =>
<PhotoLens
key={lensKey}
lens={lens}
type="text-only"
countOnHover={count}
prefetch={false}
contrast="low"
badged
/>)}
/>
: null;
const tagsContent = tags.length > 0
? <HeaderList
key="tags"
title='Tags'
icon={<IconTag
size={14}
className="translate-x-[1px] translate-y-[1px]"
/>}
maxItems={maxItemsPerCategory}
items={tagsIncludingHidden
.map(({ tag, count }) => {
switch (tag) {
case TAG_FAVS:
return <FavsTag
key={TAG_FAVS}
countOnHover={count}
type="icon-last"
prefetch={false}
contrast="low"
badged
/>;
case TAG_HIDDEN:
return <HiddenTag
key={TAG_HIDDEN}
countOnHover={count}
type="icon-last"
prefetch={false}
contrast="low"
badged
/>;
default:
return <PhotoTag
key={tag}
tag={tag}
type="text-only"
countOnHover={count}
prefetch={false}
contrast="low"
badged
/>;
}
})}
/>
: null;
const recipesContent = recipes.length > 0
? <HeaderList
key="recipes"
title="Recipes"
icon={<IconRecipe
size={16}
className="translate-x-[-1px]"
/>}
maxItems={maxItemsPerCategory}
items={recipes
.map(({ recipe, count }) =>
<PhotoRecipe
key={recipe}
recipe={recipe}
type="text-only"
countOnHover={count}
prefetch={false}
contrast="low"
badged
/>)}
/>
: null;
const filmsContent = simulations.length > 0
? <HeaderList
key="films"
title="Films"
icon={<IconFilmSimulation size={15} />}
maxItems={maxItemsPerCategory}
items={simulations
.map(({ simulation, count }) =>
<PhotoFilmSimulation
key={simulation}
simulation={simulation}
countOnHover={count}
type="text-only"
prefetch={false}
/>)}
/>
: null;
const focalLengthsContent = focalLengths.length > 0
? <HeaderList
key="focal-lengths"
title="Focal Lengths"
icon={<IconFocalLength size={13} />}
maxItems={maxItemsPerCategory}
items={focalLengths.map(({ focal, count }) =>
<PhotoFocalLength
key={focal}
focal={focal}
countOnHover={count}
type="text-only"
prefetch={false}
/>)}
/>
: null;
const photoStatsContent = photosCount > 0
? start
? <HeaderList
key="photo-stats"
title={photoQuantityText(photosCount, false)}
items={start === end
? [start]
: [`${end} `, start]}
/>
: <HeaderList
key="photo-stats"
items={[photoQuantityText(photosCount, false)]}
/>
: null;
return (
<div className="space-y-4">
{PAGE_ABOUT && <HeaderList
items={[<p
key="about"
ref={aboutRef}
className={clsx(
'max-w-60 normal-case text-dim',
htmlHasBrParagraphBreaks(PAGE_ABOUT) && 'pb-2',
)}
dangerouslySetInnerHTML={{
__html: safelyParseFormattedHtml(PAGE_ABOUT),
}}
/>]}
/>}
{CATEGORY_VISIBILITY.map(category => {
switch (category) {
case 'cameras': return camerasContent;
case 'lenses': return lensesContent;
case 'tags': return tagsContent;
case 'recipes': return recipesContent;
case 'films': return filmsContent;
case 'focal-lengths': return focalLengthsContent;
}
})}
{photoStatsContent}
</div>
);
}