From 5de0865bbd1ac90d7617f515f6cc3807a0cfdfda Mon Sep 17 00:00:00 2001 From: ngc2207 Date: Tue, 4 Feb 2025 22:52:19 +0800 Subject: [PATCH] feat(workspaces): add image upload functionality to workspace creation form --- src/config.ts | 2 + .../workspaces/api/use-create-workspace.ts | 4 +- .../components/create-workspace-form.tsx | 82 ++++++++++++++++++- src/features/workspaces/schemas.ts | 6 ++ src/features/workspaces/server/route.ts | 27 +++++- 5 files changed, 115 insertions(+), 6 deletions(-) diff --git a/src/config.ts b/src/config.ts index ec2e8a7..92bf5a9 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,2 +1,4 @@ export const DATABASE_ID = process.env.NEXT_PUBLIC_APPWRITE_DATABASE_ID!; export const WORKSPACES_ID = process.env.NEXT_PUBLIC_APPWRITE_WORKSPACES_ID!; +export const IMAGES_BUCKET_ID = + process.env.NEXT_PUBLIC_APPWRITE_IMAGES_BUCKET_ID!; diff --git a/src/features/workspaces/api/use-create-workspace.ts b/src/features/workspaces/api/use-create-workspace.ts index 06730d5..623e6dd 100644 --- a/src/features/workspaces/api/use-create-workspace.ts +++ b/src/features/workspaces/api/use-create-workspace.ts @@ -11,9 +11,9 @@ export const useCreateWorkspace = () => { const queryClient = useQueryClient(); const mutation = useMutation({ - mutationFn: async ({ json }) => { + mutationFn: async ({ form }) => { const response = await client.api.workspaces["$post"]({ - json, + form, }); if (!response.ok) { diff --git a/src/features/workspaces/components/create-workspace-form.tsx b/src/features/workspaces/components/create-workspace-form.tsx index c08fc90..2c15d52 100644 --- a/src/features/workspaces/components/create-workspace-form.tsx +++ b/src/features/workspaces/components/create-workspace-form.tsx @@ -9,6 +9,7 @@ import { FormLabel, FormMessage, } from "@/components/ui/form"; +import { useRef } from "react"; import { useForm } from "react-hook-form"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; @@ -17,6 +18,9 @@ import { Separator } from "@/components/ui/separator"; import { zodResolver } from "@hookform/resolvers/zod"; import { useCreateWorkspace } from "../api/use-create-workspace"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import Image from "next/image"; +import { Avatar, AvatarFallback } from "@/components/ui/avatar"; +import { ImageIcon } from "lucide-react"; interface CreateWorkspaceFormProps { onCancel?: () => void; @@ -25,6 +29,8 @@ interface CreateWorkspaceFormProps { export const CreateWorkspaceForm = ({ onCancel }: CreateWorkspaceFormProps) => { const { mutate, isPending } = useCreateWorkspace(); + const inputRef = useRef(null); + const form = useForm>({ resolver: zodResolver(createWorkspaceSchema), defaultValues: { @@ -33,7 +39,27 @@ export const CreateWorkspaceForm = ({ onCancel }: CreateWorkspaceFormProps) => { }); const onSubmit = (values: z.infer) => { - mutate({ json: values }); + const finalValues = { + ...values, + image: values.image instanceof File ? values.image : "", + }; + + mutate( + { form: finalValues }, + { + onSuccess: () => { + form.reset(); + // TODO: Redirect to new workspace + }, + } + ); + }; + + const handleImageChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) { + form.setValue("image", file); + } }; return ( @@ -63,6 +89,60 @@ export const CreateWorkspaceForm = ({ onCancel }: CreateWorkspaceFormProps) => { )} /> + ( +
+
+ {field.value ? ( +
+ Logo +
+ ) : ( + + + + + + )} +
+

Workspace Icon

+

+ JPG, PNG, SVG or JPEG, max 1mb +

+ + +
+
+
+ )} + />
diff --git a/src/features/workspaces/schemas.ts b/src/features/workspaces/schemas.ts index 0b706fb..ff79763 100644 --- a/src/features/workspaces/schemas.ts +++ b/src/features/workspaces/schemas.ts @@ -2,4 +2,10 @@ import { z } from "zod"; export const createWorkspaceSchema = z.object({ name: z.string().trim().min(1, "Required"), + image: z + .union([ + z.instanceof(File), + z.string().transform((value) => (value === "" ? undefined : value)), + ]) + .optional(), }); diff --git a/src/features/workspaces/server/route.ts b/src/features/workspaces/server/route.ts index 038eff2..0e0d187 100644 --- a/src/features/workspaces/server/route.ts +++ b/src/features/workspaces/server/route.ts @@ -2,18 +2,38 @@ import { Hono } from "hono"; import { ID } from "node-appwrite"; import { zValidator } from "@hono/zod-validator"; import { createWorkspaceSchema } from "../schemas"; -import { DATABASE_ID, WORKSPACES_ID } from "@/config"; import { sessionMiddleware } from "@/lib/session-middleware"; +import { DATABASE_ID, IMAGES_BUCKET_ID, WORKSPACES_ID } from "@/config"; const app = new Hono().post( "/", - zValidator("json", createWorkspaceSchema), + zValidator("form", createWorkspaceSchema), sessionMiddleware, async (c) => { const databases = c.get("databases"); + const storage = c.get("storage"); const user = c.get("user"); - const { name } = c.req.valid("json"); + const { name, image } = c.req.valid("form"); + + let uploadImageUrl: string | undefined; + + if (image instanceof File) { + const file = await storage.createFile( + IMAGES_BUCKET_ID, + ID.unique(), + image + ); + + const arrayBuffer = await storage.getFilePreview( + IMAGES_BUCKET_ID, + file.$id + ); + + uploadImageUrl = `data:image/png;base64,${Buffer.from( + arrayBuffer + ).toString("base64")}`; + } const workspace = await databases.createDocument( DATABASE_ID, @@ -22,6 +42,7 @@ const app = new Hono().post( { name, userId: user.$id, + imageUrl: uploadImageUrl, } );