Rich sort controls (#283)

* Generalize app switcher menus

* Organize sort module

* Build configuration for nav sort control

* Refine sort menu styles

* Upgrade next.js

* Reset custom sort when clicking grid/full a second time

* Light up sort button when overridden
This commit is contained in:
Sam Becker 2025-07-15 22:43:36 -05:00 committed by GitHub
parent 65f7f539a7
commit 646f32e642
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
136 changed files with 777 additions and 596 deletions

View File

@ -153,7 +153,13 @@ Application behavior can be changed by configuring the following environment var
- `uploaded-at`
- `uploaded-at-oldest-first`
- `NEXT_PUBLIC_PRIORITY_BASED_SORTING = 1` takes priority field into account when sorting photos (⚠️ enabling may have performance consequences)
- `NEXT_PUBLIC_SHOW_SORT_CONTROL = 1` shows sort control in desktop nav on grid/full homepages
- `NEXT_PUBLIC_NAV_SORT_CONTROL`
- Controls sort UI on grid/full homepages
- Accepted values:
- `none`
- `toggle` (default)
- `menu`
#### Display
- `NEXT_PUBLIC_HIDE_KEYBOARD_SHORTCUT_TOOLTIPS = 1` hides keyboard shortcut hints in areas like the main nav, and previous/next photo links

View File

@ -11,7 +11,7 @@ import {
isPathProtected,
isPathTag,
isPathTagPhoto,
} from '@/app/paths';
} from '@/app/path';
import { TAG_PRIVATE } from '@/tag';
const PHOTO_ID = 'UsKSGcbt';

View File

@ -5,7 +5,7 @@ import {
getUniqueRecipesCached,
getUniqueTagsCached,
} from '@/photo/cache';
import { PATH_ADMIN } from '@/app/paths';
import { PATH_ADMIN } from '@/app/path';
import PhotoEditPageClient from '@/photo/PhotoEditPageClient';
import {
AI_TEXT_GENERATION_ENABLED,

View File

@ -1,7 +1,7 @@
import AdminChildPage from '@/components/AdminChildPage';
import { redirect } from 'next/navigation';
import { getPhotosCached, getPhotosMetaCached } from '@/photo/cache';
import { PATH_ADMIN, PATH_ADMIN_RECIPES, pathForRecipe } from '@/app/paths';
import { PATH_ADMIN, PATH_ADMIN_RECIPES, pathForRecipe } from '@/app/path';
import PhotoLightbox from '@/photo/PhotoLightbox';
import AdminRecipeBadge from '@/admin/AdminRecipeBadge';
import AdminRecipeForm from '@/admin/AdminRecipeForm';

View File

@ -2,7 +2,7 @@ import AdminChildPage from '@/components/AdminChildPage';
import { redirect } from 'next/navigation';
import { getPhotosCached, getPhotosMetaCached } from '@/photo/cache';
import AdminTagForm from '@/admin/AdminTagForm';
import { PATH_ADMIN, PATH_ADMIN_TAGS, pathForTag } from '@/app/paths';
import { PATH_ADMIN, PATH_ADMIN_TAGS, pathForTag } from '@/app/path';
import PhotoLightbox from '@/photo/PhotoLightbox';
import AdminTagBadge from '@/admin/AdminTagBadge';

View File

@ -1,4 +1,4 @@
import { PARAM_UPLOAD_TITLE, PATH_ADMIN } from '@/app/paths';
import { PARAM_UPLOAD_TITLE, PATH_ADMIN } from '@/app/path';
import { extractImageDataFromBlobPath } from '@/photo/server';
import { redirect } from 'next/navigation';
import {

View File

@ -3,7 +3,7 @@ import AppGrid from '@/components/AppGrid';
import { getUniqueTagsCached } from '@/photo/cache';
import AdminUploadsClient from '@/admin/AdminUploadsClient';
import { redirect } from 'next/navigation';
import { PATH_ADMIN_PHOTOS } from '@/app/paths';
import { PATH_ADMIN_PHOTOS } from '@/app/path';
export const maxDuration = 60;

View File

@ -9,7 +9,7 @@ import {
PATH_ROOT,
absolutePathForPhoto,
absolutePathForPhotoImage,
} from '@/app/paths';
} from '@/app/path';
import PhotoDetailPage from '@/photo/PhotoDetailPage';
import {
getPhotosMetaCached,

View File

@ -5,7 +5,7 @@ import FilmOverview from '@/film/FilmOverview';
import { getPhotosFilmDataCached } from '@/film/data';
import { Metadata } from 'next/types';
import { cache } from 'react';
import { PATH_ROOT } from '@/app/paths';
import { PATH_ROOT } from '@/app/path';
import { redirect } from 'next/navigation';
import { staticallyGenerateCategoryIfConfigured } from '@/app/static';
import { getAppText } from '@/i18n/state/server';

View File

@ -9,7 +9,7 @@ import {
PATH_ROOT,
absolutePathForPhoto,
absolutePathForPhotoImage,
} from '@/app/paths';
} from '@/app/path';
import PhotoDetailPage from '@/photo/PhotoDetailPage';
import { getPhotosNearIdCached, getPhotosMetaCached } from '@/photo/cache';
import { cache } from 'react';

View File

@ -3,7 +3,7 @@ import FocalLengthOverview from '@/focal/FocalLengthOverview';
import { getPhotosFocalLengthDataCached } from '@/focal/data';
import { INFINITE_SCROLL_GRID_INITIAL } from '@/photo';
import { getUniqueFocalLengths } from '@/photo/db/query';
import { PATH_ROOT } from '@/app/paths';
import { PATH_ROOT } from '@/app/path';
import type { Metadata } from 'next';
import { redirect } from 'next/navigation';
import { cache } from 'react';

View File

@ -5,8 +5,8 @@ import { cache } from 'react';
import { getPhotos } from '@/photo/db/query';
import PhotoFullPage from '@/photo/PhotoFullPage';
import { getPhotosMetaCached } from '@/photo/cache';
import { SortProps } from '@/photo/db/sort';
import { getSortOptionsFromParams } from '@/photo/db/sort-path';
import { SortProps } from '@/photo/sort';
import { getSortOptionsFromParams } from '@/photo/sort/path';
import { PhotoQueryOptions } from '@/photo/db';
import { FEED_META_QUERY_OPTIONS, getFeedQueryOptions } from '@/feed';

View File

@ -6,8 +6,8 @@ import { cache } from 'react';
import PhotoGridPage from '@/photo/PhotoGridPage';
import { getDataForCategoriesCached } from '@/category/cache';
import { getPhotosMetaCached } from '@/photo/cache';
import { SortProps } from '@/photo/db/sort';
import { getSortOptionsFromParams } from '@/photo/db/sort-path';
import { SortProps } from '@/photo/sort';
import { getSortOptionsFromParams } from '@/photo/sort/path';
import { FEED_META_QUERY_OPTIONS, getFeedQueryOptions } from '@/feed';
import { PhotoQueryOptions } from '@/photo/db';

View File

@ -30,7 +30,7 @@ import RecipeModal from '@/recipe/RecipeModal';
import ThemeColors from '@/app/ThemeColors';
import AppTextProvider from '@/i18n/state/AppTextProvider';
import SharedHoverProvider from '@/components/shared-hover/SharedHoverProvider';
import { PATH_FEED_JSON, PATH_RSS_XML } from '@/app/paths';
import { PATH_FEED_JSON, PATH_RSS_XML } from '@/app/path';
import '../tailwind.css';

View File

@ -9,7 +9,7 @@ import {
PATH_ROOT,
absolutePathForPhoto,
absolutePathForPhotoImage,
} from '@/app/paths';
} from '@/app/path';
import PhotoDetailPage from '@/photo/PhotoDetailPage';
import {
getPhotosMetaCached,

View File

@ -9,7 +9,7 @@ import {
PATH_ROOT,
absolutePathForPhoto,
absolutePathForPhotoImage,
} from '@/app/paths';
} from '@/app/path';
import PhotoDetailPage from '@/photo/PhotoDetailPage';
import { getPhotoCached, getPhotosNearIdCached } from '@/photo/cache';
import { cache } from 'react';

