Refine uploads layout

This commit is contained in:
Sam Becker 2025-06-18 09:23:20 -05:00
parent 3bd6b20e76
commit 6ac797d5ac
8 changed files with 95 additions and 75 deletions

View File

@ -2,9 +2,10 @@ import { BiImageAdd } from 'react-icons/bi';
import PathLoaderButton from '@/components/primitives/PathLoaderButton'; import PathLoaderButton from '@/components/primitives/PathLoaderButton';
import { ComponentProps } from 'react'; import { ComponentProps } from 'react';
export default function AddButton( export default function AddButton({
props: ComponentProps<typeof PathLoaderButton>, children,
) { ...props
}: ComponentProps<typeof PathLoaderButton>) {
return ( return (
<PathLoaderButton <PathLoaderButton
{...props} {...props}
@ -13,7 +14,7 @@ export default function AddButton(
className="translate-x-[1px] translate-y-[1px]" className="translate-x-[1px] translate-y-[1px]"
/>} />}
> >
Add {children || 'Add'}
</PathLoaderButton> </PathLoaderButton>
); );
} }

View File

@ -149,6 +149,7 @@ export default function AdminBatchUploadActions({
onChange={setTags} onChange={setTags}
onError={setTagErrorMessage} onError={setTagErrorMessage}
readOnly={isAdding} readOnly={isAdding}
className="relative z-10"
/> />
<div className="flex gap-8"> <div className="flex gap-8">
<FieldsetFavs <FieldsetFavs

View File

@ -1,5 +1,6 @@
'use client'; 'use client';
import { Dispatch, SetStateAction } from 'react';
import { UrlAddStatus } from './AdminUploadsClient'; import { UrlAddStatus } from './AdminUploadsClient';
import AdminUploadsTableRow from './AdminUploadsTableRow'; import AdminUploadsTableRow from './AdminUploadsTableRow';
@ -12,23 +13,24 @@ export default function AdminUploadsTable({
}: { }: {
isAdding?: boolean isAdding?: boolean
urlAddStatuses: UrlAddStatus[] urlAddStatuses: UrlAddStatus[]
setUrlAddStatuses?: (urlAddStatuses: UrlAddStatus[]) => void setUrlAddStatuses?: Dispatch<SetStateAction<UrlAddStatus[]>>
isDeleting?: boolean isDeleting?: boolean
setIsDeleting?: (isDeleting: boolean) => void setIsDeleting?: Dispatch<SetStateAction<boolean>>
}) { }) {
const isComplete = urlAddStatuses.every(({ status }) => status === 'added'); const isComplete = urlAddStatuses.every(({ status }) => status === 'added');
return ( return (
<div className="space-y-4"> <div className="space-y-4">
{urlAddStatuses.map(status => {urlAddStatuses.map((status, index) =>
<AdminUploadsTableRow <AdminUploadsTableRow
key={status.url} key={status.url}
{...{ {...{
...status, ...status,
tabIndex: index + 1,
shouldRedirectToAdminPhotosOnDelete: urlAddStatuses.length <= 1,
isAdding, isAdding,
isDeleting, isDeleting,
isComplete, isComplete,
setIsDeleting, setIsDeleting,
urlAddStatuses,
setUrlAddStatuses, setUrlAddStatuses,
}} }}
/>, />,

View File

@ -11,9 +11,10 @@ import { FaRegCircleCheck } from 'react-icons/fa6';
import AddButton from './AddButton'; import AddButton from './AddButton';
import { pathForAdminUploadUrl } from '@/app/paths'; import { pathForAdminUploadUrl } from '@/app/paths';
import DeleteBlobButton from './DeleteUploadButton'; import DeleteBlobButton from './DeleteUploadButton';
import { useEffect, useRef } from 'react'; import { Dispatch, SetStateAction, useEffect, useRef } from 'react';
import { isElementEntirelyInViewport } from '@/utility/dom'; import { isElementEntirelyInViewport } from '@/utility/dom';
import FieldSetWithStatus from '@/components/FieldSetWithStatus'; import FieldSetWithStatus from '@/components/FieldSetWithStatus';
import EditButton from './EditButton';
export default function AdminUploadsTableRow({ export default function AdminUploadsTableRow({
url, url,
@ -22,19 +23,21 @@ export default function AdminUploadsTableRow({
draftTitle = '', draftTitle = '',
uploadedAt, uploadedAt,
size, size,
tabIndex,
shouldRedirectToAdminPhotosOnDelete,
isAdding, isAdding,
isDeleting, isDeleting,
isComplete, isComplete,
setIsDeleting, setIsDeleting,
urlAddStatuses,
setUrlAddStatuses, setUrlAddStatuses,
}: UrlAddStatus & { }: UrlAddStatus & {
tabIndex: number
shouldRedirectToAdminPhotosOnDelete: boolean
isAdding?: boolean isAdding?: boolean
isDeleting?: boolean isDeleting?: boolean
isComplete?: boolean isComplete?: boolean
setIsDeleting?: (isDeleting: boolean) => void setIsDeleting?: Dispatch<SetStateAction<boolean>>
urlAddStatuses: UrlAddStatus[] setUrlAddStatuses?: Dispatch<SetStateAction<UrlAddStatus[]>>
setUrlAddStatuses?: (urlAddStatuses: UrlAddStatus[]) => void
}) { }) {
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
@ -75,11 +78,67 @@ export default function AdminUploadsTableRow({
/> />
</div> </div>
<div className={clsx( <div className={clsx(
'flex flex-col w-full self-start min-w-0', 'flex self-stretch w-full min-w-0',
'gap-2 sm:gap-3', 'gap-2 sm:gap-3',
'p-2 sm:p-3', 'p-2 sm:p-3',
)}> )}>
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3 w-full">
<div className="flex flex-col grow gap-3">
<FieldSetWithStatus
label="Title"
value={draftTitle}
onChange={titleUpdated => {
setUrlAddStatuses?.(statuses => statuses.map(status => ({
...status,
draftTitle: status.url === url
? titleUpdated
: status.draftTitle,
})));
}}
placeholder="Title (optional)"
tabIndex={tabIndex}
readOnly={isAdding || isDeleting || isComplete || Boolean(status)}
hideLabel
/>
<div className="flex items-center gap-2">
{isAdding || isComplete
? <>
{status === 'added'
? <FaRegCircleCheck size={18} />
: status === 'adding' &&
<Spinner
size={19}
className="translate-y-[2px]"
/>}
</>
: <>
<AddButton
path={pathForAdminUploadUrl(url)}
disabled={isDeleting}
hideTextOnMobile={false}
tooltip="Add directly"
/>
<EditButton
path={pathForAdminUploadUrl(url)}
tooltip="Review photo details"
hideText
/>
<DeleteBlobButton
urls={[url]}
shouldRedirectToAdminPhotos={
shouldRedirectToAdminPhotosOnDelete}
onDeleteStart={() => setIsDeleting?.(true)}
onDelete={() => {
setIsDeleting?.(false);
setUrlAddStatuses?.(statuses => statuses
.filter(({ url: urlToRemove }) => urlToRemove !== url));
}}
isLoading={isDeleting}
tooltip="Delete upload"
/>
</>}
</div>
</div>
<div className={clsx( <div className={clsx(
'flex gap-2 sm:gap-3', 'flex gap-2 sm:gap-3',
'ml-0.5', 'ml-0.5',
@ -92,7 +151,10 @@ export default function AdminUploadsTableRow({
: 'Waiting' : 'Waiting'
: <> : <>
{uploadedAt {uploadedAt
? <ResponsiveDate date={uploadedAt} length="medium" /> ? <ResponsiveDate
date={uploadedAt}
titleLabel="UPLOADED AT"
/>
: '—'} : '—'}
<div className="max-sm:hidden text-dim truncate"> <div className="max-sm:hidden text-dim truncate">
{size {size
@ -101,55 +163,7 @@ export default function AdminUploadsTableRow({
</div> </div>
</>} </>}
</div> </div>
<FieldSetWithStatus
label="Title"
className="[&_input]:min-h-9 [&_input]:px-2 [&_input]:py-0"
value={draftTitle}
onChange={titleUpdated => {
setUrlAddStatuses?.(urlAddStatuses.map(status => ({
...status,
draftTitle: status.url === url
? titleUpdated
: status.draftTitle,
})));
}}
placeholder="Title (optional)"
tabIndex={urlAddStatuses
.findIndex(status => status.url === url) + 1}
hideLabel
/>
</div> </div>
<span className="flex items-center gap-2">
{isAdding || isComplete
? <>
{status === 'added'
? <FaRegCircleCheck size={18} />
: status === 'adding' &&
<Spinner
size={19}
className="translate-y-[2px]"
/>}
</>
: <>
<AddButton
path={pathForAdminUploadUrl(url)}
disabled={isDeleting}
hideTextOnMobile={false}
/>
<DeleteBlobButton
urls={[url]}
shouldRedirectToAdminPhotos={urlAddStatuses.length <= 1}
onDeleteStart={() => setIsDeleting?.(true)}
onDelete={() => {
setIsDeleting?.(false);
setUrlAddStatuses?.(urlAddStatuses
.filter(({ url: urlToRemove }) =>
urlToRemove !== url));
}}
isLoading={isDeleting}
/>
</>}
</span>
</div> </div>
</div> </div>
); );

View File

@ -12,7 +12,6 @@ export default function DeleteUploadButton({
shouldRedirectToAdminPhotos, shouldRedirectToAdminPhotos,
onDeleteStart, onDeleteStart,
onDelete, onDelete,
hideTextOnMobile,
children, children,
isLoading, isLoading,
...props ...props
@ -50,7 +49,6 @@ export default function DeleteUploadButton({
}); });
}} }}
isLoading={isLoading ?? isDeleting} isLoading={isLoading ?? isDeleting}
hideTextOnMobile={hideTextOnMobile}
> >
{children} {children}
</DeleteButton> </DeleteButton>

View File

@ -1,17 +1,19 @@
import IconEdit from '@/components/icons/IconEdit'; import IconEdit from '@/components/icons/IconEdit';
import PathLoaderButton from '@/components/primitives/PathLoaderButton'; import PathLoaderButton from '@/components/primitives/PathLoaderButton';
import { ComponentProps } from 'react';
export default function EditButton ({ export default function EditButton ({
path, children,
...props
}: { }: {
path: string, hideText?: boolean
}) { } & ComponentProps<typeof PathLoaderButton>) {
return ( return (
<PathLoaderButton <PathLoaderButton
path={path} {...props}
icon={<IconEdit size={15} className="translate-y-[0.5px]" />} icon={<IconEdit size={15} className="translate-y-[0.5px]" />}
> >
Edit {children || 'Edit'}
</PathLoaderButton> </PathLoaderButton>
); );
} }

View File

@ -69,7 +69,7 @@ export default function LoaderButton({
), ),
styleAs === 'link' && 'hover:text-dim', styleAs === 'link' && 'hover:text-dim',
styleAs === 'link-without-hover' && 'hover:text-main', styleAs === 'link-without-hover' && 'hover:text-main',
'inline-flex items-center gap-2 self-start whitespace-nowrap', 'inline-flex items-center gap-1.5 self-start whitespace-nowrap',
primary && 'primary', primary && 'primary',
hideFocusOutline && 'focus:outline-hidden', hideFocusOutline && 'focus:outline-hidden',
className, className,
@ -88,7 +88,7 @@ export default function LoaderButton({
size={14} size={14}
color={spinnerColor} color={spinnerColor}
className={clsx( className={clsx(
'translate-y-[1px]', 'translate-y-[0.5px]',
spinnerClassName, spinnerClassName,
)} )}
/> />
@ -96,7 +96,7 @@ export default function LoaderButton({
</span>} </span>}
{children && <span className={clsx( {children && <span className={clsx(
styleAs !== 'button' && isLoading && 'text-dim', styleAs !== 'button' && isLoading && 'text-dim',
hideTextOnMobile && icon !== undefined && 'hidden sm:inline-block', hideTextOnMobile && icon !== undefined && 'max-sm:hidden',
)}> )}>
{children} {children}
</span>} </span>}

View File

@ -11,6 +11,7 @@ export default function PathLoaderButton({
shouldScroll = true, shouldScroll = true,
shouldReplace, shouldReplace,
isLoading, isLoading,
onClick,
children, children,
...props ...props
}: { }: {
@ -46,7 +47,8 @@ export default function PathLoaderButton({
return ( return (
<LoaderButton <LoaderButton
{...props} {...props}
onClick={() => { onClick={e => {
onClick?.(e);
startTransition(() => { startTransition(() => {
if (shouldReplace) { if (shouldReplace) {
router.replace(path, { scroll: shouldScroll }); router.replace(path, { scroll: shouldScroll });