Improve add/edit upload functionality

This commit is contained in:
Sam Becker 2025-06-18 18:35:10 -05:00
parent 6ec4bfbfe8
commit 144e68b965
5 changed files with 119 additions and 41 deletions

View File

@ -1,4 +1,4 @@
import { PATH_ADMIN } from '@/app/paths'; import { PARAM_UPLOAD_TITLE, PATH_ADMIN } from '@/app/paths';
import { extractImageDataFromBlobPath } from '@/photo/server'; import { extractImageDataFromBlobPath } from '@/photo/server';
import { redirect } from 'next/navigation'; import { redirect } from 'next/navigation';
import { import {
@ -19,10 +19,12 @@ export const maxDuration = 60;
interface Params { interface Params {
params: Promise<{ uploadPath: string }> params: Promise<{ uploadPath: string }>
searchParams: Promise<Record<string, string | string[] | undefined>>
} }
export default async function UploadPage({ params }: Params) { export default async function UploadPage({ params, searchParams }: Params) {
const { uploadPath } = await params; const uploadPath = (await params).uploadPath;
const title = (await searchParams)[PARAM_UPLOAD_TITLE];
const { const {
blobId, blobId,
@ -62,13 +64,19 @@ export default async function UploadPage({ params }: Params) {
: undefined, : undefined,
]); ]);
if (formDataFromExif && recipeTitle) { const hasAiTextGeneration = AI_TEXT_GENERATION_ENABLED;
let textFieldsToAutoGenerate = AI_TEXT_AUTO_GENERATED_FIELDS;
if (formDataFromExif) {
if (recipeTitle) {
formDataFromExif.recipeTitle = recipeTitle; formDataFromExif.recipeTitle = recipeTitle;
} }
if (typeof title === 'string') {
const hasAiTextGeneration = AI_TEXT_GENERATION_ENABLED; formDataFromExif.title = title;
textFieldsToAutoGenerate = textFieldsToAutoGenerate
const textFieldsToAutoGenerate = AI_TEXT_AUTO_GENERATED_FIELDS; .filter(field => field !== 'title');
}
}
return ( return (
!isDataMissing !isDataMissing

View File

@ -0,0 +1,66 @@
import LoaderButton from '@/components/primitives/LoaderButton';
import { addUploadAction } from '@/photo/actions';
import {
generateLocalNaivePostgresString,
generateLocalPostgresString,
} from '@/utility/date';
import { pathForAdminUploadUrl } from '@/app/paths';
import { useRouter } from 'next/navigation';
import { BiImageAdd } from 'react-icons/bi';
import { ComponentProps, useState } from 'react';
export default function AddUploadButton({
url,
title,
onAddStart,
onAddFinish,
shouldRedirectToAdminPhotos,
...props
}: {
url: string
title?: string
onAddStart?: () => void
onAddFinish?: (success: boolean) => void
shouldRedirectToAdminPhotos: boolean
} & ComponentProps<typeof LoaderButton>) {
const router = useRouter();
const [isAddingLocal, setIsAddingLocal] = useState(false);
return (
<LoaderButton
{...props}
icon={<BiImageAdd
size={18}
className="translate-x-[1px] translate-y-[1px]"
/>}
onClick={() => {
onAddStart?.();
setIsAddingLocal(true);
addUploadAction({
url,
title,
takenAtLocal: generateLocalPostgresString(),
takenAtNaiveLocal: generateLocalNaivePostgresString(),
})
.then(() => {
if (shouldRedirectToAdminPhotos) {
router.push(pathForAdminUploadUrl(url));
} else {
onAddFinish?.(true);
setIsAddingLocal(false);
}
})
.catch(() => {
onAddFinish?.(false);
setIsAddingLocal(false);
});
}}
isLoading={isAddingLocal}
tooltip="Add directly"
hideText="never"
>
Add
</LoaderButton>
);
}

View File

@ -26,7 +26,7 @@ export default function AdminUploadsTable({
{...{ {...{
...status, ...status,
tabIndex: index + 1, tabIndex: index + 1,
shouldRedirectToAdminPhotosOnDelete: urlAddStatuses.length <= 1, shouldRedirectAfterAction: urlAddStatuses.length <= 1,
isAdding, isAdding,
isDeleting, isDeleting,
isComplete, isComplete,

View File

@ -9,13 +9,12 @@ import ResponsiveDate from '@/components/ResponsiveDate';
import Spinner from '@/components/Spinner'; import Spinner from '@/components/Spinner';
import { FaRegCircleCheck } from 'react-icons/fa6'; import { FaRegCircleCheck } from 'react-icons/fa6';
import { pathForAdminUploadUrl } from '@/app/paths'; import { pathForAdminUploadUrl } from '@/app/paths';
import DeleteBlobButton from './DeleteUploadButton'; import DeleteUploadButton from './DeleteUploadButton';
import { Dispatch, SetStateAction, 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'; import EditButton from './EditButton';
import LoaderButton from '@/components/primitives/LoaderButton'; import AddUploadButton from './AddUploadButton';
import { BiImageAdd } from 'react-icons/bi';
export default function AdminUploadsTableRow({ export default function AdminUploadsTableRow({
url, url,
@ -25,7 +24,7 @@ export default function AdminUploadsTableRow({
uploadedAt, uploadedAt,
size, size,
tabIndex, tabIndex,
shouldRedirectToAdminPhotosOnDelete, shouldRedirectAfterAction,
isAdding, isAdding,
isDeleting, isDeleting,
isComplete, isComplete,
@ -33,7 +32,7 @@ export default function AdminUploadsTableRow({
setUrlAddStatuses, setUrlAddStatuses,
}: UrlAddStatus & { }: UrlAddStatus & {
tabIndex: number tabIndex: number
shouldRedirectToAdminPhotosOnDelete: boolean shouldRedirectAfterAction: boolean
isAdding?: boolean isAdding?: boolean
isDeleting?: boolean isDeleting?: boolean
isComplete?: boolean isComplete?: boolean
@ -58,6 +57,18 @@ export default function AdminUploadsTableRow({
const isRowLoading = isAdding || isDeleting || isComplete || Boolean(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 ( return (
<div <div
ref={ref} ref={ref}
@ -90,14 +101,8 @@ export default function AdminUploadsTableRow({
<FieldSetWithStatus <FieldSetWithStatus
label="Title" label="Title"
value={draftTitle} value={draftTitle}
onChange={titleUpdated => { onChange={titleUpdated =>
setUrlAddStatuses?.(statuses => statuses.map(status => ({ updateStatus({ draftTitle: titleUpdated })}
...status,
draftTitle: status.url === url
? titleUpdated
: status.draftTitle,
})));
}}
placeholder="Title (optional)" placeholder="Title (optional)"
tabIndex={tabIndex} tabIndex={tabIndex}
readOnly={isRowLoading} readOnly={isRowLoading}
@ -115,32 +120,29 @@ export default function AdminUploadsTableRow({
/>} />}
</> </>
: <> : <>
<LoaderButton <AddUploadButton
icon={<BiImageAdd url={url}
size={18} onAddStart={() => updateStatus({
className="translate-x-[1px] translate-y-[1px]" status: 'adding',
/>} statusMessage: 'Adding ...',
})}
onAddFinish={removeRow}
shouldRedirectToAdminPhotos={shouldRedirectAfterAction}
disabled={isRowLoading} disabled={isRowLoading}
tooltip="Add directly" />
hideText="never"
>
Add
</LoaderButton>
<EditButton <EditButton
path={pathForAdminUploadUrl(url)} path={pathForAdminUploadUrl(url, draftTitle)}
disabled={isRowLoading} disabled={isRowLoading}
tooltip="Review photo details" tooltip="Review photo details"
hideText="always" hideText="always"
/> />
<DeleteBlobButton <DeleteUploadButton
urls={[url]} urls={[url]}
shouldRedirectToAdminPhotos={ shouldRedirectToAdminPhotos={shouldRedirectAfterAction}
shouldRedirectToAdminPhotosOnDelete}
onDeleteStart={() => setIsDeleting?.(true)} onDeleteStart={() => setIsDeleting?.(true)}
onDelete={() => { onDelete={() => {
setIsDeleting?.(false); setIsDeleting?.(false);
setUrlAddStatuses?.(statuses => statuses removeRow();
.filter(({ url: urlToRemove }) => urlToRemove !== url));
}} }}
disabled={isRowLoading} disabled={isRowLoading}
tooltip="Delete upload" tooltip="Delete upload"

View File

@ -67,6 +67,7 @@ export const PATH_API_PRESIGNED_URL = `${PATH_API_STORAGE}/presigned-url`;
// Modifiers // Modifiers
const EDIT = 'edit'; const EDIT = 'edit';
export const PARAM_UPLOAD_TITLE = 'title';
// Special characters // Special characters
export const MISSING_FIELD = '-'; export const MISSING_FIELD = '-';
@ -103,8 +104,9 @@ type PhotoPathParams = { photo: PhotoOrPhotoId } & PhotoSetCategory & {
showRecipe?: boolean showRecipe?: boolean
}; };
export const pathForAdminUploadUrl = (url: string) => export const pathForAdminUploadUrl = (url: string, title?: string) =>
`${PATH_ADMIN_UPLOADS}/${encodeURIComponent(url)}`; // eslint-disable-next-line max-len
`${PATH_ADMIN_UPLOADS}/${encodeURIComponent(url)}${title ? `?${PARAM_UPLOAD_TITLE}=${encodeURIComponent(title)}` : ''}`;
export const pathForAdminPhotoEdit = (photo: PhotoOrPhotoId) => export const pathForAdminPhotoEdit = (photo: PhotoOrPhotoId) =>
`${PATH_ADMIN_PHOTOS}/${getPhotoId(photo)}/${EDIT}`; `${PATH_ADMIN_PHOTOS}/${getPhotoId(photo)}/${EDIT}`;