View File

@ -9,7 +9,7 @@ import {
PATH_ROOT,
absolutePathForPhoto,
absolutePathForPhotoImage,
} from '@/app/paths';
} from '@/app/path';
import PhotoDetailPage from '@/photo/PhotoDetailPage';
import {
getPhotosMetaCached,

View File

@ -4,7 +4,7 @@ import RecentsOverview from '@/recents/RecentsOverview';
import { getPhotosRecentsDataCached } from '@/recents/data';
import { Metadata } from 'next/types';
import { cache } from 'react';
import { PATH_ROOT } from '@/app/paths';
import { PATH_ROOT } from '@/app/path';
import { redirect } from 'next/navigation';
import { getAppText } from '@/i18n/state/server';

View File

@ -9,7 +9,7 @@ import {
PATH_ROOT,
absolutePathForPhoto,
absolutePathForPhotoImage,
} from '@/app/paths';
} from '@/app/path';
import PhotoDetailPage from '@/photo/PhotoDetailPage';
import { getPhotosMetaCached, getPhotosNearIdCached } from '@/photo/cache';
import { cache } from 'react';

View File

@ -1,6 +1,6 @@
import { INFINITE_SCROLL_GRID_INITIAL } from '@/photo';
import { getUniqueRecipes } from '@/photo/db/query';
import { PATH_ROOT } from '@/app/paths';
import { PATH_ROOT } from '@/app/path';
import type { Metadata } from 'next';
import { redirect } from 'next/navigation';
import { cache } from 'react';

View File

@ -9,7 +9,7 @@ import {
PATH_ROOT,
absolutePathForPhoto,
absolutePathForPhotoImage,
} from '@/app/paths';
} from '@/app/path';
import PhotoDetailPage from '@/photo/PhotoDetailPage';
import {
getPhotosMetaCached,

View File

@ -1,6 +1,6 @@
import { auth } from '@/auth/server';
import SignInForm from '@/auth/SignInForm';
import { PATH_ADMIN, PATH_ROOT } from '@/app/paths';
import { PATH_ADMIN, PATH_ROOT } from '@/app/path';
import { clsx } from 'clsx/lite';
import { redirect } from 'next/navigation';
import LinkWithStatus from '@/components/LinkWithStatus';

View File

@ -12,7 +12,7 @@ import {
absolutePathForRecipe,
absolutePathForTag,
absolutePathForYear,
} from '@/app/paths';
} from '@/app/path';
import { isTagFavs } from '@/tag';
import { BASE_URL, GRID_HOMEPAGE_ENABLED } from '@/app/config';
import { getPhotoIdsAndUpdatedAt } from '@/photo/db/query';

View File

@ -9,7 +9,7 @@ import {
PATH_ROOT,
absolutePathForPhoto,
absolutePathForPhotoImage,
} from '@/app/paths';
} from '@/app/path';
import PhotoDetailPage from '@/photo/PhotoDetailPage';
import { getPhotosMetaCached, getPhotosNearIdCached } from '@/photo/cache';
import { cache } from 'react';

View File

@ -1,6 +1,6 @@
import { INFINITE_SCROLL_GRID_INITIAL } from '@/photo';
import { getUniqueTags } from '@/photo/db/query';
import { PATH_ROOT } from '@/app/paths';
import { PATH_ROOT } from '@/app/path';
import { generateMetaForTag } from '@/tag';
import TagOverview from '@/tag/TagOverview';
import { getPhotosTagDataCached } from '@/tag/data';

View File

@ -8,7 +8,7 @@ import {
getPhotosMetaCached,
getPhotosNearIdCached,
} from '@/photo/cache';
import { PATH_ROOT, absolutePathForPhoto } from '@/app/paths';
import { PATH_ROOT, absolutePathForPhoto } from '@/app/path';
import { TAG_PRIVATE } from '@/tag';
import { Metadata } from 'next';
import { redirect } from 'next/navigation';

View File

@ -3,7 +3,7 @@ import Note from '@/components/Note';
import AppGrid from '@/components/AppGrid';
import PhotoGrid from '@/photo/PhotoGrid';
import { getPhotosMetaCached, getPhotosNoStore } from '@/photo/cache';
import { absolutePathForTag } from '@/app/paths';
import { absolutePathForTag } from '@/app/path';
import { TAG_PRIVATE, descriptionForTaggedPhotos, titleForTag } from '@/tag';
import PrivateHeader from '@/tag/PrivateHeader';
import { Metadata } from 'next';

View File

