From 564ed29ae37803cd92e02dbf87b261210db98d58 Mon Sep 17 00:00:00 2001 From: Luc Date: Tue, 3 Dec 2024 13:26:04 +0100 Subject: [PATCH] Introduce Partial Media Uploading --- web/src/api/media.ts | 10 +- web/src/components/media/EditMediaGallery.tsx | 86 +++++++++++++++- web/src/components/media/MediaDropzone.tsx | 20 +--- web/src/components/media/MediaGallery.tsx | 6 +- web/src/components/media/MediaPreview.tsx | 97 ++++++++++++++++--- web/src/components/media/upload/t.tsx | 19 ++-- web/src/routes/item/$itemId/edit.tsx | 25 ++++- 7 files changed, 215 insertions(+), 48 deletions(-) diff --git a/web/src/api/media.ts b/web/src/api/media.ts index 3a778ae..bd376cf 100644 --- a/web/src/api/media.ts +++ b/web/src/api/media.ts @@ -6,12 +6,16 @@ type MediaResponse = { url: string; }; -export const useMedia = (id: number) => +export const useMedia = (media_id: number | undefined) => // useHttp('/api/media/' + id); { return useQuery({ - queryKey: ['media', id], + queryKey: ['media', media_id], queryFn: () => { + if (!media_id) { + return; + } + return { 1: { id: 1, @@ -28,7 +32,7 @@ export const useMedia = (id: number) => description: 'test3', url: '/test2.stl', }, - }[id]; + }[media_id]; }, }); }; diff --git a/web/src/components/media/EditMediaGallery.tsx b/web/src/components/media/EditMediaGallery.tsx index fd50026..8276b87 100644 --- a/web/src/components/media/EditMediaGallery.tsx +++ b/web/src/components/media/EditMediaGallery.tsx @@ -1,15 +1,93 @@ -import { FC } from 'react'; +import { FC, useCallback, useEffect, useReducer } from 'react'; import { Button } from '../ui/Button'; import { MediaDropzone } from './MediaDropzone'; +import { MediaPreview } from './MediaPreview'; import { AttachedMedia } from './upload/t'; -export const EditMediaGallery: FC<{ media: AttachedMedia[] }> = ({ media }) => { +export const EditMediaGallery: FC<{ + media: AttachedMedia[]; + onChange: (_media: AttachedMedia[]) => void; +}> = ({ media, onChange }) => { + // use reducer to manage media + const [state, dispatch] = useReducer( + ( + state: AttachedMedia[], + action: + | { type: 'add'; media: AttachedMedia[] } + | { type: 'update'; blob: string; media: AttachedMedia } + ) => { + switch (action.type) { + case 'add': + return [...state, ...action.media]; + case 'update': + return state.map((m) => + m.status === 'new-media' && m.blob === action.blob + ? action.media + : m + ); + } + }, + [media], + ([media]) => media + ); + + const onDrop = useCallback( + (acceptedFiles: File[]) => { + dispatch({ + type: 'add', + media: acceptedFiles.map( + (file) => + ({ + status: 'new-media', + media_id: undefined, + name: file.name, + kind: file.type, + blob: URL.createObjectURL(file), + } as AttachedMedia) + ), + }); + }, + [dispatch] + ); + + useEffect(() => { + onChange(state); + }, [state]); + return (
-
EditMediaGallery
+
+
    + {state.map((item, index) => ( +
  • + {item.status === 'new-media' ? ( + { + console.log( + 'update_media_id', + media_id + ); + + dispatch({ + type: 'update', + blob: item.blob!, + media: { ...item, media_id }, + }); + }} + /> + ) : ( + `Existing media ID: ${item.media_id}` + )} +
  • + ))} +
