Vercel/src/admin/AdminUploadsTableRow.tsx
Sam Becker 59f5c74269
Chromatic sorting (#284)
* Test color palette extraction

* Fix import

* Add hex <> oklch conversions

* Add 'hue' storage to photos

* Consolidate color modules

* Add chromatic config, track missing color data

* Bump deps

* Fix lens text test

* Finalize color storage

* Refactor color imports

* Hide form color data when disabled

* Store all average oklch color components

* Finalize color-config language

* Optimize photo syncing for color data

* Only update color data when syncing if possible

* Build out all color sorts

* Debug image colors

* Improve color debugging

* Improve color logging

* Simplify color sorting

* Bump deps

* Fix color sync logic

* Switch to sort params: ascending, descending

* Fix commandk sort menu

* Update tr-tr sorting language

* Add color capture to all photo extractions

* Add color visualization to photo form

* Standardize photo update language

* Create global debug color update function

* Improve color data capture logging

* Update maximum function duration for admin photos

* Add note to remove maxDuration

* Use AI to generate sorting color

* Conditionally use AI to analyze colors

* Manage AI color analysis batched requests

* Fix color reporting in admin photo table

* Only update color where AI fields are missing

* Temporarily upgrade admin/photos timeout

* Fix pro-based max duration

* Standardize color sorting foundations

* Update color sorting language

* Refactor color calculations

* Restore max duration time

* Update color-based sort menu labels

* Finalize color documentation

* Clean up color test actions

* Round color sort values before submitting to db

* Consolidate color server actions
2025-08-03 19:31:02 -05:00

195 lines
6.0 KiB
TypeScript

import ImageMedium from '@/components/image/ImageMedium';
import { UrlAddStatus } from './AdminUploadsClient';
import {
getExtensionFromStorageUrl,
getIdFromStorageUrl,
} from '@/platforms/storage';
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';
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 extension = getExtensionFromStorageUrl(url)?.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={getIdFromStorageUrl(url)}
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>
);
}