Hoist upload state to app state
This commit is contained in:
parent
de7ef02428
commit
85e83db991
@ -139,7 +139,7 @@ export default function AdminBatchEditPanelClient({
|
||||
</LoaderButton>
|
||||
</>}
|
||||
<LoaderButton
|
||||
icon={<IoCloseSharp size={20} className="translate-y-[0.5px]" />}
|
||||
icon={<IoCloseSharp size={19} />}
|
||||
onClick={() => setSelectedPhotoIds?.(undefined)}
|
||||
/>
|
||||
</>;
|
||||
@ -155,7 +155,7 @@ export default function AdminBatchEditPanelClient({
|
||||
<Note
|
||||
color="gray"
|
||||
className={clsx(
|
||||
'min-h-[3.5rem]',
|
||||
'min-h-[3.5rem] pr-2',
|
||||
'backdrop-blur-lg border-transparent!',
|
||||
'text-gray-900! dark:text-gray-100!',
|
||||
'bg-gray-100/90! dark:bg-gray-900/70!',
|
||||
|
||||
@ -4,22 +4,15 @@ import PhotoUpload from '@/photo/PhotoUpload';
|
||||
import { PATH_ADMIN_PHOTOS } from '@/app/paths';
|
||||
import { useAppState } from '@/state/AppState';
|
||||
import Link from 'next/link';
|
||||
import { useState } from 'react';
|
||||
import { FaArrowRight } from 'react-icons/fa';
|
||||
|
||||
export default function AdminCTA() {
|
||||
const { isUserSignedIn } = useAppState();
|
||||
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="flex justify-center pt-4">
|
||||
{isUserSignedIn
|
||||
? <PhotoUpload
|
||||
showUploadStatus={false}
|
||||
isUploading={isUploading}
|
||||
setIsUploading={setIsUploading}
|
||||
/>
|
||||
? <PhotoUpload showUploadStatus={false} />
|
||||
: <Link
|
||||
href={PATH_ADMIN_PHOTOS}
|
||||
className="button primary"
|
||||
|
||||
@ -13,10 +13,10 @@ import PathLoaderButton from '@/components/primitives/PathLoaderButton';
|
||||
import { PATH_ADMIN_OUTDATED } from '@/app/paths';
|
||||
import { Photo } from '@/photo';
|
||||
import { StorageListResponse } from '@/platforms/storage';
|
||||
import { useState } from 'react';
|
||||
import { LiaBroomSolid } from 'react-icons/lia';
|
||||
import AdminUploadsTable from './AdminUploadsTable';
|
||||
import { Timezone } from '@/utility/timezone';
|
||||
import { useAppState } from '@/state/AppState';
|
||||
|
||||
export default function AdminPhotosClient({
|
||||
photos,
|
||||
@ -37,7 +37,7 @@ export default function AdminPhotosClient({
|
||||
infiniteScrollMultiple: number
|
||||
timezone: Timezone
|
||||
}) {
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const { uploadState: { isUploading } } = useAppState();
|
||||
|
||||
return (
|
||||
<SiteGrid
|
||||
@ -47,8 +47,6 @@ export default function AdminPhotosClient({
|
||||
<div className="grow min-w-0">
|
||||
<PhotoUpload
|
||||
shouldResize={!PRESERVE_ORIGINAL_UPLOADS}
|
||||
isUploading={isUploading}
|
||||
setIsUploading={setIsUploading}
|
||||
onLastUpload={onLastPhotoUpload}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -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 (
|
||||
<SiteGrid contentMain={
|
||||
<Container
|
||||
color="gray"
|
||||
padding="tight"
|
||||
className="px-4 py-4"
|
||||
className="p-2! pl-4! text-main!"
|
||||
>
|
||||
<div className="flex w-full">
|
||||
<div className={clsx(
|
||||
@ -23,9 +25,11 @@ export default function AdminUploadPanel() {
|
||||
/>
|
||||
1 of 4: Uploading DSC-4353.jpg
|
||||
</div>
|
||||
<IoCloseSharp
|
||||
size={19}
|
||||
className="translate-y-[0.5px]"
|
||||
<LoaderButton
|
||||
icon={<IoCloseSharp
|
||||
size={18}
|
||||
className="translate-y-[0.5px]"
|
||||
/>}
|
||||
/>
|
||||
</div>
|
||||
</Container>}
|
||||
|
||||
17
src/admin/upload/index.ts
Normal file
17
src/admin/upload/index.ts
Normal file
@ -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,
|
||||
};
|
||||
@ -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<HTMLInputElement>(null);
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
|
||||
const [image, setImage] = useState<HTMLImageElement>();
|
||||
const [filesLength, setFilesLength] = useState(0);
|
||||
const [fileUploadIndex, setFileUploadIndex] = useState(0);
|
||||
const [fileUploadName, setFileUploadName] = useState('');
|
||||
const {
|
||||
uploadState: {
|
||||
isUploading,
|
||||
image,
|
||||
filesLength,
|
||||
fileUploadIndex,
|
||||
fileUploadName,
|
||||
},
|
||||
setUploadState,
|
||||
} = useAppState();
|
||||
|
||||
return (
|
||||
<div className="space-y-4 min-w-0">
|
||||
@ -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',
|
||||
)}
|
||||
>
|
||||
<ProgressButton
|
||||
type="button"
|
||||
isLoading={loading}
|
||||
isLoading={isUploading}
|
||||
progress={filesLength > 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();
|
||||
|
||||
|
||||
@ -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?.();
|
||||
}
|
||||
}
|
||||
}}
|
||||
|
||||
@ -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<void>
|
||||
isUploading: boolean
|
||||
setIsUploading: (isUploading: boolean) => void
|
||||
showUploadStatus?: boolean
|
||||
debug?: boolean
|
||||
}) {
|
||||
const [uploadError, setUploadError] = useState<string>();
|
||||
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({
|
||||
<div className="flex items-center gap-8">
|
||||
<form className="flex items-center min-w-0">
|
||||
<ImageInput
|
||||
loading={isUploading}
|
||||
shouldResize={shouldResize}
|
||||
onStart={() => {
|
||||
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}`,
|
||||
});
|
||||
});
|
||||
}
|
||||
}}
|
||||
|
||||
@ -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<SetStateAction<boolean>>
|
||||
// UPLOADS
|
||||
uploadState: UploadState
|
||||
setUploadState?: (uploadState: Partial<UploadState>) => void
|
||||
// MODAL
|
||||
isCommandKOpen?: boolean
|
||||
setIsCommandKOpen?: Dispatch<SetStateAction<boolean>>
|
||||
@ -57,6 +61,8 @@ export interface AppStateContext {
|
||||
setShouldDebugRecipeOverlays?: Dispatch<SetStateAction<boolean>>
|
||||
}
|
||||
|
||||
export const AppStateContext = createContext<AppStateContext>({});
|
||||
export const AppStateContext = createContext<AppStateContext>({
|
||||
uploadState: INITIAL_UPLOAD_STATE,
|
||||
});
|
||||
|
||||
export const useAppState = () => useContext(AppStateContext);
|
||||
|
||||
@ -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<AnimationConfig>();
|
||||
const [shouldRespondToKeyboardCommands, setShouldRespondToKeyboardCommands] =
|
||||
useState(true);
|
||||
const [uploadState, _setUploadState] =
|
||||
useState(INITIAL_UPLOAD_STATE);
|
||||
// MODAL
|
||||
const [isCommandKOpen, setIsCommandKOpen] =
|
||||
useState(false);
|
||||
const [shareModalProps, setShareModalProps] =
|
||||
useState<ShareModalProps>();
|
||||
// ADMIN
|
||||
// AUTH
|
||||
const [userEmail, setUserEmail] =
|
||||
useState<string>();
|
||||
const [isUserSignedInEager, setIsUserSignedInEager] =
|
||||
@ -85,6 +88,10 @@ export default function AppStateProvider({
|
||||
const [shouldDebugRecipeOverlays, setShouldDebugRecipeOverlays] =
|
||||
useState(false);
|
||||
|
||||
const setUploadState = useCallback((uploadState: Partial<UploadState>) => {
|
||||
_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,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user