* Begin storing optimized photo files * Increase optimized image size to 1080 * Refactor photo/storage modules * Refine storage file naming api * Simplify photo storage api * Finalize photo storage api * Start storing/serving optimized photos * Finalize optimized photo asset generation * Temporarily allow static optimization on PREVIEW branches * Restore static optimization as production-only * Remove og image inline-flex class * Tweak convert upload signature * Refactor optimized file storage * Display optimized files when they exist in photo form * Create small disclosure component * Report photo storage files more accurately * Sort optimized files * Generate optimized storage files when updating/syncing photos * Include source bucket when copying files with MinIO * Make deleting files more resilient
197 lines
6.0 KiB
TypeScript
197 lines
6.0 KiB
TypeScript
import ImageMedium from '@/components/image/ImageMedium';
|
|
import { UrlAddStatus } from './AdminUploadsClient';
|
|
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/path';
|
|
import DeleteUploadButton from './DeleteUploadButton';
|
|
import { Dispatch, SetStateAction, useEffect, useRef } from 'react';
|
|
import { isElementEntirelyInViewport } from '@/utility/dom';
|
|
import FieldsetWithStatus from '@/components/FieldsetWithStatus';
|
|
import EditButton from './EditButton';
|
|
import AddUploadButton from './AddUploadButton';
|
|
import { getFileNamePartsFromStorageUrl } from '@/platforms/storage';
|
|
|
|
export default function AdminUploadsTableRow({
|
|
url,
|
|
status,
|
|
statusMessage,
|
|
draftTitle = '',
|
|
uploadedAt,
|
|
size,
|
|
tabIndex,
|
|
shouldRedirectAfterAction,
|
|
isAdding,
|
|
isDeleting,
|
|
isComplete,
|
|
setIsDeleting,
|
|
setUrlAddStatuses,
|
|
}: UrlAddStatus & {
|
|
tabIndex: number
|
|
shouldRedirectAfterAction: boolean
|
|
isAdding?: boolean
|
|
isDeleting?: boolean
|
|
isComplete?: boolean
|
|
setIsDeleting?: Dispatch<SetStateAction<boolean>>
|
|
setUrlAddStatuses?: Dispatch<SetStateAction<UrlAddStatus[]>>
|
|
}) {
|
|
const ref = useRef<HTMLDivElement>(null);
|
|
|
|
const {
|
|
fileExtension,
|
|
fileId,
|
|
} = getFileNamePartsFromStorageUrl(url);
|
|
|
|
const extension = fileExtension?.toUpperCase();
|
|
|
|
useEffect(() => {
|
|
if (
|
|
status === 'adding' &&
|
|
!isElementEntirelyInViewport(ref.current)
|
|
) {
|
|
window.scrollTo({
|
|
top: (ref.current?.offsetTop ?? 0) - 16,
|
|
behavior: 'smooth',
|
|
});
|
|
}
|
|
}, [status]);
|
|
|
|
const isRowLoading = isAdding || isDeleting || isComplete || Boolean(status);
|
|
|
|
const updateStatus = (updatedStatus: Partial<UrlAddStatus>) => {
|
|
setUrlAddStatuses?.(statuses => statuses.map(status => status.url === url
|
|
? {
|
|
...status,
|
|
...updatedStatus,
|
|
}
|
|
: status));
|
|
};
|
|
|
|
const removeRow = () => setUrlAddStatuses?.(statuses => statuses
|
|
.filter(({ url: urlToRemove }) => urlToRemove !== url));
|
|
|
|
return (
|
|
<div
|
|
ref={ref}
|
|
className={clsx(
|
|
'flex items-center grow',
|
|
'transition-opacity',
|
|
'rounded-lg overflow-hidden',
|
|
'border-medium bg-extra-dim',
|
|
isAdding && !isComplete && status !== 'adding' && 'opacity-30',
|
|
)}
|
|
>
|
|
<div className={clsx(
|
|
'self-stretch',
|
|
'w-[40%] sm:w-auto shrink-0',
|
|
'transition-transform',
|
|
)}>
|
|
<ImageMedium
|
|
title={fileId}
|
|
src={url}
|
|
alt={url}
|
|
aspectRatio={3.0 / 2.0}
|
|
className={clsx(
|
|
'bg-dim',
|
|
'max-sm:m-2 max-sm:mr-0',
|
|
'max-sm:outline-medium max-sm:shadow-sm',
|
|
'max-sm:rounded-sm overflow-hidden',
|
|
)}
|
|
/>
|
|
</div>
|
|
<div className={clsx(
|
|
'flex self-stretch w-full min-w-0',
|
|
'gap-2 sm:gap-3',
|
|
'p-2 sm:p-3',
|
|
)}>
|
|
<div className="flex flex-col gap-6 w-full">
|
|
<div className="flex flex-col grow gap-2">
|
|
<FieldsetWithStatus
|
|
id={`title-${url}`}
|
|
label="Title"
|
|
value={draftTitle}
|
|
onChange={titleUpdated =>
|
|
updateStatus({ draftTitle: titleUpdated })}
|
|
placeholder="Title (optional)"
|
|
tabIndex={tabIndex}
|
|
readOnly={isRowLoading}
|
|
capitalize
|
|
hideLabel
|
|
/>
|
|
<div className="flex items-center gap-2">
|
|
{isAdding || isComplete
|
|
? <>
|
|
{status === 'added'
|
|
? <FaRegCircleCheck size={18} />
|
|
: status === 'adding' &&
|
|
<Spinner
|
|
size={19}
|
|
className="translate-y-[2px]"
|
|
/>}
|
|
</>
|
|
: <>
|
|
<AddUploadButton
|
|
url={url}
|
|
title={draftTitle}
|
|
onAddStart={() => updateStatus({
|
|
status: 'adding',
|
|
statusMessage: 'Adding ...',
|
|
})}
|
|
onAddFinish={removeRow}
|
|
shouldRedirectToAdminPhotos={shouldRedirectAfterAction}
|
|
disabled={isRowLoading}
|
|
tooltipSide="bottom"
|
|
/>
|
|
<EditButton
|
|
path={pathForAdminUploadUrl(url, draftTitle)}
|
|
disabled={isRowLoading}
|
|
tooltip="Review EXIF details before adding"
|
|
hideText="always"
|
|
tooltipSide="bottom"
|
|
/>
|
|
<DeleteUploadButton
|
|
urls={[url]}
|
|
shouldRedirectToAdminPhotos={shouldRedirectAfterAction}
|
|
onDeleteStart={() => setIsDeleting?.(true)}
|
|
onDelete={() => {
|
|
setIsDeleting?.(false);
|
|
removeRow();
|
|
}}
|
|
disabled={isRowLoading}
|
|
tooltip="Delete upload"
|
|
tooltipSide="bottom"
|
|
/>
|
|
</>}
|
|
</div>
|
|
</div>
|
|
<div className={clsx(
|
|
'flex gap-2 sm:gap-3',
|
|
'ml-0.5',
|
|
)}>
|
|
{isAdding || isComplete
|
|
? status === 'added'
|
|
? 'Added'
|
|
: status === 'adding'
|
|
? statusMessage ?? 'Adding ...'
|
|
: 'Waiting'
|
|
: <>
|
|
{uploadedAt
|
|
? <ResponsiveDate
|
|
date={uploadedAt}
|
|
titleLabel="UPLOADED AT"
|
|
/>
|
|
: '—'}
|
|
<div className="max-sm:hidden text-dim truncate">
|
|
{size
|
|
? `${size} ${extension}`
|
|
: extension}
|
|
</div>
|
|
</>}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|