diff --git a/app/admin/uploads/[uploadPath]/page.tsx b/app/admin/uploads/[uploadPath]/page.tsx index 3bc007e9..5a25deea 100644 --- a/app/admin/uploads/[uploadPath]/page.tsx +++ b/app/admin/uploads/[uploadPath]/page.tsx @@ -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> } -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) { - formDataFromExif.recipeTitle = recipeTitle; - } - const hasAiTextGeneration = AI_TEXT_GENERATION_ENABLED; + let textFieldsToAutoGenerate = AI_TEXT_AUTO_GENERATED_FIELDS; - const textFieldsToAutoGenerate = AI_TEXT_AUTO_GENERATED_FIELDS; + if (formDataFromExif) { + if (recipeTitle) { + formDataFromExif.recipeTitle = recipeTitle; + } + if (typeof title === 'string') { + formDataFromExif.title = title; + textFieldsToAutoGenerate = textFieldsToAutoGenerate + .filter(field => field !== 'title'); + } + } return ( !isDataMissing diff --git a/src/admin/AddUploadButton.tsx b/src/admin/AddUploadButton.tsx new file mode 100644 index 00000000..57f85b32 --- /dev/null +++ b/src/admin/AddUploadButton.tsx @@ -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) { + const router = useRouter(); + + const [isAddingLocal, setIsAddingLocal] = useState(false); + + return ( + } + 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 + + ); +} diff --git a/src/admin/AdminUploadsTable.tsx b/src/admin/AdminUploadsTable.tsx index 8968909a..1b8b1545 100644 --- a/src/admin/AdminUploadsTable.tsx +++ b/src/admin/AdminUploadsTable.tsx @@ -26,7 +26,7 @@ export default function AdminUploadsTable({ {...{ ...status, tabIndex: index + 1, - shouldRedirectToAdminPhotosOnDelete: urlAddStatuses.length <= 1, + shouldRedirectAfterAction: urlAddStatuses.length <= 1, isAdding, isDeleting, isComplete, diff --git a/src/admin/AdminUploadsTableRow.tsx b/src/admin/AdminUploadsTableRow.tsx index 503dcd23..581e4324 100644 --- a/src/admin/AdminUploadsTableRow.tsx +++ b/src/admin/AdminUploadsTableRow.tsx @@ -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) => { + setUrlAddStatuses?.(statuses => statuses.map(status => status.url === url + ? { + ...status, + ...updatedStatus, + } + : status)); + }; + + const removeRow = () => setUrlAddStatuses?.(statuses => statuses + .filter(({ url: urlToRemove }) => urlToRemove !== url)); + return (
{ - 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({ />} : <> - } + updateStatus({ + status: 'adding', + statusMessage: 'Adding ...', + })} + onAddFinish={removeRow} + shouldRedirectToAdminPhotos={shouldRedirectAfterAction} disabled={isRowLoading} - tooltip="Add directly" - hideText="never" - > - Add - + /> - setIsDeleting?.(true)} onDelete={() => { setIsDeleting?.(false); - setUrlAddStatuses?.(statuses => statuses - .filter(({ url: urlToRemove }) => urlToRemove !== url)); + removeRow(); }} disabled={isRowLoading} tooltip="Delete upload" diff --git a/src/app/paths.ts b/src/app/paths.ts index 3829471c..76168120 100644 --- a/src/app/paths.ts +++ b/src/app/paths.ts @@ -66,7 +66,8 @@ export const PATH_API_VERCEL_BLOB_UPLOAD = `${PATH_API_STORAGE}/vercel-blob`; export const PATH_API_PRESIGNED_URL = `${PATH_API_STORAGE}/presigned-url`; // Modifiers -const EDIT = 'edit'; +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}`;