diff --git a/src/admin/AdminBatchEditPanelClient.tsx b/src/admin/AdminBatchEditPanelClient.tsx index 4bc4304b..806c6808 100644 --- a/src/admin/AdminBatchEditPanelClient.tsx +++ b/src/admin/AdminBatchEditPanelClient.tsx @@ -139,7 +139,7 @@ export default function AdminBatchEditPanelClient({ } } + icon={} onClick={() => setSelectedPhotoIds?.(undefined)} /> ; @@ -155,7 +155,7 @@ export default function AdminBatchEditPanelClient({ {isUserSignedIn - ? + ? : diff --git a/src/admin/AdminUploadPanel.tsx b/src/admin/AdminUploadPanel.tsx index faaed0d9..1c8fc73f 100644 --- a/src/admin/AdminUploadPanel.tsx +++ b/src/admin/AdminUploadPanel.tsx @@ -1,4 +1,5 @@ import Container from '@/components/Container'; +import LoaderButton from '@/components/primitives/LoaderButton'; import SiteGrid from '@/components/SiteGrid'; import Spinner from '@/components/Spinner'; import clsx from 'clsx'; @@ -8,8 +9,9 @@ export default function AdminUploadPanel() { return (
1 of 4: Uploading DSC-4353.jpg
- } />
} diff --git a/src/admin/upload/index.ts b/src/admin/upload/index.ts new file mode 100644 index 00000000..b908a791 --- /dev/null +++ b/src/admin/upload/index.ts @@ -0,0 +1,17 @@ +export interface UploadState { + isUploading: boolean + uploadError: string + debugDownload?: { href: string, fileName: string } + image?: HTMLImageElement + filesLength: number + fileUploadIndex: number + fileUploadName: string +} + +export const INITIAL_UPLOAD_STATE: UploadState = { + isUploading: false, + uploadError: '', + fileUploadName: '', + filesLength: 0, + fileUploadIndex: 0, +}; diff --git a/src/components/ImageInput.tsx b/src/components/ImageInput.tsx index 3bac3e98..6ae4304c 100644 --- a/src/components/ImageInput.tsx +++ b/src/components/ImageInput.tsx @@ -1,7 +1,7 @@ 'use client'; import { blobToImage } from '@/utility/blob'; -import { useRef, useState } from 'react'; +import { useRef } from 'react'; import { CopyExif } from '@/lib/CopyExif'; import exifr from 'exifr'; import { clsx } from 'clsx/lite'; @@ -9,6 +9,7 @@ import { ACCEPTED_PHOTO_FILE_TYPES } from '@/photo'; import { FiUploadCloud } from 'react-icons/fi'; import { MAX_IMAGE_SIZE } from '@/platforms/next-image'; import ProgressButton from './primitives/ProgressButton'; +import { useAppState } from '@/state/AppState'; const INPUT_ID = 'file'; @@ -18,7 +19,6 @@ export default function ImageInput({ shouldResize, maxSize = MAX_IMAGE_SIZE, quality = 0.8, - loading, showUploadStatus = true, debug, }: { @@ -32,17 +32,22 @@ export default function ImageInput({ shouldResize?: boolean maxSize?: number quality?: number - loading?: boolean showUploadStatus?: boolean debug?: boolean }) { const inputRef = useRef(null); const canvasRef = useRef(null); - const [image, setImage] = useState(); - const [filesLength, setFilesLength] = useState(0); - const [fileUploadIndex, setFileUploadIndex] = useState(0); - const [fileUploadName, setFileUploadName] = useState(''); + const { + uploadState: { + isUploading, + image, + filesLength, + fileUploadIndex, + fileUploadName, + }, + setUploadState, + } = useAppState(); return (
@@ -51,12 +56,12 @@ export default function ImageInput({ htmlFor={INPUT_ID} className={clsx( 'shrink-0 select-none text-main', - loading && 'pointer-events-none cursor-not-allowed', + isUploading && 'pointer-events-none cursor-not-allowed', )} > 1 ? (fileUploadIndex + 1) / filesLength * 0.95 : undefined} @@ -64,12 +69,12 @@ export default function ImageInput({ size={18} className="translate-x-[-0.5px] translate-y-[0.5px]" />} - aria-disabled={loading} + aria-disabled={isUploading} onClick={() => inputRef.current?.click()} hideTextOnMobile={false} primary > - {loading + {isUploading ? filesLength > 1 ? `Uploading ${fileUploadIndex + 1} of ${filesLength}` : 'Uploading' @@ -81,17 +86,19 @@ export default function ImageInput({ type="file" className="hidden!" accept={ACCEPTED_PHOTO_FILE_TYPES.join(',')} - disabled={loading} + disabled={isUploading} multiple onChange={async e => { onStart?.(); const { files } = e.currentTarget; if (files && files.length > 0) { - setFilesLength(files.length); + setUploadState?.({ filesLength: files.length }); for (let i = 0; i < files.length; i++) { const file = files[i]; - setFileUploadIndex(i); - setFileUploadName(file.name); + setUploadState?.({ + fileUploadIndex: i, + fileUploadName: file.name, + }); const callbackArgs = { extension: file.name.split('.').pop()?.toLowerCase(), hasMultipleUploads: files.length > 1, @@ -111,7 +118,7 @@ export default function ImageInput({ // Process images that need resizing const image = await blobToImage(file); - setImage(image); + setUploadState?.({ image }); ctx.save(); diff --git a/src/components/more/MoreMenuItem.tsx b/src/components/more/MoreMenuItem.tsx index fe54671d..26e82c37 100644 --- a/src/components/more/MoreMenuItem.tsx +++ b/src/components/more/MoreMenuItem.tsx @@ -74,17 +74,21 @@ export default function MoreMenuItem({ }); } } - if (href && href !== pathname) { - if (hrefDownloadName) { - setIsLoading(true); - downloadFileFromBrowser(href, hrefDownloadName) - .finally(() => { - setIsLoading(false); - dismissMenu?.(); - }); + if (href) { + if (href !== pathname) { + if (hrefDownloadName) { + setIsLoading(true); + downloadFileFromBrowser(href, hrefDownloadName) + .finally(() => { + setIsLoading(false); + dismissMenu?.(); + }); + } else { + setTransitionDidStart(true); + startTransition(() => router.push(href)); + } } else { - setTransitionDidStart(true); - startTransition(() => router.push(href)); + dismissMenu?.(); } } }} diff --git a/src/photo/PhotoUpload.tsx b/src/photo/PhotoUpload.tsx index 1e1b7717..6ebacf17 100644 --- a/src/photo/PhotoUpload.tsx +++ b/src/photo/PhotoUpload.tsx @@ -1,32 +1,31 @@ 'use client'; -import { useState } from 'react'; import { uploadPhotoFromClient } from '@/platforms/storage'; import { useRouter } from 'next/navigation'; import { PATH_ADMIN_UPLOADS, pathForAdminUploadUrl } from '@/app/paths'; import ImageInput from '../components/ImageInput'; import { clsx } from 'clsx/lite'; +import { useAppState } from '@/state/AppState'; export default function PhotoUpload({ shouldResize, onLastUpload, - isUploading, - setIsUploading, showUploadStatus, debug, }: { shouldResize?: boolean onLastUpload?: () => Promise - isUploading: boolean - setIsUploading: (isUploading: boolean) => void showUploadStatus?: boolean debug?: boolean }) { - const [uploadError, setUploadError] = useState(); - const [debugDownload, setDebugDownload] = useState<{ - href: string - fileName: string - }>(); + const { + uploadState: { + isUploading, + uploadError, + debugDownload, + }, + setUploadState, + } = useAppState(); const router = useRouter(); @@ -38,11 +37,12 @@ export default function PhotoUpload({
{ - setIsUploading(true); - setUploadError(''); + setUploadState?.({ + isUploading: true, + uploadError: '', + }); }} onBlobReady={async ({ blob, @@ -51,12 +51,14 @@ export default function PhotoUpload({ isLastBlob, }) => { if (debug) { - setDebugDownload({ - href: URL.createObjectURL(blob), - fileName: `debug.${extension}`, + setUploadState?.({ + isUploading: false, + uploadError: '', + debugDownload: { + href: URL.createObjectURL(blob), + fileName: `debug.${extension}`, + }, }); - setIsUploading(false); - setUploadError(''); } else { return uploadPhotoFromClient( blob, @@ -75,8 +77,10 @@ export default function PhotoUpload({ } }) .catch(error => { - setIsUploading(false); - setUploadError(`Upload Error: ${error.message}`); + setUploadState?.({ + isUploading: false, + uploadError: `Upload Error: ${error.message}`, + }); }); } }} diff --git a/src/state/AppState.ts b/src/state/AppState.ts index c0091658..07d3c550 100644 --- a/src/state/AppState.ts +++ b/src/state/AppState.ts @@ -2,6 +2,7 @@ import { Dispatch, SetStateAction, createContext, useContext } from 'react'; import { AnimationConfig } from '@/components/AnimateItems'; import { ShareModalProps } from '@/share'; import { InsightIndicatorStatus } from '@/admin/insights'; +import { INITIAL_UPLOAD_STATE, UploadState } from '@/admin/upload'; export interface AppStateContext { // CORE @@ -15,6 +16,9 @@ export interface AppStateContext { clearNextPhotoAnimation?: () => void shouldRespondToKeyboardCommands?: boolean setShouldRespondToKeyboardCommands?: Dispatch> + // UPLOADS + uploadState: UploadState + setUploadState?: (uploadState: Partial) => void // MODAL isCommandKOpen?: boolean setIsCommandKOpen?: Dispatch> @@ -57,6 +61,8 @@ export interface AppStateContext { setShouldDebugRecipeOverlays?: Dispatch> } -export const AppStateContext = createContext({}); +export const AppStateContext = createContext({ + uploadState: INITIAL_UPLOAD_STATE, +}); export const useAppState = () => useContext(AppStateContext); diff --git a/src/state/AppStateProvider.tsx b/src/state/AppStateProvider.tsx index 1124cca4..0b6a93cb 100644 --- a/src/state/AppStateProvider.tsx +++ b/src/state/AppStateProvider.tsx @@ -23,6 +23,7 @@ import { } from '@/auth/client'; import { useRouter } from 'next/navigation'; import { PATH_SIGN_IN } from '@/app/paths'; +import { INITIAL_UPLOAD_STATE, UploadState } from '@/admin/upload'; export default function AppStateProvider({ children, @@ -42,12 +43,14 @@ export default function AppStateProvider({ useState(); const [shouldRespondToKeyboardCommands, setShouldRespondToKeyboardCommands] = useState(true); + const [uploadState, _setUploadState] = + useState(INITIAL_UPLOAD_STATE); // MODAL const [isCommandKOpen, setIsCommandKOpen] = useState(false); const [shareModalProps, setShareModalProps] = useState(); - // ADMIN + // AUTH const [userEmail, setUserEmail] = useState(); const [isUserSignedInEager, setIsUserSignedInEager] = @@ -85,6 +88,10 @@ export default function AppStateProvider({ const [shouldDebugRecipeOverlays, setShouldDebugRecipeOverlays] = useState(false); + const setUploadState = useCallback((uploadState: Partial) => { + _setUploadState(prev => ({ ...prev, ...uploadState })); + }, []); + const invalidateSwr = useCallback(() => setSwrTimestamp(Date.now()), []); const { data: auth, error: authError } = useSWR('getAuth', getAuthAction); @@ -136,6 +143,7 @@ export default function AppStateProvider({ const clearAuthStateAndRedirect = useCallback((shouldRedirect = true) => { setUserEmail(undefined); + setIsUserSignedInEager(false); clearAuthEmailCookie(); if (shouldRedirect) { router.push(PATH_SIGN_IN); } }, [router]); @@ -154,6 +162,9 @@ export default function AppStateProvider({ clearNextPhotoAnimation: () => setNextPhotoAnimation?.(undefined), shouldRespondToKeyboardCommands, setShouldRespondToKeyboardCommands, + // UPLOADS + uploadState, + setUploadState, // MODAL isCommandKOpen, setIsCommandKOpen,