+
- +
diff --git a/web/src/components/media/MediaDropzone.tsx b/web/src/components/media/MediaDropzone.tsx index 5c3288e..e0d78dd 100644 --- a/web/src/components/media/MediaDropzone.tsx +++ b/web/src/components/media/MediaDropzone.tsx @@ -1,16 +1,16 @@ +import { FC } from 'react'; import { useDropzone } from 'react-dropzone'; -export const MediaDropzone = () => { +export const MediaDropzone: FC<{ + onDrop: (acceptedFiles: File[]) => void; +}> = ({ onDrop }) => { const { getRootProps, getInputProps, isDragActive, acceptedFiles } = useDropzone({ onDrop: (acceptedFiles) => { - console.log(acceptedFiles); + onDrop(acceptedFiles); }, }); - // Figure out the filetypes of the accepted files - const coolData = acceptedFiles.map((file) => file.type); - return (
{ Drag and drop files here

)} - {/* {JSON.stringify(acceptedFiles)} */} - {/* {JSON.stringify(coolData)} */} - {acceptedFiles.map((file) => { - return ( -
- {file.name} - {file.name} -
- ); - })}
); }; diff --git a/web/src/components/media/MediaGallery.tsx b/web/src/components/media/MediaGallery.tsx index dbcbde1..45e9ec5 100644 --- a/web/src/components/media/MediaGallery.tsx +++ b/web/src/components/media/MediaGallery.tsx @@ -1,10 +1,10 @@ import { FC } from 'react'; -import { Button } from '../ui/Button'; -import { MediaDropzone } from './MediaDropzone'; import { MediaPreview } from './MediaPreview'; -export const MediaGallery: FC<{ media_ids: number[] }> = ({ media_ids }) => { +export const MediaGallery: FC<{ + media_ids: number[]; +}> = ({ media_ids }) => { return (
{media_ids.length > 0 ? ( diff --git a/web/src/components/media/MediaPreview.tsx b/web/src/components/media/MediaPreview.tsx index a4e905d..cdce27a 100644 --- a/web/src/components/media/MediaPreview.tsx +++ b/web/src/components/media/MediaPreview.tsx @@ -1,34 +1,100 @@ -import { FC, Suspense, useState } from 'react'; +import { useMutation } from '@tanstack/react-query'; +import clsx from 'clsx'; +import { FC, Suspense, useEffect, useState } from 'react'; +import { FiEdit, FiLoader } from 'react-icons/fi'; import { match } from 'ts-pattern'; import { useMedia } from '@/api/media'; import { ErrorBoundary } from '@/components/ErrorBoundary'; import { StlPreviewWindow } from '@/components/stl_preview/StlPreview'; -export const MediaPreview: FC<{ media_id: number; edit?: boolean }> = ({ - media_id, - edit, -}) => { +import { Button } from '../ui/Button'; + +export const MediaPreview: FC<{ + media_id?: number; + url?: string; + kind?: string; + update_media_id?: (_media_id: number) => void; +}> = ({ media_id, url, kind, update_media_id }) => { const { data: media } = useMedia(media_id); - const fileType = media?.url.split('.').pop(); + const fileType = kind ?? media?.url.split('.').pop(); + + const { + mutate: uploadMedia, + isIdle, + isPending, + isSuccess, + } = useMutation({ + mutationFn: async () => { + // artificial 5 second delay + await new Promise((resolve) => + setTimeout(resolve, 2000 + Math.random() * 3000) + ); + const media_id = 1; + + // update media_id + update_media_id?.(media_id); + }, + }); + + useEffect(() => { + if (!media_id && url && isIdle) { + uploadMedia(); + } + }, []); return ( -
+
{match(fileType) - .with('webp', () => ) - .with('stl', () => ) + .with( + 'webp', + 'image/webp', + 'png', + 'image/png', + 'svg', + 'image/svg+xml', + 'jpeg', + 'image/jpeg', + () => + ) + .with('stl', 'model/stl', () => ( + + )) .otherwise(() => (
Unknown file type {fileType}
))} + {isPending && ( +
+ Uploading... +
+ )} + {media_id && ( +
+
{media?.description}
+ +
+ )}
); }; -export const ImagePreview: FC<{ media_id: number }> = ({ media_id }) => { +export const ImagePreview: FC<{ media_id?: number; url?: string }> = ({ + media_id, + url, +}) => { const { data: media } = useMedia(media_id); const [imageNotFound, setImageNotFound] = useState(false); @@ -38,11 +104,11 @@ export const ImagePreview: FC<{ media_id: number }> = ({ media_id }) => {

Image Preview Error

Image not found

- {media?.url} + {url ?? media?.url}
) : ( {media?.description} setImageNotFound(true)} @@ -52,13 +118,16 @@ export const ImagePreview: FC<{ media_id: number }> = ({ media_id }) => { ); }; -export const StlPreview: FC<{ media_id: number }> = ({ media_id }) => { +export const StlPreview: FC<{ media_id?: number; url?: string }> = ({ + media_id, + url, +}) => { const { data: media } = useMedia(media_id); return ( Loading...
}> - + ); diff --git a/web/src/components/media/upload/t.tsx b/web/src/components/media/upload/t.tsx index c3eb57d..cf0be76 100644 --- a/web/src/components/media/upload/t.tsx +++ b/web/src/components/media/upload/t.tsx @@ -1,10 +1,15 @@ -export type AttachedMedia = { - status: 'existing-media' | 'new-media' | 'removed'; - media_id: number; - // description: string; - // url: string; // url to the media - // kind: string; // 'png' -}; +export type AttachedMedia = + | { + status: 'existing-media' | 'removed'; + media_id: number; + } + | { + status: 'new-media'; + media_id: number | undefined; // if the media is uploaded, this will be its id + name?: string; + kind?: string; + blob?: string; + }; export type EditItemForm = { name: string; diff --git a/web/src/routes/item/$itemId/edit.tsx b/web/src/routes/item/$itemId/edit.tsx index a9abbd9..1900082 100644 --- a/web/src/routes/item/$itemId/edit.tsx +++ b/web/src/routes/item/$itemId/edit.tsx @@ -76,16 +76,36 @@ export const Route = createFileRoute('/item/$itemId/edit')({ media_id, })) ?? [], }, + onSubmit: (values) => { + console.log('FORM SUBMIT', values); + }, }); return ( -
+ { + event.preventDefault(); + event.stopPropagation(); + handleSubmit(); + }} + >
{ + return value.every((m) => m.media_id) + ? undefined + : 'Not all items have finished uploading'; + }, + }} children={({ handleChange, state: { value } }) => ( - + )} />
@@ -116,6 +136,7 @@ export const Route = createFileRoute('/item/$itemId/edit')({ }) => (