From 0284624b1dde7670fa69becbb2c8b3a8eedb076b Mon Sep 17 00:00:00 2001 From: Christopher Arraya Date: Sun, 30 Jul 2023 02:13:35 -0400 Subject: [PATCH] feat: project detail w/ task rendering --- app/api/projects/[id]/route.ts | 53 +++++++++ app/api/projects/[id]/tasks/route.ts | 58 ++++++++++ app/api/tasks/route.ts | 0 app/projects/[id]/page.tsx | 166 +++++++++++++++++++++++++++ app/projects/page.tsx | 7 +- types/models.ts | 9 ++ 6 files changed, 290 insertions(+), 3 deletions(-) create mode 100644 app/api/projects/[id]/route.ts create mode 100644 app/api/projects/[id]/tasks/route.ts create mode 100644 app/api/tasks/route.ts diff --git a/app/api/projects/[id]/route.ts b/app/api/projects/[id]/route.ts new file mode 100644 index 0000000..04c4e90 --- /dev/null +++ b/app/api/projects/[id]/route.ts @@ -0,0 +1,53 @@ +import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs"; +import { Database } from "@/types/supabase"; +import prisma from "@/lib/prisma"; +import { NextRequest, NextResponse } from "next/server"; +import { revalidatePath } from "next/cache"; +import { cookies } from "next/headers"; + +export async function GET( + req: NextRequest, + { params }: { params: { id: string } } +) { + try { + const supabase = createRouteHandlerClient({ cookies }); + + const { + data: { session }, + } = await supabase.auth.getSession(); + + if (!session) { + throw new Error("Unauthorized"); + } + + const project = await prisma.project.findUnique({ + where: { id: Number(params.id) }, + include: { Task: true }, + }); + + if (!project) { + throw new Error("Project not found"); + } + + const { Task, ...projectData } = project; + + const res = { + ...projectData, + id: String(project.id), + tasks: Task.map((task) => ({ + ...task, + id: String(task.id), + projectId: String(task.projectId), + })), + }; + + const path = req.nextUrl.searchParams.get("path") || "/"; + revalidatePath(path); + + return NextResponse.json(res, { status: 200 }); + } catch (err: any) { + return NextResponse.json({ message: err.message }, { status: 401 }); + } finally { + await prisma.$disconnect(); + } +} diff --git a/app/api/projects/[id]/tasks/route.ts b/app/api/projects/[id]/tasks/route.ts new file mode 100644 index 0000000..b53c766 --- /dev/null +++ b/app/api/projects/[id]/tasks/route.ts @@ -0,0 +1,58 @@ +import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs"; +import { Database } from "@/types/supabase"; +import prisma from "@/lib/prisma"; +import { NextRequest, NextResponse } from "next/server"; +import { revalidatePath } from "next/cache"; +import { cookies } from "next/headers"; + +export async function POST( + req: NextRequest, + { params }: { params: { id: string } } +) { + try { + const supabase = createRouteHandlerClient({ cookies }); + + const { + data: { session }, + } = await supabase.auth.getSession(); + + if (!session) { + throw new Error("Unauthorized"); + } + + const { description, status, priority, dueDate, tags } = await req.json(); + + const newTask = await prisma.task.create({ + data: { + description, + status, + priority, + dueDate, + tags, + projectId: Number(params.id), + }, + }); + + await prisma.userProfile_Task.create({ + data: { + userProfileId: session.user.id, + taskId: newTask.id, + }, + }); + + const res = { + ...newTask, + id: String(newTask.id), + projectId: String(newTask.projectId), + }; + + const path = req.nextUrl.searchParams.get("path") || "/"; + revalidatePath(path); + + return NextResponse.json(res, { status: 201 }); + } catch (err: any) { + return NextResponse.json({ message: err.message }, { status: 500 }); + } finally { + await prisma.$disconnect(); + } +} diff --git a/app/api/tasks/route.ts b/app/api/tasks/route.ts new file mode 100644 index 0000000..e69de29 diff --git a/app/projects/[id]/page.tsx b/app/projects/[id]/page.tsx index e69de29..1a49e6e 100644 --- a/app/projects/[id]/page.tsx +++ b/app/projects/[id]/page.tsx @@ -0,0 +1,166 @@ +"use client"; +import { useState, useEffect } from "react"; +import { Project, Task } from "@/types/models"; +import { Button, buttonVariants } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import * as z from "zod"; + +const taskSchema = z.object({ + description: z.string().min(10, "Description must be at least 10 characters"), + status: z.string().optional(), + priority: z.string().optional(), + dueDate: z.date().optional(), + tags: z.array(z.string()).optional(), +}); + +export default function Project({ params }: { params: { id: string } }) { + const [project, setProject] = useState(); + const [loading, setLoading] = useState(true); + const [taskOpen, setTaskOpen] = useState(false); + const form = useForm>({ + resolver: zodResolver(taskSchema), + defaultValues: { + description: "", + status: "", + priority: "", + dueDate: undefined, + }, + }); + + useEffect(() => { + if (params.id) { + fetch(`/api/projects/${params.id}`) + .then((res) => { + if (!res.ok) throw new Error("HTTP status " + res.status); + return res.json(); + }) + .then((data) => { + setProject(data); + setLoading(false); + }) + .catch((err) => console.error("Failed to fetch project:", err)); + } + }, [params.id]); + + async function handleSubmit(values: z.infer) { + try { + const res = await fetch(`/api/projects/${params.id}/tasks`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(values), + }); + + if (!res.ok) throw new Error("HTTP status " + res.status); + const newTask = await res.json(); + setProject((prevProject) => { + if (!prevProject) return; + + const prevTasks = prevProject.tasks || []; + return { + ...prevProject, + tasks: [...prevTasks, newTask], + }; + }); + + setTaskOpen(false); + } catch (err) { + console.error("Failed to create task:", err); + } + } + + if (loading) return

Loading...

; + if (!project) return

No project found.

; + + return ( +
+

{project.title}

+

{project.description}

+
+

{project.title}

+

{project.description}

+ + + Add Task + + + + Add Task + +
+ + ( + + Task Description + + + + + + )} + /> + ( + + Task Status + + + + + + )} + /> + ( + + Task Priority + + + + + + )} + /> + + + +
+
+
+
+

Tasks

+ {project.tasks?.map((task: Task) => ( +
+

{task.description}

+
+ ))} +
+
+ ); +} diff --git a/app/projects/page.tsx b/app/projects/page.tsx index 78feda0..876c935 100644 --- a/app/projects/page.tsx +++ b/app/projects/page.tsx @@ -1,5 +1,6 @@ "use client"; import { useState, useEffect } from "react"; +import Link from "next/link"; import { Project } from "@/types/models"; import { Button, buttonVariants } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -65,9 +66,7 @@ export default function Projects() { }, []); async function handleSubmit(values: z.infer) { - console.log(values); try { - console.log(); const res = await fetch("/api/projects", { method: "POST", headers: { @@ -178,7 +177,9 @@ export default function Projects() {
{projects.map((project) => (
-

{project.title}

+

+ {project.title} +

{project.description}

))} diff --git a/types/models.ts b/types/models.ts index b8cb5a9..824014b 100644 --- a/types/models.ts +++ b/types/models.ts @@ -4,4 +4,13 @@ export type Project = { description: string; github: string; stack: string[]; + tasks: Task[]; +}; + +export type Task = { + id: string; + description: string; + priority: string; + dueDate: string; + tags: string[]; };