@ -9,7 +9,7 @@ import {
PATH_ROOT,
absolutePathForPhoto,
absolutePathForPhotoImage,
} from '@/app/paths';
} from '@/app/path';
import PhotoDetailPage from '@/photo/PhotoDetailPage';
import {
getPhotosMetaCached,

View File

@ -5,7 +5,7 @@ import YearOverview from '@/years/YearOverview';
import { getPhotosYearDataCached } from '@/years/data';
import { Metadata } from 'next/types';
import { cache } from 'react';
import { PATH_ROOT } from '@/app/paths';
import { PATH_ROOT } from '@/app/path';
import { redirect } from 'next/navigation';
import { staticallyGenerateCategoryIfConfigured } from '@/app/static';
import { getAppText } from '@/i18n/state/server';

View File

@ -8,7 +8,7 @@ import {
PATH_OG_SAMPLE,
PREFIX_PHOTO,
PREFIX_TAG,
} from './src/app/paths';
} from './src/app/path';
export default function middleware(req: NextRequest, res:NextResponse) {
const pathname = req.nextUrl.pathname;

View File

@ -10,8 +10,8 @@
},
"dependencies": {
"@ai-sdk/openai": "^1.3.23",
"@aws-sdk/client-s3": "3.844.0",
"@aws-sdk/s3-request-presigner": "3.844.0",
"@aws-sdk/client-s3": "3.846.0",
"@aws-sdk/s3-request-presigner": "3.846.0",
"@radix-ui/react-dialog": "^1.1.14",
"@radix-ui/react-dropdown-menu": "^2.1.15",
"@radix-ui/react-tooltip": "^1.2.7",
@ -21,16 +21,16 @@
"@vercel/analytics": "^1.5.0",
"@vercel/blob": "^1.1.1",
"@vercel/speed-insights": "^1.2.0",
"ai": "^4.3.17",
"ai": "^4.3.19",
"camelcase-keys": "^9.1.3",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"date-fns": "^4.1.0",
"date-fns-tz": "^3.2.0",
"fast-deep-equal": "^3.1.3",
"framer-motion": "^12.23.3",
"framer-motion": "^12.23.6",
"nanoid": "^5.1.5",
"next": "15.3.5",
"next": "15.4.1",
"next-auth": "5.0.0-beta.29",
"next-themes": "^0.4.6",
"pg": "^8.16.3",
@ -47,8 +47,8 @@
},
"devDependencies": {
"@eslint/eslintrc": "^3.3.1",
"@next/bundle-analyzer": "15.3.5",
"@next/eslint-plugin-next": "^15.3.5",
"@next/bundle-analyzer": "15.4.1",
"@next/eslint-plugin-next": "^15.4.1",
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/forms": "^0.5.10",
"@tailwindcss/postcss": "^4.1.11",
@ -56,14 +56,14 @@
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
"@types/jest": "^30.0.0",
"@types/node": "^24.0.13",
"@types/node": "^24.0.14",
"@types/pg": "^8.15.4",
"@types/react": "19.1.8",
"@types/react-dom": "19.1.6",
"@types/sanitize-html": "^2.16.0",
"cross-fetch": "^4.1.0",
"eslint": "9.31.0",
"eslint-config-next": "15.3.5",
"eslint-config-next": "15.4.1",
"eslint-plugin-react-hooks": "^5.2.0",
"jest": "^30.0.4",
"jest-environment-jsdom": "^30.0.4",

573
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -4,7 +4,7 @@ import {
generateLocalNaivePostgresString,
generateLocalPostgresString,
} from '@/utility/date';
import { pathForAdminUploadUrl } from '@/app/paths';
import { pathForAdminUploadUrl } from '@/app/path';
import { useRouter } from 'next/navigation';
import { ComponentProps, useState } from 'react';
import IconAddUpload from '@/components/icons/IconAddUpload';

View File

@ -1,6 +1,5 @@
'use client';
import MoreMenu from '@/components/more/MoreMenu';
import {
PATH_ADMIN_CONFIGURATION,
PATH_ADMIN_INSIGHTS,
@ -10,7 +9,7 @@ import {
PATH_ADMIN_TAGS,
PATH_ADMIN_UPLOADS,
PATH_GRID_INFERRED,
} from '@/app/paths';
} from '@/app/path';
import { useAppState } from '@/app/AppState';
import { IoArrowDown, IoArrowUp, IoCloseSharp } from 'react-icons/io5';
import { clsx } from 'clsx/lite';
@ -30,19 +29,15 @@ import InsightsIndicatorDot from './insights/InsightsIndicatorDot';
import MoreMenuItem from '@/components/more/MoreMenuItem';
import Spinner from '@/components/Spinner';
import { useAppText } from '@/i18n/state/client';
import SwitcherItemMenu from '@/components/switcher/SwitcherItemMenu';
import { MoreMenuSection } from '@/components/more/MoreMenu';
export default function AdminAppMenu({
active,
animateMenuClose,
isOpen,
setIsOpen,
className,
}: {
active?: boolean
animateMenuClose?: boolean
isOpen?: boolean
setIsOpen?: (isOpen: boolean) => void
className?: string
}) {
const {
photosCountTotal = 0,
@ -66,19 +61,18 @@ export default function AdminAppMenu({
const showAppInsightsLink = photosCountTotal > 0 && !isAltPressed;
const sectionUpload: ComponentProps<typeof MoreMenuItem>[] =
useMemo(() => ([{
label: appText.admin.uploadPhotos,
icon: <IconUpload
size={15}
className="translate-x-[0.5px] translate-y-[0.5px]"
/>,
annotation: isLoadingAdminData &&
<Spinner className="translate-y-[1.5px]" />,
action: startUpload,
}]), [appText, isLoadingAdminData, startUpload]);
const sectionUpload: MoreMenuSection = useMemo(() => ({ items: [{
label: appText.admin.uploadPhotos,
icon: <IconUpload
size={15}
className="translate-x-[0.5px] translate-y-[0.5px]"
/>,
annotation: isLoadingAdminData &&
<Spinner className="translate-y-[1.5px]" />,
action: startUpload,
}]}), [appText, isLoadingAdminData, startUpload]);
const sectionMain: ComponentProps<typeof MoreMenuItem>[] = useMemo(() => {
const sectionMain: MoreMenuSection = useMemo(() => {
const items: ComponentProps<typeof MoreMenuItem>[] = [];
if (uploadsCount) {
@ -188,7 +182,7 @@ export default function AdminAppMenu({
: PATH_ADMIN_CONFIGURATION,
});
return items;
return { items };
}, [
appText,
isSelecting,
@ -201,28 +195,24 @@ export default function AdminAppMenu({
uploadsCount,
]);
const sectionSignOut: ComponentProps<typeof MoreMenuItem>[] =
useMemo(() => ([{
const sectionSignOut: MoreMenuSection = useMemo(() => ({
items: [{
label: appText.auth.signOut,
icon: <IconSignOut size={15} />,
action: () => signOutAction().then(clearAuthStateAndRedirectIfNecessary),
}]), [appText.auth.signOut, clearAuthStateAndRedirectIfNecessary]);
}],
}), [appText.auth.signOut, clearAuthStateAndRedirectIfNecessary]);
const sections = useMemo(() =>
[sectionUpload, sectionMain, sectionSignOut]
, [sectionUpload, sectionMain, sectionSignOut]);
return (
<MoreMenu
<SwitcherItemMenu
{...{ isOpen, setIsOpen }}
icon={<div className={clsx(
'w-[28px] h-[28px]',
'overflow-hidden',
)}>
icon={<div className="w-[28px] h-[28px] overflow-hidden">
<div className={clsx(
'flex flex-col items-center justify-center gap-2',
'relative transition-transform',
animateMenuClose ? 'duration-300' : 'duration-0',
'relative flex flex-col items-center justify-center gap-2',
'translate-y-[-18px]',
)}>
<IoArrowDown size={16} className="shrink-0" />
@ -233,28 +223,12 @@ export default function AdminAppMenu({
sideOffset={12}
alignOffset={-84}
onOpen={refreshAdminData}
className={clsx(
'outline-medium',
className,
)}
classNameButton={clsx(
'p-0!',
'w-full h-full',
'flex items-center justify-center',
'hover:bg-transparent dark:hover:bg-transparent',
'active:bg-transparent dark:active:bg-transparent',
'rounded-none focus:outline-none',
active
? 'text-black dark:text-white'
: 'text-gray-400 dark:text-gray-600',
)}
classNameButtonOpen={clsx(
'bg-dim text-main!',
'[&>*>*]:translate-y-[6px]',
!animateMenuClose && '[&>*>*]:duration-300',
)}
sections={sections}
ariaLabel="Admin Menu"
classNameButtonOpen={clsx(
'[&>*>*]:translate-y-[6px]',
'[&>*>*]:duration-300',
)}
/>
);
}

View File

@ -9,7 +9,7 @@ import { IoCloseSharp } from 'react-icons/io5';
import { useEffect, useRef, useState } from 'react';
import { TAG_FAVS, Tags } from '@/tag';
import { usePathname } from 'next/navigation';
import { PATH_GRID_INFERRED } from '@/app/paths';
import { PATH_GRID_INFERRED } from '@/app/path';
import PhotoTagFieldset from './PhotoTagFieldset';
import { tagMultiplePhotosAction } from '@/photo/actions';
import { toastSuccess } from '@/toast';

View File

@ -4,7 +4,7 @@ import ErrorNote from '@/components/ErrorNote';
import FieldsetWithStatus from '@/components/FieldsetWithStatus';
import Container from '@/components/Container';
import { addUploadsAction } from '@/photo/actions';
import { PATH_ADMIN_PHOTOS } from '@/app/paths';
import { PATH_ADMIN_PHOTOS } from '@/app/path';
import { Tags } from '@/tag';
import {
generateLocalNaivePostgresString,

View File

@ -1,6 +1,6 @@
'use client';
import { PATH_ADMIN_CONFIGURATION, PATH_ADMIN_INSIGHTS } from '@/app/paths';
import { PATH_ADMIN_CONFIGURATION, PATH_ADMIN_INSIGHTS } from '@/app/path';
import ResponsiveText from '@/components/primitives/ResponsiveText';
import clsx from 'clsx/lite';
import ClearCacheButton from '@/admin/ClearCacheButton';

View File

@ -10,7 +10,7 @@ import {
PATH_ADMIN_RECIPES,
PATH_ADMIN_TAGS,
PATH_ADMIN_UPLOADS,
} from '@/app/paths';
} from '@/app/path';
import AdminNavClient from './AdminNavClient';
import { getAppText } from '@/i18n/state/server';

View File

@ -10,7 +10,7 @@ import {
checkPathPrefix,
isPathAdminInfo,
isPathTopLevelAdmin,
} from '@/app/paths';
} from '@/app/path';
import { useAppState } from '@/app/AppState';
import { clsx } from 'clsx/lite';
import { differenceInMinutes } from 'date-fns';

View File

@ -6,7 +6,7 @@ import {
PATH_ROOT,
pathForAdminPhotoEdit,
pathForTag,
} from '@/app/paths';
} from '@/app/path';
import {
deletePhotoAction,
syncPhotoAction,
@ -21,7 +21,7 @@ import {
import { isPathFavs, isPhotoFav, TAG_PRIVATE } from '@/tag';
import { usePathname } from 'next/navigation';
import { BiTrash } from 'react-icons/bi';
import MoreMenu from '@/components/more/MoreMenu';
import MoreMenu, { MoreMenuSection } from '@/components/more/MoreMenu';
import { useAppState } from '@/app/AppState';
import { RevalidatePhoto } from '@/photo/InfinitePhotoScroll';
import { MdOutlineFileDownload } from 'react-icons/md';
@ -139,7 +139,7 @@ export default function AdminPhotoMenu({
...showKeyCommands && { keyCommand: KEY_COMMANDS.sync },
});
return items;
return { items };
}, [
appText,
photo,
@ -151,31 +151,33 @@ export default function AdminPhotoMenu({
revalidatePhoto,
]);
const sectionDelete: ComponentProps<typeof MoreMenuItem>[] = useMemo(() => [{
label: appText.admin.delete,
icon: <BiTrash
size={15}
className="translate-x-[-1px]"
/>,
className: 'text-error *:hover:text-error',
color: 'red',
action: () => {
if (confirm(deleteConfirmationTextForPhoto(photo, appText))) {
return deletePhotoAction(
photo.id,
photo.url,
shouldRedirectDelete,
).then(() => {
revalidatePhoto?.(photo.id, true);
registerAdminUpdate?.();
});
}
},
...showKeyCommands && {
keyCommandModifier: KEY_COMMANDS.delete[0],
keyCommand: KEY_COMMANDS.delete[1],
},
}], [
const sectionDelete: MoreMenuSection = useMemo(() => ({
items: [{
label: appText.admin.delete,
icon: <BiTrash
size={15}
className="translate-x-[-1px]"
/>,
className: 'text-error *:hover:text-error',
color: 'red',
action: () => {
if (confirm(deleteConfirmationTextForPhoto(photo, appText))) {
return deletePhotoAction(
photo.id,
photo.url,
shouldRedirectDelete,
).then(() => {
revalidatePhoto?.(photo.id, true);
registerAdminUpdate?.();
});
}
},
...showKeyCommands && {
keyCommandModifier: KEY_COMMANDS.delete[0],
keyCommand: KEY_COMMANDS.delete[1],
},
}],
}), [
appText,
photo,
showKeyCommands,

View File

@ -5,7 +5,7 @@ import AppGrid from '@/components/AppGrid';
import AdminPhotosTable from '@/admin/AdminPhotosTable';
import AdminPhotosTableInfinite from '@/admin/AdminPhotosTableInfinite';
import PathLoaderButton from '@/components/primitives/PathLoaderButton';
import { PATH_ADMIN_PHOTOS_UPDATES } from '@/app/paths';
import { PATH_ADMIN_PHOTOS_UPDATES } from '@/app/path';
import { Photo } from '@/photo';
import { StorageListResponse } from '@/platforms/storage';
import AdminUploadsTable from './AdminUploadsTable';

View File

@ -5,7 +5,7 @@ import AdminPhotosTable from '@/admin/AdminPhotosTable';
import IconGrSync from '@/components/icons/IconGrSync';
import Note from '@/components/Note';
import AdminChildPage from '@/components/AdminChildPage';
import { PATH_ADMIN_PHOTOS } from '@/app/paths';
import { PATH_ADMIN_PHOTOS } from '@/app/path';
import { useMemo, useRef, useState } from 'react';
import { syncPhotosAction } from '@/photo/actions';
import { useRouter } from 'next/navigation';

View File

@ -5,7 +5,7 @@ import AdminTable from './AdminTable';
import { Fragment } from 'react';
import PhotoSmall from '@/photo/PhotoSmall';
import { clsx } from 'clsx/lite';
import { pathForAdminPhotoEdit, pathForPhoto } from '@/app/paths';
import { pathForAdminPhotoEdit, pathForPhoto } from '@/app/path';
import Link from 'next/link';
import PhotoDate from '@/photo/PhotoDate';
import EditButton from './EditButton';

View File

@ -1,6 +1,6 @@
'use client';
import { PATH_ADMIN_PHOTOS } from '@/app/paths';
import { PATH_ADMIN_PHOTOS } from '@/app/path';
import InfinitePhotoScroll from '../photo/InfinitePhotoScroll';
import AdminPhotosTable from './AdminPhotosTable';
import { ComponentProps } from 'react';

View File

@ -2,7 +2,7 @@
import SubmitButtonWithStatus from '@/components/SubmitButtonWithStatus';
import Link from 'next/link';
import { PATH_ADMIN_RECIPES } from '@/app/paths';
import { PATH_ADMIN_RECIPES } from '@/app/path';
import FieldsetWithStatus from '@/components/FieldsetWithStatus';
import { ReactNode, useMemo, useState } from 'react';
import { renamePhotoRecipeGloballyAction } from '@/photo/actions';

View File

@ -5,7 +5,7 @@ import { Fragment } from 'react';
import DeleteFormButton from '@/admin/DeleteFormButton';
import { photoQuantityText } from '@/photo';
import EditButton from '@/admin/EditButton';
import { pathForAdminRecipeEdit } from '@/app/paths';
import { pathForAdminRecipeEdit } from '@/app/path';
import { clsx } from 'clsx/lite';
import { formatRecipe, Recipes, sortRecipes } from '@/recipe';
import AdminRecipeBadge from './AdminRecipeBadge';

View File

@ -2,7 +2,7 @@
import SubmitButtonWithStatus from '@/components/SubmitButtonWithStatus';
import Link from 'next/link';
import { PATH_ADMIN_TAGS } from '@/app/paths';
import { PATH_ADMIN_TAGS } from '@/app/path';
import FieldsetWithStatus from '@/components/FieldsetWithStatus';
import { ReactNode, useMemo, useState } from 'react';
import { renamePhotoTagGloballyAction } from '@/photo/actions';

View File

@ -6,7 +6,7 @@ import DeleteFormButton from '@/admin/DeleteFormButton';
import { photoQuantityText } from '@/photo';
import { Tags, formatTag, sortTags } from '@/tag';
import EditButton from '@/admin/EditButton';
import { pathForAdminTagEdit } from '@/app/paths';
import { pathForAdminTagEdit } from '@/app/path';
import { clsx } from 'clsx/lite';
import AdminTagBadge from './AdminTagBadge';
import { getAppText } from '@/i18n/state/server';

View File

@ -8,7 +8,7 @@ import clsx from 'clsx/lite';
import ResponsiveDate from '@/components/ResponsiveDate';
import Spinner from '@/components/Spinner';
import { FaRegCircleCheck } from 'react-icons/fa6';
import { pathForAdminUploadUrl } from '@/app/paths';
import { pathForAdminUploadUrl } from '@/app/path';
import DeleteUploadButton from './DeleteUploadButton';
import { Dispatch, SetStateAction, useEffect, useRef } from 'react';
import { isElementEntirelyInViewport } from '@/utility/dom';

View File

@ -3,7 +3,7 @@
import { deleteUploadsAction } from '@/photo/actions';
import DeleteButton from './DeleteButton';
import { useRouter } from 'next/navigation';
import { PATH_ADMIN_PHOTOS } from '@/app/paths';
import { PATH_ADMIN_PHOTOS } from '@/app/path';
import { ComponentProps, useState } from 'react';
import LoaderButton from '@/components/primitives/LoaderButton';

View File

@ -23,8 +23,8 @@ import { DEFAULT_CATEGORY_KEYS, getHiddenCategories } from '@/category';
import { AI_AUTO_GENERATED_FIELDS_ALL } from '@/photo/ai';
import clsx from 'clsx/lite';
import Link from 'next/link';
import { PATH_FEED_JSON, PATH_RSS_XML } from '@/app/paths';
import { APP_DEFAULT_SORT_BY, SORT_BY_OPTIONS } from '@/photo/db/sort';
import { PATH_FEED_JSON, PATH_RSS_XML } from '@/app/path';
import { APP_DEFAULT_SORT_BY, SORT_BY_OPTIONS } from '@/photo/sort';
import {
AdminConfigSection,
ConfigSectionKey,
@ -86,7 +86,8 @@ export default function AdminAppConfigurationClient({
hasDefaultSortBy,
defaultSortBy,
isSortWithPriority,
showSortControl,
hasNavSortControl,
navSortControl,
// Display
showKeyboardShortcutTooltips,
showExifInfo,
@ -605,7 +606,7 @@ export default function AdminAppConfigurationClient({
case 'Sorting':
return <>
<ChecklistRow
title="Order"
title="Default order"
status={hasDefaultSortBy}
optional
>
@ -634,13 +635,13 @@ export default function AdminAppConfigurationClient({
{renderEnvVars(['NEXT_PUBLIC_PRIORITY_BASED_SORTING'])}
</ChecklistRow>
<ChecklistRow
title="Show nav button"
status={showSortControl}
title={`Nav sort control: ${navSortControl}`}
status={hasNavSortControl}
optional
>
Set environment variable to {'"1"'} to
show sort control in desktop nav on grid/full homepages:
{renderEnvVars(['NEXT_PUBLIC_SHOW_SORT_CONTROL'])}
Set environment variable to {'"none"'}, {'"toggle"'} (default),
or {'"menu"'}, to control sort UI on grid/full homepages:
{renderEnvVars(['NEXT_PUBLIC_NAV_SORT_CONTROL'])}
</ChecklistRow>
</>;
case 'Display':

View File

@ -27,7 +27,7 @@ import {
import EnvVar from '@/components/EnvVar';
import { IoSyncCircle } from 'react-icons/io5';
import clsx from 'clsx/lite';
import { PATH_ADMIN_PHOTOS_UPDATES } from '@/app/paths';
import { PATH_ADMIN_PHOTOS_UPDATES } from '@/app/path';
import { LiaBroomSolid } from 'react-icons/lia';
import { IoMdGrid } from 'react-icons/io';
import { RiSpeedMiniLine } from 'react-icons/ri';

View File

@ -27,7 +27,7 @@ import {
getAuthEmailCookie,
} from '@/auth';
import { useRouter, usePathname } from 'next/navigation';
import { isPathProtected, PATH_ROOT } from '@/app/paths';
import { isPathProtected, PATH_ROOT } from '@/app/path';
import { INITIAL_UPLOAD_STATE, UploadState } from '@/admin/upload';
import { RecipeProps } from '@/recipe';
import { nanoid } from 'nanoid';

View File

@ -1,34 +1,37 @@
import Switcher from '@/components/Switcher';
import SwitcherItem from '@/components/SwitcherItem';
import Switcher from '@/components/switcher/Switcher';
import SwitcherItem from '@/components/switcher/SwitcherItem';
import IconFull from '@/components/icons/IconFull';
import IconGrid from '@/components/icons/IconGrid';
import {
doesPathOfferSort,
PATH_FULL_INFERRED,
PATH_GRID_INFERRED,
} from '@/app/paths';
} from '@/app/path';
import IconSearch from '../components/icons/IconSearch';
import { useAppState } from '@/app/AppState';
import {
GRID_HOMEPAGE_ENABLED,
SHOW_KEYBOARD_SHORTCUT_TOOLTIPS,
SHOW_SORT_CONTROL,
NAV_SORT_CONTROL,
} from './config';
import AdminAppMenu from '@/admin/AdminAppMenu';
import Spinner from '@/components/Spinner';
import clsx from 'clsx/lite';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import useKeydownHandler from '@/utility/useKeydownHandler';
import { usePathname } from 'next/navigation';
import { KEY_COMMANDS } from '@/photo/key-commands';
import { useAppText } from '@/i18n/state/client';
import IconSort from '@/components/icons/IconSort';
import { getSortConfigFromPath } from '@/photo/db/sort-path';
import { getSortConfigFromPath } from '@/photo/sort/path';
import { motion } from 'framer-motion';
import SortMenu from '@/photo/sort/SortMenu';
import { SWR_KEYS } from '@/swr';
export type SwitcherSelection = 'full' | 'grid' | 'admin';
const GAP_CLASS = 'mr-1.5 sm:mr-2';
const GAP_CLASS_RIGHT = 'mr-1.5 sm:mr-2';
const GAP_CLASS_LEFT = 'ml-0.5 sm:ml-1';
export default function AppViewSwitcher({
currentSelection,
@ -50,20 +53,26 @@ export default function AppViewSwitcher({
invalidateSwr,
} = useAppState();
const showSortControl = SHOW_SORT_CONTROL && doesPathOfferSort(pathname);
const sortConfig = useMemo(() => getSortConfigFromPath(pathname), [pathname]);
const {
sortBy,
isSortedByDefault,
isAscending,
pathGrid,
pathFull,
pathSort,
} = getSortConfigFromPath(pathname);
pathSortToggle,
} = sortConfig;
const showSortControl =
NAV_SORT_CONTROL !== 'none' &&
doesPathOfferSort(pathname);
const hasLoadedRef = useRef(false);
useEffect(() => {
if (hasLoadedRef.current) {
// After initial load, invalidate cache every time sort changes
invalidateSwr?.('INFINITE_PHOTO_SCROLL');
invalidateSwr?.(SWR_KEYS.INFINITE_PHOTO_SCROLL);
}
hasLoadedRef.current = true;
}, [invalidateSwr, sortBy]);
@ -88,6 +97,7 @@ export default function AppViewSwitcher({
}, [pathname, isUserSignedIn]);
useKeydownHandler({ onKeyDown });
const [isSortMenuOpen, setIsSortMenuOpen] = useState(false);
const [isAdminMenuOpen, setIsAdminMenuOpen] = useState(false);
const renderItemFull =
@ -120,7 +130,7 @@ export default function AppViewSwitcher({
<div className={clsx('flex', className)}>
<Switcher
className={clsx(
GAP_CLASS,
GAP_CLASS_RIGHT,
// Apply offset due to outline strategy
'translate-x-[1px]',
)}
@ -144,7 +154,10 @@ export default function AppViewSwitcher({
<SwitcherItem
icon={<AdminAppMenu
isOpen={isAdminMenuOpen}
setIsOpen={setIsAdminMenuOpen}
setIsOpen={isOpen => {
setIsAdminMenuOpen(isOpen);
if (isOpen) { setIsSortMenuOpen(false); }
}}
/>}
tooltip={{
...!isAdminMenuOpen && SHOW_KEYBOARD_SHORTCUT_TOOLTIPS && {
@ -162,19 +175,49 @@ export default function AppViewSwitcher({
exit={{ opacity: 0, scale: 0.5 }}
transition={{ duration: 0.2, ease: 'easeInOut' }}
>
<Switcher className={clsx('max-sm:hidden', GAP_CLASS)}>
<SwitcherItem
href={pathSort}
icon={<IconSort
sort={isAscending ? 'asc' : 'desc'}
className="translate-x-[0.5px] translate-y-[1px]"
<Switcher
className={clsx('max-sm:hidden', GAP_CLASS_LEFT)}
type="borderless"
>
{NAV_SORT_CONTROL === 'menu'
? <SwitcherItem
className={clsx(
!isSortedByDefault && '*:bg-medium *:text-main!',
)}
icon={<SortMenu
{...sortConfig}
isOpen={isSortMenuOpen}
setIsOpen={isOpen => {
setIsSortMenuOpen(isOpen);
if (isOpen) { setIsAdminMenuOpen(false); }
}}
/>}
tooltip={{
...!isSortMenuOpen && SHOW_KEYBOARD_SHORTCUT_TOOLTIPS && {
content: 'Sort',
},
}}
width="narrow"
noPadding
/>
: <SwitcherItem
className={clsx(
'*:w-full *:h-full *:flex *:items-center *:justify-center',
!isSortedByDefault && '*:bg-medium *:text-main!',
)}
href={pathSortToggle}
icon={<IconSort
sort={isAscending ? 'asc' : 'desc'}
className="translate-x-[0.5px] translate-y-[1px]"
/>}
tooltip={{
content: isAscending
? appText.sort.newest
: appText.sort.oldest,
}}
width="narrow"
noPadding
/>}
tooltip={{
content: isAscending
? appText.sort.newest
: appText.sort.oldest,
}}
/>
</Switcher>
</motion.div>}
<motion.div
@ -192,6 +235,7 @@ export default function AppViewSwitcher({
keyCommandModifier: KEY_COMMANDS.search[0],
keyCommand: KEY_COMMANDS.search[1],
}}}
width="narrow"
/>
</Switcher>
</motion.div>

View File

@ -7,7 +7,7 @@ import Link from 'next/link';
import { SHOW_REPO_LINK } from '@/app/config';
import RepoLink from '../components/RepoLink';
import { usePathname } from 'next/navigation';
import { PATH_ADMIN_PHOTOS, isPathAdmin, isPathSignIn } from './paths';
import { PATH_ADMIN_PHOTOS, isPathAdmin, isPathSignIn } from './path';
import SubmitButtonWithStatus from '@/components/SubmitButtonWithStatus';
import { signOutAction } from '@/auth/actions';
import AnimateItems from '@/components/AnimateItems';

View File

@ -12,7 +12,7 @@ import {
isPathGrid,
isPathProtected,
isPathSignIn,
} from '@/app/paths';
} from '@/app/path';
import AnimateItems from '../components/AnimateItems';
import {
GRID_HOMEPAGE_ENABLED,

View File

@ -2,8 +2,8 @@
import { useState, useEffect } from 'react';
import { useTheme } from 'next-themes';
import Switcher from '@/components/Switcher';
import SwitcherItem from '@/components/SwitcherItem';
import Switcher from '@/components/switcher/Switcher';
import SwitcherItem from '@/components/switcher/SwitcherItem';
import { BiDesktop, BiMoon, BiSun } from 'react-icons/bi';
import { useAppText } from '@/i18n/state/client';

View File

@ -8,7 +8,7 @@ import {
makeUrlAbsolute,
shortenUrl,
} from '@/utility/url';
import { getSortByFromString } from '@/photo/db/sort';
import { getNavSortControlFromString, getSortByFromString } from '@/photo/sort';
// HARD-CODED GLOBAL CONFIGURATION
@ -280,8 +280,8 @@ export const USER_DEFAULT_SORT_OPTIONS = {
sortBy: USER_DEFAULT_SORT_BY,
sortWithPriority: USER_DEFAULT_SORT_WITH_PRIORITY,
};
export const SHOW_SORT_CONTROL =
process.env.NEXT_PUBLIC_SHOW_SORT_CONTROL === '1';
export const NAV_SORT_CONTROL =
getNavSortControlFromString(process.env.NEXT_PUBLIC_NAV_SORT_CONTROL);
// DISPLAY
@ -422,7 +422,8 @@ export const APP_CONFIGURATION = {
hasDefaultSortBy: Boolean(process.env.NEXT_PUBLIC_DEFAULT_SORT),
defaultSortBy: USER_DEFAULT_SORT_BY,
isSortWithPriority: USER_DEFAULT_SORT_WITH_PRIORITY,
showSortControl: SHOW_SORT_CONTROL,
hasNavSortControl: Boolean(process.env.NEXT_PUBLIC_NAV_SORT_CONTROL),
navSortControl: NAV_SORT_CONTROL,
// Display
showKeyboardShortcutTooltips: SHOW_KEYBOARD_SHORTCUT_TOOLTIPS,
showExifInfo: SHOW_EXIF_DATA,

View File

@ -24,10 +24,10 @@ export const PATH_FULL_INFERRED = GRID_HOMEPAGE_ENABLED
: PATH_ROOT;
// Sort
export const PARAM_SORT_TYPE_TAKEN_AT = 'taken-at';
export const PARAM_SORT_TYPE_UPLOADED_AT = 'uploaded-at';
export const PARAM_SORT_ORDER_NEWEST = 'newest-first';
export const PARAM_SORT_ORDER_OLDEST = 'oldest-first';
export const PARAM_SORT_TYPE_TAKEN_AT = 'taken-at';
export const PARAM_SORT_TYPE_UPLOADED_AT = 'uploaded-at';
export const PARAM_SORT_ORDER_NEWEST = 'newest-first';
export const PARAM_SORT_ORDER_OLDEST = 'oldest-first';
export const doesPathOfferSort = (pathname: string) =>
pathname === PATH_ROOT ||
pathname.startsWith(PATH_GRID) ||

View File

@ -19,7 +19,7 @@ import {
import { useSearchParams } from 'next/navigation';
import { useAppState } from '@/app/AppState';
import { clsx } from 'clsx/lite';
import { PATH_ADMIN_PHOTOS } from '@/app/paths';
import { PATH_ADMIN_PHOTOS } from '@/app/path';
import IconLock from '@/components/icons/IconLock';
import { useAppText } from '@/i18n/state/client';

View File

@ -1,4 +1,4 @@
import { isPathProtected } from '@/app/paths';
import { isPathProtected } from '@/app/path';
import NextAuth, { User } from 'next-auth';
import Credentials from 'next-auth/providers/credentials';

View File

@ -1,7 +1,7 @@
'use client';
import { Photo, PhotoDateRange } from '@/photo';
import { pathForCamera, pathForCameraImage } from '@/app/paths';
import { pathForCamera, pathForCameraImage } from '@/app/path';
import OGTile, { OGTilePropsCore } from '@/components/og/OGTile';
import { Camera } from '.';
import { descriptionForCameraPhotos, titleForCamera } from './meta';

View File

@ -1,4 +1,4 @@
import { absolutePathForCamera } from '@/app/paths';
import { absolutePathForCamera } from '@/app/path';
import { PhotoSetAttributes } from '../category';
import ShareModal from '@/share/ShareModal';
import CameraOGTile from './CameraOGTile';

View File

@ -1,7 +1,7 @@
'use client';
import { AiFillApple } from 'react-icons/ai';
import { pathForCamera } from '@/app/paths';
import { pathForCamera } from '@/app/path';
import { Camera, formatCameraText } from '.';
import EntityLink, {
EntityLinkExternalProps,

View File

@ -8,7 +8,7 @@ import { Camera, cameraFromPhoto, formatCameraText } from '.';
import {
absolutePathForCamera,
absolutePathForCameraImage,
} from '@/app/paths';
} from '@/app/path';
import { AppTextState } from '@/i18n/state';
// Meta functions moved to separate file to avoid

View File

@ -32,7 +32,7 @@ import {
pathForTag,
pathForYear,
PREFIX_RECENTS,
} from '../app/paths';
} from '../app/path';
import Modal from '../components/Modal';
import { clsx } from 'clsx/lite';
import { useDebounce } from 'use-debounce';

View File

@ -1,7 +1,7 @@
import { ReactNode } from 'react';
import AppGrid from './AppGrid';
import { clsx } from 'clsx/lite';
import { PATH_ROOT } from '@/app/paths';
import { PATH_ROOT } from '@/app/path';
import Link from 'next/link';
export default function HttpStatusPage({

View File

@ -6,7 +6,7 @@ import { clsx } from 'clsx/lite';
import useClickInsideOutside from '@/utility/useClickInsideOutside';
import { useRouter } from 'next/navigation';
import AnimateItems from './AnimateItems';
import { PATH_ROOT } from '@/app/paths';
import { PATH_ROOT } from '@/app/path';
import usePrefersReducedMotion from '@/utility/usePrefersReducedMotion';
import useEscapeHandler from '@/utility/useEscapeHandler';
import { useTheme } from 'next-themes';

View File

@ -10,6 +10,11 @@ import { clsx } from 'clsx/lite';
import { FiMoreHorizontal } from 'react-icons/fi';
import MoreMenuItem from './MoreMenuItem';
export type MoreMenuSection = {
label?: string
items: ComponentProps<typeof MoreMenuItem>[]
}
export default function MoreMenu({
sections,
icon,
@ -26,7 +31,7 @@ export default function MoreMenu({
onOpen,
...props
}: {
sections: ComponentProps<typeof MoreMenuItem>[][]
sections: MoreMenuSection[]
icon?: ReactNode
header?: ReactNode
className?: string
@ -84,7 +89,7 @@ export default function MoreMenu({
'min-w-[8rem]',
'component-surface',
'py-1',
'shadow-lg shadow-gray-900/10 dark:shadow-900',
'not-dark:shadow-lg not-dark:shadow-gray-900/10',
'data-[side=top]:dark:shadow-[0_0px_40px_rgba(0,0,0,0.6)]',
'data-[side=bottom]:dark:shadow-[0_10px_40px_rgba(0,0,0,0.6)]',
'data-[side=top]:animate-fade-in-from-bottom',
@ -99,7 +104,7 @@ export default function MoreMenu({
{header}
</div>}
<div className="divide-y divide-medium">
{sections.map((section, index) =>
{sections.map(({ label, items }, index) =>
<div
key={index}
className={clsx(
@ -107,11 +112,17 @@ export default function MoreMenu({
'[&:not(:last-child)]:pb-1',
)}
>
{section.map(props =>
<div key={props.label} className="px-1">
{label && <div className={clsx(
'px-3.5 pt-1.5 pb-0.5 select-none',
'text-extra-dim uppercase text-xs font-medium tracking-wide',
)}>
{label}
</div>}
{items.map(item =>
<div key={item.label} className="px-1">
<MoreMenuItem
{...item}
dismissMenu={dismissMenu}
{...props}
/>
</div>,
)}

View File

@ -1,11 +1,12 @@
import { clsx } from 'clsx/lite';
import { SHOULD_PREFETCH_ALL_LINKS } from '@/app/config';
import { ComponentProps, ReactNode, RefObject } from 'react';
import Spinner from './Spinner';
import LinkWithIconLoader from './LinkWithIconLoader';
import Tooltip from './Tooltip';
import Spinner from '../Spinner';
import LinkWithIconLoader from '../LinkWithIconLoader';
import Tooltip from '../Tooltip';
const WIDTH_CLASS = 'w-[42px]';
const WIDTH_CLASS = 'w-[42px]';
const WIDTH_CLASS_NARROW = 'w-[36px]';
export default function SwitcherItem({
icon,
@ -19,6 +20,7 @@ export default function SwitcherItem({
noPadding,
prefetch = SHOULD_PREFETCH_ALL_LINKS,
tooltip,
width = 'normal',
}: {
icon: ReactNode
title?: string
@ -31,10 +33,12 @@ export default function SwitcherItem({
noPadding?: boolean
prefetch?: boolean
tooltip?: ComponentProps<typeof Tooltip>
width?: 'narrow' | 'normal'
}) {
const widthClass = width === 'narrow' ? WIDTH_CLASS_NARROW : WIDTH_CLASS;
const className = clsx(
'flex items-center justify-center',
`${WIDTH_CLASS} h-[28px]`,
`${widthClass} h-[28px]`,
isInteractive && 'cursor-pointer',
isInteractive && 'hover:bg-gray-100/60 active:bg-gray-100',
isInteractive && 'dark:hover:bg-gray-900/75 dark:active:bg-gray-900',
@ -75,7 +79,7 @@ export default function SwitcherItem({
tooltip
? <Tooltip
{...tooltip}
classNameTrigger={WIDTH_CLASS}
classNameTrigger={widthClass}
delayDuration={500}
>
{content}

View File

@ -0,0 +1,36 @@
'use client';
import MoreMenu from '@/components/more/MoreMenu';
import { clsx } from 'clsx/lite';
import { ComponentProps } from 'react';
export default function SwitcherItemMenu({
className,
classNameButton,
classNameButtonOpen,
...props
}: ComponentProps<typeof MoreMenu>) {
return (
<MoreMenu
{...props}
className={clsx(
'outline-medium',
className,
)}
classNameButton={clsx(
'p-0!',
'w-full h-full',
'flex items-center justify-center',
'hover:bg-transparent dark:hover:bg-transparent',
'active:bg-transparent dark:active:bg-transparent',
'rounded-none focus:outline-none',
'text-gray-400 dark:text-gray-600 hover:text-main',
classNameButton,
)}
classNameButtonOpen={clsx(
'bg-dim text-main!',
classNameButtonOpen,
)}
/>
);
}

View File

@ -4,7 +4,7 @@ import {
INFINITE_SCROLL_FULL_INITIAL,
INFINITE_SCROLL_GRID_INITIAL,
} from '../photo';
import { SortBy } from '../photo/db/sort';
import { SortBy } from '../photo/sort';
import { FEED_PHOTO_REQUEST_LIMIT } from './programmatic';
const FEED_BASE_QUERY_OPTIONS: PhotoQueryOptions = {

View File

@ -1,4 +1,4 @@
import { absolutePathForPhoto } from '@/app/paths';
import { absolutePathForPhoto } from '@/app/path';
import {
FEED_PHOTO_WIDTH_LARGE,
FEED_PHOTO_WIDTH_MEDIUM,

View File

@ -6,7 +6,7 @@ import {
generateFeedMedia,
getCoreFeedFields,
} from './programmatic';
import { ABSOLUTE_PATH_RSS_XML, absolutePathForPhoto } from '@/app/paths';
import { ABSOLUTE_PATH_RSS_XML, absolutePathForPhoto } from '@/app/path';
import { formatDate } from '@/utility/date';
import { formatStringForXml } from '@/utility/string';
import { BASE_URL, META_DESCRIPTION, META_TITLE } from '@/app/config';

View File

@ -4,7 +4,7 @@ import { Photo, PhotoDateRange } from '@/photo';
import {
pathForFilm,
pathForFilmImage,
} from '@/app/paths';
} from '@/app/path';
import OGTile, { OGTilePropsCore } from '@/components/og/OGTile';
import { descriptionForFilmPhotos, titleForFilm } from '.';
import { useAppText } from '@/i18n/state/client';

View File

@ -1,4 +1,4 @@
import { absolutePathForFilm } from '@/app/paths';
import { absolutePathForFilm } from '@/app/path';
import { PhotoSetAttributes } from '../category';
import ShareModal from '@/share/ShareModal';
import FilmOGTile from './FilmOGTile';

View File

@ -1,7 +1,7 @@
'use client';
import PhotoFilmIcon from './PhotoFilmIcon';
import { pathForFilm } from '@/app/paths';
import { pathForFilm } from '@/app/path';
import EntityLink, {
EntityLinkExternalProps,
} from '@/components/entity/EntityLink';

View File

@ -7,7 +7,7 @@ import {
import {
absolutePathForFilm,
absolutePathForFilmImage,
} from '@/app/paths';
} from '@/app/path';
import {
FUJIFILM_SIMULATION_FORM_INPUT_OPTIONS,
labelForFujifilmSimulation,

View File

@ -4,7 +4,7 @@ import { Photo, PhotoDateRange } from '@/photo';
import {
pathForFocalLength,
pathForFocalLengthImage,
} from '@/app/paths';
} from '@/app/path';
import OGTile, { OGTilePropsCore } from '@/components/og/OGTile';
import { descriptionForFocalLengthPhotos, titleForFocalLength } from '.';
import { useAppText } from '@/i18n/state/client';

View File

@ -1,4 +1,4 @@
import { absolutePathForFocalLength } from '@/app/paths';
import { absolutePathForFocalLength } from '@/app/path';
import { PhotoSetAttributes } from '../category';
import ShareModal from '@/share/ShareModal';
import FocalLengthOGTile from './FocalLengthOGTile';

View File

@ -1,6 +1,6 @@
'use client';
import { pathForFocalLength } from '@/app/paths';
import { pathForFocalLength } from '@/app/path';
import EntityLink, {
EntityLinkExternalProps,
} from '@/components/entity/EntityLink';

View File

@ -7,7 +7,7 @@ import {
import {
absolutePathForFocalLength,
absolutePathForFocalLengthImage,
} from '@/app/paths';
} from '@/app/path';
import { AppTextState } from '@/i18n/state';
import { CategoryQueryMeta } from '@/category';

View File

@ -1,5 +1,5 @@
import { Photo, PhotoDateRange } from '@/photo';
import { pathForLens, pathForLensImage } from '@/app/paths';
import { pathForLens, pathForLensImage } from '@/app/path';
import OGTile, { OGTilePropsCore } from '@/components/og/OGTile';
import { Lens } from '.';
import { titleForLens, descriptionForLensPhotos } from './meta';

View File

@ -1,4 +1,4 @@
import { absolutePathForLens } from '@/app/paths';
import { absolutePathForLens } from '@/app/path';
import { PhotoSetAttributes } from '../category';
import ShareModal from '@/share/ShareModal';
import { formatLensText, Lens } from '.';

View File

@ -1,6 +1,6 @@
'use client';
import { pathForLens } from '@/app/paths';
import { pathForLens } from '@/app/path';
import { Lens, formatLensText } from '.';
import EntityLink, {
EntityLinkExternalProps,

View File

@ -1,7 +1,7 @@
import { Photo } from '@/photo';
import { parameterize } from '@/utility/string';
import { formatAppleLensText, isLensApple } from '../platforms/apple';
import { MISSING_FIELD } from '@/app/paths';
import { MISSING_FIELD } from '@/app/path';
import { formatGoogleLensText, isLensGoogle } from '../platforms/google';
import { CategoryQueryMeta } from '@/category';

View File

@ -8,7 +8,7 @@ import { Lens, lensFromPhoto, formatLensText } from '.';
import {
absolutePathForLens,
absolutePathForLensImage,
} from '@/app/paths';
} from '@/app/path';
import { AppTextState } from '@/i18n/state';
// Meta functions moved to separate file to avoid

View File

@ -17,7 +17,7 @@ import { clsx } from 'clsx/lite';
import { useAppState } from '@/app/AppState';
import useVisible from '@/utility/useVisible';
import { ADMIN_DB_OPTIMIZE_ENABLED } from '@/app/config';
import { SortBy } from './db/sort';
import { SortBy } from './sort';
import { SWR_KEYS } from '@/swr';
const SIZE_KEY_SEPARATOR = '__';

View File

@ -2,7 +2,7 @@
import AdminChildPage from '@/components/AdminChildPage';
import { Photo } from '.';
import { PATH_ADMIN_PHOTOS } from '@/app/paths';
import { PATH_ADMIN_PHOTOS } from '@/app/path';
import { PhotoFormData, convertPhotoToFormData } from './form';
import PhotoForm from './form/PhotoForm';
import { Tags } from '@/tag';

View File

@ -1,6 +1,6 @@
'use client';
import { getEscapePath } from '@/app/paths';
import { getEscapePath } from '@/app/path';
import { useRouter, usePathname } from 'next/navigation';
import { useCallback } from 'react';
import useEscapeHandler from '../utility/useEscapeHandler';

View File

@ -4,7 +4,7 @@ import {
} from '.';
import PhotosLarge from './PhotosLarge';
import PhotosLargeInfinite from './PhotosLargeInfinite';
import { SortBy } from './db/sort';
import { SortBy } from './sort';
export default function PhotoFullPage({
photos,

View File

@ -7,7 +7,7 @@ import { clsx } from 'clsx/lite';
import AnimateItems from '@/components/AnimateItems';
import { ComponentProps, useCallback, useState, ReactNode } from 'react';
import { GRID_SPACE_CLASSNAME } from '@/components';
import { SortBy } from './db/sort';
import { SortBy } from './sort';
export default function PhotoGridContainer({
cacheKey,

View File

@ -4,7 +4,7 @@ import { INFINITE_SCROLL_GRID_MULTIPLE } from '.';
import InfinitePhotoScroll from './InfinitePhotoScroll';
import PhotoGrid from './PhotoGrid';
import { ComponentProps } from 'react';
import { SortBy } from './db/sort';
import { SortBy } from './sort';
export default function PhotoGridInfinite({
cacheKey,

View File

@ -1,7 +1,7 @@
'use client';
import { Photo } from '.';
import { PATH_GRID_INFERRED } from '@/app/paths';
import { PATH_GRID_INFERRED } from '@/app/path';
import PhotoGridSidebar from './PhotoGridSidebar';
import PhotoGridContainer from './PhotoGridContainer';
import { ComponentProps, useEffect, useRef } from 'react';
@ -10,7 +10,7 @@ import clsx from 'clsx/lite';
import useElementHeight from '@/utility/useElementHeight';
import MaskedScroll from '@/components/MaskedScroll';
import { IS_RECENTS_FIRST } from '@/app/config';
import { SortBy } from './db/sort';
import { SortBy } from './sort';
export default function PhotoGridPageClient({
photos,

View File

@ -15,7 +15,7 @@ import AppGrid from '@/components/AppGrid';
import ImageLarge from '@/components/image/ImageLarge';
import { clsx } from 'clsx/lite';
import Link from 'next/link';
import { pathForFocalLength, pathForPhoto } from '@/app/paths';
import { pathForFocalLength, pathForPhoto } from '@/app/path';
import PhotoTags from '@/tag/PhotoTags';
import ShareButton from '@/share/ShareButton';
import DownloadButton from '@/components/DownloadButton';

View File

@ -5,7 +5,7 @@ import { Photo, titleForPhoto } from '@/photo';
import { PhotoSetCategory } from '@/category';
import { AnimationConfig } from '../components/AnimateItems';
import { useAppState } from '@/app/AppState';
import { pathForPhoto } from '@/app/paths';
import { pathForPhoto } from '@/app/path';
import { clsx } from 'clsx/lite';
import LinkWithStatus from '@/components/LinkWithStatus';
import Spinner from '@/components/Spinner';

View File

@ -8,7 +8,7 @@ import {
import { PhotoSetCategory } from '../category';
import ImageMedium from '@/components/image/ImageMedium';
import { clsx } from 'clsx/lite';
import { pathForPhoto } from '@/app/paths';
import { pathForPhoto } from '@/app/path';
import { SHOULD_PREFETCH_ALL_LINKS } from '@/app/config';
import { useRef } from 'react';
import useVisible from '@/utility/useVisible';

View File

@ -6,7 +6,7 @@ import {
titleForPhoto,
} from '@/photo';
import { PhotoSetCategory } from '../category';
import { pathForPhoto, pathForPhotoImage } from '@/app/paths';
import { pathForPhoto, pathForPhotoImage } from '@/app/path';
import OGTile, { OGTilePropsCore } from '@/components/og/OGTile';
export default function PhotoOGTile({

View File

@ -9,7 +9,7 @@ import {
} from '@/photo';
import { PhotoSetCategory } from '../category';
import PhotoLink from './PhotoLink';
import { pathForAdminPhotoEdit, pathForPhoto } from '@/app/paths';
import { pathForAdminPhotoEdit, pathForPhoto } from '@/app/path';
import { useAppState } from '@/app/AppState';
import { AnimationConfig } from '@/components/AnimateItems';
import { clsx } from 'clsx/lite';

Some files were not shown because too many files have changed in this diff Show More