Skip to content

Commit

Permalink
Introduce Partial Media Uploading
Browse files Browse the repository at this point in the history
  • Loading branch information
lucemans committed Dec 3, 2024
1 parent 64da37e commit 564ed29
Show file tree
Hide file tree
Showing 7 changed files with 215 additions and 48 deletions.
10 changes: 7 additions & 3 deletions web/src/api/media.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@ type MediaResponse = {
url: string;
};

export const useMedia = (id: number) =>
export const useMedia = (media_id: number | undefined) =>
// useHttp<MediaResponse>('/api/media/' + id);
{
return useQuery({
queryKey: ['media', id],
queryKey: ['media', media_id],
queryFn: () => {
if (!media_id) {
return;
}

return {
1: {
id: 1,
Expand All @@ -28,7 +32,7 @@ export const useMedia = (id: number) =>
description: 'test3',
url: '/test2.stl',
},
}[id];
}[media_id];
},
});
};
86 changes: 82 additions & 4 deletions web/src/components/media/EditMediaGallery.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="card flex-col md:flex-row flex items-stretch gap-2">
<div className="grow">EditMediaGallery</div>
<div className="grow">
<ul className="grid grid-cols-2 gap-2">
{state.map((item, index) => (
<li key={index}>
{item.status === 'new-media' ? (
<MediaPreview
media_id={item.media_id}
url={item.blob}
kind={item.kind}
update_media_id={(media_id) => {
console.log(
'update_media_id',
media_id
);

dispatch({
type: 'update',
blob: item.blob!,
media: { ...item, media_id },
});
}}
/>
) : (
`Existing media ID: ${item.media_id}`
)}
</li>
))}
</ul>
</div>
<div className="flex flex-col gap-2 md:w-1/3">
<MediaDropzone />
<MediaDropzone onDrop={onDrop} />
<Button>Add Existing</Button>
</div>
</div>
Expand Down
20 changes: 5 additions & 15 deletions web/src/components/media/MediaDropzone.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div
className="border border-dashed border-gray-300 rounded-lg p-4"
Expand All @@ -26,16 +26,6 @@ export const MediaDropzone = () => {
Drag and drop files here
</p>
)}
{/* {JSON.stringify(acceptedFiles)} */}
{/* {JSON.stringify(coolData)} */}
{acceptedFiles.map((file) => {
return (
<div key={file.name}>
{file.name}
<img src={URL.createObjectURL(file)} alt={file.name} />
</div>
);
})}
</div>
);
};
6 changes: 3 additions & 3 deletions web/src/components/media/MediaGallery.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="card flex items-stretch">
{media_ids.length > 0 ? (
Expand Down
97 changes: 83 additions & 14 deletions web/src/components/media/MediaPreview.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="aspect-video bg-neutral-100 max-w-md w-full border border-neutral-200 rounded-md">
<div
className={clsx(
'relative aspect-video bg-neutral-100 max-w-md w-full rounded-md',
isPending && 'border-blue-400 border-2',
isIdle && 'border-neutral-200 border',
isSuccess && 'border-green-400 border-2'
)}
>
{match(fileType)
.with('webp', () => <ImagePreview media_id={media_id} />)
.with('stl', () => <StlPreview media_id={media_id} />)
.with(
'webp',
'image/webp',
'png',
'image/png',
'svg',
'image/svg+xml',
'jpeg',
'image/jpeg',
() => <ImagePreview media_id={media_id} url={url} />
)
.with('stl', 'model/stl', () => (
<StlPreview media_id={media_id} url={url} />
))
.otherwise(() => (
<div className="p-3 border-orange-500 border-2 rounded-md bg-orange-100 h-full">
<span>Unknown file type</span>
<span>{fileType}</span>
</div>
))}
{isPending && (
<div className="flex items-center gap-2 justify-center w-full border-t-2 border-t-inherit mt-1">
Uploading... <FiLoader className="animate-spin" />
</div>
)}
{media_id && (
<div className="flex justify-between items-center p-2 border-t-inherit border-t">
<div className="pl-4">{media?.description}</div>
<Button className="">
<FiEdit />
</Button>
</div>
)}
</div>
);
};

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);

Expand All @@ -38,11 +104,11 @@ export const ImagePreview: FC<{ media_id: number }> = ({ media_id }) => {
<div className="p-3 border-red-500 border-2 rounded-md bg-red-100 h-full">
<p className="font-bold">Image Preview Error</p>
<p>Image not found</p>
<code>{media?.url}</code>
<code>{url ?? media?.url}</code>
</div>
) : (
<img
src={media?.url}
src={url ?? media?.url}
alt={media?.description}
className="w-full h-full object-contain"
onError={() => setImageNotFound(true)}
Expand All @@ -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 (
<ErrorBoundary>
<Suspense fallback={<div>Loading...</div>}>
<StlPreviewWindow stlUrl={media?.url} />
<StlPreviewWindow stlUrl={url ?? media?.url} />
</Suspense>
</ErrorBoundary>
);
Expand Down
19 changes: 12 additions & 7 deletions web/src/components/media/upload/t.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
25 changes: 23 additions & 2 deletions web/src/routes/item/$itemId/edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,16 +76,36 @@ export const Route = createFileRoute('/item/$itemId/edit')({
media_id,
})) ?? [],
},
onSubmit: (values) => {
console.log('FORM SUBMIT', values);
},
});

return (
<SCPage title={`Edit Item ${itemId}`}>
<form className="card space-y-4" onSubmit={handleSubmit}>
<form
className="card space-y-4"
onSubmit={(event) => {
event.preventDefault();
event.stopPropagation();
handleSubmit();
}}
>
<div className="flex flex-col">
<Field
name="media"
validators={{
onChange: ({ value }) => {
return value.every((m) => m.media_id)
? undefined
: 'Not all items have finished uploading';
},
}}
children={({ handleChange, state: { value } }) => (
<EditMediaGallery media={value} />
<EditMediaGallery
media={value}
onChange={handleChange}
/>
)}
/>
<div className="px-2 pt-4 space-y-2">
Expand Down Expand Up @@ -116,6 +136,7 @@ export const Route = createFileRoute('/item/$itemId/edit')({
}) => (
<BaseInput
label="Name"
name="name"
value={name}
onChange={handleChange}
onBlur={handleBlur}
Expand Down

0 comments on commit 564ed29

Please sign in to comment.