Refactor photo/menu form components

This commit is contained in:
Sam Becker 2026-02-28 10:56:58 -06:00
parent 741bcf32f7
commit dddf1f39c5
17 changed files with 110 additions and 73 deletions

View File

@ -1,26 +1,60 @@
import AdminAboutEditPage from '@/about/AdminAboutEditPage';
import { getAbout } from '@/about/query';
import { PRESERVE_ORIGINAL_UPLOADS } from '@/app/config';
import { getPhotoNoStore } from '@/photo/cache';
import { feedQueryOptions } from '@/feed';
import {
getPhotosCached,
getPhotosMetaCached,
} from '@/photo/cache';
import { getPhoto } from '@/photo/query';
const PHOTO_CHOOSER_QUERY_OPTIONS = feedQueryOptions({
isGrid: true,
excludeFromFeeds: false,
});
export default async function AboutEditPage() {
const about = await getAbout().catch(() => undefined);
const [
{
about,
photoAvatar,
photoHero,
},
photos,
photosCount,
photosHidden,
] = await Promise.all([
getAbout().then(async about => {
const photoAvatar = about?.photoIdAvatar
? await getPhoto(about?.photoIdAvatar ?? '', true)
.catch(() => undefined)
: undefined;
const photoAvatar = about?.photoIdAvatar
? await getPhotoNoStore(about?.photoIdAvatar ?? '', true)
.catch(() => undefined)
: undefined;
const photoHero = about?.photoIdHero
? await getPhoto(about?.photoIdHero ?? '', true)
.catch(() => undefined)
: undefined;
const photoHero = about?.photoIdHero
? await getPhotoNoStore(about?.photoIdHero ?? '', true)
.catch(() => undefined)
: undefined;
return {
about,
photoAvatar,
photoHero,
};
}),
getPhotosCached(PHOTO_CHOOSER_QUERY_OPTIONS),
getPhotosMetaCached(PHOTO_CHOOSER_QUERY_OPTIONS)
.then(({ count }) => count),
getPhotosCached({ hidden: 'only', limit: 1000 }),
]);
return (
<AdminAboutEditPage {...{
about,
photoAvatar,
photoHero,
photos,
photosCount,
photosHidden,
shouldResizeImages: !PRESERVE_ORIGINAL_UPLOADS,
}} />
);

View File

@ -8,12 +8,12 @@ import { getPhotosMetaCached } from '@/photo/cache';
import { SortProps } from '@/photo/sort';
import { getSortOptionsFromParams } from '@/photo/sort/path';
import { PhotoQueryOptions } from '@/db';
import { FEED_META_QUERY_OPTIONS, getFeedQueryOptions } from '@/feed';
import { FEED_META_QUERY_OPTIONS, feedQueryOptions } from '@/feed';
export const maxDuration = 60;
const getPhotosCached = cache((options: PhotoQueryOptions) =>
getPhotos(getFeedQueryOptions({
getPhotos(feedQueryOptions({
isGrid: false,
...options,
})));

View File

@ -6,12 +6,12 @@ import { getPhotos } from '@/photo/query';
import PhotoFullPage from '@/photo/PhotoFullPage';
import { getPhotosMetaCached } from '@/photo/cache';
import { USER_DEFAULT_SORT_OPTIONS } from '@/app/config';
import { FEED_META_QUERY_OPTIONS, getFeedQueryOptions } from '@/feed';
import { FEED_META_QUERY_OPTIONS, feedQueryOptions } from '@/feed';
export const dynamic = 'force-static';
export const maxDuration = 60;
const getPhotosCached = cache(() => getPhotos(getFeedQueryOptions({
const getPhotosCached = cache(() => getPhotos(feedQueryOptions({
isGrid: false,
})));

View File

@ -8,13 +8,13 @@ import { getDataForCategoriesCached } from '@/category/cache';
import { getPhotosMetaCached } from '@/photo/cache';
import { SortProps } from '@/photo/sort';
import { getSortOptionsFromParams } from '@/photo/sort/path';
import { FEED_META_QUERY_OPTIONS, getFeedQueryOptions } from '@/feed';
import { FEED_META_QUERY_OPTIONS, feedQueryOptions } from '@/feed';
import { PhotoQueryOptions } from '@/db';
export const maxDuration = 60;
const getPhotosCached = cache((options: PhotoQueryOptions) =>
getPhotos(getFeedQueryOptions({
getPhotos(feedQueryOptions({
isGrid: true,
...options,
})));

View File

@ -7,12 +7,12 @@ import PhotoGridPage from '@/photo/PhotoGridPage';
import { getDataForCategoriesCached } from '@/category/cache';
import { getPhotosMetaCached } from '@/photo/cache';
import { USER_DEFAULT_SORT_OPTIONS } from '@/app/config';
import { FEED_META_QUERY_OPTIONS, getFeedQueryOptions } from '@/feed';
import { FEED_META_QUERY_OPTIONS, feedQueryOptions } from '@/feed';
export const dynamic = 'force-static';
export const maxDuration = 60;
const getPhotosCached = cache(() => getPhotos(getFeedQueryOptions({
const getPhotosCached = cache(() => getPhotos(feedQueryOptions({
isGrid: true,
})));

View File

@ -9,12 +9,12 @@ import PhotoFullPage from '@/photo/PhotoFullPage';
import PhotoGridPage from '@/photo/PhotoGridPage';
import { getDataForCategoriesCached } from '@/category/cache';
import { getPhotosMetaCached } from '@/photo/cache';
import { FEED_META_QUERY_OPTIONS, getFeedQueryOptions } from '@/feed';
import { FEED_META_QUERY_OPTIONS, feedQueryOptions } from '@/feed';
export const dynamic = 'force-static';
export const maxDuration = 60;
const getPhotosCached = cache(() => getPhotos(getFeedQueryOptions({
const getPhotosCached = cache(() => getPhotos(feedQueryOptions({
isGrid: GRID_HOMEPAGE_ENABLED,
})));

View File

@ -14,6 +14,7 @@ import PhotoMedium from '@/photo/PhotoMedium';
import clsx from 'clsx/lite';
import useDynamicPhoto from '@/photo/useDynamicPhoto';
import { useAppText } from '@/i18n/state/client';
import FieldsetPhotoChooser from '@/photo/form/FieldsetPhotoChooser';
export default function AdminAboutEditPage({
about,
@ -23,6 +24,9 @@ export default function AdminAboutEditPage({
about?: About
photoAvatar?: Photo
photoHero?: Photo
photos?: Photo[]
photosCount?: number
photosHidden?: Photo[]
shouldResizeImages?: boolean
}) {
const appText = useAppText();
@ -58,6 +62,13 @@ export default function AdminAboutEditPage({
action={updateAboutAction}
>
<div className="space-y-4">
<FieldsetPhotoChooser
label="Avatar Photo"
value={aboutForm?.photoIdAvatar ?? ''}
onChange={photoIdAvatar => setAboutForm(form =>
({ ...form, photoIdAvatar: convertUrlToPhotoId(photoIdAvatar) }))}
photo={photoAvatar}
/>
<PhotoAvatar photo={photoAvatar} />
<FieldsetWithStatus
id="photoIdAvatar"

View File

@ -10,8 +10,8 @@ import StatusIcon from '@/components/StatusIcon';
import clsx from 'clsx/lite';
import { useState } from 'react';
import { Photo } from '@/photo';
import FieldsetPhotoQuery from '@/photo/FieldsetPhotoQuery';
import FieldsetPhotoChooser from '@/photo/FieldsetPhotoChooser';
import FieldsetPhotoQuery from '@/photo/form/FieldsetPhotoQuery';
import FieldsetPhotoChooser from '@/photo/form/FieldsetPhotoChooser';
export default function ComponentsPageClient({
photo,

View File

@ -11,21 +11,7 @@ import { FiMoreHorizontal } from 'react-icons/fi';
import MoreMenuItem from './MoreMenuItem';
import { clearGlobalFocus } from '@/utility/dom';
import { FaChevronRight } from 'react-icons/fa6';
const surfaceStyles = (className?: string) => clsx(
'z-10',
'min-w-[8rem]',
'component-surface',
'py-1',
'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=right]:dark:shadow-[0_10px_40px_rgba(0,0,0,0.6)]',
'data-[side=top]:animate-fade-in-from-bottom',
'data-[side=bottom]:animate-fade-in-from-top',
'data-[side=right]:animate-fade-in-from-top',
className,
);
import { menuSurfaceStyles } from '../primitives/surface';
export type MoreMenuSection = {
label?: string
@ -115,7 +101,7 @@ export default function MoreMenu({
onCloseAutoFocus={e => e.preventDefault()}
align={align}
sideOffset={sideOffset}
className={surfaceStyles(className)}
className={menuSurfaceStyles(className)}
>
{header && <div className={clsx(
'px-3 pt-3 pb-2 text-dim uppercase',
@ -171,7 +157,7 @@ export default function MoreMenu({
</DropdownMenu.SubTrigger>
<DropdownMenu.Portal>
<DropdownMenu.SubContent
className={surfaceStyles()}
className={menuSurfaceStyles()}
>
{item.items.map(item =>
<div key={item.label} className="px-1">

View File

@ -2,7 +2,7 @@
import { ReactNode, useRef, useState, ComponentProps } from 'react';
import * as Tooltip from '@radix-ui/react-tooltip';
import MenuSurface from './MenuSurface';
import ComponentSurface from './surface/ComponentSurface';
import clsx from 'clsx/lite';
import useClickInsideOutside from '@/utility/useClickInsideOutside';
import KeyCommand from './KeyCommand';
@ -31,7 +31,7 @@ export default function TooltipPrimitive({
children: ReactNode
className?: string
classNameTrigger?: string
color?: ComponentProps<typeof MenuSurface>['color']
color?: ComponentProps<typeof ComponentSurface>['color']
keyCommand?: string
keyCommandModifier?: ComponentProps<typeof KeyCommand>['modifier']
supportMobile?: boolean
@ -126,9 +126,9 @@ export default function TooltipPrimitive({
)}
>
{content &&
<MenuSurface {...{ color, className }}>
<ComponentSurface {...{ color, className }}>
{content}
</MenuSurface>}
</ComponentSurface>}
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>

View File

@ -1,7 +1,7 @@
import { ReactNode, RefObject } from 'react';
import clsx from 'clsx/lite';
export default function MenuSurface({
export default function ComponentSurface({
ref,
children,
className,

View File

@ -0,0 +1,16 @@
import clsx from 'clsx/lite';
export const menuSurfaceStyles = (className?: string) => clsx(
'z-10',
'min-w-[8rem]',
'component-surface',
'py-1',
'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=right]:dark:shadow-[0_10px_40px_rgba(0,0,0,0.6)]',
'data-[side=top]:animate-fade-in-from-bottom',
'data-[side=bottom]:animate-fade-in-from-top',
'data-[side=right]:animate-fade-in-from-top',
className,
);

View File

@ -10,7 +10,7 @@ import {
} from 'react';
import { SharedHoverContext, SharedHoverProps } from './state';
import { AnimatePresence, motion } from 'framer-motion';
import MenuSurface from '../primitives/MenuSurface';
import ComponentSurface from '../primitives/surface/ComponentSurface';
import clsx from 'clsx/lite';
const WINDOW_CHANGE_EVENTS = ['mouseup', 'mousewheel', 'resize'];
@ -133,7 +133,7 @@ export default function SharedHoverProvider({
className="fixed"
style={hoverStyle}
>
<MenuSurface
<ComponentSurface
className="max-w-none p-1!"
color={hoverProps.color}
>
@ -158,7 +158,7 @@ export default function SharedHoverProvider({
: 'border-medium',
)} />
</div>
</MenuSurface>
</ComponentSurface>
</motion.div>}
</AnimatePresence>
</div>

View File

@ -6,7 +6,7 @@ import {
SetStateAction,
use,
} from 'react';
import MenuSurface from '../primitives/MenuSurface';
import ComponentSurface from '../primitives/surface/ComponentSurface';
export type SharedHoverProps = {
key: string
@ -14,7 +14,7 @@ export type SharedHoverProps = {
height: number
offsetAbove: number
offsetBelow: number
color?: ComponentProps<typeof MenuSurface>['color']
color?: ComponentProps<typeof ComponentSurface>['color']
}
export type SharedHoverState = {

View File

@ -13,21 +13,23 @@ const FEED_BASE_QUERY_OPTIONS: PhotoQueryOptions = {
// PAGE FEED QUERY OPTIONS
export const getFeedQueryOptions = ({
export const feedQueryOptions = ({
isGrid,
sortBy = USER_DEFAULT_SORT_OPTIONS.sortBy,
sortWithPriority = USER_DEFAULT_SORT_OPTIONS.sortWithPriority,
...options
}: {
isGrid: boolean,
sortBy?: SortBy,
sortWithPriority?: boolean,
}): PhotoQueryOptions => ({
} & PhotoQueryOptions): PhotoQueryOptions => ({
...FEED_BASE_QUERY_OPTIONS,
sortBy,
sortWithPriority,
limit: isGrid
? INFINITE_SCROLL_GRID_INITIAL
: INFINITE_SCROLL_FULL_INITIAL,
...options,
});
export const FEED_META_QUERY_OPTIONS: PhotoQueryOptions = {

View File

@ -1,9 +1,10 @@
import FieldsetWithStatus from '@/components/FieldsetWithStatus';
import { altTextForPhoto, doesPhotoNeedBlurCompatibility, Photo } from '.';
import { altTextForPhoto, doesPhotoNeedBlurCompatibility, Photo } from '..';
import clsx from 'clsx/lite';
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
import ImageMedium from '@/components/image/ImageMedium';
import PhotoGridInfinite from './PhotoGridInfinite';
import PhotoGridInfinite from '../PhotoGridInfinite';
import { menuSurfaceStyles } from '@/components/primitives/surface';
export default function FieldsetPhotoChooser({
label,
@ -24,7 +25,7 @@ export default function FieldsetPhotoChooser({
<button type="button" className="p-1.5">
{photo &&
<span className={clsx(
'flex w-[8rem]',
'flex size-[6rem]',
'border border-medium rounded-[4px]',
'overflow-hidden select-none active:opacity-75',
)}>
@ -43,20 +44,7 @@ export default function FieldsetPhotoChooser({
onCloseAutoFocus={e => e.preventDefault()}
align="start"
sideOffset={10}
// alignOffset={-10}
className={clsx(
'z-20',
'min-w-[8rem]',
'component-surface',
'p-1.5',
'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=right]:dark:shadow-[0_10px_40px_rgba(0,0,0,0.6)]',
'data-[side=top]:animate-fade-in-from-bottom',
'data-[side=bottom]:animate-fade-in-from-top',
'data-[side=right]:animate-fade-in-from-top',
)}>
className={menuSurfaceStyles('z-20 px-1.5 py-1.5')}>
<div className={clsx(
'w-[14rem] max-h-[20rem] rounded-[3px] overflow-y-auto',
'space-y-1',
@ -73,4 +61,4 @@ export default function FieldsetPhotoChooser({
</DropdownMenu.Root>
</>
);
}
}

View File

@ -1,12 +1,12 @@
'use client';
import FieldsetWithStatus from '@/components/FieldsetWithStatus';
import { Photo } from '.';
import { Photo } from '..';
import { useEffect, useState } from 'react';
import { AnnotatedTag } from './form';
import { AnnotatedTag } from '../form';
import { useDebounce } from 'use-debounce';
import PhotoSmall from './PhotoSmall';
import { getPhotosAction } from './actions';
import PhotoSmall from '../PhotoSmall';
import { getPhotosAction } from '../actions';
const convertPhotoToAnnotatedTag = (photo: Photo): AnnotatedTag => ({
value: photo.id,