Skip to content

Commit c232bb7

Browse files
authored
Drag drop (#341)
* Add new drag and drop file input * catch error
1 parent 0350938 commit c232bb7

File tree

6 files changed

+114
-56
lines changed

6 files changed

+114
-56
lines changed

package-lock.json

Lines changed: 37 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"path-browserify": "^1.0.1",
4545
"react": "^18.3.1",
4646
"react-dom": "^18.3.1",
47+
"react-dropzone": "^14.3.8",
4748
"react-intersection-observer": "^9.13.1",
4849
"react-katex": "^3.0.1",
4950
"react-number-format": "^5.4.2",

src/components/ProgramLoader/Loader.tsx

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { ProgramFileUpload } from "@/components/ProgramLoader/ProgramFileUpload.
1111
import { useNavigate } from "react-router";
1212
import { Links } from "./Links";
1313
import { Separator } from "../ui/separator";
14+
import { TriangleAlert } from "lucide-react";
1415

1516
export const Loader = ({ setIsDialogOpen }: { setIsDialogOpen?: (val: boolean) => void }) => {
1617
const dispatch = useAppDispatch();
@@ -26,7 +27,7 @@ export const Loader = ({ setIsDialogOpen }: { setIsDialogOpen?: (val: boolean) =
2627
}, [isLoading]);
2728

2829
const handleLoad = useCallback(
29-
async (_event: unknown, program?: ProgramUploadFileOutput) => {
30+
async (program?: ProgramUploadFileOutput) => {
3031
setIsSubmitted(true);
3132

3233
if (!programLoad && !program) return;
@@ -50,35 +51,39 @@ export const Loader = ({ setIsDialogOpen }: { setIsDialogOpen?: (val: boolean) =
5051

5152
return (
5253
<div className="flex flex-col w-full h-full bg-card">
53-
<h2 className="text-lg sm:mb-4 bg-brand-dark dark:bg-brand/65 text-white text-xs font-light px-3 py-2 sm:rounded-ss-lg sm:rounded-se-lg">
54+
<h2 className="sm:mb-4 bg-brand-dark dark:bg-brand/65 text-white text-xs font-light px-3 py-2 sm:rounded-ss-lg sm:rounded-se-lg">
5455
Start with an example program or upload your file
5556
</h2>
5657
<div className="flex flex-col p-7 justify-around h-full">
5758
<Examples
5859
onProgramLoad={(val) => {
5960
setProgramLoad(val);
6061
setIsSubmitted(false);
61-
handleLoad(undefined, val);
62+
handleLoad(val);
6263
}}
6364
/>
6465

65-
<ProgramFileUpload
66-
onFileUpload={(val) => {
67-
setProgramLoad(val);
68-
setIsSubmitted(false);
69-
}}
70-
onParseError={(error) => {
71-
setError(error);
72-
}}
73-
/>
66+
<div className="mt-2 mb-6">
67+
<ProgramFileUpload
68+
onFileUpload={(val) => {
69+
setProgramLoad(val);
70+
setIsSubmitted(false);
71+
// handleLoad(val);
72+
}}
73+
/>
74+
</div>
7475
<Links />
75-
{error && isSubmitted && <p className="text-red-500 whitespace-pre-line">{error}</p>}
76+
{error && isSubmitted && (
77+
<p className="flex items-center text-destructive-foreground mt-3 text-[11px] whitespace-pre-line">
78+
<TriangleAlert className="mr-2" height="18px" /> {error}
79+
</p>
80+
)}
7681
</div>
7782
<div className="px-5">
7883
<Separator />
7984
</div>
8085
<div className="m-6 mb-9 flex justify-end">
81-
<Button className="mt-3 min-w-[92px]" id="load-button" type="button" onClick={handleLoad}>
86+
<Button className="mt-3 min-w-[92px]" id="load-button" type="button" onClick={() => handleLoad()}>
8287
Load
8388
</Button>
8489
</div>

src/components/ProgramLoader/ProgramFileUpload.tsx

Lines changed: 55 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,25 @@
1-
import { Input } from "../ui/input";
2-
import { ProgramUploadFileOutput } from "./types";
1+
import { useDropzone } from "react-dropzone";
2+
import { ProgramUploadFileInput, ProgramUploadFileOutput } from "./types";
33
import { mapUploadFileInputToOutput } from "./utils";
44
import { decodeStandardProgram } from "@typeberry/pvm-debugger-adapter";
55
import { MemoryChunkItem, PageMapItem, RegistersArray } from "@/types/pvm.ts";
66
import { SafeParseReturnType, z } from "zod";
77
import { useAppSelector } from "@/store/hooks";
88
import { selectInitialState } from "@/store/debugger/debuggerSlice";
99
import { getAsChunks, getAsPageMap } from "@/lib/utils.ts";
10+
import { TriangleAlert, UploadCloud } from "lucide-react";
11+
import { Button } from "../ui/button";
12+
import { useState } from "react";
1013

11-
const validateJsonTestCaseSchema = (json: unknown) => {
14+
type ProgramFileUploadProps = {
15+
onFileUpload: (val: ProgramUploadFileOutput) => void;
16+
close?: () => void;
17+
};
18+
19+
type RawProgramUploadFileInput = unknown;
20+
type ValidationResult = SafeParseReturnType<RawProgramUploadFileInput, ProgramUploadFileInput>;
21+
22+
const validateJsonTestCaseSchema = (json: RawProgramUploadFileInput): ValidationResult => {
1223
const pageMapSchema = z.object({
1324
address: z.number(),
1425
length: z.number(),
@@ -38,10 +49,8 @@ const validateJsonTestCaseSchema = (json: unknown) => {
3849
return schema.safeParse(json);
3950
};
4051

41-
const generateErrorMessageFromZodValidation = (result: SafeParseReturnType<unknown, unknown>) => {
42-
if (!result.error) {
43-
return false;
44-
}
52+
const generateErrorMessageFromZodValidation = (result: ValidationResult): string => {
53+
if (!result.error) return "Unknown error occurred";
4554

4655
const formattedErrors = result.error.errors.map((err) => {
4756
const path = err.path.join(" > ") || "root";
@@ -51,18 +60,9 @@ const generateErrorMessageFromZodValidation = (result: SafeParseReturnType<unkno
5160
return `File validation failed with the following errors:\n\n${formattedErrors.join("\n")}`;
5261
};
5362

54-
export const ProgramFileUpload = ({
55-
onFileUpload,
56-
onParseError,
57-
close,
58-
}: {
59-
onFileUpload: (val: ProgramUploadFileOutput) => void;
60-
onParseError: (error: string) => void;
61-
close?: () => void;
62-
}) => {
63+
export const ProgramFileUpload: React.FC<ProgramFileUploadProps> = ({ onFileUpload, close }) => {
6364
const initialState = useAppSelector(selectInitialState);
64-
65-
let fileReader: FileReader;
65+
const [error, setError] = useState<string>();
6666

6767
const handleFileRead = (e: ProgressEvent<FileReader>) => {
6868
const arrayBuffer = e.target?.result;
@@ -78,7 +78,7 @@ export const ProgramFileUpload = ({
7878

7979
if (!result.success) {
8080
const errorMessage = generateErrorMessageFromZodValidation(result);
81-
onParseError(errorMessage || "");
81+
setError(errorMessage || "");
8282
} else {
8383
onFileUpload(mapUploadFileInputToOutput(jsonFile));
8484
}
@@ -112,30 +112,48 @@ export const ProgramFileUpload = ({
112112
});
113113
}
114114
}
115+
} else {
116+
setError("Failed to read the file");
115117
}
116118
};
117119

118-
const handleProgramUpload = (file: Blob) => {
119-
fileReader = new FileReader();
120-
fileReader.onloadend = handleFileRead;
121-
fileReader.readAsArrayBuffer(file);
120+
const onDrop = (acceptedFiles: File[]) => {
121+
if (acceptedFiles.length) {
122+
const file = acceptedFiles[0];
123+
const fileReader = new FileReader();
124+
fileReader.onloadend = handleFileRead;
125+
fileReader.readAsArrayBuffer(file);
126+
close?.();
127+
}
122128
};
123129

130+
const { getRootProps, getInputProps, open } = useDropzone({
131+
onDrop,
132+
accept: { "application/octet-stream": [".bin", ".pvm"], "application/json": [".json"] },
133+
noClick: true,
134+
});
135+
124136
return (
125-
<div className="pb-5">
126-
<Input
127-
className="mt-5 mr-3"
128-
id="test-file"
129-
type="file"
130-
accept=".bin,.pvm,.json"
131-
onClick={(e) => e.stopPropagation()}
132-
onChange={(e) => {
133-
if (e.target.files?.length) {
134-
handleProgramUpload(e.target.files[0]);
135-
close?.();
136-
}
137-
}}
138-
/>
137+
<div>
138+
<div
139+
{...getRootProps()}
140+
className="flex items-center justify-between border-dashed border-2 py-3 px-4 rounded-lg w-full mx-auto"
141+
>
142+
<div className="flex items-center space-x-6">
143+
<UploadCloud className="text-title-secondary-foreground" width="30px" height="30px" />
144+
<p className="text-[10px] text-title-secondary-foreground">Select a file or drag and drop here</p>
145+
</div>
146+
<Button className="text-[10px] py-1 h-9" variant="outlineBrand" onClick={open}>
147+
Select file
148+
</Button>
149+
<input {...getInputProps()} className="hidden" />
150+
</div>
151+
152+
{error && (
153+
<p className="flex items-center text-destructive-foreground mt-3 text-[11px] whitespace-pre-line">
154+
<TriangleAlert className="mr-2" height="18px" /> {error}
155+
</p>
156+
)}
139157
</div>
140158
);
141159
};

src/globals.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
--accent-foreground: var(--foreground);
4242
/* Defaults */
4343
--destructive: 0 84.2% 60.2%;
44-
--destructive-foreground: 210 40% 98%;
44+
--destructive-foreground: 1 61% 56%;
4545
/* */
4646
--border: 0 0% 94%;
4747
--input: var(--border);

src/pages/ProgramLoader.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ const ProgramLoader = () => {
7272
}, []);
7373

7474
return (
75-
<div className="max-w-[505px] sm:my-[100px] max-sm:h-full sm:rounded-lg sm:border-2">
75+
<div className="max-w-[505px] sm:my-[100px] sm:mr-[72px] max-sm:h-full sm:rounded-lg sm:border-2">
7676
<Loader />
7777
</div>
7878
);

0 commit comments

Comments
 (0)