From c90be4ad89950c8627eb58a1e9063fe70ae4485b Mon Sep 17 00:00:00 2001 From: debuggingfuture Date: Mon, 28 Apr 2025 18:17:21 +0900 Subject: [PATCH 1/5] fix: fileupload toast --- apps/storybook/src/stories/filecoin/UploadDropzone.tsx | 7 ------- apps/storybook/src/stories/filecoin/UploadForm.tsx | 3 +-- .../src/components/attestations/attestation-form.tsx | 3 +-- packages/ui-react/src/hooks/eas/use-upload-attestation.ts | 6 ++---- 4 files changed, 4 insertions(+), 15 deletions(-) diff --git a/apps/storybook/src/stories/filecoin/UploadDropzone.tsx b/apps/storybook/src/stories/filecoin/UploadDropzone.tsx index 9cb04b8c..e49086f1 100644 --- a/apps/storybook/src/stories/filecoin/UploadDropzone.tsx +++ b/apps/storybook/src/stories/filecoin/UploadDropzone.tsx @@ -23,7 +23,6 @@ type UploadResponse = { export type UploadDropzoneProps = { uploadFiles: ({ files }: { files: File[] }) => Promise; - filePrefix?: string; isAcceptMultiple?: boolean; isAcceptDirectory?: boolean; }; @@ -36,7 +35,6 @@ const UploadDropzone = (props: UploadDropzoneProps) => { {}, ); - const filePrefix = props.filePrefix || ""; const { uploadFiles, isAcceptMultiple = true, @@ -93,12 +91,7 @@ const UploadDropzone = (props: UploadDropzoneProps) => { const uploadFilesCallback = async ( files: File[], ): Promise => { - console.log("upload file", files); - - // TODO handle filePrefix - const results = await uploadFiles({ files }); - console.log("results", results); return { success: !!results?.data?.Hash, message: "File uploaded successfully", diff --git a/apps/storybook/src/stories/filecoin/UploadForm.tsx b/apps/storybook/src/stories/filecoin/UploadForm.tsx index 717abe39..e27be277 100644 --- a/apps/storybook/src/stories/filecoin/UploadForm.tsx +++ b/apps/storybook/src/stories/filecoin/UploadForm.tsx @@ -239,8 +239,7 @@ export const UploadFormWithFields = >({ const uploadProgressCallback = (data: DownloadProgress) => { setProgress(data); }; - toast.success({ - title: "You submitted the following values:", + toast.success("File submitted", { description: (
 					{JSON.stringify(data, null, 2)}
diff --git a/packages/ui-react/src/components/attestations/attestation-form.tsx b/packages/ui-react/src/components/attestations/attestation-form.tsx
index 6dae7723..987a39a6 100644
--- a/packages/ui-react/src/components/attestations/attestation-form.tsx
+++ b/packages/ui-react/src/components/attestations/attestation-form.tsx
@@ -63,8 +63,7 @@ export const AttestationForm = ({
 				? getShortHex(uid)
 				: `attested ${txnReceipt?.transactionHash}`;
 
-			toast.success({
-				title: "Attestation success",
+			toast.success("Attestation success", {
 				description,
 				action: (
 					
diff --git a/packages/ui-react/src/hooks/eas/use-upload-attestation.ts b/packages/ui-react/src/hooks/eas/use-upload-attestation.ts
index 4b2b8be1..6cdf6b98 100644
--- a/packages/ui-react/src/hooks/eas/use-upload-attestation.ts
+++ b/packages/ui-react/src/hooks/eas/use-upload-attestation.ts
@@ -65,8 +65,7 @@ export const useUploadAttestationWithLighthouse = ({
 					accountAddress,
 					signedMessage,
 				);
-				toast.success({
-					title: `Encrypted Upload Successful for file : ${name}`,
+				toast.success(`Encrypted Upload Successful for file : ${name}`, {
 					description: getLighthouseGatewayUrl(cid),
 				});
 			} else {
@@ -74,8 +73,7 @@ export const useUploadAttestationWithLighthouse = ({
 					JSON.stringify(compiledPayload),
 					lighthouseApiKey,
 				);
-				toast.success({
-					title: `Upload Successful for file : ${name}`,
+				toast.success(`Upload Successful for file : ${name}`, {
 					description: getLighthouseGatewayUrl(cid),
 				});
 			}

From 17448aa782561152ef1b25198ababab95a7994d7 Mon Sep 17 00:00:00 2001
From: debuggingfuture 
Date: Tue, 29 Apr 2025 15:50:35 +0900
Subject: [PATCH 2/5] fix: upload form paths

---
 .../filecoin/UploadDropzone.stories.tsx       |   3 +-
 .../stories/filecoin/UploadForm.stories.tsx   |   4 +-
 .../components/filecoin/UploadDropzone.tsx    | 256 ++++++++++++++++
 .../src/components/filecoin/UploadForm.tsx    | 278 ++++++++++++++++++
 .../ui-react/src/components/shadcn/alert.tsx  |  59 ++++
 5 files changed, 597 insertions(+), 3 deletions(-)
 create mode 100644 packages/ui-react/src/components/filecoin/UploadDropzone.tsx
 create mode 100644 packages/ui-react/src/components/filecoin/UploadForm.tsx
 create mode 100644 packages/ui-react/src/components/shadcn/alert.tsx

diff --git a/apps/storybook/src/stories/filecoin/UploadDropzone.stories.tsx b/apps/storybook/src/stories/filecoin/UploadDropzone.stories.tsx
index b73a755b..c4c745f2 100644
--- a/apps/storybook/src/stories/filecoin/UploadDropzone.stories.tsx
+++ b/apps/storybook/src/stories/filecoin/UploadDropzone.stories.tsx
@@ -1,9 +1,10 @@
 import type { Meta, StoryObj } from "@storybook/react";
 
 import config from "@geist/domain/config";
+import UploadDropzone from "@geist/ui-react/components/filecoin/UploadDropzone";
 import { uploadFiles } from "@geist/ui-react/lib/filecoin/lighthouse/browser";
 import { withToaster } from "../decorators/toaster";
-import UploadDropzone from "./UploadDropzone";
+
 import { uploadSuccessToast } from "./upload-toast";
 
 const meta = {
diff --git a/apps/storybook/src/stories/filecoin/UploadForm.stories.tsx b/apps/storybook/src/stories/filecoin/UploadForm.stories.tsx
index 1cf2cc35..bd1dc756 100644
--- a/apps/storybook/src/stories/filecoin/UploadForm.stories.tsx
+++ b/apps/storybook/src/stories/filecoin/UploadForm.stories.tsx
@@ -11,12 +11,12 @@ import {
 	uploadFiles as uploadFilesStoracha,
 } from "@geist/ui-react/lib/filecoin/storacha/isomorphic";
 
-import { withToaster } from "../decorators/toaster";
 import {
 	type UploadFilesParams,
 	UploadForm,
 	UploadFormType,
-} from "./UploadForm";
+} from "@geist/ui-react/components/filecoin/UploadForm";
+import { withToaster } from "../decorators/toaster";
 import { uploadSuccessToast } from "./upload-toast";
 
 import config from "@geist/domain/config";
diff --git a/packages/ui-react/src/components/filecoin/UploadDropzone.tsx b/packages/ui-react/src/components/filecoin/UploadDropzone.tsx
new file mode 100644
index 00000000..ab57b02a
--- /dev/null
+++ b/packages/ui-react/src/components/filecoin/UploadDropzone.tsx
@@ -0,0 +1,256 @@
+import { Upload, X } from "lucide-react";
+import type React from "react";
+import { useCallback, useRef, useState } from "react";
+import { Alert, AlertDescription } from "#components/shadcn/alert";
+import { Button } from "#components/shadcn/button";
+import { Card } from "#components/shadcn/card";
+import { Progress } from "#components/shadcn/progress";
+
+type FileWithPreview = File & {
+	preview: string;
+};
+
+type UploadError = {
+	message: string;
+	code: "FILE_TOO_LARGE" | "INVALID_TYPE" | "UPLOAD_FAILED";
+};
+
+type UploadResponse = {
+	success: boolean;
+	message: string;
+	url?: string;
+};
+
+export type UploadDropzoneProps = {
+	uploadFiles: ({ files }: { files: File[] }) => Promise;
+	filePrefix?: string;
+	isAcceptMultiple?: boolean;
+	isAcceptDirectory?: boolean;
+};
+
+const UploadDropzone = (props: UploadDropzoneProps) => {
+	const [files, setFiles] = useState([]);
+	const [error, setError] = useState(null);
+	const [isDragActive, setIsDragActive] = useState(false);
+	const [uploadProgress, setUploadProgress] = useState>(
+		{},
+	);
+
+	const filePrefix = props.filePrefix || "";
+	const {
+		uploadFiles,
+		isAcceptMultiple = true,
+		isAcceptDirectory = false,
+	} = props;
+
+	const [isUploading, setIsUploading] = useState(false);
+	const dropZoneRef = useRef(null);
+
+	const ACCEPTED_TYPES = [
+		"image/jpeg",
+		"image/png",
+		"image/gif",
+		"application/pdf",
+	];
+	const MAX_SIZE = 5 * 1024 * 1024; // 5MB
+
+	const handleDragEnter = (e: React.DragEvent): void => {
+		e.preventDefault();
+		e.stopPropagation();
+		setIsDragActive(true);
+	};
+
+	const handleDragLeave = (e: React.DragEvent): void => {
+		e.preventDefault();
+		e.stopPropagation();
+		if (e.target === dropZoneRef.current) {
+			setIsDragActive(false);
+		}
+	};
+
+	const handleDragOver = (e: React.DragEvent): void => {
+		e.preventDefault();
+		e.stopPropagation();
+	};
+
+	const validateFile = (file: File): UploadError | null => {
+		if (!ACCEPTED_TYPES.includes(file.type)) {
+			return {
+				message:
+					"Invalid file type. Please upload images (JPEG, PNG, GIF) or PDF files.",
+				code: "INVALID_TYPE",
+			};
+		}
+		if (file.size > MAX_SIZE) {
+			return {
+				message: "File too large. Maximum size is 5MB.",
+				code: "FILE_TOO_LARGE",
+			};
+		}
+		return null;
+	};
+
+	const uploadFilesCallback = async (
+		files: File[],
+	): Promise => {
+		console.log("upload file", files);
+
+		// TODO handle filePrefix
+
+		const results = await uploadFiles({ files });
+		console.log("results", results);
+		return {
+			success: !!results?.data?.Hash,
+			message: "File uploaded successfully",
+		};
+	};
+
+	const handleFiles = useCallback(async (fileList: FileList) => {
+		setError(null);
+		const newFiles: FileWithPreview[] = [];
+
+		for (const file of Array.from(fileList)) {
+			const validationError = validateFile(file);
+			if (validationError) {
+				setError(validationError);
+				return;
+			}
+
+			const fileWithPreview = Object.assign(file, {
+				preview: URL.createObjectURL(file),
+			});
+			newFiles.push(fileWithPreview);
+		}
+
+		setFiles((prev) => [...prev, ...newFiles]);
+
+		// Upload files
+		setIsUploading(true);
+		try {
+			await uploadFilesCallback(newFiles);
+		} catch (error) {
+			console.error("error", error);
+			setError({
+				message: "Failed to upload files. Please try again.",
+				code: "UPLOAD_FAILED",
+			});
+		} finally {
+			setIsUploading(false);
+		}
+	}, []);
+
+	const handleDrop = (e: React.DragEvent): void => {
+		e.preventDefault();
+		e.stopPropagation();
+		setIsDragActive(false);
+		handleFiles(e.dataTransfer.files);
+	};
+
+	const handleFileInput = (e: React.ChangeEvent): void => {
+		if (e.target.files) {
+			handleFiles(e.target.files);
+		}
+	};
+
+	const removeFile = (name: string): void => {
+		setFiles((files) => files.filter((file) => file.name !== name));
+		setUploadProgress((prev) => {
+			const newProgress = { ...prev };
+			delete newProgress[name];
+			return newProgress;
+		});
+	};
+
+	const inputFileProps = {
+		multiple: isAcceptMultiple,
+	};
+
+	if (isAcceptDirectory) {
+		//@ts-ignore
+		inputFileProps.webkitdirectory = "true";
+	}
+
+	return (
+		
+ {isAcceptDirectory ? "📁Accept directory" : "📄Accept files"} + +
+ + +
+ + {error && ( + + {error.message} + + )} + + {files.length > 0 && ( +
+

Files:

+ {files.map((file) => ( +
+
+
+
+

{file.name}

+

+ {(file.size / 1024).toFixed(1)} KB +

+
+
+ +
+ {uploadProgress[file.name] !== undefined && + uploadProgress[file.name] < 100 && ( + + )} +
+ ))} +
+ )} +
+
+ ); +}; + +export default UploadDropzone; diff --git a/packages/ui-react/src/components/filecoin/UploadForm.tsx b/packages/ui-react/src/components/filecoin/UploadForm.tsx new file mode 100644 index 00000000..58cfc71b --- /dev/null +++ b/packages/ui-react/src/components/filecoin/UploadForm.tsx @@ -0,0 +1,278 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import { type ZodType, z } from "zod"; + +import { FileInputField } from "@geist/ui-react/components/file/file-input-field"; +import type { DownloadProgress } from "ky"; +import React from "react"; +import { toast } from "sonner"; +import { Button } from "#components/shadcn/button"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "#components/shadcn/form"; +import { Progress } from "#components/shadcn/progress"; +import { Textarea } from "#components/shadcn/textarea"; + +const lorem = `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque semper porttitor massa, non placerat dolor rutrum vel. Morbi eu elit vitae odio hendrerit mollis. Proin at nibh auctor, laoreet ante vel, commodo leo. Sed viverra neque id lectus dictum, non accumsan tortor rhoncus. Fusce consectetur est vitae viverra pellentesque. Nunc pharetra felis libero, at rhoncus est euismod et. Morbi ac ultrices lectus, quis commodo eros. Etiam vestibulum finibus imperdiet. Nulla dictum tempor neque ac varius. +Duis sed malesuada odio. Aenean fermentum tristique nunc a dictum. Donec posuere varius pharetra. Sed vitae nisi leo. Nam eget velit id erat sagittis molestie. Fusce feugiat turpis nec neque sodales, sit amet lobortis velit tempus. Curabitur nisi quam, consectetur in velit ac, gravida convallis ante. Etiam condimentum, ligula ut pharetra vehicula, odio ligula laoreet sem, et convallis metus mauris ut tellus. Fusce libero risus, vulputate a suscipit commodo, tincidunt vel ex. Duis quis ultrices ex, in feugiat dolor. Nullam ultrices lorem augue, ac pellentesque velit finibus vel. + +Pellentesque rutrum luctus dapibus. Etiam mollis congue quam vel interdum. Sed eu bibendum nunc. Etiam non laoreet est, a tempus est. Integer id neque sit amet elit porta feugiat eu imperdiet justo. Aliquam mi elit, bibendum at ex ut, sollicitudin vulputate risus. Nunc sed massa in nibh lacinia elementum. Suspendisse sodales sollicitudin vulputate. Vestibulum ac nisi eu lectus sodales ullamcorper sit amet id tortor. Etiam lorem nulla, ornare et magna vel, iaculis suscipit risus. Nullam facilisis eros nec turpis varius, non tristique eros sollicitudin. Morbi congue dui eu quam pellentesque, vel bibendum tortor ornare. Ut sed sapien quis ipsum congue tempus. Aliquam erat volutpat. Suspendisse congue congue urna. Integer gravida massa vitae ex volutpat, in sollicitudin diam ultricies. + +Nulla at ornare purus, at laoreet nibh. Fusce molestie ex sit amet tristique tempor. Donec pulvinar erat vitae tellus auctor faucibus. Proin eleifend nunc sit amet dui commodo imperdiet. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Nulla fringilla vehicula quam, condimentum blandit lorem sollicitudin sed. Phasellus et ultrices nibh. In consectetur diam justo, non ultricies nisl mollis dictum. Aenean sit amet nibh hendrerit nulla rhoncus rutrum viverra quis ipsum. Maecenas neque augue, pulvinar mollis facilisis ac, hendrerit vitae nunc. + +Sed in faucibus ipsum. In in arcu ornare, maximus eros ac, volutpat turpis. Maecenas dolor sem, eleifend sed ornare quis, placerat eu risus. Phasellus nisl justo, imperdiet sed finibus at, iaculis vel lectus. Donec quis risus ac augue porta gravida. Ut vestibulum posuere nisi in consectetur. Sed sed libero sit amet est commodo interdum nec congue arcu. Aliquam gravida leo libero, vel euismod leo viverra quis. Donec maximus, ligula a bibendum molestie, eros risus lacinia felis, a sagittis nisi lectus a mauris. Phasellus ac libero eget mauris sodales tristique. Pellentesque tristique, tellus id rhoncus blandit, elit metus sagittis eros, quis condimentum neque dolor ac lorem. Nullam sed eros lorem. Suspendisse dapibus nisi sit amet mauris congue, sit amet pulvinar orci venenatis. +`; + +export type UploadFilesParams = T & { + uploadProgressCallback?: (data: DownloadProgress) => void; +}; + +export type UploadFormParams = { + isShowProgress?: boolean; + uploadFiles: (params: UploadFilesParams) => Promise; +}; + +export enum UploadFormType { + Text = "text", + File = "file", + FileMultiple = "file-multiple", + FileDirectory = "file-directory", + MultifieldsAsDirectory = "multifields-as-directory", +} + +export const UPLOAD_FORM_BY_TYPE = {} as Record< + UploadFormType, + { + schema: any; + defaultValues: any; + createFormFields: (form: any) => any; + } +>; + +const createFormFieldsWithFile = ( + fileFieldArgs: { + isMultipleFiles?: boolean; + isAcceptDirectory?: boolean; + } = {}, +) => { + return (form: any) => ( + ( + + File + + + + Upload file to Filecoin + + + )} + /> + ); +}; + +UPLOAD_FORM_BY_TYPE[UploadFormType.Text] = { + schema: z.object({ + file: z.string(), + }), + defaultValues: { + file: lorem, + }, + createFormFields: (form: any) => ( + ( + + File + +
+