From 55f3f53cae9dbd85c31043e77d42dfca1400afb3 Mon Sep 17 00:00:00 2001 From: Igor Papandinas <26460174+ipapandinas@users.noreply.github.com> Date: Sat, 11 Jan 2025 13:27:33 +0100 Subject: [PATCH] feat: task form UI --- app/create/task/_components/form.tsx | 182 +++++++++++++++++++++++++++ app/create/task/page.tsx | 15 +++ components/auth/auth-menu.tsx | 12 ++ package.json | 1 + pnpm-lock.yaml | 35 ++++++ 5 files changed, 245 insertions(+) create mode 100644 app/create/task/_components/form.tsx create mode 100644 app/create/task/page.tsx diff --git a/app/create/task/_components/form.tsx b/app/create/task/_components/form.tsx new file mode 100644 index 0000000..f60fd39 --- /dev/null +++ b/app/create/task/_components/form.tsx @@ -0,0 +1,182 @@ +"use client"; + +import React from "react"; +import { Button } from "@nextui-org/button"; +import { Input, Textarea } from "@nextui-org/input"; +import { Select, SelectItem } from "@nextui-org/select"; +import { Form } from "@nextui-org/form"; +import { container } from "@/components/primitives"; +import { Project } from "@/types/project"; +import { getIconSrc } from "@/utils/icons"; +import { Avatar } from "@nextui-org/avatar"; + +export default function TaskForm({ projects }: { projects: Project[] }) { + const [submitted, setSubmitted] = React.useState<any>(null); + const [errors, setErrors] = React.useState<any>({}); + + const onSubmit = (e: any) => { + e.preventDefault(); + const data = Object.fromEntries(new FormData(e.currentTarget)); + + // Custom validation checks + const newErrors: any = {}; + + // Username validation + if (data.title === "admin") { + newErrors.title = "Nice try! Choose a different username"; + } + + if (Object.keys(newErrors).length > 0) { + setErrors(newErrors); + + return; + } + + // Clear errors and submit + setErrors({}); + setSubmitted(data); + }; + + return ( + <Form + className={`${container()} max-w-[768px]`} + validationBehavior="native" + validationErrors={errors} + onReset={() => setSubmitted(null)} + onSubmit={onSubmit} + > + <div> + <div className="border-default-100 border-b-1 pb-2 mb-2"> + <h1 + id="guidelines" + className="text-foreground text-2xl font-semibold" + > + Create a new task + </h1> + <span className="text-default-200"> + Submit a task manually for a project you maintain or{" "} + <a className="text-primary underline" href="/import/github-task"> + import directly from a GitHub issue + </a>{" "} + for faster input. + </span> + </div> + <span className="italic"> + Required fields are marked with an asterisk (*). + </span> + </div> + <div className="flex flex-col gap-4 w-full mt-4"> + <div className="flex gap-4"> + <Select + className="basis-1/4" + isRequired + items={projects} + label="Project" + labelPlacement="outside" + name="project" + placeholder="Select project" + classNames={{ + value: "capitalize", + trigger: "data-[hover=true]:bg-opacity-hover", + }} + renderValue={(items) => + items.map((item) => { + if (item.data == null) { + return undefined; + } + const { avatar, id, name, slug } = item.data; + const avatarSrc = getIconSrc(slug, avatar) ?? undefined; + return ( + <div key={id} className="flex items-center gap-2"> + <Avatar + alt={`${item.data?.name}'s avatar`} + className="bg-foreground p-0.5 w-5 h-5" + classNames={{ + img: "rounded-full", + }} + src={avatarSrc} + /> + <span>{name}</span> + </div> + ); + }) + } + > + {({ avatar, id, name, slug }) => { + const avatarSrc = getIconSrc(slug, avatar) ?? undefined; + return ( + <SelectItem + key={id} + value={slug} + startContent={ + <Avatar + alt={`${name}'s avatar`} + className="bg-foreground p-0.5 w-5 h-5" + classNames={{ + img: "rounded-full", + }} + src={avatarSrc} + /> + } + classNames={{ + title: "whitespace-break-spaces", + }} + > + {name} + </SelectItem> + ); + }} + </Select> + + <Input + isRequired + errorMessage={({ validationDetails }) => { + if (validationDetails.valueMissing) { + return "Please enter the task's title"; + } + + return errors.name; + }} + label="Title" + labelPlacement="outside" + name="title" + placeholder="Enter task title" + type="text" + classNames={{ + base: "basis-3/4", + inputWrapper: "data-[hover=true]:bg-opacity-hover", + }} + /> + </div> + + <Textarea + isRequired + errorMessage={({ validationDetails }) => { + if (validationDetails.valueMissing) { + return "Please enter the task's description"; + } + }} + classNames={{ + inputWrapper: "data-[hover=true]:bg-opacity-hover", + }} + label="Description" + labelPlacement="outside" + name="description" + placeholder="What's this task about?" + /> + + <div className="flex gap-4 justify-end"> + <Button color="primary" type="submit"> + Submit + </Button> + </div> + </div> + + {submitted && ( + <div className="text-small text-default-500 mt-4"> + Submitted data: <pre>{JSON.stringify(submitted, null, 2)}</pre> + </div> + )} + </Form> + ); +} diff --git a/app/create/task/page.tsx b/app/create/task/page.tsx new file mode 100644 index 0000000..d5d9759 --- /dev/null +++ b/app/create/task/page.tsx @@ -0,0 +1,15 @@ +import { getAllProjects } from "@/lib/api/projects"; +import TaskForm from "./_components/form"; + +export default async function CreateTask() { + const projects = await getAllProjects().catch((error) => { + console.error("Error fetching all projects:", error); + return []; + }); + + return ( + <> + <TaskForm projects={projects} /> + </> + ); +} diff --git a/components/auth/auth-menu.tsx b/components/auth/auth-menu.tsx index 81c8ac2..0164718 100644 --- a/components/auth/auth-menu.tsx +++ b/components/auth/auth-menu.tsx @@ -1,6 +1,8 @@ "use client"; import { signIn, signOut, useSession } from "next-auth/react"; +import Link from "next/link"; +import { Link as NuiLink } from "@nextui-org/link"; import { Avatar } from "@nextui-org/avatar"; import { Button } from "@nextui-org/button"; import { @@ -40,6 +42,16 @@ const AuthMenu: React.FC = () => { <p className="font-semibold">{session.user?.name}</p> </DropdownItem> } + <DropdownItem key="create-task" onPress={() => signOut()}> + <NuiLink + href="/create/task" + color="foreground" + title="Create a new task" + as={Link} + > + Create Task + </NuiLink> + </DropdownItem> <DropdownItem key="logout" color="danger" onPress={() => signOut()}> Log Out </DropdownItem> diff --git a/package.json b/package.json index 568d8b8..5ddc820 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@nextui-org/chip": "^2.2.5", "@nextui-org/divider": "^2.2.5", "@nextui-org/dropdown": "^2.3.8", + "@nextui-org/form": "^2.1.8", "@nextui-org/image": "^2.2.4", "@nextui-org/input": "^2.4.7", "@nextui-org/link": "2.2.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f587aa9..98e67b4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,6 +35,9 @@ importers: '@nextui-org/dropdown': specifier: ^2.3.8 version: 2.3.8(@nextui-org/system@2.4.5(@nextui-org/theme@2.4.4(tailwindcss@3.4.17))(framer-motion@11.15.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@nextui-org/theme@2.4.4(tailwindcss@3.4.17))(framer-motion@11.15.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@nextui-org/form': + specifier: ^2.1.8 + version: 2.1.8(@nextui-org/system@2.4.5(@nextui-org/theme@2.4.4(tailwindcss@3.4.17))(framer-motion@11.15.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@nextui-org/theme@2.4.4(tailwindcss@3.4.17))(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@nextui-org/image': specifier: ^2.2.4 version: 2.2.4(@nextui-org/system@2.4.5(@nextui-org/theme@2.4.4(tailwindcss@3.4.17))(framer-motion@11.15.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@nextui-org/theme@2.4.4(tailwindcss@3.4.17))(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -527,6 +530,14 @@ packages: react: '>=18' react-dom: '>=18' + '@nextui-org/form@2.1.8': + resolution: {integrity: sha512-Xn/dUO5zDG7zukbql1MDYh4Xwe1vnIVMRTHgckbkBtXXVNqgoTU09TTfy8WOJ0pMDX4GrZSBAZ86o37O+IHbaA==} + peerDependencies: + '@nextui-org/system': '>=2.4.0' + '@nextui-org/theme': '>=2.4.0' + react: '>=18' + react-dom: '>=18' + '@nextui-org/framer-utils@2.1.5': resolution: {integrity: sha512-z/dM29nwngCFhNCuxtCEqMbmMXG4xtXEMZh4N8FxOBEimye+2/6DIt7v0KwCY/Tx2t2URpgjCT22I8Now/SaAA==} peerDependencies: @@ -619,6 +630,11 @@ packages: peerDependencies: react: '>=18 || >=19.0.0-rc.0' + '@nextui-org/react-utils@2.1.3': + resolution: {integrity: sha512-o61fOS+S8p3KtgLLN7ub5gR0y7l517l9eZXJabUdnVcZzZjTqEijWjzjIIIyAtYAlL4d+WTXEOROuc32sCmbqw==} + peerDependencies: + react: '>=18 || >=19.0.0-rc.0' + '@nextui-org/ripple@2.2.6': resolution: {integrity: sha512-8bdE+nPZdT/U8H0fRaZBOAB3npfKGnv1KO2JWL2jSnN7wkO4kXZwcHqZT3aRFxqWvdsPXj8IajMpnMoM7LNYkA==} peerDependencies: @@ -3289,6 +3305,19 @@ snapshots: react: 19.0.0 react-dom: 19.0.0(react@19.0.0) + '@nextui-org/form@2.1.8(@nextui-org/system@2.4.5(@nextui-org/theme@2.4.4(tailwindcss@3.4.17))(framer-motion@11.15.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@nextui-org/theme@2.4.4(tailwindcss@3.4.17))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@nextui-org/react-utils': 2.1.3(react@19.0.0) + '@nextui-org/shared-utils': 2.1.2 + '@nextui-org/system': 2.4.5(@nextui-org/theme@2.4.4(tailwindcss@3.4.17))(framer-motion@11.15.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@nextui-org/theme': 2.4.4(tailwindcss@3.4.17) + '@react-aria/utils': 3.26.0(react@19.0.0) + '@react-stately/form': 3.1.0(react@19.0.0) + '@react-types/form': 3.7.8(react@19.0.0) + '@react-types/shared': 3.26.0(react@19.0.0) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + '@nextui-org/framer-utils@2.1.5(@nextui-org/theme@2.4.4(tailwindcss@3.4.17))(framer-motion@11.15.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@nextui-org/shared-utils': 2.1.2 @@ -3486,6 +3515,12 @@ snapshots: '@nextui-org/shared-utils': 2.1.2 react: 19.0.0 + '@nextui-org/react-utils@2.1.3(react@19.0.0)': + dependencies: + '@nextui-org/react-rsc-utils': 2.1.1(react@19.0.0) + '@nextui-org/shared-utils': 2.1.2 + react: 19.0.0 + '@nextui-org/ripple@2.2.6(@nextui-org/system@2.4.5(@nextui-org/theme@2.4.4(tailwindcss@3.4.17))(framer-motion@11.15.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@nextui-org/theme@2.4.4(tailwindcss@3.4.17))(framer-motion@11.15.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@nextui-org/dom-animation': 2.1.1(framer-motion@11.15.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0))