From 8722107c51ad357d907025bc17d192044a846cda Mon Sep 17 00:00:00 2001 From: Oliver Bryan <04oliverbryan@gmail.com> Date: Mon, 29 Dec 2025 05:37:16 +0000 Subject: [PATCH] CreateProject component --- .../src/components/create-project.tsx | 225 ++++++++++++++++++ 1 file changed, 225 insertions(+) create mode 100644 packages/frontend/src/components/create-project.tsx diff --git a/packages/frontend/src/components/create-project.tsx b/packages/frontend/src/components/create-project.tsx new file mode 100644 index 0000000..0d3e7f7 --- /dev/null +++ b/packages/frontend/src/components/create-project.tsx @@ -0,0 +1,225 @@ +import { type FormEvent, useMemo, useState } from "react"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogClose, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { cn, getAuthHeaders } from "@/lib/utils"; + +const blobify = (value: string) => + value + .toUpperCase() + .replace(/[^A-Z0-9]/g, "") + .slice(0, 4); + +export function CreateProject({ + organisationId, + trigger, + completeAction, +}: { + organisationId?: number; + trigger?: React.ReactNode; + completeAction?: () => void | Promise; +}) { + const serverURL = import.meta.env.VITE_SERVER_URL?.trim() || "http://localhost:3000"; + + const userId = JSON.parse(localStorage.getItem("user") || "{}").id as number | undefined; + + const [open, setOpen] = useState(false); + const [name, setName] = useState(""); + const [blob, setBlob] = useState(""); + + const [nameTouched, setNameTouched] = useState(false); + const [blobTouched, setBlobTouched] = useState(false); + const [blobManuallyEdited, setBlobManuallyEdited] = useState(false); + const [submitAttempted, setSubmitAttempted] = useState(false); + + const [submitting, setSubmitting] = useState(false); + const [error, setError] = useState(null); + + const nameInvalid = useMemo( + () => ((nameTouched || submitAttempted) && name.trim() === "" ? "Cannot be empty" : ""), + [nameTouched, submitAttempted, name], + ); + + const blobInvalid = useMemo(() => { + if (!(blobTouched || submitAttempted)) return ""; + if (blob.trim() === "") return "Cannot be empty"; + if (blob.length !== 4) return "Must be 4 characters"; + return ""; + }, [blobTouched, submitAttempted, blob]); + + const reset = () => { + setName(""); + setBlob(""); + + setNameTouched(false); + setBlobTouched(false); + setBlobManuallyEdited(false); + setSubmitAttempted(false); + + setSubmitting(false); + setError(null); + }; + + const onOpenChange = (nextOpen: boolean) => { + setOpen(nextOpen); + if (!nextOpen) { + reset(); + } + }; + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + setError(null); + setSubmitAttempted(true); + + if (name.trim() === "" || blob.length !== 4) { + return; + } + + if (!userId) { + setError("you must be logged in to create a project"); + return; + } + + if (!organisationId) { + setError("select an organisation first"); + return; + } + + setSubmitting(true); + try { + const url = new URL(`${serverURL}/project/create`); + url.searchParams.set("blob", blob); + url.searchParams.set("name", name.trim()); + url.searchParams.set("creatorId", `${userId}`); + url.searchParams.set("organisationId", `${organisationId}`); + + const res = await fetch(url.toString(), { + headers: getAuthHeaders(), + }); + + if (!res.ok) { + const message = await res.text(); + setError(message || `failed to create project (${res.status})`); + setSubmitting(false); + return; + } + + setOpen(false); + reset(); + try { + await completeAction?.(); + } catch (actionErr) { + console.error(actionErr); + } + } catch (err) { + console.error(err); + setError("failed to create project"); + setSubmitting(false); + } + }; + + return ( + + + {trigger || ( + + )} + + + + + Create Project + + +
+
+
+ + { + const nextName = e.target.value; + setName(nextName); + + if (!blobManuallyEdited) { + setBlob(blobify(nextName)); + } + }} + onBlur={() => setNameTouched(true)} + aria-invalid={nameInvalid !== ""} + placeholder="Demo Project" + required + /> +
+ {nameInvalid !== "" ? ( + + ) : ( + + )} +
+
+ +
+ + { + setBlob(blobify(e.target.value)); + setBlobManuallyEdited(true); + }} + onBlur={() => setBlobTouched(true)} + aria-invalid={blobInvalid !== ""} + placeholder="DEMO" + required + /> +
+ {blobInvalid !== "" ? ( + + ) : ( + + )} +
+
+ +
+ {error ? ( + + ) : ( + + )} +
+ +
+ + + + +
+
+
+
+
+ ); +}