From f781ddbc608cfb6c17db4f128d8ba3035993239b Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Sat, 22 Mar 2025 23:51:10 -0500 Subject: [PATCH 1/9] Collapse long sidebar sections --- src/components/HeaderList.tsx | 39 +++++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/src/components/HeaderList.tsx b/src/components/HeaderList.tsx index b652538a..3b058f5a 100644 --- a/src/components/HeaderList.tsx +++ b/src/components/HeaderList.tsx @@ -1,6 +1,12 @@ +'use client'; + import { clsx } from 'clsx/lite'; import AnimateItems from './AnimateItems'; -import { ReactNode } from 'react'; +import { ReactNode, useState } from 'react'; +import LoaderButton from './primitives/LoaderButton'; +import { IoChevronDownOutline, IoChevronUpOutline } from 'react-icons/io5'; + +const SIDEBAR_MAX_COLLAPSE_ITEMS = 5; export default function HeaderList({ title, @@ -13,6 +19,10 @@ export default function HeaderList({ icon?: ReactNode, items: ReactNode[] }) { + const [isExpanded, setIsExpanded] = useState(false); + + const hasMoreItems = items.length > SIDEBAR_MAX_COLLAPSE_ITEMS; + return ( ] :[] as ReactNode[] - ).concat(items)} + ) + .concat(items.slice(0, isExpanded + ? items.length + : SIDEBAR_MAX_COLLAPSE_ITEMS)) + .concat(hasMoreItems + ? [ + setIsExpanded(!isExpanded)} + className={clsx( + 'mt-1', + 'text-xs tracking-wide', + 'border-medium rounded-md', + 'px-1.5 h-6!', + 'hover:bg-dim active:bg-main', + )} + > + { + {isExpanded ? 'HIDE' : 'MORE'} + {isExpanded + ? + : } + } + , + ] + : null)} classNameItem="text-dim uppercase" /> ); From 6c967b9970599043a699b2d9d1cf4a77d83a1a2a Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Sun, 23 Mar 2025 11:46:29 -0500 Subject: [PATCH 2/9] Collapse sidebar based on configuration --- src/app/config.ts | 2 ++ src/components/HeaderList.tsx | 34 ++++++++++++++++++++-------------- src/photo/PhotoGridSidebar.tsx | 7 +++++-- 3 files changed, 27 insertions(+), 16 deletions(-) diff --git a/src/app/config.ts b/src/app/config.ts index b20bb9f1..25f16db0 100644 --- a/src/app/config.ts +++ b/src/app/config.ts @@ -220,6 +220,8 @@ export const SHOW_FILM_SIMULATIONS = CATEGORY_VISIBILITY.includes('films'); export const SHOW_FOCAL_LENGTHS = CATEGORY_VISIBILITY.includes('focal-lengths'); +export const COLLAPSE_SIDEBAR_CATEGORIES = + process.env.NEXT_PUBLIC_EXPAND_SIDEBAR_CATEGORIES !== '1'; export const SHOW_EXIF_DATA = process.env.NEXT_PUBLIC_HIDE_EXIF_DATA !== '1'; export const SHOW_ZOOM_CONTROLS = diff --git a/src/components/HeaderList.tsx b/src/components/HeaderList.tsx index 3b058f5a..be6be34a 100644 --- a/src/components/HeaderList.tsx +++ b/src/components/HeaderList.tsx @@ -5,29 +5,33 @@ import AnimateItems from './AnimateItems'; import { ReactNode, useState } from 'react'; import LoaderButton from './primitives/LoaderButton'; import { IoChevronDownOutline, IoChevronUpOutline } from 'react-icons/io5'; - -const SIDEBAR_MAX_COLLAPSE_ITEMS = 5; +import { COLLAPSE_SIDEBAR_CATEGORIES } from '@/app/config'; export default function HeaderList({ title, className, icon, items, + maxItems = 5, }: { title?: string, className?: string, icon?: ReactNode, - items: ReactNode[] + items: ReactNode[], + maxItems?: number, }) { const [isExpanded, setIsExpanded] = useState(false); - const hasMoreItems = items.length > SIDEBAR_MAX_COLLAPSE_ITEMS; + const hasItemsToExpand = + COLLAPSE_SIDEBAR_CATEGORIES && + // Don't show expand button if it only reveals 1 item + items.length > (maxItems + 1); return ( {icon && @@ -48,26 +52,28 @@ export default function HeaderList({ } {title} ] - :[] as ReactNode[] + : [] as ReactNode[] ) - .concat(items.slice(0, isExpanded - ? items.length - : SIDEBAR_MAX_COLLAPSE_ITEMS)) - .concat(hasMoreItems + .concat(items.slice( + 0, + hasItemsToExpand && !isExpanded ? maxItems : items.length, + )) + .concat(hasItemsToExpand ? [ setIsExpanded(!isExpanded)} + styleAs="link" className={clsx( 'mt-1', - 'text-xs tracking-wide', + 'text-xs font-medium tracking-wider', 'border-medium rounded-md', - 'px-1.5 h-6!', + 'px-[5px] h-5!', 'hover:bg-dim active:bg-main', )} > { - {isExpanded ? 'HIDE' : 'MORE'} + {isExpanded ? 'LESS' : 'MORE'} {isExpanded ? : } diff --git a/src/photo/PhotoGridSidebar.tsx b/src/photo/PhotoGridSidebar.tsx index 2ac56a48..5c08be71 100644 --- a/src/photo/PhotoGridSidebar.tsx +++ b/src/photo/PhotoGridSidebar.tsx @@ -56,7 +56,10 @@ export default function PhotoGridSidebar({ ? } + icon={} items={cameras .sort(sortCamerasWithCount) .map(({ cameraKey, camera, count }) => @@ -99,7 +102,7 @@ export default function PhotoGridSidebar({ title='Tags' icon={} items={tagsIncludingHidden.map(({ tag, count }) => { switch (tag) { From 7c3db85600c4c9e4efcb10688fba1bb786d0ecc8 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Sun, 23 Mar 2025 12:02:57 -0500 Subject: [PATCH 3/9] Sort categories by count --- src/app/CommandK.tsx | 13 +++--- src/category/index.ts | 5 +++ src/photo/PhotoGridSidebar.tsx | 80 +++++++++++++++++----------------- src/tag/index.ts | 17 ++++---- 4 files changed, 60 insertions(+), 55 deletions(-) diff --git a/src/app/CommandK.tsx b/src/app/CommandK.tsx index a32066b0..36e12f14 100644 --- a/src/app/CommandK.tsx +++ b/src/app/CommandK.tsx @@ -14,6 +14,7 @@ import { SHOW_RECIPES, } from './config'; import { getUniqueFocalLengths } from '@/photo/db/query'; +import { sortCategoryByCount } from '@/category'; export default async function CommandK() { const [ @@ -41,12 +42,12 @@ export default async function CommandK() { ]); return ; diff --git a/src/category/index.ts b/src/category/index.ts index 970c97f9..713f7b79 100644 --- a/src/category/index.ts +++ b/src/category/index.ts @@ -66,3 +66,8 @@ export const getOrderedCategoriesFromString = ( .map(category => category.trim().toLocaleLowerCase() as CategoryKey) .filter(category => CATEGORY_KEYS.includes(category)) : DEFAULT_CATEGORY_KEYS; + +export const sortCategoryByCount = ( + a: { count: number }, + b: { count: number }, +) => b.count - a.count; diff --git a/src/photo/PhotoGridSidebar.tsx b/src/photo/PhotoGridSidebar.tsx index 5c08be71..d379a87c 100644 --- a/src/photo/PhotoGridSidebar.tsx +++ b/src/photo/PhotoGridSidebar.tsx @@ -1,13 +1,11 @@ 'use client'; -import { sortCamerasWithCount } from '@/camera'; 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 { sortFilmSimulationsWithCount } from '@/simulation'; import FavsTag from '../tag/FavsTag'; import { useAppState } from '@/state/AppState'; import { useMemo } from 'react'; @@ -18,17 +16,15 @@ import { safelyParseFormattedHtml, } from '@/utility/html'; import { clsx } from 'clsx/lite'; -import { sortRecipesWithCount } from '@/recipe'; 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 { sortLensesWithCount } from '@/lens'; import PhotoLens from '@/lens/PhotoLens'; import IconFocalLength from '@/components/icons/IconFocalLength'; -import { PhotoSetCategories } from '@/category'; +import { PhotoSetCategories, sortCategoryByCount } from '@/category'; import PhotoFocalLength from '@/focal/PhotoFocalLength'; export default function PhotoGridSidebar({ @@ -61,7 +57,7 @@ export default function PhotoGridSidebar({ className="translate-x-[0.5px]" />} items={cameras - .sort(sortCamerasWithCount) + .sort(sortCategoryByCount) .map(({ cameraKey, camera, count }) => } items={lenses - .sort(sortLensesWithCount) + .sort(sortCategoryByCount) .map(({ lensKey, lens, count }) => } - items={tagsIncludingHidden.map(({ tag, count }) => { - switch (tag) { - case TAG_FAVS: - return ; - case TAG_HIDDEN: - return ; - default: - return ; - } - })} + items={tagsIncludingHidden + .map(({ tag, count }) => { + switch (tag) { + case TAG_FAVS: + return ; + case TAG_HIDDEN: + return ; + default: + return ; + } + })} /> : null; @@ -147,7 +144,8 @@ export default function PhotoGridSidebar({ size={16} className="translate-x-[-1px]" />} - items={sortRecipesWithCount(recipes) + items={recipes + .sort(sortCategoryByCount) .map(({ recipe, count }) => } items={simulations - .sort(sortFilmSimulationsWithCount) + .sort(sortCategoryByCount) .map(({ simulation, count }) => export const isTagHidden = (tag: string) => tag.toLowerCase() === TAG_HIDDEN; -export const addHiddenToTags = (tags: Tags, photosCountHidden = 0) => { - if (photosCountHidden > 0) { - return tags +export const addHiddenToTags = (tags: Tags, photosCountHidden = 0) => + photosCountHidden > 0 + ? tags .filter(({ tag }) => tag === TAG_FAVS) .concat({ tag: TAG_HIDDEN, count: photosCountHidden }) - .concat(tags.filter(({ tag }) => tag !== TAG_FAVS)); - } else { - return tags; - } -}; + .concat(tags + .filter(({ tag }) => tag !== TAG_FAVS) + .sort(sortCategoryByCount), + ) + : tags; export const convertTagsForForm = (tags: Tags = []) => sortTagsObjectWithoutFavs(tags) From cdc83758b262e83d9d54ed74c4ee3b1e79fb3eb8 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Sun, 23 Mar 2025 16:57:55 -0500 Subject: [PATCH 4/9] Sort sidebar content by count --- app/grid/page.tsx | 6 ++--- app/page.tsx | 8 +++--- src/admin/AdminRecipeTable.tsx | 4 +-- src/admin/AdminTagTable.tsx | 4 +-- src/app/CommandK.tsx | 41 ++++++++----------------------- src/category/data.ts | 45 ++++++++++++++++++++++++++++++++++ src/category/index.ts | 4 +++ src/photo/PhotoGridSidebar.tsx | 6 +---- src/photo/PhotoLarge.tsx | 4 +-- src/photo/data.ts | 41 ------------------------------- src/recipe/index.ts | 4 +-- src/simulation/index.ts | 4 +++ src/tag/index.ts | 20 ++++++++++----- 13 files changed, 93 insertions(+), 98 deletions(-) create mode 100644 src/category/data.ts delete mode 100644 src/photo/data.ts diff --git a/app/grid/page.tsx b/app/grid/page.tsx index 34a8d025..f6d8aad3 100644 --- a/app/grid/page.tsx +++ b/app/grid/page.tsx @@ -4,7 +4,7 @@ import { } from '@/photo'; import PhotosEmptyState from '@/photo/PhotosEmptyState'; import { Metadata } from 'next/types'; -import { getPhotoSidebarData } from '@/photo/data'; +import { getDataForCategories } from '@/category/data'; import { getPhotos, getPhotosMeta } from '@/photo/db/query'; import { cache } from 'react'; import PhotoGridPage from '@/photo/PhotoGridPage'; @@ -28,8 +28,8 @@ export default async function GridPage() { cameras, lenses, tags, - simulations, recipes, + simulations, focalLengths, ] = await Promise.all([ getPhotosCached() @@ -37,7 +37,7 @@ export default async function GridPage() { getPhotosMeta() .then(({ count }) => count) .catch(() => 0), - ...getPhotoSidebarData(), + ...getDataForCategories(), ]); return ( diff --git a/app/page.tsx b/app/page.tsx index 0a91f53a..39e9797d 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -8,7 +8,7 @@ import { Metadata } from 'next/types'; import { cache } from 'react'; import { getPhotos, getPhotosMeta } from '@/photo/db/query'; import { GRID_HOMEPAGE_ENABLED } from '@/app/config'; -import { getPhotoSidebarData } from '@/photo/data'; +import { getDataForCategories } from '@/category/data'; import PhotoGridPage from '@/photo/PhotoGridPage'; import PhotoFeedPage from '@/photo/PhotoFeedPage'; @@ -34,8 +34,8 @@ export default async function HomePage() { cameras, lenses, tags, - simulations, recipes, + simulations, focalLengths, ] = await Promise.all([ getPhotosCached() @@ -44,7 +44,7 @@ export default async function HomePage() { .then(({ count }) => count) .catch(() => 0), ...(GRID_HOMEPAGE_ENABLED - ? getPhotoSidebarData() + ? getDataForCategories() : [[], [], [], [], [], [], []]), ]); @@ -58,8 +58,8 @@ export default async function HomePage() { cameras, lenses, tags, - simulations, recipes, + simulations, focalLengths, }} /> diff --git a/src/admin/AdminRecipeTable.tsx b/src/admin/AdminRecipeTable.tsx index 7840a8fd..37f5232b 100644 --- a/src/admin/AdminRecipeTable.tsx +++ b/src/admin/AdminRecipeTable.tsx @@ -7,7 +7,7 @@ import { photoQuantityText } from '@/photo'; import EditButton from '@/admin/EditButton'; import { pathForAdminRecipeEdit } from '@/app/paths'; import { clsx } from 'clsx/lite'; -import { formatRecipe, Recipes, sortRecipesWithCount } from '@/recipe'; +import { formatRecipe, Recipes, sortRecipes } from '@/recipe'; import AdminRecipeBadge from './AdminRecipeBadge'; export default function AdminRecipeTable({ @@ -17,7 +17,7 @@ export default function AdminRecipeTable({ }) { return ( - {sortRecipesWithCount(recipes).map(({ recipe, count }) => + {sortRecipes(recipes).map(({ recipe, count }) =>
diff --git a/src/admin/AdminTagTable.tsx b/src/admin/AdminTagTable.tsx index 8f961344..759dc495 100644 --- a/src/admin/AdminTagTable.tsx +++ b/src/admin/AdminTagTable.tsx @@ -4,7 +4,7 @@ import AdminTable from '@/admin/AdminTable'; import { Fragment } from 'react'; import DeleteFormButton from '@/admin/DeleteFormButton'; import { photoQuantityText } from '@/photo'; -import { Tags, formatTag, sortTagsObject } from '@/tag'; +import { Tags, formatTag, sortTags } from '@/tag'; import EditButton from '@/admin/EditButton'; import { pathForAdminTagEdit } from '@/app/paths'; import { clsx } from 'clsx/lite'; @@ -17,7 +17,7 @@ export default function AdminTagTable({ }) { return ( - {sortTagsObject(tags).map(({ tag, count }) => + {sortTags(tags).map(({ tag, count }) =>
diff --git a/src/app/CommandK.tsx b/src/app/CommandK.tsx index 36e12f14..af1d9413 100644 --- a/src/app/CommandK.tsx +++ b/src/app/CommandK.tsx @@ -1,20 +1,8 @@ import CommandKClient from '@/components/cmdk/CommandKClient'; -import { - getPhotosMetaCached, - getUniqueCamerasCached, - getUniqueFilmSimulationsCached, - getUniqueLensesCached, - getUniqueRecipesCached, - getUniqueTagsCached, -} from '@/photo/cache'; +import { getPhotosMetaCached } from '@/photo/cache'; import { photoQuantityText } from '@/photo'; -import { - ADMIN_DEBUG_TOOLS_ENABLED, - SHOW_FILM_SIMULATIONS, - SHOW_RECIPES, -} from './config'; -import { getUniqueFocalLengths } from '@/photo/db/query'; -import { sortCategoryByCount } from '@/category'; +import { ADMIN_DEBUG_TOOLS_ENABLED } from './config'; +import { getDataForCategories } from '@/category/data'; export default async function CommandK() { const [ @@ -29,25 +17,16 @@ export default async function CommandK() { getPhotosMetaCached() .then(({ count }) => count) .catch(() => 0), - getUniqueCamerasCached().catch(() => []), - getUniqueLensesCached().catch(() => []), - getUniqueTagsCached().catch(() => []), - SHOW_RECIPES - ? getUniqueRecipesCached().catch(() => []) - : [], - SHOW_FILM_SIMULATIONS - ? getUniqueFilmSimulationsCached().catch(() => []) - : [], - getUniqueFocalLengths().catch(() => []), + ...getDataForCategories(), ]); return ; diff --git a/src/category/data.ts b/src/category/data.ts new file mode 100644 index 00000000..1cb56b35 --- /dev/null +++ b/src/category/data.ts @@ -0,0 +1,45 @@ +import { + getUniqueCameras, + getUniqueFilmSimulations, + getUniqueFocalLengths, + getUniqueLenses, + getUniqueRecipes, + getUniqueTags, +} from '@/photo/db/query'; +import { + SHOW_FILM_SIMULATIONS, + SHOW_FOCAL_LENGTHS, + SHOW_LENSES, + SHOW_RECIPES, +} from '@/app/config'; +import { sortTagsByCount } from '@/tag'; +import { sortCategoriesByCount } from '@/category'; + +export const getDataForCategories = () => [ + getUniqueCameras() + .then(sortCategoriesByCount) + .catch(() => []), + SHOW_LENSES + ? getUniqueLenses() + .then(sortCategoriesByCount) + .catch(() => []) + : [], + getUniqueTags() + .then(sortTagsByCount) + .catch(() => []), + SHOW_RECIPES + ? getUniqueRecipes() + .then(sortCategoriesByCount) + .catch(() => []) + : [], + SHOW_FILM_SIMULATIONS + ? getUniqueFilmSimulations() + .then(sortCategoriesByCount) + .catch(() => []) + : [], + SHOW_FOCAL_LENGTHS + ? getUniqueFocalLengths() + .then(sortCategoriesByCount) + .catch(() => []) + : [], +] as const; diff --git a/src/category/index.ts b/src/category/index.ts index 713f7b79..f12ea34d 100644 --- a/src/category/index.ts +++ b/src/category/index.ts @@ -71,3 +71,7 @@ export const sortCategoryByCount = ( a: { count: number }, b: { count: number }, ) => b.count - a.count; + +export const sortCategoriesByCount = ( + categories: T[], +) => categories.sort(sortCategoryByCount); diff --git a/src/photo/PhotoGridSidebar.tsx b/src/photo/PhotoGridSidebar.tsx index d379a87c..3bae4eae 100644 --- a/src/photo/PhotoGridSidebar.tsx +++ b/src/photo/PhotoGridSidebar.tsx @@ -24,7 +24,7 @@ import IconFilmSimulation from '@/components/icons/IconFilmSimulation'; import IconLens from '@/components/icons/IconLens'; import PhotoLens from '@/lens/PhotoLens'; import IconFocalLength from '@/components/icons/IconFocalLength'; -import { PhotoSetCategories, sortCategoryByCount } from '@/category'; +import { PhotoSetCategories } from '@/category'; import PhotoFocalLength from '@/focal/PhotoFocalLength'; export default function PhotoGridSidebar({ @@ -57,7 +57,6 @@ export default function PhotoGridSidebar({ className="translate-x-[0.5px]" />} items={cameras - .sort(sortCategoryByCount) .map(({ cameraKey, camera, count }) => } items={lenses - .sort(sortCategoryByCount) .map(({ lensKey, lens, count }) => } items={recipes - .sort(sortCategoryByCount) .map(({ recipe, count }) => } items={simulations - .sort(sortCategoryByCount) .map(({ simulation, count }) => [ - getUniqueCameras().catch(() => []), - SHOW_LENSES ? getUniqueLenses().catch(() => []) : [], - getUniqueTags().then(sortTagsObject).catch(() => []), - SHOW_FILM_SIMULATIONS ? getUniqueFilmSimulations().catch(() => []) : [], - SHOW_RECIPES ? getUniqueRecipes().catch(() => []) : [], - SHOW_FOCAL_LENGTHS ? getUniqueFocalLengths().catch(() => []) : [], -] as const; - -export const getPhotoSidebarDataCached = () => [ - getUniqueCamerasCached(), - SHOW_LENSES ? getUniqueLensesCached() : [], - getUniqueTagsCached().then(sortTagsObject), - SHOW_FILM_SIMULATIONS ? getUniqueFilmSimulationsCached() : [], - SHOW_RECIPES ? getUniqueRecipesCached() : [], - SHOW_FOCAL_LENGTHS ? getUniqueFocalLengthsCached() : [], -] as const; diff --git a/src/recipe/index.ts b/src/recipe/index.ts index dc0fff5a..832ca371 100644 --- a/src/recipe/index.ts +++ b/src/recipe/index.ts @@ -143,11 +143,11 @@ export const getPhotoWithRecipeFromPhotos = ( ? preferredPhoto : photos.find(photoHasRecipe); -export const sortRecipesWithCount = (recipes: Recipes = []) => +export const sortRecipes = (recipes: Recipes = []) => recipes.sort((a, b) => a.recipe.localeCompare(b.recipe)); export const convertRecipesForForm = (recipes: Recipes = []) => - sortRecipesWithCount(recipes) + sortRecipes(recipes) .map(({ recipe, count }) => ({ value: recipe, annotation: formatCount(count), diff --git a/src/simulation/index.ts b/src/simulation/index.ts index 5b283887..a370083d 100644 --- a/src/simulation/index.ts +++ b/src/simulation/index.ts @@ -22,6 +22,10 @@ export type FilmSimulationWithCount = { export type FilmSimulations = FilmSimulationWithCount[] +export const sortFilmSimulations = ( + simulations: FilmSimulations, +) => simulations.sort(sortFilmSimulationsWithCount); + export const sortFilmSimulationsWithCount = ( a: FilmSimulationWithCount, b: FilmSimulationWithCount, diff --git a/src/tag/index.ts b/src/tag/index.ts index bbce06e8..2111bcfa 100644 --- a/src/tag/index.ts +++ b/src/tag/index.ts @@ -50,25 +50,33 @@ export const titleForTag = ( export const shareTextForTag = (tag: string) => isTagFavs(tag) ? 'Favorite photos' : `Photos tagged '${formatTag(tag)}'`; -export const sortTags = ( +export const sortTagsArray = ( tags: string[], tagToExclude?: string, ) => tags .filter(tag => tag !== tagToExclude) .sort((a, b) => isTagFavs(a) ? -1 : a.localeCompare(b)); -export const sortTagsObject = ( +export const sortTags = ( tags: Tags, - tagToHide?: string, + tagToExclude?: string, ) => tags - .filter(({ tag }) => tag!== tagToHide) + .filter(({ tag }) => tag!== tagToExclude) .sort(({ tag: a }, { tag: b }) => isTagFavs(a) ? -1 : a.localeCompare(b)); +export const sortTagsByCount = ( + tags: Tags, + tagToExclude?: string, +) => tags + .filter(({ tag }) => tag !== tagToExclude) + .sort(({ tag: tagA, count: a }, { count: b }) => + isTagFavs(tagA) ? -1 : b - a); + export const sortTagsWithoutFavs = (tags: string[]) => - sortTags(tags, TAG_FAVS); + sortTagsArray(tags, TAG_FAVS); export const sortTagsObjectWithoutFavs = (tags: Tags) => - sortTagsObject(tags, TAG_FAVS); + sortTags(tags, TAG_FAVS); export const descriptionForTaggedPhotos = ( photos: Photo[] = [], From 5c8dbcc64ba8b65b5c5182c1a70f43169da840d9 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Sun, 23 Mar 2025 17:20:29 -0500 Subject: [PATCH 5/9] Document sidebar collapse configuration --- README.md | 1 + src/admin/AdminAppConfigurationClient.tsx | 10 ++++++++++ src/app/config.ts | 3 ++- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 27f0854e..ce9aa53d 100644 --- a/README.md +++ b/README.md @@ -135,6 +135,7 @@ Application behavior can be changed by configuring the following environment var - `recipes` (default) - `films` (default) - `focal-lengths` +- `NEXT_PUBLIC_EXHAUSTIVE_SIDEBAR_CATEGORIES = 1` shows all sidebar category content - `NEXT_PUBLIC_HIDE_EXIF_DATA = 1` hides EXIF data in photo details and OG images (potentially useful for portfolios, which don't focus on photography) - `NEXT_PUBLIC_HIDE_ZOOM_CONTROLS = 1` hides fullscreen photo zoom controls - `NEXT_PUBLIC_HIDE_TAKEN_AT_TIME = 1` hides taken at time from photo meta diff --git a/src/admin/AdminAppConfigurationClient.tsx b/src/admin/AdminAppConfigurationClient.tsx index e286e57c..437a2baa 100644 --- a/src/admin/AdminAppConfigurationClient.tsx +++ b/src/admin/AdminAppConfigurationClient.tsx @@ -73,6 +73,7 @@ export default function AdminAppConfigurationClient({ // Display categoryVisibility, hasCategoryVisibility, + collapseSidebarCategories, showExifInfo, showZoomControls, showTakenAtTimeHidden, @@ -520,6 +521,15 @@ export default function AdminAppConfigurationClient({ (default: {`"${DEFAULT_CATEGORY_KEYS.join(',')}"`}): {renderEnvVars(['NEXT_PUBLIC_CATEGORY_VISIBILITY'])} + + Set environment variable to {'"1"'} to show all sidebar + category content + {renderEnvVars(['NEXT_PUBLIC_EXHAUSTIVE_SIDEBAR_CATEGORIES'])} + Date: Sun, 23 Mar 2025 17:53:52 -0500 Subject: [PATCH 6/9] Base expand/collapse on sidebar header count --- src/category/index.ts | 29 ++++++++++++++++++++++++++ src/photo/PhotoGridSidebar.tsx | 38 +++++++++++++++++++++++++++------- 2 files changed, 60 insertions(+), 7 deletions(-) diff --git a/src/category/index.ts b/src/category/index.ts index f12ea34d..be9f044a 100644 --- a/src/category/index.ts +++ b/src/category/index.ts @@ -75,3 +75,32 @@ export const sortCategoryByCount = ( export const sortCategoriesByCount = ( categories: T[], ) => categories.sort(sortCategoryByCount); + +const convertCategoryKeysToCategoryNames = + (categoryKeys: CategoryKeys): (keyof PhotoSetCategories)[] => { + return categoryKeys.map(key => { + return key === 'films' + ? 'simulations' + : key === 'focal-lengths' + ? 'focalLengths' + : key; + }); + }; + +export const getCategoryItemsCount = ( + categoryKeys: CategoryKeys, + categories: PhotoSetCategories, +) => + convertCategoryKeysToCategoryNames(categoryKeys).reduce((acc, key) => + acc + (categories[key]?.length ?? 0) + , 0); + +export const getCategoriesWithItemsCount = ( + categoryKeys: CategoryKeys, + categories: PhotoSetCategories, +) => + convertCategoryKeysToCategoryNames(categoryKeys).reduce((acc, key) => + (categories[key]?.length ?? 0) > 0 + ? acc + 1 + : acc + , 0); diff --git a/src/photo/PhotoGridSidebar.tsx b/src/photo/PhotoGridSidebar.tsx index 3bae4eae..eef5a795 100644 --- a/src/photo/PhotoGridSidebar.tsx +++ b/src/photo/PhotoGridSidebar.tsx @@ -24,22 +24,40 @@ import IconFilmSimulation from '@/components/icons/IconFilmSimulation'; import IconLens from '@/components/icons/IconLens'; import PhotoLens from '@/lens/PhotoLens'; import IconFocalLength from '@/components/icons/IconFocalLength'; -import { PhotoSetCategories } from '@/category'; +import { + getCategoriesWithItemsCount, + PhotoSetCategories, +} from '@/category'; import PhotoFocalLength from '@/focal/PhotoFocalLength'; +const SIDEBAR_ITEM_MAX_COUNT = 24; + export default function PhotoGridSidebar({ - cameras, - lenses, - tags, - simulations, - recipes, - focalLengths, photosCount, photosDateRange, + ...categories }: PhotoSetCategories & { photosCount: number photosDateRange?: PhotoDateRange }) { + const { + cameras, + lenses, + tags, + simulations, + recipes, + focalLengths, + } = categories; + + const itemsCount = getCategoriesWithItemsCount( + CATEGORY_VISIBILITY, + categories, + ); + + const maxItemsPerCategory = Math.floor( + SIDEBAR_ITEM_MAX_COUNT / itemsCount, + ); + const { start, end } = dateRangeForPhotos(undefined, photosDateRange); const { photosCountHidden } = useAppState(); @@ -56,6 +74,7 @@ export default function PhotoGridSidebar({ size={15} className="translate-x-[0.5px]" />} + maxItems={maxItemsPerCategory} items={cameras .map(({ cameraKey, camera, count }) => } + maxItems={maxItemsPerCategory} items={lenses .map(({ lensKey, lens, count }) => } + maxItems={maxItemsPerCategory} items={tagsIncludingHidden .map(({ tag, count }) => { switch (tag) { @@ -142,6 +163,7 @@ export default function PhotoGridSidebar({ size={16} className="translate-x-[-1px]" />} + maxItems={maxItemsPerCategory} items={recipes .map(({ recipe, count }) => } + maxItems={maxItemsPerCategory} items={simulations .map(({ simulation, count }) => } + maxItems={maxItemsPerCategory} items={focalLengths.map(({ focal, count }) => Date: Sun, 23 Mar 2025 18:20:20 -0500 Subject: [PATCH 7/9] Refine sidebar collapse behavior --- src/components/HeaderList.tsx | 13 +++++++++++-- src/photo/PhotoGridSidebar.tsx | 2 +- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/components/HeaderList.tsx b/src/components/HeaderList.tsx index be6be34a..2becf235 100644 --- a/src/components/HeaderList.tsx +++ b/src/components/HeaderList.tsx @@ -69,11 +69,20 @@ export default function HeaderList({ 'text-xs font-medium tracking-wider', 'border-medium rounded-md', 'px-[5px] h-5!', - 'hover:bg-dim active:bg-main', + 'hover:bg-dim hover:text-main active:bg-main', + 'group', )} > { - {isExpanded ? 'LESS' : 'MORE'} + {isExpanded + ? 'LESS' + : <> + MORE + + {' '} + {items.length - maxItems} + + } {isExpanded ? : } diff --git a/src/photo/PhotoGridSidebar.tsx b/src/photo/PhotoGridSidebar.tsx index eef5a795..0759bd38 100644 --- a/src/photo/PhotoGridSidebar.tsx +++ b/src/photo/PhotoGridSidebar.tsx @@ -30,7 +30,7 @@ import { } from '@/category'; import PhotoFocalLength from '@/focal/PhotoFocalLength'; -const SIDEBAR_ITEM_MAX_COUNT = 24; +const SIDEBAR_ITEM_MAX_COUNT = 28; export default function PhotoGridSidebar({ photosCount, From c9fe0bdc99c6fd51183c1a1b327b12d0d3f9ff01 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Sun, 23 Mar 2025 18:32:58 -0500 Subject: [PATCH 8/9] Intelligently use vertical space in sidebar --- src/photo/PhotoGridPage.tsx | 9 ++++++++- src/photo/PhotoGridSidebar.tsx | 16 +++++++++++----- src/utility/useElementHeight.ts | 18 ++++++++++++++++++ 3 files changed, 37 insertions(+), 6 deletions(-) create mode 100644 src/utility/useElementHeight.ts diff --git a/src/photo/PhotoGridPage.tsx b/src/photo/PhotoGridPage.tsx index bf5c775f..b5857e3a 100644 --- a/src/photo/PhotoGridPage.tsx +++ b/src/photo/PhotoGridPage.tsx @@ -4,10 +4,11 @@ import { Photo } from '.'; import { PATH_GRID_INFERRED } from '@/app/paths'; import PhotoGridSidebar from './PhotoGridSidebar'; import PhotoGridContainer from './PhotoGridContainer'; -import { useEffect } from 'react'; +import { useEffect, useRef } from 'react'; import { useAppState } from '@/state/AppState'; import clsx from 'clsx/lite'; import { PhotoSetCategories } from '@/category'; +import useElementHeight from '@/utility/useElementHeight'; export default function PhotoGridPage({ photos, @@ -17,6 +18,8 @@ export default function PhotoGridPage({ photos: Photo[] photosCount: number }) { + const ref = useRef(null); + const { setSelectedPhotoIds } = useAppState(); useEffect( @@ -24,6 +27,8 @@ export default function PhotoGridPage({ [setSelectedPhotoIds], ); + const containerHeight = useElementHeight(ref); + return (
diff --git a/src/photo/PhotoGridSidebar.tsx b/src/photo/PhotoGridSidebar.tsx index 0759bd38..3b36904e 100644 --- a/src/photo/PhotoGridSidebar.tsx +++ b/src/photo/PhotoGridSidebar.tsx @@ -30,15 +30,17 @@ import { } from '@/category'; import PhotoFocalLength from '@/focal/PhotoFocalLength'; -const SIDEBAR_ITEM_MAX_COUNT = 28; +const APPROXIMATE_ITEM_HEIGHT = 34; export default function PhotoGridSidebar({ photosCount, photosDateRange, + containerHeight, ...categories }: PhotoSetCategories & { photosCount: number photosDateRange?: PhotoDateRange + containerHeight?: number }) { const { cameras, @@ -49,14 +51,18 @@ export default function PhotoGridSidebar({ focalLengths, } = categories; - const itemsCount = getCategoriesWithItemsCount( + const categoriesCount = getCategoriesWithItemsCount( CATEGORY_VISIBILITY, categories, ); - const maxItemsPerCategory = Math.floor( - SIDEBAR_ITEM_MAX_COUNT / itemsCount, - ); + const maxItemsPerCategory = containerHeight + ? Math.max( + Math.floor(containerHeight / categoriesCount / APPROXIMATE_ITEM_HEIGHT), + // Always show at least 2 items + 2, + ) + : undefined; const { start, end } = dateRangeForPhotos(undefined, photosDateRange); diff --git a/src/utility/useElementHeight.ts b/src/utility/useElementHeight.ts new file mode 100644 index 00000000..8f0d992f --- /dev/null +++ b/src/utility/useElementHeight.ts @@ -0,0 +1,18 @@ +import { useState } from 'react'; + +import { RefObject, useEffect } from 'react'; + +export default function useElementHeight( + element: RefObject, +) { + const [height, setHeight] = useState(element.current?.clientHeight); + + useEffect(() => { + const handleResize = () => setHeight(element.current?.clientHeight); + handleResize(); + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, [element]); + + return height; +} From 293df270a11eeacdf7b30f40265e8382d964221f Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Sun, 23 Mar 2025 18:57:12 -0500 Subject: [PATCH 9/9] Refine sidebar collapse heights --- src/photo/PhotoGridSidebar.tsx | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/photo/PhotoGridSidebar.tsx b/src/photo/PhotoGridSidebar.tsx index 3b36904e..356f07be 100644 --- a/src/photo/PhotoGridSidebar.tsx +++ b/src/photo/PhotoGridSidebar.tsx @@ -8,7 +8,7 @@ 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 } from 'react'; +import { useMemo, useRef } from 'react'; import HiddenTag from '@/tag/HiddenTag'; import { CATEGORY_VISIBILITY, SITE_ABOUT } from '@/app/config'; import { @@ -29,8 +29,10 @@ import { 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, @@ -56,9 +58,15 @@ export default function PhotoGridSidebar({ categories, ); - const maxItemsPerCategory = containerHeight + const aboutRef = useRef(null); + const aboutHeight = useElementHeight(aboutRef); + const height = containerHeight + ? containerHeight - (aboutHeight ? aboutHeight + ABOUT_HEIGHT_OFFSET : 0) + : undefined; + + const maxItemsPerCategory = height ? Math.max( - Math.floor(containerHeight / categoriesCount / APPROXIMATE_ITEM_HEIGHT), + Math.floor(height / categoriesCount / APPROXIMATE_ITEM_HEIGHT), // Always show at least 2 items 2, ) @@ -239,6 +247,7 @@ export default function PhotoGridSidebar({ {SITE_ABOUT &&