Add server actions to get/override EXIF data

This commit is contained in:
Sam Becker 2023-11-01 00:10:42 -05:00
parent bf78ced898
commit 8bb5c2990b
6 changed files with 90 additions and 11 deletions

View File

@ -1,7 +1,6 @@
'use client'; 'use client';
import { LegacyRef } from 'react'; import { LegacyRef } from 'react';
// @ts-ignore
import { useFormStatus } from 'react-dom'; import { useFormStatus } from 'react-dom';
import Spinner from './Spinner'; import Spinner from './Spinner';
import { cc } from '@/utility/css'; import { cc } from '@/utility/css';

View File

@ -1,7 +1,6 @@
'use client'; 'use client';
import { HTMLProps } from 'react'; import { HTMLProps } from 'react';
// @ts-ignore
import { useFormStatus } from 'react-dom'; import { useFormStatus } from 'react-dom';
import Spinner from './Spinner'; import Spinner from './Spinner';
import { cc } from '@/utility/css'; import { cc } from '@/utility/css';

View File

@ -5,26 +5,40 @@ import { Photo } from '.';
import { PATH_ADMIN_PHOTOS } from '@/site/paths'; import { PATH_ADMIN_PHOTOS } from '@/site/paths';
import SubmitButtonWithStatus from '@/components/SubmitButtonWithStatus'; import SubmitButtonWithStatus from '@/components/SubmitButtonWithStatus';
import { BiRefresh } from 'react-icons/bi'; import { BiRefresh } from 'react-icons/bi';
import { convertPhotoToFormData } from './form'; import { PhotoFormData, convertPhotoToFormData } from './form';
import PhotoForm from './PhotoForm'; import PhotoForm from './PhotoForm';
import { useFormState } from 'react-dom';
import { getExifDataAction } from './actions';
export default function PhotoEditPageClient({ export default function PhotoEditPageClient({
photo, photo,
}: { }: {
photo: Photo photo: Photo
}) { }) {
const [updatedExifData, action] = useFormState<Partial<PhotoFormData>>(
getExifDataAction,
{ url: photo.url},
);
return ( return (
<AdminChildPage <AdminChildPage
backPath={PATH_ADMIN_PHOTOS} backPath={PATH_ADMIN_PHOTOS}
backLabel="Photos" backLabel="Photos"
breadcrumb={photo.title || photo.id} breadcrumb={photo.title || photo.id}
accessory={<SubmitButtonWithStatus icon={<BiRefresh size={18} />}> accessory={
Refresh EXIF <form action={action}>
</SubmitButtonWithStatus>} <input name="photoUrl" value={photo.url} hidden readOnly />
<SubmitButtonWithStatus
icon={<BiRefresh size={18} className="translate-y-[-1.5px]" />}
>
Refresh EXIF
</SubmitButtonWithStatus>
</form>}
> >
<PhotoForm <PhotoForm
type="edit" type="edit"
initialPhotoForm={convertPhotoToFormData(photo)} initialPhotoForm={convertPhotoToFormData(photo)}
updatedExifData={updatedExifData}
/> />
</AdminChildPage> </AdminChildPage>
); );

View File

@ -23,16 +23,26 @@ const THUMBNAIL_HEIGHT = 200;
export default function PhotoForm({ export default function PhotoForm({
initialPhotoForm, initialPhotoForm,
updatedExifData,
type = 'create', type = 'create',
debugBlur, debugBlur,
}: { }: {
initialPhotoForm: Partial<PhotoFormData> initialPhotoForm: Partial<PhotoFormData>
updatedExifData?: Partial<PhotoFormData>
type?: 'create' | 'edit' type?: 'create' | 'edit'
debugBlur?: boolean debugBlur?: boolean
}) { }) {
const [formData, setFormData] = const [formData, setFormData] =
useState<Partial<PhotoFormData>>(initialPhotoForm); useState<Partial<PhotoFormData>>(initialPhotoForm);
useEffect(() => {
// Update form when EXIF data is refreshed by parent
setFormData(currentForm => ({
...currentForm,
...updatedExifData,
}));
}, [updatedExifData]);
// Generate local date strings when // Generate local date strings when
// none can be harvested from EXIF // none can be harvested from EXIF
useEffect(() => { useEffect(() => {

View File

@ -6,8 +6,14 @@ import {
sqlDeletePhotoTagGlobally, sqlDeletePhotoTagGlobally,
sqlUpdatePhoto, sqlUpdatePhoto,
sqlRenamePhotoTagGlobally, sqlRenamePhotoTagGlobally,
getPhoto,
} from '@/services/postgres'; } from '@/services/postgres';
import { convertFormDataToPhoto } from './form'; import {
PhotoFormData,
convertFormDataToPhotoDbInsert,
convertPhotoFormDataToPhotoDbInsert,
convertPhotoToFormData,
} from './form';
import { redirect } from 'next/navigation'; import { redirect } from 'next/navigation';
import { import {
convertUploadToPhoto, convertUploadToPhoto,
@ -20,9 +26,10 @@ import {
revalidatePhotosKey, revalidatePhotosKey,
} from '@/cache'; } from '@/cache';
import { PATH_ADMIN_PHOTOS, PATH_ADMIN_TAGS } from '@/site/paths'; import { PATH_ADMIN_PHOTOS, PATH_ADMIN_TAGS } from '@/site/paths';
import { extractFormDataFromUploadPath } from './server';
export async function createPhotoAction(formData: FormData) { export async function createPhotoAction(formData: FormData) {
const photo = convertFormDataToPhoto(formData, true); const photo = convertFormDataToPhotoDbInsert(formData, true);
const updatedUrl = await convertUploadToPhoto(photo.url, photo.id); const updatedUrl = await convertUploadToPhoto(photo.url, photo.id);
@ -36,7 +43,7 @@ export async function createPhotoAction(formData: FormData) {
} }
export async function updatePhotoAction(formData: FormData) { export async function updatePhotoAction(formData: FormData) {
const photo = convertFormDataToPhoto(formData); const photo = convertFormDataToPhotoDbInsert(formData);
await sqlUpdatePhoto(photo); await sqlUpdatePhoto(photo);
@ -84,7 +91,40 @@ export async function deleteBlobPhotoAction(formData: FormData) {
if (formData.get('redirectToPhotos') === 'true') { if (formData.get('redirectToPhotos') === 'true') {
redirect(PATH_ADMIN_PHOTOS); redirect(PATH_ADMIN_PHOTOS);
} }
}; }
export async function getExifDataAction(
photoFormPrevious: Partial<PhotoFormData>,
): Promise<Partial<PhotoFormData>> {
const { url } = photoFormPrevious;
if (url) {
const { photoForm } = await extractFormDataFromUploadPath(url);
if (photoForm) {
return photoForm;
}
}
return {};
}
export async function syncPhotoExifDataAction(formData: FormData) {
const photoId = formData.get('photoId') as string;
if (photoId) {
const photo = await getPhoto(photoId);
if (photo) {
const {
photoForm: photoFormExif,
} = await extractFormDataFromUploadPath(photo.url);
if (photoFormExif) {
const photoFormDbInsert = convertPhotoFormDataToPhotoDbInsert({
...convertPhotoToFormData(photo),
...photoFormExif,
});
await sqlUpdatePhoto(photoFormDbInsert);
revalidatePhotosKey();
}
}
}
}
export async function syncCacheAction() { export async function syncCacheAction() {
revalidateAllKeysAndPaths(); revalidateAllKeysAndPaths();

View File

@ -123,7 +123,7 @@ export const convertExifToFormData = (
: undefined, : undefined,
}); });
export const convertFormDataToPhoto = ( export const convertFormDataToPhotoDbInsert = (
formData: FormData, formData: FormData,
generateId?: boolean, generateId?: boolean,
): PhotoDbInsert => { ): PhotoDbInsert => {
@ -178,3 +178,20 @@ export const convertFormDataToPhoto = (
hidden: photoForm.hidden === 'true', hidden: photoForm.hidden === 'true',
}; };
}; };
const convertPhotoFormDataToFormData = (
photoFormData: PhotoFormData,
) => {
const formData = new FormData();
for (const key in photoFormData) {
formData.append(key, photoFormData[key as keyof PhotoFormData]);
}
return formData;
};
export const convertPhotoFormDataToPhotoDbInsert = (
photoFormData: PhotoFormData,
) => {
const formData = convertPhotoFormDataToFormData(photoFormData);
return convertFormDataToPhotoDbInsert(formData);
};