Merge pull request #222 from sambecker/collapsible-sidebar
Collapse long sidebar sections
This commit is contained in:
commit
9c5db09430
@ -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
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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,
|
||||
}}
|
||||
/>
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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 }} />
|
||||
|
||||
@ -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 }} />
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
45
src/category/data.ts
Normal 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;
|
||||
@ -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);
|
||||
|
||||
@ -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"
|
||||
/>
|
||||
);
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
@ -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),
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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)
|
||||
|
||||
18
src/utility/useElementHeight.ts
Normal file
18
src/utility/useElementHeight.ts
Normal 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;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user