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 { redirect } from 'next/navigation';
import {
@ -19,10 +19,12 @@ export const maxDuration = 60;
interface Params {
params: Promise<{ uploadPath: string }>
searchParams: Promise<Record<string, string | string[] | undefined>>
}
export default async function UploadPage({ params }: Params) {
const { uploadPath } = await params;
export default async function UploadPage({ params, searchParams }: Params) {
const uploadPath = (await params).uploadPath;
const title = (await searchParams)[PARAM_UPLOAD_TITLE];
const {
blobId,
@ -62,13 +64,19 @@ export default async function UploadPage({ params }: Params) {
: undefined,
]);
if (formDataFromExif && recipeTitle) {
const hasAiTextGeneration = AI_TEXT_GENERATION_ENABLED;
let textFieldsToAutoGenerate = AI_TEXT_AUTO_GENERATED_FIELDS;
if (formDataFromExif) {
if (recipeTitle) {
formDataFromExif.recipeTitle = recipeTitle;
}
const hasAiTextGeneration = AI_TEXT_GENERATION_ENABLED;
const textFieldsToAutoGenerate = AI_TEXT_AUTO_GENERATED_FIELDS;
if (typeof title === 'string') {
formDataFromExif.title = title;
textFieldsToAutoGenerate = textFieldsToAutoGenerate
.filter(field => field !== 'title');
}
}
return (
!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,
tabIndex: index + 1,
shouldRedirectToAdminPhotosOnDelete: urlAddStatuses.length <= 1,
shouldRedirectAfterAction: urlAddStatuses.length <= 1,
isAdding,
isDeleting,
isComplete,

View File

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

View File

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