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 AdminAboutEditPage from '@/about/AdminAboutEditPage';
import { getAbout } from '@/about/query'; import { getAbout } from '@/about/query';
import { PRESERVE_ORIGINAL_UPLOADS } from '@/app/config'; 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() { 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 const photoHero = about?.photoIdHero
? await getPhotoNoStore(about?.photoIdAvatar ?? '', true) ? await getPhoto(about?.photoIdHero ?? '', true)
.catch(() => undefined) .catch(() => undefined)
: undefined; : undefined;
const photoHero = about?.photoIdHero return {
? await getPhotoNoStore(about?.photoIdHero ?? '', true) about,
.catch(() => undefined) photoAvatar,
: undefined; photoHero,
};
}),
getPhotosCached(PHOTO_CHOOSER_QUERY_OPTIONS),
getPhotosMetaCached(PHOTO_CHOOSER_QUERY_OPTIONS)
.then(({ count }) => count),
getPhotosCached({ hidden: 'only', limit: 1000 }),
]);
return ( return (
<AdminAboutEditPage {...{ <AdminAboutEditPage {...{
about, about,
photoAvatar, photoAvatar,
photoHero, photoHero,
photos,
photosCount,
photosHidden,
shouldResizeImages: !PRESERVE_ORIGINAL_UPLOADS, shouldResizeImages: !PRESERVE_ORIGINAL_UPLOADS,
}} /> }} />
); );

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,21 +11,7 @@ import { FiMoreHorizontal } from 'react-icons/fi';
import MoreMenuItem from './MoreMenuItem'; import MoreMenuItem from './MoreMenuItem';
import { clearGlobalFocus } from '@/utility/dom'; import { clearGlobalFocus } from '@/utility/dom';
import { FaChevronRight } from 'react-icons/fa6'; import { FaChevronRight } from 'react-icons/fa6';
import { menuSurfaceStyles } from '../primitives/surface';
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,
);
export type MoreMenuSection = { export type MoreMenuSection = {
label?: string label?: string
@ -115,7 +101,7 @@ export default function MoreMenu({
onCloseAutoFocus={e => e.preventDefault()} onCloseAutoFocus={e => e.preventDefault()}
align={align} align={align}
sideOffset={sideOffset} sideOffset={sideOffset}
className={surfaceStyles(className)} className={menuSurfaceStyles(className)}
> >
{header && <div className={clsx( {header && <div className={clsx(
'px-3 pt-3 pb-2 text-dim uppercase', 'px-3 pt-3 pb-2 text-dim uppercase',
@ -171,7 +157,7 @@ export default function MoreMenu({
</DropdownMenu.SubTrigger> </DropdownMenu.SubTrigger>
<DropdownMenu.Portal> <DropdownMenu.Portal>
<DropdownMenu.SubContent <DropdownMenu.SubContent
className={surfaceStyles()} className={menuSurfaceStyles()}
> >
{item.items.map(item => {item.items.map(item =>
<div key={item.label} className="px-1"> <div key={item.label} className="px-1">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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