From af693b91410d9432e5b30b42b24520ff9471e4d0 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Wed, 1 Nov 2023 23:20:46 -0500 Subject: [PATCH] Finalize exif syncing behaviors --- src/admin/AdminGrid.tsx | 2 +- src/admin/BlobUrls.tsx | 43 +++++++++------- src/admin/DeleteButton.tsx | 15 ++++-- src/app/(auth-state)/admin/photos/page.tsx | 51 +++++++++++++------ src/app/(auth-state)/admin/tags/page.tsx | 26 ++++++---- src/app/layout.tsx | 2 +- src/components/ShareModal.tsx | 8 +-- src/components/SubmitButtonWithStatus.tsx | 4 +- src/photo/PhotoEditPageClient.tsx | 25 +++++++-- src/photo/PhotoForm.tsx | 34 +++++++++++-- src/photo/actions.ts | 2 +- src/site/SiteChecklistClient.tsx | 11 ++-- src/site/globals.css | 2 +- src/tag/TagHeader.tsx | 22 +++++--- .../ToasterWithThemes.tsx | 0 src/toast/index.tsx | 26 ++++++++++ src/utility/object.ts | 13 +++++ 17 files changed, 199 insertions(+), 87 deletions(-) rename src/{components => toast}/ToasterWithThemes.tsx (100%) create mode 100644 src/toast/index.tsx create mode 100644 src/utility/object.ts diff --git a/src/admin/AdminGrid.tsx b/src/admin/AdminGrid.tsx index 16cf493c..bd97a5b6 100644 --- a/src/admin/AdminGrid.tsx +++ b/src/admin/AdminGrid.tsx @@ -16,7 +16,7 @@ export default function AdminGrid ({
{children} diff --git a/src/admin/BlobUrls.tsx b/src/admin/BlobUrls.tsx index fdeab93c..b00813e2 100644 --- a/src/admin/BlobUrls.tsx +++ b/src/admin/BlobUrls.tsx @@ -41,25 +41,30 @@ export default function BlobUrls({ > {pathForBlobUrl(url)} - - - - - - +
+ + + + + + +
;})} ); diff --git a/src/admin/DeleteButton.tsx b/src/admin/DeleteButton.tsx index 99307aea..3c9ec428 100644 --- a/src/admin/DeleteButton.tsx +++ b/src/admin/DeleteButton.tsx @@ -1,11 +1,16 @@ import SubmitButtonWithStatus from '@/components/SubmitButtonWithStatus'; -import { FaTimes } from 'react-icons/fa'; +import { cc } from '@/utility/css'; +import { BiTrash } from 'react-icons/bi'; export default function DeleteButton () { return } - > - Delete - ; + icon={} + className={cc( + 'text-red-500 dark:text-red-600', + 'active:!bg-red-100/50 active:dark:!bg-red-950/50', + '!border-red-200 hover:!border-red-300', + 'dark:!border-red-900/75 dark:hover:!border-red-900', + )} + />; } diff --git a/src/app/(auth-state)/admin/photos/page.tsx b/src/app/(auth-state)/admin/photos/page.tsx index 43409cb1..b419cded 100644 --- a/src/app/(auth-state)/admin/photos/page.tsx +++ b/src/app/(auth-state)/admin/photos/page.tsx @@ -5,7 +5,7 @@ import PhotoTiny from '@/photo/PhotoTiny'; import { cc } from '@/utility/css'; import FormWithConfirm from '@/components/FormWithConfirm'; import SiteGrid from '@/components/SiteGrid'; -import { deletePhotoAction } from '@/photo/actions'; +import { deletePhotoAction, syncPhotoExifDataAction } from '@/photo/actions'; import { pathForAdminPhotos, pathForPhoto, @@ -28,6 +28,8 @@ import DeleteButton from '@/admin/DeleteButton'; import EditButton from '@/admin/EditButton'; import BlobUrls from '@/admin/BlobUrls'; import { PRO_MODE_ENABLED } from '@/site/config'; +import SubmitButtonWithStatus from '@/components/SubmitButtonWithStatus'; +import { GrSync } from 'react-icons/gr'; export const runtime = 'edge'; @@ -76,11 +78,11 @@ export default async function AdminTagsPage({ )} photo={photo} /> -
+
}
{photo.takenAtNaive}
- - - - - - +
+ + + + } /> + + + + + + +
)} {showMorePhotos && diff --git a/src/app/(auth-state)/admin/tags/page.tsx b/src/app/(auth-state)/admin/tags/page.tsx index 4a11fb6e..0e29cfdb 100644 --- a/src/app/(auth-state)/admin/tags/page.tsx +++ b/src/app/(auth-state)/admin/tags/page.tsx @@ -10,6 +10,7 @@ import PhotoTag from '@/tag/PhotoTag'; import { formatTag } from '@/tag'; import EditButton from '@/admin/EditButton'; import { pathForAdminTagEdit } from '@/site/paths'; +import { cc } from '@/utility/css'; export const runtime = 'edge'; @@ -30,16 +31,21 @@ export default async function AdminPhotosPage() {
{photoQuantityText(count, false)}
- - - - - +
+ + + + + +
)}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx index fdbbcd7c..129dd900 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -6,7 +6,7 @@ import { BASE_URL, SITE_DESCRIPTION, SITE_TITLE } from '@/site/config'; import StateProvider from '@/state/AppStateProvider'; import ThemeProviderClient from '@/site/ThemeProviderClient'; import Nav from '@/site/Nav'; -import ToasterWithThemes from '@/components/ToasterWithThemes'; +import ToasterWithThemes from '@/toast/ToasterWithThemes'; import PhotoEscapeHandler from '@/photo/PhotoEscapeHandler'; import '../site/globals.css'; diff --git a/src/components/ShareModal.tsx b/src/components/ShareModal.tsx index 484124fb..727e40ac 100644 --- a/src/components/ShareModal.tsx +++ b/src/components/ShareModal.tsx @@ -4,10 +4,9 @@ import Modal from '@/components/Modal'; import { TbPhotoShare } from 'react-icons/tb'; import { cc } from '@/utility/css'; import { BiCopy } from 'react-icons/bi'; -import { toast } from 'sonner'; -import { FiCheckSquare } from 'react-icons/fi'; import { ReactNode } from 'react'; import { shortenUrl } from '@/utility/url'; +import { toastSuccess } from '@/toast'; export default function ShareModal({ title = 'Share', @@ -52,10 +51,7 @@ export default function ShareModal({ )} onClick={() => { navigator.clipboard.writeText(pathShare); - toast( - 'Link to photo copied', - { icon: }, - ); + toastSuccess('Link to photo copied'); }} > diff --git a/src/components/SubmitButtonWithStatus.tsx b/src/components/SubmitButtonWithStatus.tsx index 60026d46..49158630 100644 --- a/src/components/SubmitButtonWithStatus.tsx +++ b/src/components/SubmitButtonWithStatus.tsx @@ -46,11 +46,11 @@ export default function SubmitButtonWithStatus(props: Props) { ? : icon} } - {children} - + } ); }; diff --git a/src/photo/PhotoEditPageClient.tsx b/src/photo/PhotoEditPageClient.tsx index ffb9f157..fe5d87ee 100644 --- a/src/photo/PhotoEditPageClient.tsx +++ b/src/photo/PhotoEditPageClient.tsx @@ -4,22 +4,32 @@ import AdminChildPage from '@/components/AdminChildPage'; import { Photo } from '.'; import { PATH_ADMIN_PHOTOS } from '@/site/paths'; import SubmitButtonWithStatus from '@/components/SubmitButtonWithStatus'; -import { BiRefresh } from 'react-icons/bi'; import { PhotoFormData, convertPhotoToFormData } from './form'; import PhotoForm from './PhotoForm'; import { useFormState } from 'react-dom'; import { getExifDataAction } from './actions'; +import { GrSync } from 'react-icons/gr'; +import { areSimpleObjectsEqual } from '@/utility/object'; export default function PhotoEditPageClient({ photo, }: { photo: Photo }) { + const seedExifData = { url: photo.url }; + const [updatedExifData, action] = useFormState>( getExifDataAction, - { url: photo.url}, + seedExifData, ); + const hasExifDataBeenFound = !areSimpleObjectsEqual( + updatedExifData, + seedExifData, + ); + + console.log({ hasExifDataBeenFound }); + return ( } + icon={} > - Refresh EXIF + EXIF } > ); diff --git a/src/photo/PhotoForm.tsx b/src/photo/PhotoForm.tsx index 36816bbd..ec215456 100644 --- a/src/photo/PhotoForm.tsx +++ b/src/photo/PhotoForm.tsx @@ -17,6 +17,7 @@ import { generateLocalNaivePostgresString, generateLocalPostgresString, } from '@/utility/date'; +import { toastSuccess, toastWarning } from '@/toast'; const THUMBNAIL_WIDTH = 300; const THUMBNAIL_HEIGHT = 200; @@ -35,12 +36,35 @@ export default function PhotoForm({ const [formData, setFormData] = useState>(initialPhotoForm); + // Update form when EXIF data + // is refreshed by parent useEffect(() => { - // Update form when EXIF data is refreshed by parent - setFormData(currentForm => ({ - ...currentForm, - ...updatedExifData, - })); + if (Object.keys(updatedExifData ?? {}).length > 0) { + const changedKeys: string[] = []; + + setFormData(currentForm => { + Object.entries(updatedExifData ?? {}) + .forEach(([key, value]) => { + if (currentForm[key as keyof PhotoFormData] !== value) { + changedKeys.push(key); + } + }); + + return { + ...currentForm, + ...updatedExifData, + }; + }); + + if (changedKeys.length > 0) { + toastSuccess( + `Updated EXIF fields: ${changedKeys.join(', ')}`, + 8000, + ); + } else { + toastWarning('No new EXIF data found'); + } + } }, [updatedExifData]); // Generate local date strings when diff --git a/src/photo/actions.ts b/src/photo/actions.ts index 168da4ad..6d30cdce 100644 --- a/src/photo/actions.ts +++ b/src/photo/actions.ts @@ -106,7 +106,7 @@ export async function getExifDataAction( } export async function syncPhotoExifDataAction(formData: FormData) { - const photoId = formData.get('photoId') as string; + const photoId = formData.get('id') as string; if (photoId) { const photo = await getPhoto(photoId); if (photo) { diff --git a/src/site/SiteChecklistClient.tsx b/src/site/SiteChecklistClient.tsx index ae2e9759..2b1b03c1 100644 --- a/src/site/SiteChecklistClient.tsx +++ b/src/site/SiteChecklistClient.tsx @@ -4,7 +4,7 @@ import { useTransition } from 'react'; import { useRouter } from 'next/navigation'; import { cc } from '@/utility/css'; import ChecklistRow from '../components/ChecklistRow'; -import { FiCheckSquare, FiExternalLink } from 'react-icons/fi'; +import { FiExternalLink } from 'react-icons/fi'; import { BiCog, BiCopy, @@ -14,9 +14,9 @@ import { BiRefresh, } from 'react-icons/bi'; import IconButton from '@/components/IconButton'; -import { toast } from 'sonner'; import InfoBlock from '@/components/InfoBlock'; import Checklist from '@/components/Checklist'; +import { toastSuccess } from '@/toast'; export default function SiteChecklistClient({ hasPostgres, @@ -84,12 +84,7 @@ export default function SiteChecklistClient({ className={cc(subtle && 'text-gray-300 dark:text-gray-700')} onClick={() => { navigator.clipboard.writeText(text); - toast( - `${label} copied to clipboard`, { - icon: , - duration: 4000, - }, - ); + toastSuccess(`${label} copied to clipboard`); }} />; diff --git a/src/site/globals.css b/src/site/globals.css index 7a080dc2..bd0db754 100644 --- a/src/site/globals.css +++ b/src/site/globals.css @@ -25,7 +25,7 @@ bg-white dark:bg-black border-gray-200 dark:border-gray-700 font-mono text-base leading-tight - min-h-[2.25rem] + min-h-[2.4rem] } input[type=text], input[type=email], input[type=password], select { @apply diff --git a/src/tag/TagHeader.tsx b/src/tag/TagHeader.tsx index 9f3b8d7e..541ee836 100644 --- a/src/tag/TagHeader.tsx +++ b/src/tag/TagHeader.tsx @@ -3,6 +3,7 @@ import PhotoTag from './PhotoTag'; import { descriptionForTaggedPhotos } from '.'; import { pathForTagShare } from '@/site/paths'; import PhotoHeader from '@/photo/PhotoHeader'; +import AnimateItems from '@/components/AnimateItems'; export default function TagHeader({ tag, @@ -16,14 +17,19 @@ export default function TagHeader({ count?: number }) { return ( - } - entityVerb="Tagged" - entityDescription={descriptionForTaggedPhotos(photos, undefined, count)} - photos={photos} - selectedPhoto={selectedPhoto} - sharePath={pathForTagShare(tag)} - count={count} + } + entityVerb="Tagged" + entityDescription={descriptionForTaggedPhotos(photos, undefined, count)} + photos={photos} + selectedPhoto={selectedPhoto} + sharePath={pathForTagShare(tag)} + count={count} + />]} /> ); } diff --git a/src/components/ToasterWithThemes.tsx b/src/toast/ToasterWithThemes.tsx similarity index 100% rename from src/components/ToasterWithThemes.tsx rename to src/toast/ToasterWithThemes.tsx diff --git a/src/toast/index.tsx b/src/toast/index.tsx new file mode 100644 index 00000000..19703b0c --- /dev/null +++ b/src/toast/index.tsx @@ -0,0 +1,26 @@ +import type { ReactNode } from 'react'; +import { AiOutlineWarning } from 'react-icons/ai'; +import { FiCheckSquare } from 'react-icons/fi'; +import { toast } from 'sonner'; + +const DEFAULT_DURATION = 4000; + +export const toastSuccess = ( + message: ReactNode, + duration = DEFAULT_DURATION, +) => toast( + message, { + icon: , + duration, + }, +); + +export const toastWarning = ( + message: ReactNode, + duration = DEFAULT_DURATION, +) => toast( + message, { + icon: , + duration, + }, +); diff --git a/src/utility/object.ts b/src/utility/object.ts new file mode 100644 index 00000000..bc0d025e --- /dev/null +++ b/src/utility/object.ts @@ -0,0 +1,13 @@ +type SimpleObject = Record; + +export const areSimpleObjectsEqual = ( + obj1: SimpleObject, + obj2: SimpleObject, +): boolean => { + const obj1Keys = Object.keys(obj1); + const obj2Keys = Object.keys(obj2); + + return obj1Keys.length === obj2Keys.length + ? obj1Keys.every((key) => obj1[key] === obj2[key]) + : false; +};