diff --git a/app/components/DragAndDropForm.tsx b/app/components/DragAndDropForm.tsx index 6626cfa37..5d19f1893 100644 --- a/app/components/DragAndDropForm.tsx +++ b/app/components/DragAndDropForm.tsx @@ -1,5 +1,5 @@ -import { ArrowCircleDownIcon } from "@heroicons/react/outline"; -import { useCallback, useRef } from "react"; +import { ArrowCircleDownIcon, ExclamationCircleIcon } from "@heroicons/react/outline"; +import { useCallback, useRef, useState } from "react"; import { useDropzone } from "react-dropzone"; import { Form, useSubmit } from "remix"; import invariant from "tiny-invariant"; @@ -8,11 +8,14 @@ export function DragAndDropForm() { const formRef = useRef(null); const filenameInputRef = useRef(null); const rawJsonInputRef = useRef(null); + const [errorMessage, setErrorMessage] = useState(null); const submit = useSubmit(); const onDrop = useCallback( (acceptedFiles: Array) => { + setErrorMessage(null); + if (!formRef.current || !filenameInputRef.current) { return; } @@ -25,10 +28,19 @@ export function DragAndDropForm() { const reader = new FileReader(); - reader.onabort = () => console.log("file reading was aborted"); - reader.onerror = () => console.log("file reading has failed"); + reader.onabort = () => { + console.log("file reading was aborted"); + setErrorMessage("File reading was aborted. Please try again."); + }; + + reader.onerror = () => { + console.log("file reading has failed"); + setErrorMessage("Failed to read the file. Please try again."); + }; + reader.onload = () => { if (reader.result == null) { + setErrorMessage("No file content was read. Please try again."); return; } @@ -44,20 +56,37 @@ export function DragAndDropForm() { invariant(rawJsonInputRef.current, "rawJsonInputRef is null"); invariant(jsonValue, "jsonValue is undefined"); - rawJsonInputRef.current.value = jsonValue; - - submit(formRef.current); + try { + JSON.parse(jsonValue); + rawJsonInputRef.current.value = jsonValue; + submit(formRef.current); + } catch (error) { + const errorMsg = error instanceof Error ? error.message : "Invalid JSON format"; + setErrorMessage(`JSON parsing error: ${errorMsg}`); + } }; reader.readAsArrayBuffer(firstFile); filenameInputRef.current.value = firstFile.name; }, - [formRef.current, filenameInputRef.current, rawJsonInputRef.current] + [formRef.current, filenameInputRef.current, rawJsonInputRef.current, submit] ); + const onDropRejected = useCallback((fileRejections: any[]) => { + const rejection = fileRejections[0]; + if (rejection?.errors?.[0]?.code === 'file-too-large') { + setErrorMessage(`File is too large. Maximum size is 5MB. Your file is ${(rejection.file.size / (1024 * 1024)).toFixed(2)}MB.`); + } else if (rejection?.errors?.[0]?.code === 'file-invalid-type') { + setErrorMessage('Please select a valid JSON file.'); + } else { + setErrorMessage(`File rejected: ${rejection?.errors?.[0]?.message || 'Unknown error'}`); + } + }, []); + const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDropAccepted: onDrop, + onDropRejected: onDropRejected, maxFiles: 1, - maxSize: 1024 * 1024 * 1, + maxSize: 1024 * 1024 * 5, multiple: false, accept: "application/json", }); @@ -75,13 +104,25 @@ export function DragAndDropForm() { isDragActive ? "text-lime-500" : "" }`} /> -

- {isDragActive - ? "Now drop to open it…" - : "Drop a JSON file here, or click to select"} -

+
+

+ {isDragActive + ? "Now drop to open it…" + : "Drop a JSON file here, or click to select"} +

+

+ Maximum file size: 5MB +

+
+ {errorMessage && ( +
+ +

{errorMessage}

+
+ )} + diff --git a/app/jsonDoc.server.ts b/app/jsonDoc.server.ts index a3b82f9d9..9aa85ad9c 100644 --- a/app/jsonDoc.server.ts +++ b/app/jsonDoc.server.ts @@ -86,6 +86,14 @@ export async function createFromRawJson( options?: CreateJsonOptions ): Promise { const docId = createId(); + + try { + JSON.parse(contents); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : "Invalid JSON format"; + throw new Error(`JSON parsing failed: ${errorMessage}`); + } + const doc: JSONDocument = { id: docId, type: "raw", @@ -94,7 +102,6 @@ export async function createFromRawJson( readOnly: options?.readOnly ?? false, }; - JSON.parse(contents); await DOCUMENTS.put(docId, JSON.stringify(doc), { expirationTtl: options?.ttl ?? undefined, metadata: options?.metadata ?? undefined, @@ -159,6 +166,7 @@ function isJSON(possibleJson: string): boolean { JSON.parse(possibleJson); return true; } catch (e: any) { - throw new Error(e.message); + const errorMessage = e instanceof Error ? e.message : "Invalid JSON format"; + throw new Error(`JSON validation failed: ${errorMessage}`); } } diff --git a/app/routes/actions/createFromFile.ts b/app/routes/actions/createFromFile.ts index 1318c67e3..fa101b363 100644 --- a/app/routes/actions/createFromFile.ts +++ b/app/routes/actions/createFromFile.ts @@ -2,6 +2,11 @@ import { ActionFunction, redirect } from "remix"; import invariant from "tiny-invariant"; import { sendEvent } from "~/graphJSON.server"; import { createFromRawJson } from "~/jsonDoc.server"; +import { + commitSession, + getSession, + setErrorMessage, +} from "../../services/toast.server"; type CreateFromFileError = { filename?: boolean; @@ -10,6 +15,7 @@ type CreateFromFileError = { export const action: ActionFunction = async ({ request, context }) => { const formData = await request.formData(); + const toastCookie = await getSession(request.headers.get("cookie")); const filename = formData.get("filename"); const rawJson = formData.get("rawJson"); @@ -25,18 +31,52 @@ export const action: ActionFunction = async ({ request, context }) => { invariant(typeof filename === "string", "filename must be a string"); invariant(typeof rawJson === "string", "rawJson must be a string"); - const doc = await createFromRawJson(filename, rawJson); + try { + const doc = await createFromRawJson(filename, rawJson); - const url = new URL(request.url); + const url = new URL(request.url); - context.waitUntil( - sendEvent({ - type: "create", - from: "file", - id: doc.id, - source: url.searchParams.get("utm_source") ?? url.hostname, - }) - ); + context.waitUntil( + sendEvent({ + type: "create", + from: "file", + id: doc.id, + source: url.searchParams.get("utm_source") ?? url.hostname, + }) + ); - return redirect(`/j/${doc.id}`); + return redirect(`/j/${doc.id}`); + } catch (error) { + if (error instanceof Error) { + if (error.message.includes("JSON")) { + setErrorMessage( + toastCookie, + "Invalid JSON format", + "The file contains invalid JSON. Please check the file and try again." + ); + } else if (error.message.includes("too large") || error.message.includes("size")) { + setErrorMessage( + toastCookie, + "File too large", + "The JSON file is too large to process. Please try a smaller file." + ); + } else { + setErrorMessage( + toastCookie, + "Processing error", + "Failed to process the JSON file. Please try again." + ); + } + } else { + setErrorMessage( + toastCookie, + "Unknown error", + "An unexpected error occurred while processing the file." + ); + } + + return redirect("/", { + headers: { "Set-Cookie": await commitSession(toastCookie) }, + }); + } };