Improve add/edit upload functionality
This commit is contained in:
parent
6ec4bfbfe8
commit
144e68b965
@ -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
|
||||||
|
|||||||
66
src/admin/AddUploadButton.tsx
Normal file
66
src/admin/AddUploadButton.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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,
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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}`;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user