Merge pull request #222 from sambecker/collapsible-sidebar

Collapse long sidebar sections
This commit is contained in:
Sam Becker 2025-03-23 19:01:21 -05:00 committed by GitHub
commit 9c5db09430
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 311 additions and 151 deletions

View File

@ -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

View File

@ -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 (

View File

@ -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,
}}
/>

View File

@ -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'])}
</ChecklistRow>
<ChecklistRow
title="Collapse sidebar categories"
status={collapseSidebarCategories}
optional
>
Set environment variable to {'"1"'} to show all sidebar
category content
{renderEnvVars(['NEXT_PUBLIC_EXHAUSTIVE_SIDEBAR_CATEGORIES'])}
</ChecklistRow>
<ChecklistRow
title="Show EXIF data"
status={showExifInfo}

View File

@ -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 (
<AdminTable>
{sortRecipesWithCount(recipes).map(({ recipe, count }) =>
{sortRecipes(recipes).map(({ recipe, count }) =>
<Fragment key={recipe}>
<div className="pr-2 col-span-2">
<AdminRecipeBadge {...{ recipe, count }} />

View File

@ -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 (
<AdminTable>
{sortTagsObject(tags).map(({ tag, count }) =>
{sortTags(tags).map(({ tag, count }) =>
<Fragment key={tag}>
<div className="pr-2 col-span-2">
<AdminTagBadge {...{ tag, count }} />

View File

@ -1,19 +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 { ADMIN_DEBUG_TOOLS_ENABLED } from './config';
import { getDataForCategories } from '@/category/data';
export default async function CommandK() {
const [
@ -28,16 +17,7 @@ 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 <CommandKClient

View File

@ -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_EXHAUSTIVE_SIDEBAR_CATEGORIES !== '1';
export const SHOW_EXIF_DATA =
process.env.NEXT_PUBLIC_HIDE_EXIF_DATA !== '1';
export const SHOW_ZOOM_CONTROLS =
@ -324,6 +326,7 @@ export const APP_CONFIGURATION = {
hasCategoryVisibility:
Boolean(process.env.NEXT_PUBLIC_CATEGORY_VISIBILITY),
categoryVisibility: CATEGORY_VISIBILITY,
collapseSidebarCategories: COLLAPSE_SIDEBAR_CATEGORIES,
showExifInfo: SHOW_EXIF_DATA,
showZoomControls: SHOW_ZOOM_CONTROLS,
showTakenAtTimeHidden: SHOW_TAKEN_AT_TIME,

45
src/category/data.ts Normal file
View File

@ -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;

View File

@ -66,3 +66,41 @@ 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;
export const sortCategoriesByCount = <T extends { count: number }>(
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);

View File

@ -1,23 +1,37 @@
'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';
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 hasItemsToExpand =
COLLAPSE_SIDEBAR_CATEGORIES &&
// Don't show expand button if it only reveals 1 item
items.length > (maxItems + 1);
return (
<AnimateItems
className={clsx(
className,
'space-y-1',
className,
)}
scaleOffset={0.95}
duration={0.5}
@ -29,7 +43,7 @@ export default function HeaderList({
'text-gray-900',
'dark:text-gray-100',
'flex items-center mb-1 gap-1',
'uppercase',
'uppercase select-none',
)}
>
{icon &&
@ -38,8 +52,44 @@ export default function HeaderList({
</span>}
{title}
</div>]
:[] as ReactNode[]
).concat(items)}
: [] as ReactNode[]
)
.concat(items.slice(
0,
hasItemsToExpand && !isExpanded ? maxItems : items.length,
))
.concat(hasItemsToExpand
? [
<LoaderButton
key="expand-button"
onClick={() => setIsExpanded(!isExpanded)}
styleAs="link"
className={clsx(
'mt-1',
'text-xs font-medium tracking-wider',
'border-medium rounded-md',
'px-[5px] h-5!',
'hover:bg-dim hover:text-main active:bg-main',
'group',
)}
>
{<span className="flex items-center gap-1">
{isExpanded
? 'LESS'
: <>
MORE
<span className="hidden group-hover:inline text-dim!">
{' '}
{items.length - maxItems}
</span>
</>}
{isExpanded
? <IoChevronUpOutline size={12} />
: <IoChevronDownOutline size={12} />}
</span>}
</LoaderButton>,
]
: null)}
classNameItem="text-dim uppercase"
/>
);

View File

@ -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<HTMLDivElement>(null);
const { setSelectedPhotoIds } = useAppState();
useEffect(
@ -24,6 +27,8 @@ export default function PhotoGridPage({
[setSelectedPhotoIds],
);
const containerHeight = useElementHeight(ref);
return (
<PhotoGridContainer
cacheKey={`page-${PATH_GRID_INFERRED}`}
@ -31,6 +36,7 @@ export default function PhotoGridPage({
count={photosCount}
sidebar={
<div
ref={ref}
className={clsx(
'sticky top-0 -mb-5 -mt-5',
'max-h-screen h-full',
@ -47,6 +53,7 @@ export default function PhotoGridPage({
<PhotoGridSidebar {...{
...categories,
photosCount,
containerHeight,
}}
/>
</div>

View File

@ -1,16 +1,14 @@
'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';
import { useMemo, useRef } from 'react';
import HiddenTag from '@/tag/HiddenTag';
import { CATEGORY_VISIBILITY, SITE_ABOUT } from '@/app/config';
import {
@ -18,32 +16,62 @@ 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 {
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({
cameras,
lenses,
tags,
simulations,
recipes,
focalLengths,
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();
@ -56,9 +84,12 @@ export default function PhotoGridSidebar({
? <HeaderList
key="cameras"
title="Cameras"
icon={<IconCamera size={15} />}
icon={<IconCamera
size={15}
className="translate-x-[0.5px]"
/>}
maxItems={maxItemsPerCategory}
items={cameras
.sort(sortCamerasWithCount)
.map(({ cameraKey, camera, count }) =>
<PhotoCamera
key={cameraKey}
@ -78,8 +109,8 @@ export default function PhotoGridSidebar({
key="lenses"
title="Lenses"
icon={<IconLens size={15} />}
maxItems={maxItemsPerCategory}
items={lenses
.sort(sortLensesWithCount)
.map(({ lensKey, lens, count }) =>
<PhotoLens
key={lensKey}
@ -99,40 +130,42 @@ export default function PhotoGridSidebar({
title='Tags'
icon={<IconTag
size={14}
className="translate-y-[1px]"
className="translate-x-[1px] translate-y-[1px]"
/>}
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
/>;
}
})}
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;
@ -144,7 +177,8 @@ export default function PhotoGridSidebar({
size={16}
className="translate-x-[-1px]"
/>}
items={sortRecipesWithCount(recipes)
maxItems={maxItemsPerCategory}
items={recipes
.map(({ recipe, count }) =>
<PhotoRecipe
key={recipe}
@ -163,8 +197,8 @@ export default function PhotoGridSidebar({
key="films"
title="Films"
icon={<IconFilmSimulation size={15} />}
maxItems={maxItemsPerCategory}
items={simulations
.sort(sortFilmSimulationsWithCount)
.map(({ simulation, count }) =>
<PhotoFilmSimulation
key={simulation}
@ -181,6 +215,7 @@ export default function PhotoGridSidebar({
key="focal-lengths"
title="Focal Lengths"
icon={<IconFocalLength size={13} />}
maxItems={maxItemsPerCategory}
items={focalLengths.map(({ focal, count }) =>
<PhotoFocalLength
key={focal}
@ -212,6 +247,7 @@ export default function PhotoGridSidebar({
{SITE_ABOUT && <HeaderList
items={[<p
key="about"
ref={aboutRef}
className={clsx(
'max-w-60 normal-case text-dim',
htmlHasBrParagraphBreaks(SITE_ABOUT) && 'pb-2',

View File

@ -21,7 +21,7 @@ import DownloadButton from '@/components/DownloadButton';
import PhotoCamera from '../camera/PhotoCamera';
import { cameraFromPhoto } from '@/camera';
import PhotoFilmSimulation from '@/simulation/PhotoFilmSimulation';
import { sortTags } from '@/tag';
import { sortTagsArray } from '@/tag';
import DivDebugBaselineGrid from '@/components/DivDebugBaselineGrid';
import PhotoLink from './PhotoLink';
import {
@ -124,7 +124,7 @@ export default function PhotoLarge({
refTriggers,
});
const tags = sortTags(photo.tags, primaryTag);
const tags = sortTagsArray(photo.tags, primaryTag);
const camera = cameraFromPhoto(photo);
const lens = lensFromPhoto(photo);

View File

@ -1,41 +0,0 @@
import {
getUniqueCamerasCached,
getUniqueFilmSimulationsCached,
getUniqueFocalLengthsCached,
getUniqueLensesCached,
getUniqueRecipesCached,
getUniqueTagsCached,
} from '@/photo/cache';
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 { sortTagsObject } from '@/tag';
export const getPhotoSidebarData = () => [
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;

View File

@ -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),

View File

@ -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,

View File

@ -15,6 +15,7 @@ import {
formatCount,
formatCountDescriptive,
} from '@/utility/string';
import { sortCategoryByCount } from '@/category';
// Reserved tags
export const TAG_FAVS = 'favs';
@ -49,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[] = [],
@ -105,16 +114,16 @@ export const isPathFavs = (pathname?: string) =>
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)

View File

@ -0,0 +1,18 @@
import { useState } from 'react';
import { RefObject, useEffect } from 'react';
export default function useElementHeight(
element: RefObject<HTMLElement | null>,
) {
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;
}