'use client'; import { useEffect, useMemo, useState } from 'react'; import { FORM_METADATA_ENTRIES, PhotoFormData, convertFormKeysToLabels, formHasTextContent, getChangedFormFields, getFormErrors, isFormValid, } from '.'; import FieldSetWithStatus from '@/components/FieldSetWithStatus'; import { createPhotoAction, updatePhotoAction } from '../actions'; import SubmitButtonWithStatus from '@/components/SubmitButtonWithStatus'; import Link from 'next/link'; import { clsx } from 'clsx/lite'; import { PATH_ADMIN_PHOTOS, PATH_ADMIN_UPLOADS } from '@/site/paths'; import { toastSuccess, toastWarning } from '@/toast'; import { getDimensionsFromSize } from '@/utility/size'; import ImageWithFallback from '@/components/ImageWithFallback'; import { TagsWithMeta, sortTagsObjectWithoutFavs } from '@/tag'; import { formatCount, formatCountDescriptive } from '@/utility/string'; import { AiContent } from '../ai/useAiImageQueries'; import AiButton from '../ai/AiButton'; import Spinner from '@/components/Spinner'; import usePreventNavigation from '@/utility/usePreventNavigation'; import { useAppState } from '@/state/AppState'; import UpdateBlurDataButton from '../UpdateBlurDataButton'; import { getNextImageUrlForManipulation } from '@/services/next-image'; import { BLUR_ENABLED } from '@/site/config'; import { PhotoDbInsert } from '..'; const THUMBNAIL_SIZE = 300; export default function PhotoForm({ type = 'create', initialPhotoForm, updatedExifData, updatedBlurData, uniqueTags, aiContent, onTitleChange, onTextContentChange, onFormStatusChange, }: { type?: 'create' | 'edit' initialPhotoForm: Partial updatedExifData?: Partial updatedBlurData?: string uniqueTags?: TagsWithMeta aiContent?: AiContent onTitleChange?: (updatedTitle: string) => void onTextContentChange?: (hasContent: boolean) => void, onFormStatusChange?: (pending: boolean) => void }) { const [formData, setFormData] = useState>(initialPhotoForm); const [formErrors, setFormErrors] = useState(getFormErrors(initialPhotoForm)); const { invalidateSwr, shouldDebugBlur } = useAppState(); const changedFormKeys = useMemo(() => getChangedFormFields(initialPhotoForm, formData), [initialPhotoForm, formData]); const formHasChanged = changedFormKeys.length > 0; const onlyChangedFieldIsBlurData = changedFormKeys.length === 1 && changedFormKeys[0] === 'blurData'; usePreventNavigation(formHasChanged && !onlyChangedFieldIsBlurData); const canFormBeSubmitted = (type === 'create' || formHasChanged) && isFormValid(formData) && !aiContent?.isLoading; // Update form when EXIF data // is refreshed by parent useEffect(() => { if (Object.keys(updatedExifData ?? {}).length > 0) { const changedKeys: (keyof PhotoFormData)[] = []; setFormData(currentForm => { Object.entries(updatedExifData ?? {}) .forEach(([key, value]) => { if (currentForm[key as keyof PhotoFormData] !== value) { changedKeys.push(key as keyof PhotoFormData); } }); return { ...currentForm, ...updatedExifData, }; }); if (changedKeys.length > 0) { const fields = convertFormKeysToLabels(changedKeys); toastSuccess( `Updated EXIF fields: ${fields.join(', ')}`, 8000, ); } else { toastWarning('No new EXIF data found'); } } }, [updatedExifData]); const { width, height, } = getDimensionsFromSize(THUMBNAIL_SIZE, formData.aspectRatio); const url = formData.url ?? ''; useEffect(() => { if (updatedBlurData) { setFormData(data => updatedBlurData ? { ...data, blurData: updatedBlurData } : data); } else if (!BLUR_ENABLED) { setFormData(data => ({ ...data, blurData: '' })); } }, [updatedBlurData]); useEffect(() => setFormData(data => aiContent?.title ? { ...data, title: aiContent?.title } : data), [aiContent?.title]); useEffect(() => setFormData(data => aiContent?.caption ? { ...data, caption: aiContent?.caption } : data), [aiContent?.caption]); useEffect(() => setFormData(data => aiContent?.tags ? { ...data, tags: aiContent?.tags } : data), [aiContent?.tags]); useEffect(() => setFormData(data => aiContent?.semanticDescription ? { ...data, semanticDescription: aiContent?.semanticDescription } : data), [aiContent?.semanticDescription]); useEffect(() => { onTextContentChange?.(formHasTextContent(formData)); }, [onTextContentChange, formData]); const isFieldGeneratingAi = (key: keyof PhotoFormData) => { switch (key) { case 'title': return aiContent?.isLoadingTitle; case 'caption': return aiContent?.isLoadingCaption; case 'tags': return aiContent?.isLoadingTags; case 'semanticDescription': return aiContent?.isLoadingSemantic; default: return false; } }; const accessoryForField = (key: keyof PhotoFormData) => { if (aiContent) { switch (key) { case 'title': return ; case 'caption': return ; case 'tags': return ; case 'semanticDescription': return ; case 'blurData': return shouldDebugBlur && type === 'edit' && formData.url ? setFormData(data => ({ ...data, blurData }))} /> : null; } } }; const shouldHideField = ( key: keyof PhotoDbInsert | 'favorite', hideIfEmpty?: boolean, shouldHide?: (formData: Partial) => boolean, ) => { if ( key === 'blurData' && type === 'create' && !BLUR_ENABLED && !shouldDebugBlur ) { return true; } else { return ( (hideIfEmpty && !formData[key]) || shouldHide?.(formData) ); } }; return (
Analyzing image
blur()} > {/* Fields */}
{FORM_METADATA_ENTRIES( sortTagsObjectWithoutFavs(uniqueTags ?? []) .map(({ tag, count }) => ({ value: tag, annotation: formatCount(count), annotationAria: formatCountDescriptive(count, 'tagged'), })), aiContent !== undefined, ) .map(([key, { label, note, required, selectOptions, selectOptionsDefaultLabel, tagOptions, readOnly, validate, validateStringMaxLength, capitalize, hideIfEmpty, shouldHide, loadingMessage, type, }]) => !shouldHideField(key, hideIfEmpty, shouldHide) && { const formUpdated = { ...formData, [key]: value }; setFormData(formUpdated); if (validate) { setFormErrors({ ...formErrors, [key]: validate(value) }); } else if (validateStringMaxLength !== undefined) { setFormErrors({ ...formErrors, [key]: value.length > validateStringMaxLength ? `${validateStringMaxLength} characters or less` : undefined, }); } if (key === 'title') { onTitleChange?.(value.trim()); } }} selectOptions={selectOptions} selectOptionsDefaultLabel={selectOptionsDefaultLabel} tagOptions={tagOptions} required={required} readOnly={readOnly} capitalize={capitalize} placeholder={loadingMessage && !formData[key] ? loadingMessage : undefined} loading={ (loadingMessage && !formData[key] ? true : false) || isFieldGeneratingAi(key)} type={type} accessory={accessoryForField(key)} />)}
{/* Actions */}
Cancel {type === 'create' ? 'Create' : 'Update'}
); };