Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 55 additions & 14 deletions app/components/DragAndDropForm.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -8,11 +8,14 @@ export function DragAndDropForm() {
const formRef = useRef<HTMLFormElement>(null);
const filenameInputRef = useRef<HTMLInputElement>(null);
const rawJsonInputRef = useRef<HTMLInputElement>(null);
const [errorMessage, setErrorMessage] = useState<string | null>(null);

const submit = useSubmit();

const onDrop = useCallback(
(acceptedFiles: Array<File>) => {
setErrorMessage(null);

if (!formRef.current || !filenameInputRef.current) {
return;
}
Expand All @@ -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;
}

Expand All @@ -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",
});
Expand All @@ -75,13 +104,25 @@ export function DragAndDropForm() {
isDragActive ? "text-lime-500" : ""
}`}
/>
<p className={`${isDragActive ? "text-lime-500" : ""}`}>
{isDragActive
? "Now drop to open it…"
: "Drop a JSON file here, or click to select"}
</p>
<div>
<p className={`${isDragActive ? "text-lime-500" : ""}`}>
{isDragActive
? "Now drop to open it…"
: "Drop a JSON file here, or click to select"}
</p>
<p className="text-xs text-slate-400 mt-1">
Maximum file size: 5MB
</p>
</div>
</div>

{errorMessage && (
<div className="mt-3 flex items-center rounded-md bg-red-900/20 border border-red-500/30 p-3">
<ExclamationCircleIcon className="h-5 w-5 text-red-400 mr-2 flex-shrink-0" />
<p className="text-red-300 text-sm">{errorMessage}</p>
</div>
)}

<input type="hidden" name="filename" ref={filenameInputRef} />
<input type="hidden" name="rawJson" ref={rawJsonInputRef} />
</div>
Expand Down
12 changes: 10 additions & 2 deletions app/jsonDoc.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,14 @@ export async function createFromRawJson(
options?: CreateJsonOptions
): Promise<JSONDocument> {
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: <const>"raw",
Expand All @@ -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,
Expand Down Expand Up @@ -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}`);
}
}
62 changes: 51 additions & 11 deletions app/routes/actions/createFromFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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");

Expand All @@ -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) },
});
}
};