Refine uploads layout
This commit is contained in:
parent
3bd6b20e76
commit
6ac797d5ac
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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,
|
||||||
}}
|
}}
|
||||||
/>,
|
/>,
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>}
|
||||||
|
|||||||
@ -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 });
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user