project forms
This commit is contained in:
40
package.json
40
package.json
@@ -18,13 +18,13 @@
|
|||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@clerk/nextjs": "^6.21.0",
|
"@clerk/nextjs": "^6.23.2",
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.7.2",
|
"@fortawesome/fontawesome-svg-core": "^6.7.2",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
||||||
"@fortawesome/react-fontawesome": "^0.2.2",
|
"@fortawesome/react-fontawesome": "^0.2.2",
|
||||||
"@gsap/react": "^2.1.2",
|
"@gsap/react": "^2.1.2",
|
||||||
"@hookform/resolvers": "^5.0.1",
|
"@hookform/resolvers": "^5.1.1",
|
||||||
"@neondatabase/serverless": "^1.0.0",
|
"@neondatabase/serverless": "^1.0.1",
|
||||||
"@radix-ui/react-accordion": "^1.2.11",
|
"@radix-ui/react-accordion": "^1.2.11",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.14",
|
"@radix-ui/react-alert-dialog": "^1.1.14",
|
||||||
"@radix-ui/react-aspect-ratio": "^1.1.7",
|
"@radix-ui/react-aspect-ratio": "^1.1.7",
|
||||||
@@ -52,11 +52,12 @@
|
|||||||
"@radix-ui/react-toggle-group": "^1.1.10",
|
"@radix-ui/react-toggle-group": "^1.1.10",
|
||||||
"@radix-ui/react-tooltip": "^1.2.7",
|
"@radix-ui/react-tooltip": "^1.2.7",
|
||||||
"@t3-oss/env-nextjs": "^0.12.0",
|
"@t3-oss/env-nextjs": "^0.12.0",
|
||||||
"@tanstack/react-query": "^5.80.6",
|
"@tanstack/react-query": "^5.81.5",
|
||||||
"@trpc/client": "^11.3.1",
|
"@tanstack/react-query-next-experimental": "^5.81.5",
|
||||||
"@trpc/next": "^11.3.1",
|
"@trpc/client": "^11.4.3",
|
||||||
"@trpc/react-query": "^11.3.1",
|
"@trpc/next": "^11.4.3",
|
||||||
"@trpc/server": "^11.3.1",
|
"@trpc/react-query": "^11.4.3",
|
||||||
|
"@trpc/server": "^11.4.3",
|
||||||
"@uiw/react-md-editor": "^4.0.7",
|
"@uiw/react-md-editor": "^4.0.7",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
@@ -76,28 +77,29 @@
|
|||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-day-picker": "8.10.1",
|
"react-day-picker": "8.10.1",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-hook-form": "^7.57.0",
|
"react-hook-form": "^7.59.0",
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
"react-resizable-panels": "^3.0.2",
|
"react-resizable-panels": "^3.0.3",
|
||||||
"recharts": "^2.15.3",
|
"recharts": "^2.15.4",
|
||||||
"rehype-highlight": "^7.0.2",
|
"rehype-highlight": "^7.0.2",
|
||||||
"rehype-raw": "^7.0.0",
|
"rehype-raw": "^7.0.0",
|
||||||
"server-only": "^0.0.1",
|
"server-only": "^0.0.1",
|
||||||
"sonner": "^2.0.5",
|
"sonner": "^2.0.5",
|
||||||
"tailwind-merge": "^3.3.0",
|
"tailwind-merge": "^3.3.1",
|
||||||
"tailwindcss-motion": "^1.1.0",
|
"tailwindcss-motion": "^1.1.1",
|
||||||
|
"type-fest": "^4.41.0",
|
||||||
"vaul": "^1.1.2",
|
"vaul": "^1.1.2",
|
||||||
"zod": "^3.25.53"
|
"zod": "^3.25.70"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "1.9.4",
|
"@biomejs/biome": "1.9.4",
|
||||||
"@tailwindcss/postcss": "^4.1.8",
|
"@tailwindcss/postcss": "^4.1.11",
|
||||||
"@types/node": "^20.17.58",
|
"@types/node": "^20.19.4",
|
||||||
"@types/react": "^19.1.6",
|
"@types/react": "^19.1.8",
|
||||||
"@types/react-dom": "^19.1.6",
|
"@types/react-dom": "^19.1.6",
|
||||||
"drizzle-kit": "^0.30.6",
|
"drizzle-kit": "^0.30.6",
|
||||||
"postcss": "^8.5.4",
|
"postcss": "^8.5.6",
|
||||||
"tailwindcss": "^4.1.8",
|
"tailwindcss": "^4.1.11",
|
||||||
"tw-animate-css": "^1.3.4",
|
"tw-animate-css": "^1.3.4",
|
||||||
"typescript": "^5.8.3"
|
"typescript": "^5.8.3"
|
||||||
},
|
},
|
||||||
|
|||||||
1478
pnpm-lock.yaml
generated
1478
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
|
import { ReactQueryStreamedHydration } from '@tanstack/react-query-next-experimental'
|
||||||
import { httpBatchLink } from "@trpc/client";
|
import { httpBatchLink } from "@trpc/client";
|
||||||
import React, { useState } from "react"
|
import React, { useState } from "react"
|
||||||
import { trpc } from "./Client";
|
import { trpc } from "./Client";
|
||||||
@@ -18,12 +19,26 @@ function getBaseUrl() {
|
|||||||
return `http://localhost:${process.env.PORT ?? 3000}`;
|
return `http://localhost:${process.env.PORT ?? 3000}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TrpcProvider({children}:{children: React.ReactNode}) {
|
let clientQueryClient: QueryClient | undefined = undefined;
|
||||||
const [queryClient] = useState(() => new QueryClient({ defaultOptions: {
|
const makeQueryClient = () => new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
queries: {
|
queries: {
|
||||||
experimental_prefetchInRender: true
|
experimental_prefetchInRender: true
|
||||||
}
|
}
|
||||||
}}));
|
}
|
||||||
|
});
|
||||||
|
function getQueryClient() {
|
||||||
|
if (typeof window === "undefined") {
|
||||||
|
// Server: always make a new query client
|
||||||
|
return makeQueryClient();
|
||||||
|
} else {
|
||||||
|
// Browser: make a new query client if we don't already have one
|
||||||
|
if (!clientQueryClient) clientQueryClient = makeQueryClient();
|
||||||
|
return clientQueryClient;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export default function TrpcProvider({ children }: { children: React.ReactNode }) {
|
||||||
|
const queryClient = getQueryClient();
|
||||||
const [trpcClient] = useState(() => {
|
const [trpcClient] = useState(() => {
|
||||||
return trpc.createClient({
|
return trpc.createClient({
|
||||||
links: [
|
links: [
|
||||||
@@ -36,7 +51,11 @@ export default function TrpcProvider({children}:{children: React.ReactNode}) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<trpc.Provider client={trpcClient} queryClient={queryClient}>
|
<trpc.Provider client={trpcClient} queryClient={queryClient}>
|
||||||
<QueryClientProvider client={queryClient}> {children} </QueryClientProvider>
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<ReactQueryStreamedHydration>
|
||||||
|
{children}
|
||||||
|
</ReactQueryStreamedHydration>
|
||||||
|
</QueryClientProvider>
|
||||||
</trpc.Provider>
|
</trpc.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export default async function AdminSideBar() {
|
|||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
<SidebarMenuButton asChild>
|
<SidebarMenuButton asChild>
|
||||||
<Link href={"/"}> Some Project Action </Link>
|
<Link href={"/admin/project/create"}> Create Project </Link>
|
||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
|
|||||||
27
src/app/admin/project/_components/CollapsibleForm.tsx
Normal file
27
src/app/admin/project/_components/CollapsibleForm.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { ChevronsUpDown, Plus } from "lucide-react"
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "~/components/ui/collapsible";
|
||||||
|
import { type Element } from "~/lib/utils";
|
||||||
|
import CreateUpdateProjectForm from "./CreateForm";
|
||||||
|
import type { ProjectRouterOutputs } from "~/server/routers/project";
|
||||||
|
export default function CollapsibleForm(params:{project:Element<ProjectRouterOutputs['list']>|undefined}) {
|
||||||
|
return (
|
||||||
|
<Collapsible >
|
||||||
|
<CollapsibleTrigger asChild>
|
||||||
|
<Button variant={"ghost"}>
|
||||||
|
{ params.project ?
|
||||||
|
<>{params.project.title} <ChevronsUpDown/></> : <>New <Plus/></>
|
||||||
|
}
|
||||||
|
</Button>
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
<CollapsibleContent className="autoAlpha">
|
||||||
|
{
|
||||||
|
params.project ?
|
||||||
|
<CreateUpdateProjectForm className="w-full" project={params.project}/> :
|
||||||
|
<CreateUpdateProjectForm className="w-full" project={params.project}/>
|
||||||
|
|
||||||
|
}
|
||||||
|
</CollapsibleContent>
|
||||||
|
</Collapsible>
|
||||||
|
)
|
||||||
|
}
|
||||||
153
src/app/admin/project/_components/CreateForm.tsx
Normal file
153
src/app/admin/project/_components/CreateForm.tsx
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
"use client"
|
||||||
|
import { insertSchema } from "~/lib/schema/project/project"
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod'
|
||||||
|
import { useForm } from 'react-hook-form'
|
||||||
|
import { z } from "zod";
|
||||||
|
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "~/components/ui/form";
|
||||||
|
import { Input } from "~/components/ui/input";
|
||||||
|
import { Select, SelectContent, SelectTrigger, SelectItem, SelectValue } from "~/components/ui/select";
|
||||||
|
import { trpc } from "~/app/_trpc/Client";
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
|
import * as Card from '~/components/ui/card'
|
||||||
|
import type { Entries } from 'type-fest'
|
||||||
|
import { type Element } from "~/lib/utils";
|
||||||
|
import type { ProjectRouterOutputs } from "~/server/routers/project";
|
||||||
|
import { Suspense } from "react";
|
||||||
|
export default function CreateUpdateProjectForm(params:{className:string|undefined, project:Element<ProjectRouterOutputs['list']>|undefined}) {
|
||||||
|
const [techStacks,] = trpc.project.stack.list.useSuspenseQuery()
|
||||||
|
const form = useForm<z.infer<typeof insertSchema>>({
|
||||||
|
resolver: zodResolver(insertSchema),
|
||||||
|
defaultValues: {
|
||||||
|
id: params.project ? params.project.id : crypto.randomUUID(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const createMutation = trpc.project.create.useMutation({
|
||||||
|
onSuccess: (data) => { form.setValue("id", data[0] ? data[0].id : "") }
|
||||||
|
})
|
||||||
|
const updateMutation = trpc.project.update.useMutation({
|
||||||
|
onSuccess: (data) => {
|
||||||
|
if (null !== data) {
|
||||||
|
let entries = Object.entries(data) as Entries<typeof data>
|
||||||
|
entries.forEach( (entry) => {
|
||||||
|
form.setValue(entry[0],entry[1])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
function onSubmit(values: z.infer<typeof insertSchema>) {
|
||||||
|
params.project ?
|
||||||
|
updateMutation.mutate({by: {id: values.id}, update: { ...values}}) :
|
||||||
|
createMutation.mutate(values)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Card.Card className={params.className ? params.className : "w-5/6 lg:w-1/2"}>
|
||||||
|
<Card.CardHeader>
|
||||||
|
<Card.CardTitle>
|
||||||
|
Create Entry
|
||||||
|
</Card.CardTitle>
|
||||||
|
</Card.CardHeader>
|
||||||
|
<Card.CardContent>
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
className="space-y-8"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="stackId"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
Stack
|
||||||
|
</FormLabel>
|
||||||
|
<Select onValueChange={field.onChange} defaultValue={field.value ? field.value : ""}>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder={field.value ? field.value : ""} />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
<Suspense fallback={(<> </>)}>
|
||||||
|
{techStacks.map((stack) => {
|
||||||
|
return (<SelectItem key={stack.id} value={stack.id}> { stack.stackItems ? stack.stackItems.join("-") : "Empty Stack" } </SelectItem>)
|
||||||
|
})}
|
||||||
|
</Suspense>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="title"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
Title
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="title" onChange={field.onChange} value={field.value == null ? undefined : field.value} />
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="sourceType"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
Source Type
|
||||||
|
</FormLabel>
|
||||||
|
<Select onValueChange={field.onChange} defaultValue={field.value ? field.value : "open"}>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder={field.value} />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="open"> open </SelectItem>
|
||||||
|
<SelectItem value="closed"> closed </SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="releaseStatus"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-col">
|
||||||
|
<FormLabel>Release Status</FormLabel>
|
||||||
|
<Select onValueChange={field.onChange} defaultValue={field.value ? field.value : "released"}>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder={field.value} />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="released"> released </SelectItem>
|
||||||
|
<SelectItem value="unreleased"> unreleased </SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Button type="submit"> Create </Button>
|
||||||
|
<FormMessage className={updateMutation.status == "success" || createMutation.status == "success" ? "text-green-500" : "text-red-500"}>
|
||||||
|
{ params.project ?
|
||||||
|
(
|
||||||
|
<>{updateMutation.error ? updateMutation.error.message : updateMutation.status}</>
|
||||||
|
) :
|
||||||
|
(
|
||||||
|
<>{createMutation.error ? createMutation.error.message : createMutation.status}</>
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
</FormMessage>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</Card.CardContent>
|
||||||
|
</Card.Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
7
src/app/admin/project/create/page.tsx
Normal file
7
src/app/admin/project/create/page.tsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import CreateUpdateProjectForm from "../_components/CreateForm";
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return (
|
||||||
|
<CreateUpdateProjectForm className="" project={undefined}/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -61,10 +61,15 @@ export const ProjectRouter = router({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
const { input } = opts;
|
const { input } = opts;
|
||||||
return await db.update(project)
|
const updateProj = await db.update(project)
|
||||||
.set(input.update)
|
.set(input.update)
|
||||||
.returning()
|
.returning()
|
||||||
.where(eq(project.id,input.by.id))
|
.where(eq(project.id,input.by.id));
|
||||||
|
if (updateProj[0] !== undefined) {
|
||||||
|
return updateProj[0]
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
stack: StackRouter,
|
stack: StackRouter,
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user