From f6bc8652256295110fd42776a2c3f5d0f3f96506 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Wed, 18 Jun 2025 21:35:24 -0500 Subject: [PATCH] Improve upload state management --- app/layout.tsx | 8 +++++++- src/admin/AddUploadButton.tsx | 7 ++----- src/admin/AdminBatchEditPanel.tsx | 8 ++++++-- src/admin/AdminBatchUploadActions.tsx | 14 ++++++++------ src/admin/AdminUploadsClient.tsx | 9 +++++++-- src/admin/AdminUploadsTableRow.tsx | 6 +++--- src/components/icons/IconAddUpload.tsx | 15 +++++++++++++++ src/photo/PhotoUploadWithStatus.tsx | 15 +++++++++++---- src/photo/form/PhotoForm.tsx | 4 +++- src/state/AppState.ts | 2 +- 10 files changed, 63 insertions(+), 25 deletions(-) create mode 100644 src/components/icons/IconAddUpload.tsx diff --git a/app/layout.tsx b/app/layout.tsx index 3eeaa1e2..6dfcff78 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -119,7 +119,13 @@ export default function RootLayout({ revalidatePath('/admin', 'layout'); }} /> - + { + 'use server'; + // Update upload count in admin nav + revalidatePath('/admin', 'layout'); + }} + /> {children} diff --git a/src/admin/AddUploadButton.tsx b/src/admin/AddUploadButton.tsx index 57f85b32..6590fd1f 100644 --- a/src/admin/AddUploadButton.tsx +++ b/src/admin/AddUploadButton.tsx @@ -6,8 +6,8 @@ import { } from '@/utility/date'; import { pathForAdminUploadUrl } from '@/app/paths'; import { useRouter } from 'next/navigation'; -import { BiImageAdd } from 'react-icons/bi'; import { ComponentProps, useState } from 'react'; +import IconAddUpload from '@/components/icons/IconAddUpload'; export default function AddUploadButton({ url, @@ -30,10 +30,7 @@ export default function AddUploadButton({ return ( } + icon={} onClick={() => { onAddStart?.(); setIsAddingLocal(true); diff --git a/src/admin/AdminBatchEditPanel.tsx b/src/admin/AdminBatchEditPanel.tsx index 9abb67da..66c58565 100644 --- a/src/admin/AdminBatchEditPanel.tsx +++ b/src/admin/AdminBatchEditPanel.tsx @@ -1,9 +1,13 @@ import { getUniqueTagsCached } from '@/photo/cache'; import AdminBatchEditPanelClient from './AdminBatchEditPanelClient'; -export default async function AdminBatchEditPanel() { +export default async function AdminBatchEditPanel({ + onBatchActionComplete, +}: { + onBatchActionComplete?: () => Promise +}) { const uniqueTags = await getUniqueTagsCached().catch(() => []); return ( - + ); } diff --git a/src/admin/AdminBatchUploadActions.tsx b/src/admin/AdminBatchUploadActions.tsx index 76066fbe..9af1db7c 100644 --- a/src/admin/AdminBatchUploadActions.tsx +++ b/src/admin/AdminBatchUploadActions.tsx @@ -14,7 +14,7 @@ import sleep from '@/utility/sleep'; import { readStreamableValue } from 'ai/rsc'; import { useRouter } from 'next/navigation'; import { Dispatch, SetStateAction, useRef, useState } from 'react'; -import { BiCheckCircle, BiImageAdd } from 'react-icons/bi'; +import { BiCheckCircle } from 'react-icons/bi'; import ProgressButton from '@/components/primitives/ProgressButton'; import { UrlAddStatus } from './AdminUploadsClient'; import PhotoTagFieldset from './PhotoTagFieldset'; @@ -23,6 +23,7 @@ import { useAppState } from '@/state/AppState'; import { pluralize } from '@/utility/string'; import FieldsetFavs from '@/photo/form/FieldsetFavs'; import FieldsetHidden from '@/photo/form/FieldsetHidden'; +import IconAddUpload from '@/components/icons/IconAddUpload'; const UPLOAD_BATCH_SIZE = 2; @@ -35,6 +36,7 @@ export default function AdminBatchUploadActions({ setUrlAddStatuses, isDeleting, setIsDeleting, + onBatchActionComplete, }: { uploadUrls: string[] uploadTitles: string[] @@ -44,6 +46,7 @@ export default function AdminBatchUploadActions({ setUrlAddStatuses: Dispatch> isDeleting: boolean setIsDeleting: Dispatch> + onBatchActionComplete?: () => Promise }) { const { updateAdminData } = useAppState(); @@ -177,10 +180,7 @@ export default function AdminBatchUploadActions({ } icon={isAddingComplete ? - : + : } onClick={async () => { // eslint-disable-next-line max-len @@ -208,6 +208,7 @@ export default function AdminBatchUploadActions({ setAddingProgress(1); setIsAdding(false); setIsAddingComplete(true); + await onBatchActionComplete?.(); await sleep(1000).then(() => router.push(PATH_ADMIN_PHOTOS)); } catch (e: any) { @@ -225,9 +226,10 @@ export default function AdminBatchUploadActions({ setIsDeleting(true)} - onDelete={didFail => { + onDelete={async didFail => { if (!didFail) { updateAdminData?.({ uploadsCount: 0 }); + await onBatchActionComplete?.(); router.push(PATH_ADMIN_PHOTOS); } else { setIsDeleting(false); diff --git a/src/admin/AdminUploadsClient.tsx b/src/admin/AdminUploadsClient.tsx index 5a35c3e6..b6a8c290 100644 --- a/src/admin/AdminUploadsClient.tsx +++ b/src/admin/AdminUploadsClient.tsx @@ -2,7 +2,7 @@ import { StorageListItem, StorageListResponse } from '@/platforms/storage'; import AdminBatchUploadActions from './AdminBatchUploadActions'; -import { useMemo, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { Tags } from '@/tag'; import AdminUploadsTable from './AdminUploadsTable'; @@ -20,14 +20,19 @@ export default function AdminUploadsClient({ urls: StorageListResponse uniqueTags?: Tags }) { - const [isAdding, setIsAdding] = useState(false); const [urlAddStatuses, setUrlAddStatuses] = useState(urls); + useEffect(() => { + // Overwrite local state when server state changes + setUrlAddStatuses(urls); + }, [urls]); + const uploadUrls = useMemo(() => urlAddStatuses .map(({ url }) => url), [urlAddStatuses]); const uploadTitles = useMemo(() => urlAddStatuses .map(({ draftTitle }) => draftTitle ?? ''), [urlAddStatuses]); + const [isAdding, setIsAdding] = useState(false); const [isDeleting, setIsDeleting] = useState(false); return ( diff --git a/src/admin/AdminUploadsTableRow.tsx b/src/admin/AdminUploadsTableRow.tsx index 581e4324..9d92aa8b 100644 --- a/src/admin/AdminUploadsTableRow.tsx +++ b/src/admin/AdminUploadsTableRow.tsx @@ -96,8 +96,8 @@ export default function AdminUploadsTableRow({ 'gap-2 sm:gap-3', 'p-2 sm:p-3', )}> -
-
+
+
; +} diff --git a/src/photo/PhotoUploadWithStatus.tsx b/src/photo/PhotoUploadWithStatus.tsx index df19173a..cedb38c7 100644 --- a/src/photo/PhotoUploadWithStatus.tsx +++ b/src/photo/PhotoUploadWithStatus.tsx @@ -1,7 +1,7 @@ 'use client'; import { uploadPhotoFromClient } from '@/platforms/storage'; -import { useRouter } from 'next/navigation'; +import { usePathname, useRouter } from 'next/navigation'; import { PATH_ADMIN_UPLOADS, pathForAdminUploadUrl } from '@/app/paths'; import ImageInput from '../components/ImageInput'; import { clsx } from 'clsx/lite'; @@ -49,6 +49,8 @@ export default function PhotoUploadWithStatus({ const router = useRouter(); + const pathname = usePathname(); + useEffect(() => { // Hide upload panel while button is shown if (showButton) { @@ -127,9 +129,14 @@ export default function PhotoUploadWithStatus({ if (isLastBlob) { await onLastUpload?.(); shouldResetUploadStateAfterPending.current = true; - startTransition(() => hasMultipleUploads - ? router.push(PATH_ADMIN_UPLOADS) - : router.push(pathForAdminUploadUrl(url))); + if (pathname === PATH_ADMIN_UPLOADS) { + setUploadState?.({ isUploading: false }); + router.refresh(); + } else { + startTransition(() => hasMultipleUploads + ? router.push(PATH_ADMIN_UPLOADS) + : router.push(pathForAdminUploadUrl(url))); + } } }) .catch(error => { diff --git a/src/photo/form/PhotoForm.tsx b/src/photo/form/PhotoForm.tsx index 32a3b685..f283316b 100644 --- a/src/photo/form/PhotoForm.tsx +++ b/src/photo/form/PhotoForm.tsx @@ -47,6 +47,7 @@ import PhotoFilmIcon from '@/film/PhotoFilmIcon'; import FieldsetFavs from './FieldsetFavs'; import FieldsetHidden from './FieldsetHidden'; import { useAppText } from '@/i18n/state/client'; +import IconAddUpload from '@/components/icons/IconAddUpload'; const THUMBNAIL_SIZE = 300; @@ -469,12 +470,13 @@ export default function PhotoForm({ Cancel } disabled={!canFormBeSubmitted} onFormStatusChange={onFormStatusChange} onFormSubmit={invalidateSwr} primary > - {type === 'create' ? 'Create' : 'Update'} + {type === 'create' ? 'Add' : 'Update'}
> insightsIndicatorStatus?: InsightsIndicatorStatus // UPLOAD - startUpload?: (onStart?: () => void) => void + startUpload?: () => Promise uploadInputRef?: RefObject uploadState: UploadState setUploadState?: (uploadState: Partial) => void