testing setup
This commit is contained in:
234
package.json
234
package.json
@@ -1,110 +1,128 @@
|
||||
{
|
||||
"name": "gregorlohaus.com",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "next build",
|
||||
"check": "biome check .",
|
||||
"check:unsafe": "biome check --write --unsafe .",
|
||||
"check:write": "biome check --write .",
|
||||
"db:generate": "drizzle-kit generate",
|
||||
"db:migrate": "drizzle-kit migrate",
|
||||
"db:push": "drizzle-kit push",
|
||||
"db:studio": "drizzle-kit studio",
|
||||
"dev": "next dev --turbo",
|
||||
"preview": "next build && next start",
|
||||
"start": "next start",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@clerk/nextjs": "^6.27.1",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.7.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
||||
"@fortawesome/react-fontawesome": "^0.2.3",
|
||||
"@gsap/react": "^2.1.2",
|
||||
"@hookform/resolvers": "^5.2.0",
|
||||
"@neondatabase/serverless": "^1.0.1",
|
||||
"@radix-ui/react-accordion": "^1.2.11",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.14",
|
||||
"@radix-ui/react-aspect-ratio": "^1.1.7",
|
||||
"@radix-ui/react-avatar": "^1.1.10",
|
||||
"@radix-ui/react-checkbox": "^1.3.2",
|
||||
"@radix-ui/react-collapsible": "^1.1.11",
|
||||
"@radix-ui/react-context-menu": "^2.2.15",
|
||||
"@radix-ui/react-dialog": "^1.1.14",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||
"@radix-ui/react-hover-card": "^1.1.14",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-menubar": "^1.1.15",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.13",
|
||||
"@radix-ui/react-popover": "^1.1.14",
|
||||
"@radix-ui/react-progress": "^1.1.7",
|
||||
"@radix-ui/react-radio-group": "^1.3.7",
|
||||
"@radix-ui/react-scroll-area": "^1.2.9",
|
||||
"@radix-ui/react-select": "^2.2.5",
|
||||
"@radix-ui/react-separator": "^1.1.7",
|
||||
"@radix-ui/react-slider": "^1.3.5",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-switch": "^1.2.5",
|
||||
"@radix-ui/react-tabs": "^1.1.12",
|
||||
"@radix-ui/react-toggle": "^1.1.9",
|
||||
"@radix-ui/react-toggle-group": "^1.1.10",
|
||||
"@radix-ui/react-tooltip": "^1.2.7",
|
||||
"@t3-oss/env-nextjs": "^0.12.0",
|
||||
"@tanstack/react-query": "^5.83.0",
|
||||
"@tanstack/react-query-next-experimental": "^5.83.0",
|
||||
"@trpc/client": "^11.4.3",
|
||||
"@trpc/next": "^11.4.3",
|
||||
"@trpc/react-query": "^11.4.3",
|
||||
"@trpc/server": "^11.4.3",
|
||||
"@uiw/react-md-editor": "^4.0.8",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"date-format": "^3.0.0",
|
||||
"drizzle-orm": "^0.41.0",
|
||||
"drizzle-zod": "^0.7.1",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"glazejs": "^2.0.1",
|
||||
"gsap": "^3.13.0",
|
||||
"input-otp": "^1.4.2",
|
||||
"lucide-react": "^0.503.0",
|
||||
"next": "15.4.0-canary.17",
|
||||
"next-themes": "^0.4.6",
|
||||
"postgres": "^3.4.7",
|
||||
"react": "^19.1.1",
|
||||
"react-day-picker": "8.10.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-hook-form": "^7.61.1",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-resizable-panels": "^3.0.3",
|
||||
"recharts": "^2.15.4",
|
||||
"rehype-highlight": "^7.0.2",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"server-only": "^0.0.1",
|
||||
"sonner": "^2.0.6",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwindcss-motion": "^1.1.1",
|
||||
"type-fest": "^4.41.0",
|
||||
"vaul": "^1.1.2",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "1.9.4",
|
||||
"@tailwindcss/postcss": "^4.1.11",
|
||||
"@types/node": "^20.19.9",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"drizzle-kit": "^0.30.6",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"tw-animate-css": "^1.3.6",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"ct3aMetadata": {
|
||||
"initVersion": "7.39.3"
|
||||
},
|
||||
"packageManager": "pnpm@10.7.1"
|
||||
"name": "gregorlohaus.com",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "next build",
|
||||
"check": "biome check .",
|
||||
"check:unsafe": "biome check --write --unsafe .",
|
||||
"check:write": "biome check --write .",
|
||||
"db:generate": "drizzle-kit generate",
|
||||
"db:migrate": "drizzle-kit migrate",
|
||||
"db:push": "drizzle-kit push",
|
||||
"db:studio": "drizzle-kit studio",
|
||||
"dev": "next dev --turbo",
|
||||
"preview": "next build && next start",
|
||||
"start": "next start",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "vitest --coverage --typecheck"
|
||||
},
|
||||
"dependencies": {
|
||||
"@clerk/nextjs": "^6.27.1",
|
||||
"@electric-sql/pglite": "^0.3.7",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.7.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
||||
"@fortawesome/react-fontawesome": "^0.2.3",
|
||||
"@gsap/react": "^2.1.2",
|
||||
"@hookform/resolvers": "^5.2.0",
|
||||
"@neondatabase/serverless": "^1.0.1",
|
||||
"@radix-ui/react-accordion": "^1.2.11",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.14",
|
||||
"@radix-ui/react-aspect-ratio": "^1.1.7",
|
||||
"@radix-ui/react-avatar": "^1.1.10",
|
||||
"@radix-ui/react-checkbox": "^1.3.2",
|
||||
"@radix-ui/react-collapsible": "^1.1.11",
|
||||
"@radix-ui/react-context-menu": "^2.2.15",
|
||||
"@radix-ui/react-dialog": "^1.1.14",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||
"@radix-ui/react-hover-card": "^1.1.14",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-menubar": "^1.1.15",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.13",
|
||||
"@radix-ui/react-popover": "^1.1.14",
|
||||
"@radix-ui/react-progress": "^1.1.7",
|
||||
"@radix-ui/react-radio-group": "^1.3.7",
|
||||
"@radix-ui/react-scroll-area": "^1.2.9",
|
||||
"@radix-ui/react-select": "^2.2.5",
|
||||
"@radix-ui/react-separator": "^1.1.7",
|
||||
"@radix-ui/react-slider": "^1.3.5",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-switch": "^1.2.5",
|
||||
"@radix-ui/react-tabs": "^1.1.12",
|
||||
"@radix-ui/react-toggle": "^1.1.9",
|
||||
"@radix-ui/react-toggle-group": "^1.1.10",
|
||||
"@radix-ui/react-tooltip": "^1.2.7",
|
||||
"@t3-oss/env-nextjs": "^0.12.0",
|
||||
"@tanstack/react-query": "^5.83.0",
|
||||
"@tanstack/react-query-next-experimental": "^5.83.0",
|
||||
"@trpc/client": "^11.4.3",
|
||||
"@trpc/next": "^11.4.3",
|
||||
"@trpc/react-query": "^11.4.3",
|
||||
"@trpc/server": "^11.4.3",
|
||||
"@uiw/react-md-editor": "^4.0.8",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"date-format": "^3.0.0",
|
||||
"drizzle-orm": "^0.44.5",
|
||||
"drizzle-zod": "^0.7.1",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"glazejs": "^2.0.1",
|
||||
"gsap": "^3.13.0",
|
||||
"input-otp": "^1.4.2",
|
||||
"lucide-react": "^0.503.0",
|
||||
"next": "15.4.0-canary.17",
|
||||
"next-themes": "^0.4.6",
|
||||
"postgres": "^3.4.7",
|
||||
"react": "^19.1.1",
|
||||
"react-day-picker": "9.8.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-hook-form": "^7.61.1",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-resizable-panels": "^3.0.3",
|
||||
"recharts": "^2.15.4",
|
||||
"rehype-highlight": "^7.0.2",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"server-only": "^0.0.1",
|
||||
"sonner": "^2.0.6",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwindcss-motion": "^1.1.1",
|
||||
"type-fest": "^4.41.0",
|
||||
"vaul": "^1.1.2",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "1.9.4",
|
||||
"@swc/jest": "^0.2.39",
|
||||
"@tailwindcss/postcss": "^4.1.11",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/jest-dom": "^6.6.4",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/node": "^20.19.9",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@vitejs/plugin-react": "^5.0.0",
|
||||
"@vitest/coverage-v8": "^3.2.4",
|
||||
"dotenv": "^17.2.1",
|
||||
"drizzle-kit": "^0.30.6",
|
||||
"jest": "^30.0.5",
|
||||
"jest-environment-jsdom": "^30.0.5",
|
||||
"jsdom": "^26.1.0",
|
||||
"next-router-mock": "^1.0.2",
|
||||
"pg-mem": "^3.0.5",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"ts-node": "^10.9.2",
|
||||
"tw-animate-css": "^1.3.6",
|
||||
"typescript": "^5.8.3",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"ct3aMetadata": {
|
||||
"initVersion": "7.39.3"
|
||||
},
|
||||
"packageManager": "pnpm@10.7.1"
|
||||
}
|
||||
|
||||
6370
pnpm-lock.yaml
generated
6370
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
import { isAdmin } from "../actions"
|
||||
import { isAdmin } from "~/app/actions"
|
||||
|
||||
export default async function AdminWrap({children,}: Readonly<{ children: React.ReactNode }>) {
|
||||
if (await isAdmin()) {
|
||||
|
||||
@@ -42,7 +42,6 @@ export default function CalendarFormField<T extends FieldValues>(params: { contr
|
||||
disabled={(date) =>
|
||||
date > new Date() || date < new Date("1900-01-01")
|
||||
}
|
||||
initialFocus
|
||||
captionLayout="dropdown"
|
||||
/>
|
||||
</PopoverContent>
|
||||
|
||||
29
src/app/_components/Form/Components/CollapsibleForm.tsx
Normal file
29
src/app/_components/Form/Components/CollapsibleForm.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { ChevronsUpDown, Plus } from "lucide-react";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "~/components/ui/collapsible";
|
||||
|
||||
type FormProps<T> = {
|
||||
className?:string,
|
||||
entity?:T
|
||||
}
|
||||
|
||||
export default function CollapsibleForm<T>(params: {entityName:string, entity?:T, entityLabelIndex?:keyof T,form:React.ComponentType<FormProps<T>>}) {
|
||||
return (
|
||||
<Collapsible>
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button variant={"ghost"}>
|
||||
{ params.entity ?
|
||||
<>{params.entityLabelIndex ? params.entity[params.entityLabelIndex] : params.entityName} <ChevronsUpDown/></> : <> New {params.entityName} <Plus/></>
|
||||
}
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="autoAlpha">
|
||||
{
|
||||
params.entity ?
|
||||
<params.form className="w-full" entity={params.entity}/> :
|
||||
<params.form className="w-full"/>
|
||||
}
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
)
|
||||
}
|
||||
18
src/app/_components/Form/Components/DeleteButton.tsx
Normal file
18
src/app/_components/Form/Components/DeleteButton.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { DeleteIcon } from "lucide-react";
|
||||
import type { UseTRPCMutationOptions, UseTRPCMutationResult } from "node_modules/@trpc/react-query/dist/getQueryKey.d-CruH3ncI.mjs";
|
||||
import { Button } from "~/components/ui/button";
|
||||
|
||||
export default function DeleteButton<TD, TE, TV, TC>(params: { mutation: UseTRPCMutationResult<TD, TE, TV, TC>, id?: string }) {
|
||||
if (params.id) {
|
||||
return (
|
||||
<Button variant='destructive' onClick={() => params.mutation.mutate(({ id: params.id } as TV))}>
|
||||
<DeleteIcon />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Button variant='destructive' disabled>
|
||||
<DeleteIcon />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
47
src/app/_components/Form/Components/FormScaffold.tsx
Normal file
47
src/app/_components/Form/Components/FormScaffold.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import * as Card from '~/components/ui/card'
|
||||
import { Form } from "~/components/ui/form";
|
||||
import { DependentFormMessaage, SubmitButton, DeleteButton } from '~/app/_components/Form/Components';
|
||||
import type { FieldValues, SubmitHandler, UseFormReturn } from 'react-hook-form';
|
||||
import type { ReactNode } from 'react';
|
||||
import DependentText from '../../DependentText';
|
||||
import type { UseTRPCMutationResult } from 'node_modules/@trpc/react-query/dist/getQueryKey.d-CruH3ncI.mjs';
|
||||
interface Error {
|
||||
message: string
|
||||
}
|
||||
export default function FormScaffold<T extends FieldValues, TD, TE extends Error, TV, TC, TTD, TTE extends Error, TTV, TTC, TTTD, TTTE extends Error, TTTV, TTTC>(params: {
|
||||
form: UseFormReturn<T>,
|
||||
onSubmit: SubmitHandler<T>,
|
||||
createMutation: UseTRPCMutationResult<TD, TE, TV, TC>,
|
||||
updateMutation: UseTRPCMutationResult<TTD, TTE, TTV, TTC>,
|
||||
deleteMutation: UseTRPCMutationResult<TTTD, TTTE, TTTV, TTTC>,
|
||||
title: string,
|
||||
children: ReactNode,
|
||||
id?: string,
|
||||
className?: string
|
||||
}) {
|
||||
const { form, onSubmit, createMutation, deleteMutation, updateMutation, title, id, className, children } = params
|
||||
return (
|
||||
<Card.Card className={className ? className : "w-5/6 lg:w-1/2"}>
|
||||
<Card.CardHeader>
|
||||
<Card.CardTitle>
|
||||
<DependentText bool={id ? true : false} true={`Update ${title}`} false={`Create ${title}`} />
|
||||
</Card.CardTitle>
|
||||
</Card.CardHeader>
|
||||
<Card.CardContent>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-8"
|
||||
>
|
||||
{children}
|
||||
<SubmitButton id={id} />
|
||||
<div className='flex flex-row justify-between'>
|
||||
<DependentFormMessaage falseStatus={createMutation.status} trueStatus={updateMutation.status} falseError={createMutation.error} trueError={updateMutation.error} bool={id ? true : false} />
|
||||
<DeleteButton mutation={deleteMutation} id={id} />
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</Card.CardContent>
|
||||
</Card.Card>
|
||||
)
|
||||
}
|
||||
10
src/app/_components/Form/Components/SubmitButton.tsx
Normal file
10
src/app/_components/Form/Components/SubmitButton.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Button } from "~/components/ui/button";
|
||||
import DependentText from "../../DependentText";
|
||||
|
||||
export default function SubmitButton(params: {id?:string}) {
|
||||
return (
|
||||
<Button type="submit">
|
||||
<DependentText bool={params.id?true:false} true='Update' false='Create' />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
5
src/app/_components/Form/Components/index.ts
Normal file
5
src/app/_components/Form/Components/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export { default as DependentFormMessaage } from './MutationFormMessage'
|
||||
export { default as SubmitButton } from './SubmitButton'
|
||||
export { default as DeleteButton } from './DeleteButton'
|
||||
export { default as FormScaffold } from './FormScaffold'
|
||||
export { default as CollapsibleForm } from './CollapsibleForm'
|
||||
23
src/app/_components/Form/Fields/BooleanFormField.tsx
Normal file
23
src/app/_components/Form/Fields/BooleanFormField.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import type { Control, FieldValues, Path } from "react-hook-form";
|
||||
import { Checkbox } from "~/components/ui/checkbox";
|
||||
import { FormControl, FormField, FormItem, FormLabel } from "~/components/ui/form";
|
||||
|
||||
export default function BooleanFormField<T extends FieldValues>(params: { control: Control<T>, name: Path<T>, label: string }) {
|
||||
return (
|
||||
<FormField
|
||||
control={params.control}
|
||||
name={params.name}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{params.label}</FormLabel>
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value ? field.value : false}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
54
src/app/_components/Form/Fields/CalenderFormField.tsx
Normal file
54
src/app/_components/Form/Fields/CalenderFormField.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import { format } from "date-fns";
|
||||
import { CalendarIcon } from "lucide-react";
|
||||
import type { Control, FieldValues, Path } from "react-hook-form";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { Calendar } from "~/components/ui/calendar";
|
||||
import { FormControl, FormField, FormItem, FormLabel } from "~/components/ui/form";
|
||||
import { Input } from "~/components/ui/input";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "~/components/ui/popover";
|
||||
import { cn } from "~/lib/utils";
|
||||
export default function CalendarFormField<T extends FieldValues>(params: { control: Control<T>, name: Path<T>, label: string }) {
|
||||
return (
|
||||
<FormField
|
||||
control={params.control}
|
||||
name={params.name}
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col">
|
||||
<FormLabel>{params.label}</FormLabel>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<FormControl>
|
||||
<Button
|
||||
variant={"outline"}
|
||||
className={cn(
|
||||
"w-[240px] pl-3 text-left font-normal",
|
||||
!field.value && "text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
{field.value ? (
|
||||
format(field.value, "PPP")
|
||||
) : (
|
||||
<span>Pick a date</span>
|
||||
)}
|
||||
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={field.value}
|
||||
onSelect={field.onChange}
|
||||
disabled={(date) =>
|
||||
date > new Date() || date < new Date("1900-01-01")
|
||||
}
|
||||
initialFocus
|
||||
captionLayout="dropdown"
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
25
src/app/_components/Form/Fields/MdeFormField.tsx
Normal file
25
src/app/_components/Form/Fields/MdeFormField.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import MDEditor from "@uiw/react-md-editor";
|
||||
import type { Control, FieldValues, Path } from "react-hook-form";
|
||||
import { FormControl, FormField, FormItem, FormLabel } from "~/components/ui/form";
|
||||
export default function MdeFormField<T extends FieldValues>(params: { control: Control<T>, name: Path<T>, label: string, dataColorMode: "dark"|"light" }) {
|
||||
return (
|
||||
<FormField
|
||||
control={params.control}
|
||||
name={params.name}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
Description
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<MDEditor
|
||||
value={field.value ? field.value : ""}
|
||||
onChange={field.onChange}
|
||||
data-color-mode={params.dataColorMode}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
92
src/app/_components/Form/Fields/MultiBooleanFormField.tsx
Normal file
92
src/app/_components/Form/Fields/MultiBooleanFormField.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import type { CheckedState } from "@radix-ui/react-checkbox";
|
||||
import { ChevronDownIcon } from "lucide-react";
|
||||
import { createContext,useContext, useState } from "react";
|
||||
import { useFormContext, type Control, type ControllerRenderProps, type FieldValues, type Path } from "react-hook-form";
|
||||
import type { Entries } from "type-fest";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { Checkbox } from "~/components/ui/checkbox";
|
||||
import { FormControl, FormField, FormItem, FormLabel } from "~/components/ui/form";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "~/components/ui/popover";
|
||||
|
||||
interface MultiBooleanFieldContextProps {
|
||||
checkedValues:Record<string,boolean>;
|
||||
setCheckedValue:(arg0:Record<string,boolean>) => void;
|
||||
}
|
||||
const MultiBooleanFieldContext = createContext<MultiBooleanFieldContextProps|undefined>(undefined)
|
||||
|
||||
function InnerMultiBooleanFormField(params: { options: string[], onChange: (arg0: string[]) => void }) {
|
||||
const context = useContext(MultiBooleanFieldContext)
|
||||
if (context === undefined) {
|
||||
return (<></>)
|
||||
}
|
||||
const onCheckedItemChange = (key: string) => {
|
||||
return function onCheckedChange(checked: CheckedState) {
|
||||
console.log(key,checked)
|
||||
let state = context.checkedValues;
|
||||
if (checked === "indeterminate") {
|
||||
console.log('checked was intermediate')
|
||||
return
|
||||
} else {
|
||||
state[key] = checked
|
||||
}
|
||||
context.setCheckedValue(state)
|
||||
let stateArr: string[] = []
|
||||
for (key in state) {
|
||||
if (state[key]) {
|
||||
stateArr.push(key)
|
||||
}
|
||||
}
|
||||
console.log('calling field on change with:', stateArr)
|
||||
params.onChange(stateArr)
|
||||
}
|
||||
}
|
||||
const checked = (key: string) => {
|
||||
if (context.checkedValues[key] === undefined) {
|
||||
return undefined
|
||||
}
|
||||
return context.checkedValues[key]
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{
|
||||
params.options.map((opt) => (
|
||||
<FormItem key={opt}>
|
||||
<div className="flex flex-row justify-between py-2 border-b-1">
|
||||
<FormLabel>{opt}</FormLabel>
|
||||
<Checkbox checked={checked(opt)} onCheckedChange={onCheckedItemChange(opt)} />
|
||||
</div>
|
||||
</FormItem>
|
||||
))
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default function MultiBooleanFormField<T extends FieldValues>(params: { control: Control<T>, name: Path<T>, label: string, options: string[], defaultValues?: string[] }) {
|
||||
const [open,setOpen] = useState<boolean>(false)
|
||||
const [checkedValues, setCheckedValues] = useState<Record<string, boolean>>(params.defaultValues == undefined ? {} : params.defaultValues.reduce((acc,current,index) => { acc[current] = true; return acc },{}))
|
||||
return (
|
||||
<FormField
|
||||
control={params.control}
|
||||
name={params.name}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant={'ghost'}>
|
||||
<FormLabel>{params.label}</FormLabel>
|
||||
<ChevronDownIcon/>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<FormControl>
|
||||
<PopoverContent>
|
||||
<MultiBooleanFieldContext.Provider value={{checkedValues: checkedValues, setCheckedValue: setCheckedValues}}>
|
||||
<InnerMultiBooleanFormField onChange={field.onChange} options={params.options} />
|
||||
</MultiBooleanFieldContext.Provider>
|
||||
</PopoverContent>
|
||||
</FormControl>
|
||||
</Popover>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import type { ReactNode } from "react";
|
||||
import type { Control, FieldValues, Path } from "react-hook-form";
|
||||
import { FormField,FormControl, FormItem, FormLabel } from "~/components/ui/form";
|
||||
import { Select, SelectContent, SelectTrigger, SelectValue } from "~/components/ui/select";
|
||||
export default function SelectFormField<T extends FieldValues>(params: { control: Control<T>, name: Path<T>, label: string, defaultValue:string|undefined, placeholder:string|undefined, children: ReactNode}) {
|
||||
export default function SelectFormField<T extends FieldValues>(params: { control: Control<T>, name: Path<T>, label: string, defaultValue?:string|null, placeholder?:string|null, children: ReactNode}) {
|
||||
return (
|
||||
<FormField
|
||||
control={params.control}
|
||||
@@ -12,10 +12,10 @@ export default function SelectFormField<T extends FieldValues>(params: { control
|
||||
<FormLabel>
|
||||
{params.label}
|
||||
</FormLabel>
|
||||
<Select onValueChange={field.onChange} value={field.value == null ? undefined : field.value} defaultValue={params.defaultValue}>
|
||||
<Select onValueChange={field.onChange} value={field.value == null ? undefined : field.value} defaultValue={params.defaultValue ? params.defaultValue : field.value == null ? undefined : field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={params.placeholder} />
|
||||
<SelectValue placeholder={params.placeholder ? undefined : params.placeholder} />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
@@ -9,7 +9,7 @@ export default function TexttInputFormField<T extends FieldValues>(params: { con
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col">
|
||||
<FormLabel>{params.label}</FormLabel>
|
||||
<Input placeholder="release link" onChange={field.onChange} value={field.value == null ? undefined : field.value} />
|
||||
<Input placeholder={params.name} onChange={field.onChange} value={field.value == null ? undefined : field.value} />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
6
src/app/_components/Form/Fields/index.ts
Normal file
6
src/app/_components/Form/Fields/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export { default as BooleanFormField } from './BooleanFormField'
|
||||
export { default as TextInputFormField } from './TextInputFormField'
|
||||
export { default as MultiBooleanFormField } from './MultiBooleanFormField'
|
||||
export { default as SelectFormField } from './SelectFormField'
|
||||
export { default as MdeFormField } from './MdeFormField'
|
||||
export { default as CalenderFormField } from './CalenderFormField'
|
||||
@@ -9,7 +9,7 @@ export default function MdeFormField<T extends FieldValues>(params: { control: C
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
Description
|
||||
{params.label}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<MDEditor
|
||||
|
||||
@@ -18,6 +18,7 @@ export default async function AdminSideBar() {
|
||||
<SimpleSidebarGroup lable="Projects">
|
||||
<Link href={"/admin/project/create"}> Create Project </Link>
|
||||
<Link href={"/admin/project/techStack/create"}> Create Stack </Link>
|
||||
<Link href={"/admin/project/list"}> Project List </Link>
|
||||
</SimpleSidebarGroup>
|
||||
<SimpleSidebarGroup lable="Blog">
|
||||
<Link href={"/"}> Some Blog Action </Link>
|
||||
|
||||
@@ -10,7 +10,7 @@ export default function Page() {
|
||||
const {data} = trpc.category.select.useQuery({id: id})
|
||||
if (data !== undefined && data.length > 0) {
|
||||
return (
|
||||
<CreateUpdateCvCategoryForm category={data[0]}/>
|
||||
<CreateUpdateCvCategoryForm entity={data[0]}/>
|
||||
)
|
||||
}
|
||||
return (<></>)
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
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 CreateUpdateCvCategoryForm from "./CreateUpdateForm";
|
||||
import type { RouterOutputs } from "~/server/routers/_app";
|
||||
export default function CollapsibleCvCategoryForm(params:{category?:Element<RouterOutputs['category']['select']>}) {
|
||||
return (
|
||||
<Collapsible >
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button variant={"ghost"}>
|
||||
{ params.category ?
|
||||
<>{params.category.name} <ChevronsUpDown/></> : <>New <Plus/></>
|
||||
}
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="autoAlpha">
|
||||
{
|
||||
params.category ?
|
||||
<CreateUpdateCvCategoryForm className="w-full" category={params.category} /> :
|
||||
<CreateUpdateCvCategoryForm className="w-full"/>
|
||||
|
||||
}
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
)
|
||||
}
|
||||
@@ -2,96 +2,53 @@
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { z } from "zod";
|
||||
import { Form, FormControl, FormField, FormItem, FormLabel } 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 { IterableElement } from 'type-fest'
|
||||
import { entitySchemas, makeOnSuccess } from "~/lib/utils";
|
||||
import type { RouterOutputs } from "~/server/routers/_app";
|
||||
import DependentFormMessaage from '~/app/_components/MutationFormMessage';
|
||||
import { CollapsibleForm, FormScaffold } from '~/app/_components/Form/Components';
|
||||
import { useState } from 'react';
|
||||
import DependentText from '~/app/_components/DependentText';
|
||||
export default function CreateUpdateCvCategoryForm(params:{className?:string,category?:IterableElement<RouterOutputs['category']['select']>,isUpdate?:boolean}) {
|
||||
import { SelectFormField, TextInputFormField } from '~/app/_components/Form/Fields';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import { SelectItem } from '~/components/ui/select';
|
||||
export default function CreateUpdateCvCategoryForm(params: { className?: string, entity?: IterableElement<RouterOutputs['category']['select']> }) {
|
||||
const schemas = entitySchemas('cvCategory')
|
||||
const [isUpdate,setIsUpdate] = useState<boolean>(params.isUpdate ? params.isUpdate : (params.category ? true : false))
|
||||
const [id, setId] = useState<string | undefined>(params.entity ? params.entity.id : undefined)
|
||||
const form = useForm<z.infer<typeof schemas.insert>>({
|
||||
resolver: zodResolver(schemas.insert),
|
||||
defaultValues: {
|
||||
id: params.category ? params.category.id : crypto.randomUUID(),
|
||||
name: params.category ? params.category.name : "",
|
||||
layoutPosition: params.category ? params.category.layoutPosition : "col1"
|
||||
id: params.entity ? params.entity.id : crypto.randomUUID(),
|
||||
name: params.entity ? params.entity.name : "",
|
||||
layoutPosition: params.entity ? params.entity.layoutPosition : "col1"
|
||||
}
|
||||
})
|
||||
const createMutation = trpc.category.insert.useMutation({onSuccess: makeOnSuccess('create',form,setIsUpdate)})
|
||||
const updateMutation = trpc.category.update.useMutation({onSuccess: makeOnSuccess('update',form)})
|
||||
let path = usePathname()
|
||||
let router = useRouter()
|
||||
const createMutation = trpc.category.insert.useMutation({ onSuccess: makeOnSuccess('create', form, setId) })
|
||||
const updateMutation = trpc.category.update.useMutation({ onSuccess: makeOnSuccess('update', form) })
|
||||
const deleteMutation = trpc.category.delete.useMutation({ onSuccess: makeOnSuccess('delete', form, undefined, path, router) })
|
||||
function onSubmit(values: z.infer<typeof schemas.insert>) {
|
||||
isUpdate ?
|
||||
id ?
|
||||
updateMutation.mutate(values) :
|
||||
createMutation.mutate(values)
|
||||
}
|
||||
//TODO use SelectFormField and TextInputFormField
|
||||
return (
|
||||
<Card.Card className={params.className ? params.className : "w-5/6 lg:w-1/2"}>
|
||||
<Card.CardHeader>
|
||||
<Card.CardTitle>
|
||||
<DependentText bool={isUpdate} true='Update Category' false='Create Category'/>
|
||||
</Card.CardTitle>
|
||||
</Card.CardHeader>
|
||||
<Card.CardContent>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-8"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
Name
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="name" onChange={field.onChange} value={field.value ? field.value : ""} />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
>
|
||||
</FormField>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="layoutPosition"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
Layout Position
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value == null ? undefined : field.value}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={form.getValues().layoutPosition} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{schemas.insert.shape.layoutPosition.unwrap().unwrap().options.map((o) => (
|
||||
<SelectItem key={o} value={o}> {o} </SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
>
|
||||
</FormField>
|
||||
<Button type="submit">
|
||||
<DependentText bool={isUpdate} true='Update' false='Create'/>
|
||||
</Button>
|
||||
<DependentFormMessaage falseStatus={createMutation.status} trueStatus={updateMutation.status} falseError={createMutation.error} trueError={updateMutation.error} bool={isUpdate}/>
|
||||
</form>
|
||||
</Form>
|
||||
</Card.CardContent>
|
||||
</Card.Card>
|
||||
<FormScaffold
|
||||
form={form}
|
||||
createMutation={createMutation}
|
||||
updateMutation={updateMutation}
|
||||
deleteMutation={deleteMutation}
|
||||
onSubmit={onSubmit}
|
||||
title='Category'
|
||||
id={id}
|
||||
className={params.className}
|
||||
>
|
||||
<TextInputFormField control={form.control} name='name' label='Name' />
|
||||
<SelectFormField control={form.control} name='layoutPosition' label='Layout Position' placeholder={form.getValues().layoutPosition == null ? undefined : form.getValues().layoutPosition}>
|
||||
{schemas.insert.shape.layoutPosition.unwrap().unwrap().options.map((o) => (
|
||||
<SelectItem key={o} value={o}> {o} </SelectItem>
|
||||
))}
|
||||
</SelectFormField>
|
||||
</FormScaffold>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,12 +5,12 @@ import { useGSAP } from '@gsap/react'
|
||||
import { Suspense, useRef } from "react";
|
||||
import * as Card from '~/components/ui/card'
|
||||
import { useGsapContext } from "~/app/_providers/GsapProvicer";
|
||||
import CollapsibleCvEntryForm from "../../entry/_components/CollapsibleForm";
|
||||
import CollapsibleCvCategoryForm from "../_components/CollapsibleForm";
|
||||
import { CollapsibleForm } from "~/app/_components/Form/Components";
|
||||
import CreateUpdateCvEntryForm from "../../entry/_components/CreateUpdateForm";
|
||||
import CreateUpdateCvCategoryForm from "../_components/CreateUpdateForm";
|
||||
export default function CvPage() {
|
||||
const categories = trpc.category.select.useQuery({},{refetchInterval:1000});
|
||||
const entires = trpc.entry.select.useSuspenseQuery({},{refetchInterval:1000})
|
||||
const categories = trpc.category.select.useQuery({}, { refetchInterval: 1000 });
|
||||
const entires = trpc.entry.select.useSuspenseQuery({}, { refetchInterval: 1000 })
|
||||
const gsap = useGsapContext()
|
||||
const container = useRef<HTMLDivElement>(null);
|
||||
useGSAP(() => {
|
||||
@@ -34,31 +34,34 @@ export default function CvPage() {
|
||||
</Link>
|
||||
<Card.CardContent className="flex flex-row">
|
||||
<div className="flex flex-col w-full">
|
||||
<CreateUpdateCvCategoryForm category={cat} className="w-full" />
|
||||
<br />
|
||||
<span>Entries:</span>
|
||||
<Suspense fallback={(<></>)}>
|
||||
<div className="w-full">
|
||||
{
|
||||
entires[0].filter((e) => {return e.categoryId == cat.id}).length > 0 ? (
|
||||
<>
|
||||
{entires[0].filter((e) => {return e.categoryId == cat.id}).map((entry) => (
|
||||
<CollapsibleCvEntryForm key={entry.id} entry={entry}/>
|
||||
))}
|
||||
</>
|
||||
) : (<></>)
|
||||
}
|
||||
</div>
|
||||
</Suspense>
|
||||
<div className="flex flex-col w-full">
|
||||
<CollapsibleCvEntryForm categoryId={cat.id} />
|
||||
</div>
|
||||
<CreateUpdateCvCategoryForm entity={cat} className="w-full" />
|
||||
<Card.Card className="w-full">
|
||||
<Card.CardHeader>
|
||||
<Card.CardTitle>
|
||||
Entries:
|
||||
</Card.CardTitle>
|
||||
</Card.CardHeader>
|
||||
<Card.CardContent className="w-full">
|
||||
<Suspense fallback={(<></>)}>
|
||||
{
|
||||
entires[0].filter((e) => { return e.categoryId == cat.id }).length > 0 ? (
|
||||
<>
|
||||
{entires[0].filter((e) => { return e.categoryId == cat.id }).map((entry) => (
|
||||
<CollapsibleForm entityName="Entry" form={CreateUpdateCvEntryForm} entity={entry} entityLabelIndex="title" />
|
||||
))}
|
||||
</>
|
||||
) : (<></>)
|
||||
}
|
||||
</Suspense>
|
||||
<CollapsibleForm entityName="Entry" form={CreateUpdateCvEntryForm} />
|
||||
</Card.CardContent>
|
||||
</Card.Card>
|
||||
</div>
|
||||
</Card.CardContent>
|
||||
</Card.Card>
|
||||
)
|
||||
})}
|
||||
<CollapsibleCvCategoryForm />
|
||||
<CollapsibleForm entityName="Category" form={CreateUpdateCvCategoryForm} />
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -9,7 +9,7 @@ export default function Page() {
|
||||
const {data} = trpc.entry.select.useQuery({id: id})
|
||||
if (data !== undefined && data.length > 0) {
|
||||
return (
|
||||
<CreateUpdateCvEntryForm entry={data[0]}/>
|
||||
<CreateUpdateCvEntryForm entity={data[0]}/>
|
||||
)
|
||||
}
|
||||
return (<></>)
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import { ChevronsUpDown, Plus } from "lucide-react"
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "~/components/ui/collapsible";
|
||||
import CreateUpdateCvEntryForm from "./CreateUpdateForm";
|
||||
import type { IterableElement } from "type-fest";
|
||||
import type { RouterOutputs } from "~/server/routers/_app";
|
||||
export default function CollapsibleCvEntryForm(params:{entry?:IterableElement<RouterOutputs['entry']['select']>, categoryId?: string}) {
|
||||
return (
|
||||
<Collapsible>
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button variant={"ghost"}>
|
||||
{ params.entry ?
|
||||
<>{params.entry.title} <ChevronsUpDown/></> : <>New <Plus/></>
|
||||
}
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="autoAlpha">
|
||||
{
|
||||
params.entry ?
|
||||
<CreateUpdateCvEntryForm className="w-full" entry={params.entry}/> :
|
||||
<CreateUpdateCvEntryForm className="w-full"/>
|
||||
}
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
)
|
||||
}
|
||||
@@ -1,119 +1,73 @@
|
||||
'use client'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { useForm, type UseFormReturn } from 'react-hook-form'
|
||||
import { format } from 'date-fns'
|
||||
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 { SelectItem } from "~/components/ui/select";
|
||||
import { trpc } from "~/app/_trpc/Client";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import * as Card from '~/components/ui/card'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "~/components/ui/popover";
|
||||
import { CalendarIcon } from "lucide-react";
|
||||
import { Calendar } from "~/components/ui/calendar";
|
||||
import { cn, entitySchemas, ft, makeOnSuccess, tt, type DatesAreString, type Pretty } from "~/lib/utils";
|
||||
import { Checkbox } from "~/components/ui/checkbox"
|
||||
import MDEditor from '@uiw/react-md-editor'
|
||||
import { entitySchemas, ft, makeOnSuccess, tt } from "~/lib/utils";
|
||||
import { useRelationShip } from '~/lib/hooks';
|
||||
import { useTheme } from "next-themes"
|
||||
import type { Entries, IterableElement } from 'type-fest';
|
||||
import type { IterableElement } from 'type-fest';
|
||||
import type { RouterOutputs } from '~/server/routers/_app';
|
||||
import DependentText from '~/app/_components/DependentText';
|
||||
import DependentFormMessaage from '~/app/_components/MutationFormMessage';
|
||||
import { useEffect, useState } from 'react';
|
||||
import SelectFormField from '~/app/_components/SelectFormField';
|
||||
import TexttInputFormField from '~/app/_components/TextInputFormField';
|
||||
import MdeFormField from '~/app/_components/MdeFormField';
|
||||
import CalendarFormField from '~/app/_components/CalenderFormField';
|
||||
|
||||
export default function CreateUpdateCvEntryForm(params: { className?: string, entry?: IterableElement<RouterOutputs['entry']['select']>, isUpdate?: boolean }) {
|
||||
const [isUpdate, setIsUpdate] = useState<boolean>(params.isUpdate ? params.isUpdate : (params.entry ? true : false))
|
||||
const [categoryId,setCategoryId] = useState<string|undefined>()
|
||||
const [categoryName,setCategoryName] = useState<string|undefined>()
|
||||
import { FormScaffold } from '~/app/_components/Form/Components';
|
||||
import { useState } from 'react';
|
||||
import { SelectFormField, TextInputFormField, MdeFormField, CalenderFormField, BooleanFormField } from '~/app/_components/Form/Fields'
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
export default function CreateUpdateCvEntryForm(params: { className?: string, entity?: IterableElement<RouterOutputs['entry']['select']>, isUpdate?: boolean }) {
|
||||
const [id, setId] = useState<string | undefined>(params.entity ? params.entity.id : undefined)
|
||||
const { theme } = useTheme()
|
||||
const schemas = entitySchemas('cvEntry')
|
||||
const {data:categories,isSuccess: categoriesSuccess} = trpc.category.select.useQuery({})
|
||||
useEffect(() => {
|
||||
if (isUpdate) {
|
||||
return
|
||||
}
|
||||
setCategoryId(categories?.at(0)?.id)
|
||||
if (categories !== undefined && categories[0]?.name !== null) {
|
||||
setCategoryName(categories[0]?.name)
|
||||
}
|
||||
},[categoriesSuccess])
|
||||
const {data: categories,id:categoryId,name:categoryName,success:categoriesSuccess} = useRelationShip(trpc.category.select.useQuery({}),'name',id)
|
||||
let defaultValues = {
|
||||
id: params.entry ? params.entry.id : crypto.randomUUID(),
|
||||
title: params.entry ? params.entry.title : "",
|
||||
description: params.entry ? params.entry.description : "",
|
||||
categoryId: params.entry ? params.entry.categoryId : categoriesSuccess ? categories.at(0)?.id : "",
|
||||
fromTime: params.entry ? ft(params.entry).fromTime : new Date(),
|
||||
toTime: params.entry ? tt(params.entry).toTime : new Date(),
|
||||
hideDates: params.entry ? params.entry.hideDates : false,
|
||||
id: params.entity ? params.entity.id : crypto.randomUUID(),
|
||||
title: params.entity ? params.entity.title : "",
|
||||
description: params.entity ? params.entity.description : "",
|
||||
categoryId: params.entity ? params.entity.categoryId : categoriesSuccess ? categories?.at(0)?.id : "",
|
||||
fromTime: params.entity ? ft(params.entity).fromTime : new Date(),
|
||||
toTime: params.entity ? tt(params.entity).toTime : new Date(),
|
||||
hideDates: params.entity ? params.entity.hideDates : false,
|
||||
};
|
||||
const form = useForm<z.infer<typeof schemas.insert>>({
|
||||
resolver: zodResolver(schemas.insert),
|
||||
defaultValues: defaultValues,
|
||||
// values: params.entry ? ut(ct(tt(ff(params.entry)))) : defaultValues
|
||||
})
|
||||
const createMutation = trpc.entry.insert.useMutation({onSuccess: makeOnSuccess('create',form,setIsUpdate)})
|
||||
const updateMutation = trpc.entry.update.useMutation({onSuccess: makeOnSuccess('update',form)})
|
||||
let path = usePathname()
|
||||
let router = useRouter()
|
||||
const createMutation = trpc.entry.insert.useMutation({ onSuccess: makeOnSuccess('create', form, setId) })
|
||||
const updateMutation = trpc.entry.update.useMutation({ onSuccess: makeOnSuccess('update', form) })
|
||||
const deleteMutation = trpc.entry.delete.useMutation({ onSuccess: makeOnSuccess('delete', form, undefined, path, router) })
|
||||
function onSubmit(values: z.infer<typeof schemas.insert>) {
|
||||
isUpdate ?
|
||||
id ?
|
||||
updateMutation.mutate(values) :
|
||||
createMutation.mutate(values)
|
||||
}
|
||||
//TODO use SelectFormField and TextInputFormField
|
||||
return (
|
||||
<Card.Card className={params.className ? params.className : "w-5/6 lg:w-1/2"}>
|
||||
<Card.CardHeader>
|
||||
<Card.CardTitle>
|
||||
<DependentText bool={isUpdate} true='Update Entry' false='Create Entry' />
|
||||
</Card.CardTitle>
|
||||
</Card.CardHeader>
|
||||
<Card.CardContent>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-8"
|
||||
>
|
||||
<SelectFormField control={form.control} name='categoryId' label='Category' defaultValue={categoryId} placeholder={categoryName}>
|
||||
{
|
||||
categoriesSuccess ?
|
||||
<>
|
||||
{categories.map((c) => {
|
||||
return (<SelectItem key={c.id} value={c.id}> {c.name} </SelectItem>)
|
||||
})}
|
||||
</> :
|
||||
<SelectItem key="abc" value="abcd"/>
|
||||
}
|
||||
</SelectFormField>
|
||||
<TexttInputFormField control={form.control} name='title' label='Title'/>
|
||||
<MdeFormField control={form.control} name='description' label='Description' dataColorMode={(theme as "dark" | "light") ? (theme as "dark" | "light") : "dark"}/>
|
||||
<CalendarFormField control={form.control} name='fromTime' label='From Date'/>
|
||||
<CalendarFormField control={form.control} name='toTime' label='To Date'/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="hideDates"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Hide dates</FormLabel>
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value ? field.value : false}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button type="submit">
|
||||
<DependentText bool={isUpdate} true='Update' false='Create' />
|
||||
</Button>
|
||||
<DependentFormMessaage falseStatus={createMutation.status} trueStatus={updateMutation.status} falseError={createMutation.error} trueError={updateMutation.error} bool={isUpdate} />
|
||||
</form>
|
||||
</Form>
|
||||
</Card.CardContent>
|
||||
</Card.Card>
|
||||
<FormScaffold
|
||||
form={form}
|
||||
createMutation={createMutation}
|
||||
updateMutation={updateMutation}
|
||||
deleteMutation={deleteMutation}
|
||||
onSubmit={onSubmit}
|
||||
title='Entry'
|
||||
id={id}
|
||||
className={params.className}
|
||||
>
|
||||
<SelectFormField control={form.control} name='categoryId' label='Category' defaultValue={categoryId} placeholder={categoryName}>
|
||||
{
|
||||
categoriesSuccess ?
|
||||
<>
|
||||
{categories.map((c) => {
|
||||
return (<SelectItem key={c.id} value={c.id}> {c.name} </SelectItem>)
|
||||
})}
|
||||
</> :
|
||||
<SelectItem key="abc" value="abcd" />
|
||||
}
|
||||
</SelectFormField>
|
||||
<TextInputFormField control={form.control} name='title' label='Title' />
|
||||
<MdeFormField control={form.control} name='description' label='Description' dataColorMode={(theme as "dark" | "light") ? (theme as "dark" | "light") : "dark"} />
|
||||
<CalenderFormField control={form.control} name='fromTime' label='From Date' />
|
||||
<CalenderFormField control={form.control} name='toTime' label='To Date' />
|
||||
<BooleanFormField control={form.control} name='hideDates' label='Hide Dates' />
|
||||
</FormScaffold>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
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 "./CreateUpdateProjectForm";
|
||||
import type { RouterOutputs } from "~/server/routers/_app";
|
||||
export default function CollapsibleForm(params:{project?:Element<RouterOutputs['project']['select']>}) {
|
||||
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"/>
|
||||
|
||||
}
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
'use server'
|
||||
import { expect, test } from 'vitest'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import CreateUpdateProjectForm from './CreateUpdateProjectForm'
|
||||
import TrpcProvider from '~/../test/trpc/vitest.trpcProvider.mock'
|
||||
|
||||
test('CreateUpdateProjectForm', () => {
|
||||
render(
|
||||
<TrpcProvider>
|
||||
<CreateUpdateProjectForm/>
|
||||
</TrpcProvider>
|
||||
)
|
||||
let options = screen.getAllByRole('combobox')
|
||||
console.log(options)
|
||||
})
|
||||
@@ -2,99 +2,76 @@
|
||||
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 { SelectItem } 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, IterableElement } from 'type-fest'
|
||||
import type { IterableElement } from 'type-fest'
|
||||
import { entitySchemas, makeOnSuccess } from "~/lib/utils";
|
||||
import { Suspense, useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import type { RouterOutputs } from '~/server/routers/_app';
|
||||
import TexttInputFormField from '~/app/_components/TextInputFormField';
|
||||
import SelectFormField from '~/app/_components/SelectFormField';
|
||||
import DependentFormMessaage from '~/app/_components/MutationFormMessage';
|
||||
import DependentText from '~/app/_components/DependentText';
|
||||
export default function CreateUpdateProjectForm(params: { className?: string, project?: IterableElement<RouterOutputs['project']['select']>, isUpdate?: boolean }) {
|
||||
const [isUpdate, setIsUpdate] = useState<boolean>(params.isUpdate ? params.isUpdate : (params.project ? true : false))
|
||||
const [stackId, setstackId] = useState<string | undefined>()
|
||||
const [stackName, setstackName] = useState<string | undefined>()
|
||||
import { SelectFormField, TextInputFormField } from '~/app/_components/Form/Fields'
|
||||
import { FormScaffold } from '~/app/_components/Form/Components';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import { makeUseRelationShipWithNameIndex } from '~/lib/hooks';
|
||||
export default function CreateUpdateProjectForm(params: { className?: string, entity?: IterableElement<RouterOutputs['project']['select']> }) {
|
||||
const [id, setId] = useState<string|undefined>(params.entity ? params.entity.id : undefined)
|
||||
const schemas = entitySchemas('project')
|
||||
const { data: stacks, isSuccess: stacksSuccess } = trpc.techStack.select.useQuery({})
|
||||
useEffect(() => {
|
||||
if (isUpdate) {
|
||||
return
|
||||
}
|
||||
setstackId(stacks?.at(0)?.id)
|
||||
if (stacks !== undefined) {
|
||||
setstackName(stacks.at(0)?.stackItems ? stacks.at(0)?.stackItems?.join("-") : "")
|
||||
}
|
||||
}, [stacksSuccess])
|
||||
console.log('using trpc')
|
||||
const { data:stacks,id:stackId, name:stackName,success:stacksSuccess, error:stackError } = makeUseRelationShipWithNameIndex('stackItems')(trpc.techStack.select.useQuery({}),id,(items) => {return items ? items.join('-') : ""})
|
||||
console.log(stackError)
|
||||
const form = useForm<z.infer<typeof schemas.insert>>({
|
||||
resolver: zodResolver(schemas.insert),
|
||||
defaultValues: {
|
||||
id: params.project ? params.project.id : crypto.randomUUID(),
|
||||
stackId: params.project ? params.project.stackId : stacksSuccess ? stacks?.at(0)?.id : "",
|
||||
releaseStatus: params.project ? params.project.releaseStatus : "unreleased",
|
||||
releaseLink: params.project ? params.project.releaseLink : "",
|
||||
sourceType: params.project ? params.project.sourceType : "open",
|
||||
sourceLink: params.project ? params.project.sourceLink : ""
|
||||
id: id ? id : crypto.randomUUID(),
|
||||
stackId: params.entity ? params.entity.stackId : stacksSuccess ? stacks?.at(0)?.id : "",
|
||||
releaseStatus: params.entity ? params.entity.releaseStatus : "unreleased",
|
||||
releaseLink: params.entity ? params.entity.releaseLink : "",
|
||||
sourceType: params.entity ? params.entity.sourceType : "open",
|
||||
sourceLink: params.entity ? params.entity.sourceLink : ""
|
||||
}
|
||||
})
|
||||
const createMutation = trpc.project.insert.useMutation({
|
||||
onSuccess: makeOnSuccess('create',form,setIsUpdate)
|
||||
})
|
||||
const updateMutation = trpc.project.update.useMutation({
|
||||
onSuccess: makeOnSuccess('update',form)
|
||||
})
|
||||
let path = usePathname()
|
||||
let router = useRouter()
|
||||
const createMutation = trpc.project.insert.useMutation({ onSuccess: makeOnSuccess('create', form, setId) })
|
||||
const updateMutation = trpc.project.update.useMutation({ onSuccess: makeOnSuccess('update', form) })
|
||||
const deleteMutation = trpc.project.delete.useMutation({ onSuccess: makeOnSuccess('delete', form, undefined, path, router) })
|
||||
function onSubmit(values: z.infer<typeof schemas.insert>) {
|
||||
params.project ?
|
||||
id ?
|
||||
updateMutation.mutate(values) :
|
||||
createMutation.mutate(values)
|
||||
}
|
||||
return (
|
||||
<Card.Card className={params.className ? params.className : "w-5/6 lg:w-1/2"}>
|
||||
<Card.CardHeader>
|
||||
<Card.CardTitle>
|
||||
<DependentText bool={isUpdate} true='Update Project' false='Create Project'/>
|
||||
</Card.CardTitle>
|
||||
</Card.CardHeader>
|
||||
<Card.CardContent>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-8"
|
||||
>
|
||||
<SelectFormField control={form.control} name='stackId' label='Stack' defaultValue={stackId} placeholder={stackName} >
|
||||
{
|
||||
stacksSuccess ?
|
||||
<>
|
||||
{stacks.map((stack) => {
|
||||
return (<SelectItem key={stack.id} value={stack.id}> {stack.stackItems ? stack.stackItems.join("-") : "Empty Stack"} </SelectItem>)
|
||||
})}
|
||||
</> :
|
||||
<SelectItem key='placeholder' value="placeholder" />
|
||||
}
|
||||
</SelectFormField>
|
||||
<TexttInputFormField control={form.control} name='title' label='Title' />
|
||||
<SelectFormField control={form.control} name='sourceType' label='Source Type' defaultValue={'open'} placeholder='open' >
|
||||
<SelectItem value="open"> open </SelectItem>
|
||||
<SelectItem value="closed"> closed </SelectItem>
|
||||
</SelectFormField>
|
||||
<TexttInputFormField control={form.control} label='Source Link' name='sourceLink' />
|
||||
<SelectFormField control={form.control} name='releaseStatus' label='Release Status' defaultValue={'unreleased'} placeholder='unreleased' >
|
||||
<SelectItem value="released"> released </SelectItem>
|
||||
<SelectItem value="unreleased"> unreleased </SelectItem>
|
||||
</SelectFormField>
|
||||
<TexttInputFormField control={form.control} label='Release Link' name='releaseLink' />
|
||||
<Button type="submit">
|
||||
<DependentText bool={isUpdate} true='Update' false='Create'/>
|
||||
</Button>
|
||||
<DependentFormMessaage falseStatus={createMutation.status} trueStatus={updateMutation.status} falseError={createMutation.error} trueError={updateMutation.error} bool={isUpdate}/>
|
||||
</form>
|
||||
</Form>
|
||||
</Card.CardContent>
|
||||
</Card.Card>
|
||||
<FormScaffold
|
||||
form={form}
|
||||
createMutation={createMutation}
|
||||
updateMutation={updateMutation}
|
||||
deleteMutation={deleteMutation}
|
||||
onSubmit={onSubmit}
|
||||
title='Project'
|
||||
id={id}
|
||||
className={params.className}
|
||||
>
|
||||
<SelectFormField control={form.control} name='stackId' label='Stack' defaultValue={stackId} placeholder={stackName} >
|
||||
{
|
||||
stacksSuccess ?
|
||||
<>
|
||||
{stacks?.map((stack) => {
|
||||
return (<SelectItem key={stack.id} value={stack.id}> {stack.stackItems ? stack.stackItems.join("-") : "Empty Stack"} </SelectItem>)
|
||||
})}
|
||||
</> :
|
||||
<SelectItem key='placeholder' value="placeholder" />
|
||||
}
|
||||
</SelectFormField>
|
||||
<TextInputFormField control={form.control} name='title' label='Title' />
|
||||
<SelectFormField control={form.control} name='sourceType' label='Source Type' defaultValue={'open'} placeholder='open' >
|
||||
<SelectItem value="open"> open </SelectItem>
|
||||
<SelectItem value="closed"> closed </SelectItem>
|
||||
</SelectFormField>
|
||||
<TextInputFormField control={form.control} label='Source Link' name='sourceLink' />
|
||||
<SelectFormField control={form.control} name='releaseStatus' label='Release Status' defaultValue={'unreleased'} placeholder='unreleased' >
|
||||
<SelectItem value="released"> released </SelectItem>
|
||||
<SelectItem value="unreleased"> unreleased </SelectItem>
|
||||
</SelectFormField>
|
||||
<TextInputFormField control={form.control} label='Release Link' name='releaseLink' />
|
||||
</FormScaffold>
|
||||
)
|
||||
}
|
||||
|
||||
60
src/app/admin/project/list/page.tsx
Normal file
60
src/app/admin/project/list/page.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
'use client'
|
||||
import * as Card from '~/components/ui/card'
|
||||
import { trpc } from "~/app/_trpc/Client"
|
||||
import CreateUpdateProjectForm from '../_components/CreateUpdateProjectForm'
|
||||
import CreateUpdateStackForm from '../techStack/_components/CreateUpdateForm'
|
||||
import { CollapsibleForm } from '~/app/_components/Form/Components'
|
||||
import { Suspense } from 'react'
|
||||
|
||||
export default function ProjectList() {
|
||||
const projects = trpc.project.select.useQuery({}, { refetchInterval: 1000 })
|
||||
const techStacks = trpc.techStack.select.useSuspenseQuery({}, { refetchInterval: 1000 })
|
||||
return (
|
||||
<div className="w-5/6 lg:w-1/2 flex flex-col gap-3">
|
||||
{
|
||||
projects.data == undefined ?
|
||||
<></> :
|
||||
<>
|
||||
{projects.data.map((project) => {
|
||||
return (
|
||||
<Card.Card className="gsapan" key={project.id}>
|
||||
<Card.CardHeader>
|
||||
<Card.CardTitle>
|
||||
Project: {project.title}
|
||||
</Card.CardTitle>
|
||||
</Card.CardHeader>
|
||||
<Card.CardContent className="flex flex-row">
|
||||
<div className="flex flex-col w-full">
|
||||
<CreateUpdateProjectForm entity={project} className="w-full" />
|
||||
<Card.Card className="w-full">
|
||||
<Card.CardHeader>
|
||||
<Card.CardTitle>
|
||||
TechStack:
|
||||
</Card.CardTitle>
|
||||
</Card.CardHeader>
|
||||
<Card.CardContent className="w-full">
|
||||
<Suspense fallback={(<></>)}>
|
||||
{
|
||||
techStacks[0].filter((e) => { return e.id == project.stackId }).length > 0 ? (
|
||||
<>
|
||||
{techStacks[0].filter((e) => { return e.id == project.stackId }).map((stack) => (
|
||||
<CollapsibleForm entityName="Stack" form={CreateUpdateStackForm} entity={stack} entityLabelIndex="stackItems"/>
|
||||
))}
|
||||
</>
|
||||
) : (<></>)
|
||||
}
|
||||
</Suspense>
|
||||
<CollapsibleForm entityName="Stack" form={CreateUpdateStackForm} />
|
||||
</Card.CardContent>
|
||||
</Card.Card>
|
||||
</div>
|
||||
</Card.CardContent>
|
||||
</Card.Card>
|
||||
)
|
||||
})}
|
||||
<CollapsibleForm entityName="Project" form={CreateUpdateProjectForm} />
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -2,104 +2,52 @@
|
||||
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 { Form } from "~/components/ui/form";
|
||||
import { trpc } from "~/app/_trpc/Client";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import * as Card from '~/components/ui/card'
|
||||
import type { Entries, IterableElement } from 'type-fest'
|
||||
import type { IterableElement } from 'type-fest'
|
||||
import { stackItemEnum } from "~/server/db/schema";
|
||||
import { ToggleGroup, ToggleGroupItem } from "~/components/ui/toggle-group";
|
||||
import type { RouterOutputs } from '~/server/routers/_app';
|
||||
import { entitySchemas } from '~/lib/utils';
|
||||
import { entitySchemas, makeOnSuccess } from '~/lib/utils';
|
||||
import { useState } from 'react';
|
||||
import { MultiBooleanFormField } from "~/app/_components/Form/Fields"
|
||||
import { DependentFormMessaage, FormScaffold } from '~/app/_components/Form/Components';
|
||||
import DependentText from '~/app/_components/DependentText';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
|
||||
export default function CreateUpdateStackForm(params:{className?:string, techStack?:IterableElement<RouterOutputs['techStack']['select']>,isUpdate?: boolean }) {
|
||||
export default function CreateUpdateStackForm(params: { className?: string, entity?: IterableElement<RouterOutputs['techStack']['select']> }) {
|
||||
const schemas = entitySchemas('techStack')
|
||||
const form = useForm<z.infer<typeof schemas.insert>>({
|
||||
resolver: zodResolver(schemas.insert),
|
||||
defaultValues: {
|
||||
id: params.techStack ? params.techStack.id : crypto.randomUUID(),
|
||||
stackItems: params.techStack ? params.techStack.stackItems : [],
|
||||
}
|
||||
})
|
||||
const [isUpdate, setIsUpdate] = useState<boolean>(params.isUpdate ? params.isUpdate : (params.techStack ? true : false))
|
||||
const createMutation = trpc.techStack.insert.useMutation({
|
||||
onSuccess: (data) => {
|
||||
form.setValue("id", data[0] ? data[0].id : "");
|
||||
setIsUpdate(true)
|
||||
}
|
||||
})
|
||||
const updateMutation = trpc.techStack.update.useMutation({
|
||||
onSuccess: (data) => {
|
||||
if (data.length > 0 && data[0] !== undefined) {
|
||||
let entries = Object.entries(data[0]) as Entries<typeof data[0]>
|
||||
entries.forEach((entry) => {
|
||||
form.setValue(entry[0], entry[1])
|
||||
})
|
||||
}
|
||||
id: params.entity ? params.entity.id : crypto.randomUUID(),
|
||||
stackItems: params.entity ? params.entity.stackItems : [],
|
||||
}
|
||||
})
|
||||
const [id, setId] = useState<string | undefined>(params.entity ? params.entity.id : undefined)
|
||||
let path = usePathname()
|
||||
let router = useRouter()
|
||||
const createMutation = trpc.techStack.insert.useMutation({ onSuccess: makeOnSuccess('create', form, setId) })
|
||||
const updateMutation = trpc.techStack.update.useMutation({ onSuccess: makeOnSuccess('update', form) })
|
||||
const deleteMutation = trpc.techStack.delete.useMutation({ onSuccess: makeOnSuccess('delete', form, undefined, path, router) })
|
||||
function onSubmit(values: z.infer<typeof schemas.insert>) {
|
||||
params.techStack ?
|
||||
params.entity ?
|
||||
updateMutation.mutate(values) :
|
||||
createMutation.mutate(values)
|
||||
}
|
||||
return (
|
||||
<Card.Card className={params.className ? params.className : "w-5/6 lg:w-1/2"}>
|
||||
<Card.CardHeader>
|
||||
<Card.CardTitle>
|
||||
{params.techStack ? "Update" : "Create"} Tech Stack
|
||||
</Card.CardTitle>
|
||||
</Card.CardHeader>
|
||||
<Card.CardContent>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-8"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="stackItems"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
Stack Items
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<ToggleGroup
|
||||
variant="outline"
|
||||
type="multiple"
|
||||
onValueChange={field.onChange}
|
||||
className="flex flex-wrap"
|
||||
>
|
||||
{stackItemEnum.enumValues.map((v) => {
|
||||
return (
|
||||
<ToggleGroupItem className="w-fit" key={v} value={v}>
|
||||
{v}
|
||||
</ToggleGroupItem>
|
||||
)
|
||||
})}
|
||||
</ToggleGroup>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button type="submit"> {params.techStack ? "Update" : "Create"} </Button>
|
||||
<FormMessage className={updateMutation.status == "success" || createMutation.status == "success" ? "text-green-500" : "text-red-500"}>
|
||||
{ params.techStack ?
|
||||
(
|
||||
<>{updateMutation.error ? updateMutation.error.message : updateMutation.status}</>
|
||||
) :
|
||||
(
|
||||
<>{createMutation.error ? createMutation.error.message : createMutation.status}</>
|
||||
)
|
||||
|
||||
}
|
||||
</FormMessage>
|
||||
</form>
|
||||
</Form>
|
||||
</Card.CardContent>
|
||||
</Card.Card>
|
||||
<FormScaffold
|
||||
form={form}
|
||||
createMutation={createMutation}
|
||||
updateMutation={updateMutation}
|
||||
deleteMutation={deleteMutation}
|
||||
onSubmit={onSubmit}
|
||||
title='Entry'
|
||||
id={id}
|
||||
className={params.className}
|
||||
>
|
||||
<MultiBooleanFormField control={form.control} name='stackItems' label='Stack Items' options={stackItemEnum.enumValues} defaultValues={params.entity?.stackItems} />
|
||||
</FormScaffold>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -12,16 +12,16 @@ type CvCategoryProps = {
|
||||
children?: React.ReactElement<Parameters<typeof CvEntry>>
|
||||
}
|
||||
export default function CvCategory(props:CvCategoryProps) {
|
||||
const query = trpc.cv.category.get.useQuery({id: props.initialData? props.initialData.id : ""});
|
||||
const query = trpc.category.select.useQuery({id: props.initialData? props.initialData.id : ""});
|
||||
if (query.data !== undefined) {
|
||||
return (
|
||||
<Card className={cn(props.layout == "row" ? "w-full" : "","gsapan")}>
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
{query.data?.name}
|
||||
{query.data[0].name}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
{(query.data?.cvEntry.length ? query.data?.cvEntry.length : 0 ) > 0 ?
|
||||
{(query.data?.at(0)?.cvEntry.length ? query.data?.cvEntry.length : 0 ) > 0 ?
|
||||
<CardContent className={cn(props.layout == "row" ? "flex flex-row flex-wrap justify-center lg:justify-between" : "flex flex-col","gap-[1rem]","overflow-scroll")}>
|
||||
{query.data?.cvEntry.map((entry) => (
|
||||
<CvEntry className={props.layout == "row" ? "w-full lg:w-fit" : undefined} key={entry.id} initialData={entry}/>
|
||||
|
||||
@@ -1,75 +1,213 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react"
|
||||
import { DayPicker } from "react-day-picker"
|
||||
import {
|
||||
ChevronDownIcon,
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
} from "lucide-react"
|
||||
import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker"
|
||||
|
||||
import { cn } from "~/lib/utils"
|
||||
import { buttonVariants } from "~/components/ui/button"
|
||||
import { Button, buttonVariants } from "~/components/ui/button"
|
||||
|
||||
function Calendar({
|
||||
className,
|
||||
classNames,
|
||||
showOutsideDays = true,
|
||||
captionLayout = "label",
|
||||
buttonVariant = "ghost",
|
||||
formatters,
|
||||
components,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DayPicker>) {
|
||||
}: React.ComponentProps<typeof DayPicker> & {
|
||||
buttonVariant?: React.ComponentProps<typeof Button>["variant"]
|
||||
}) {
|
||||
const defaultClassNames = getDefaultClassNames()
|
||||
|
||||
return (
|
||||
<DayPicker
|
||||
showOutsideDays={showOutsideDays}
|
||||
className={cn("p-3", className)}
|
||||
className={cn(
|
||||
"bg-background group/calendar p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
|
||||
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
|
||||
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
|
||||
className
|
||||
)}
|
||||
captionLayout={captionLayout}
|
||||
formatters={{
|
||||
formatMonthDropdown: (date) =>
|
||||
date.toLocaleString("default", { month: "short" }),
|
||||
...formatters,
|
||||
}}
|
||||
classNames={{
|
||||
months: "flex flex-col sm:flex-row gap-2",
|
||||
month: "flex flex-col gap-4",
|
||||
caption: "flex justify-center pt-1 relative items-center w-full",
|
||||
caption_label: "text-sm font-medium",
|
||||
nav: "flex items-center gap-1",
|
||||
nav_button: cn(
|
||||
buttonVariants({ variant: "outline" }),
|
||||
"size-7 bg-transparent p-0 opacity-50 hover:opacity-100"
|
||||
root: cn("w-fit", defaultClassNames.root),
|
||||
months: cn(
|
||||
"flex gap-4 flex-col md:flex-row relative",
|
||||
defaultClassNames.months
|
||||
),
|
||||
nav_button_previous: "absolute left-1",
|
||||
nav_button_next: "absolute right-1",
|
||||
table: "w-full border-collapse space-x-1",
|
||||
head_row: "flex",
|
||||
head_cell:
|
||||
"text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]",
|
||||
row: "flex w-full mt-2",
|
||||
cell: cn(
|
||||
"relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-range-end)]:rounded-r-md",
|
||||
props.mode === "range"
|
||||
? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
|
||||
: "[&:has([aria-selected])]:rounded-md"
|
||||
month: cn("flex flex-col w-full gap-4", defaultClassNames.month),
|
||||
nav: cn(
|
||||
"flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between",
|
||||
defaultClassNames.nav
|
||||
),
|
||||
button_previous: cn(
|
||||
buttonVariants({ variant: buttonVariant }),
|
||||
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
|
||||
defaultClassNames.button_previous
|
||||
),
|
||||
button_next: cn(
|
||||
buttonVariants({ variant: buttonVariant }),
|
||||
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
|
||||
defaultClassNames.button_next
|
||||
),
|
||||
month_caption: cn(
|
||||
"flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)",
|
||||
defaultClassNames.month_caption
|
||||
),
|
||||
dropdowns: cn(
|
||||
"w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5",
|
||||
defaultClassNames.dropdowns
|
||||
),
|
||||
dropdown_root: cn(
|
||||
"relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md",
|
||||
defaultClassNames.dropdown_root
|
||||
),
|
||||
dropdown: cn(
|
||||
"absolute bg-popover inset-0 opacity-0",
|
||||
defaultClassNames.dropdown
|
||||
),
|
||||
caption_label: cn(
|
||||
"select-none font-medium",
|
||||
captionLayout === "label"
|
||||
? "text-sm"
|
||||
: "rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5",
|
||||
defaultClassNames.caption_label
|
||||
),
|
||||
table: "w-full border-collapse",
|
||||
weekdays: cn("flex", defaultClassNames.weekdays),
|
||||
weekday: cn(
|
||||
"text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none",
|
||||
defaultClassNames.weekday
|
||||
),
|
||||
week: cn("flex w-full mt-2", defaultClassNames.week),
|
||||
week_number_header: cn(
|
||||
"select-none w-(--cell-size)",
|
||||
defaultClassNames.week_number_header
|
||||
),
|
||||
week_number: cn(
|
||||
"text-[0.8rem] select-none text-muted-foreground",
|
||||
defaultClassNames.week_number
|
||||
),
|
||||
day: cn(
|
||||
buttonVariants({ variant: "ghost" }),
|
||||
"size-8 p-0 font-normal aria-selected:opacity-100"
|
||||
"relative w-full h-full p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none",
|
||||
defaultClassNames.day
|
||||
),
|
||||
day_range_start:
|
||||
"day-range-start aria-selected:bg-primary aria-selected:text-primary-foreground",
|
||||
day_range_end:
|
||||
"day-range-end aria-selected:bg-primary aria-selected:text-primary-foreground",
|
||||
day_selected:
|
||||
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
|
||||
day_today: "bg-accent text-accent-foreground",
|
||||
day_outside:
|
||||
"day-outside text-muted-foreground aria-selected:text-muted-foreground",
|
||||
day_disabled: "text-muted-foreground opacity-50",
|
||||
day_range_middle:
|
||||
"aria-selected:bg-accent aria-selected:text-accent-foreground",
|
||||
day_hidden: "invisible",
|
||||
range_start: cn(
|
||||
"rounded-l-md bg-accent",
|
||||
defaultClassNames.range_start
|
||||
),
|
||||
range_middle: cn("rounded-none", defaultClassNames.range_middle),
|
||||
range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end),
|
||||
today: cn(
|
||||
"bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
|
||||
defaultClassNames.today
|
||||
),
|
||||
outside: cn(
|
||||
"text-muted-foreground aria-selected:text-muted-foreground",
|
||||
defaultClassNames.outside
|
||||
),
|
||||
disabled: cn(
|
||||
"text-muted-foreground opacity-50",
|
||||
defaultClassNames.disabled
|
||||
),
|
||||
hidden: cn("invisible", defaultClassNames.hidden),
|
||||
...classNames,
|
||||
}}
|
||||
components={{
|
||||
IconLeft: ({ className, ...props }) => (
|
||||
<ChevronLeft className={cn("size-4", className)} {...props} />
|
||||
),
|
||||
IconRight: ({ className, ...props }) => (
|
||||
<ChevronRight className={cn("size-4", className)} {...props} />
|
||||
),
|
||||
Root: ({ className, rootRef, ...props }) => {
|
||||
return (
|
||||
<div
|
||||
data-slot="calendar"
|
||||
ref={rootRef}
|
||||
className={cn(className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
},
|
||||
Chevron: ({ className, orientation, ...props }) => {
|
||||
if (orientation === "left") {
|
||||
return (
|
||||
<ChevronLeftIcon className={cn("size-4", className)} {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
if (orientation === "right") {
|
||||
return (
|
||||
<ChevronRightIcon
|
||||
className={cn("size-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ChevronDownIcon className={cn("size-4", className)} {...props} />
|
||||
)
|
||||
},
|
||||
DayButton: CalendarDayButton,
|
||||
WeekNumber: ({ children, ...props }) => {
|
||||
return (
|
||||
<td {...props}>
|
||||
<div className="flex size-(--cell-size) items-center justify-center text-center">
|
||||
{children}
|
||||
</div>
|
||||
</td>
|
||||
)
|
||||
},
|
||||
...components,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Calendar }
|
||||
function CalendarDayButton({
|
||||
className,
|
||||
day,
|
||||
modifiers,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DayButton>) {
|
||||
const defaultClassNames = getDefaultClassNames()
|
||||
|
||||
const ref = React.useRef<HTMLButtonElement>(null)
|
||||
React.useEffect(() => {
|
||||
if (modifiers.focused) ref.current?.focus()
|
||||
}, [modifiers.focused])
|
||||
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
data-day={day.date.toLocaleDateString()}
|
||||
data-selected-single={
|
||||
modifiers.selected &&
|
||||
!modifiers.range_start &&
|
||||
!modifiers.range_end &&
|
||||
!modifiers.range_middle
|
||||
}
|
||||
data-range-start={modifiers.range_start}
|
||||
data-range-end={modifiers.range_end}
|
||||
data-range-middle={modifiers.range_middle}
|
||||
className={cn(
|
||||
"data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 dark:hover:text-accent-foreground flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md [&>span]:text-xs [&>span]:opacity-70",
|
||||
defaultClassNames.day,
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Calendar, CalendarDayButton }
|
||||
|
||||
@@ -76,7 +76,10 @@ function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="drawer-header"
|
||||
className={cn("flex flex-col gap-1.5 p-4", className)}
|
||||
className={cn(
|
||||
"flex flex-col gap-0.5 p-4 group-data-[vaul-drawer-direction=bottom]/drawer-content:text-center group-data-[vaul-drawer-direction=top]/drawer-content:text-center md:gap-1.5 md:text-left",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
58
src/lib/hooks.ts
Normal file
58
src/lib/hooks.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
'use client'
|
||||
|
||||
import type { UseTRPCQueryResult } from "node_modules/@trpc/react-query/dist/getQueryKey.d-CruH3ncI.mjs"
|
||||
import { useEffect, useState } from "react"
|
||||
|
||||
function useRelationShipSuccess<T extends Record<string,any> & {id:string},K extends keyof T>(
|
||||
relationShipData: T[] | undefined,
|
||||
relationShipNameIndex: K,
|
||||
setRelationShipId: (arg0: string | undefined) => void,
|
||||
setRelationShipName: (arg0: string | undefined) => void,
|
||||
realtionShipDataSuccess: boolean,
|
||||
id: string|undefined,
|
||||
nameCallBack?: (arg0:T[K]) => string,
|
||||
){
|
||||
useEffect(() => {
|
||||
if (id === undefined) {
|
||||
return
|
||||
}
|
||||
setRelationShipId(relationShipData?.at(0)?.id)
|
||||
if (relationShipData !== undefined && relationShipData[0] !==undefined) {
|
||||
|
||||
if (relationShipData[0][relationShipNameIndex] !== null) {
|
||||
if (nameCallBack !== undefined) {
|
||||
setRelationShipName(nameCallBack(relationShipData[0][relationShipNameIndex]))
|
||||
return
|
||||
}
|
||||
setRelationShipName(relationShipData[0][relationShipNameIndex])
|
||||
}
|
||||
}
|
||||
}, [realtionShipDataSuccess])
|
||||
}
|
||||
|
||||
export function useRelationShip<T extends Record<string,any> & {id: string},TE>(
|
||||
queryResult: UseTRPCQueryResult<T[],TE>,
|
||||
relationShipNameIndex: keyof T,
|
||||
dependsOnId:string|undefined,
|
||||
) {
|
||||
const [id, setRelationShipId] = useState<string | undefined>()
|
||||
const [name, setRealtionShipName] = useState<string | undefined>()
|
||||
const { data: data, isSuccess: success } = queryResult
|
||||
useRelationShipSuccess(data,relationShipNameIndex,setRelationShipId,setRealtionShipName,success,dependsOnId)
|
||||
return {data,id,name,success}
|
||||
}
|
||||
|
||||
export function makeUseRelationShipWithNameIndex<K extends string>(key:K) {
|
||||
return function useRelationShip<T extends Record<K, any> & { id: string },TE>(
|
||||
queryResult: UseTRPCQueryResult<T[],TE>,
|
||||
dependsOnId:string|undefined,
|
||||
nameCallBack: (arg0:T[K]) => string
|
||||
) {
|
||||
const [id, setRelationShipId] = useState<string | undefined>()
|
||||
const [name, setRealtionShipName] = useState<string | undefined>()
|
||||
const { data: data, isSuccess: success, error } = queryResult
|
||||
useRelationShipSuccess(data,key,setRelationShipId,setRealtionShipName,success,dependsOnId,nameCallBack)
|
||||
return {data,error,id,name,success}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { createSchemaFactory, createSelectSchema, type BuildSchema } from 'drizz
|
||||
import * as schema from "~/server/db/schema";
|
||||
import { PgTable } from "drizzle-orm/pg-core";
|
||||
import type { FieldValues, Path, UseFormReturn } from "react-hook-form";
|
||||
import type { AppRouterInstance } from "next/dist/shared/lib/app-router-context.shared-runtime";
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
@@ -15,19 +16,19 @@ export type SchemaKeys<S> = {
|
||||
}[keyof S]
|
||||
|
||||
export type Pretty<T> = {
|
||||
[K in keyof T]: T[K];
|
||||
[K in keyof T]:
|
||||
T[K] extends InstanceType<new (...args: any[]) => any> ? T[K]
|
||||
: T[K] extends object ? Pretty<T[K]>
|
||||
: T[K];
|
||||
} & {}
|
||||
|
||||
|
||||
const { createInsertSchema, createUpdateSchema } = createSchemaFactory({
|
||||
coerce: {
|
||||
date: true
|
||||
}
|
||||
})
|
||||
|
||||
export type DatesAreString<T extends {[x:string]: any}> = {
|
||||
[K in keyof T] : T[K] extends Date ? string : T[K]
|
||||
}
|
||||
|
||||
// type X = {
|
||||
// foo: Date
|
||||
// bar: number
|
||||
@@ -35,7 +36,8 @@ export type DatesAreString<T extends {[x:string]: any}> = {
|
||||
|
||||
// type Y = DatesAreString<X>
|
||||
|
||||
export function makeOnSuccess<T extends FieldValues>(uc: 'update' | 'create', form: UseFormReturn<T>, setIsUpdate?: (arg0: boolean) => void) {
|
||||
|
||||
export function makeOnSuccess<T extends FieldValues>(uc: 'update' | 'create' | 'delete', form: UseFormReturn<T>, setId?: (arg0?: undefined) => void, path?: string, router?: AppRouterInstance) {
|
||||
switch (uc) {
|
||||
case 'update':
|
||||
return (data: T[]) => {
|
||||
@@ -46,22 +48,32 @@ export function makeOnSuccess<T extends FieldValues>(uc: 'update' | 'create', fo
|
||||
}
|
||||
}
|
||||
case 'create':
|
||||
if (setIsUpdate !== undefined) {
|
||||
if (setId !== undefined) {
|
||||
return (data: T[]) => {
|
||||
form.setValue(("id" as Path<T>), data[0] ? data[0].id : "");
|
||||
setIsUpdate(true)
|
||||
setId(data[0] ? data[0].id : undefined)
|
||||
}
|
||||
}
|
||||
case 'delete':
|
||||
if (path !== undefined && router !== undefined) {
|
||||
return (_data: T[]) => {
|
||||
if (path.includes('list')) {
|
||||
router.refresh();
|
||||
return;
|
||||
}
|
||||
router.back()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function entitySchemas<T extends SchemaKeys<Schema>>(table: T)
|
||||
// : {
|
||||
// insert: Pretty<BuildSchema<'insert', Schema[T]['_']['columns'], undefined>>
|
||||
// update: Pretty<BuildSchema<'update', Schema[T]['_']['columns'], undefined>>
|
||||
// select: Pretty<BuildSchema<'select', Schema[T]['_']['columns'], undefined>>
|
||||
// }
|
||||
{
|
||||
: {
|
||||
insert: Pretty<BuildSchema<'insert', Schema[T]['_']['columns'], undefined>>
|
||||
update: Pretty<BuildSchema<'update', Schema[T]['_']['columns'], undefined>>
|
||||
select: Pretty<BuildSchema<'select', Schema[T]['_']['columns'], undefined>>
|
||||
} {
|
||||
const insertSchema = createInsertSchema<Schema[T]>(schema[table]);
|
||||
const updateSchema = createUpdateSchema<Schema[T]>(schema[table]);
|
||||
const selectSchema = createSelectSchema<Schema[T]>(schema[table]);
|
||||
|
||||
@@ -43,14 +43,13 @@ export function trpcCrudRouterFromDrizzleEntity<T extends SchemaKeys<Schema>>(ta
|
||||
)
|
||||
}
|
||||
const { input } = opts;
|
||||
console.log(input)
|
||||
if (input === undefined) {
|
||||
throw new TRPCError({ message: "no update input", code: "BAD_REQUEST" })
|
||||
}
|
||||
if (!isKeyOf('id',input) || !isKeyOf('id',schema[table])) {
|
||||
if (!isKeyOf('id', input) || !isKeyOf('id', schema[table])) {
|
||||
throw new TRPCError({ message: "no id provided", code: "BAD_REQUEST" })
|
||||
}
|
||||
return await db.update(schema[table]).set(input).where(eq(schema[table]['id'],input['id'])).returning().execute();
|
||||
return await db.update(schema[table]).set(input).where(eq(schema[table]['id'], input['id'])).returning().execute();
|
||||
}),
|
||||
insert: publicProcedure.input(schemas.insert).mutation(async (opts) => {
|
||||
let admin = await isAdmin();
|
||||
|
||||
19
src/server/routers/_app.server.test.ts
Normal file
19
src/server/routers/_app.server.test.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { QueryProcedure } from '@trpc/server/unstable-core-do-not-import';
|
||||
import type { Entries } from 'type-fest';
|
||||
import { assertType, expect, test } from 'vitest'
|
||||
import { trpcRouter } from '~/server/routers/_app';
|
||||
|
||||
const routerEntries = Object.entries(trpcRouter) as Entries<typeof trpcRouter>
|
||||
routerEntries.forEach(([key, value]) => {
|
||||
switch (key) {
|
||||
case '_def': break;
|
||||
case 'createCaller': break;
|
||||
default:
|
||||
test.concurrent(`default router ${key}`, async ({ annotate }) => {
|
||||
expect(value.select).toBeInstanceOf(Function)
|
||||
expect(value.update).toBeInstanceOf(Function)
|
||||
expect(value.delete).toBeInstanceOf(Function)
|
||||
expect(value.insert).toBeInstanceOf(Function)
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -8,7 +8,6 @@ const { router : project } = trpcCrudRouterFromDrizzleEntity('project')
|
||||
const { router : techStack } = trpcCrudRouterFromDrizzleEntity('techStack')
|
||||
const { router : category } = trpcCrudRouterFromDrizzleEntity('cvCategory')
|
||||
const { router : entry } = trpcCrudRouterFromDrizzleEntity('cvEntry')
|
||||
const root = {}
|
||||
export const trpcRouter = router({
|
||||
project: project,
|
||||
techStack: techStack,
|
||||
|
||||
11
test/mocks/vitest.drizzle.mock.ts
Normal file
11
test/mocks/vitest.drizzle.mock.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { vi } from "vitest";
|
||||
import { drizzle } from 'drizzle-orm/pglite';
|
||||
import { PGlite } from "@electric-sql/pglite";
|
||||
import * as schema from "~/server/db/schema"
|
||||
const client = new PGlite()
|
||||
const db = drizzle({client:client,schema:schema})
|
||||
vi.mock("~/server/db", () => {
|
||||
return {
|
||||
db: db
|
||||
}
|
||||
})
|
||||
9
test/mocks/vitest.isAdmin.mock.ts
Normal file
9
test/mocks/vitest.isAdmin.mock.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { vi } from 'vitest'
|
||||
vi.mock('~/app/actions', () => {
|
||||
return {
|
||||
isAdmin: vi.fn(() => {
|
||||
console.log("_____idAdminMockCall_____")
|
||||
return true
|
||||
})
|
||||
}
|
||||
})
|
||||
15
test/mocks/vitest.nextnavigation.mock.ts
Normal file
15
test/mocks/vitest.nextnavigation.mock.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { vi } from "vitest"
|
||||
vi.mock("next/navigation", () => {
|
||||
return {
|
||||
useRouter: vi.fn(() => {
|
||||
return {
|
||||
push: () => {},
|
||||
replace: () => {},
|
||||
prefetch: () => {},
|
||||
}
|
||||
}),
|
||||
usePathname: vi.fn(() => '/'),
|
||||
useSearchParams: vi.fn(() => new URLSearchParams()),
|
||||
useServerInsertedHTML:vi.fn(() => {})
|
||||
}
|
||||
})
|
||||
16
test/trpc/vitest.trpcProvider.mock.tsx
Normal file
16
test/trpc/vitest.trpcProvider.mock.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import React from 'react'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { trpc } from '~/app/_trpc/Client'
|
||||
import { createTestTrpcClient } from './vitest.trpcProxyClient'
|
||||
|
||||
export default function TrpcProvider({ children }:{children: React.ReactNode}) {
|
||||
const queryClient = new QueryClient()
|
||||
const trpcClient = createTestTrpcClient()
|
||||
console.log("using test provider")
|
||||
return (
|
||||
<trpc.Provider client={trpcClient} queryClient={queryClient}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
</trpc.Provider>
|
||||
)}
|
||||
26
test/trpc/vitest.trpcProxyClient.ts
Normal file
26
test/trpc/vitest.trpcProxyClient.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { createTRPCProxyClient, httpBatchLink } from '@trpc/client'
|
||||
import { fetchRequestHandler } from '@trpc/server/adapters/fetch'
|
||||
import { trpcRouter, type TrpcRouter } from '~/server/routers/_app'
|
||||
|
||||
// This simulates the server without HTTP
|
||||
function serverHandler(path: string, req: Request) {
|
||||
return fetchRequestHandler({
|
||||
endpoint: '/api/trpc',
|
||||
router: trpcRouter,
|
||||
req,
|
||||
createContext: () => ({}),
|
||||
})
|
||||
}
|
||||
|
||||
export function createTestTrpcClient() {
|
||||
return createTRPCProxyClient<TrpcRouter>({
|
||||
links: [
|
||||
httpBatchLink({
|
||||
url: 'http://localhost/api/trpc',
|
||||
fetch: (input, init) => {
|
||||
return serverHandler(input as string, new Request(input as string, init))
|
||||
},
|
||||
}),
|
||||
],
|
||||
})
|
||||
}
|
||||
59
vitest.config.mts
Normal file
59
vitest.config.mts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { defineConfig } from 'vitest/config'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import tsconfigPaths from 'vite-tsconfig-paths'
|
||||
import dotenv from 'dotenv'
|
||||
import path from 'path'
|
||||
const env = dotenv.config({path: './.env'})
|
||||
console.log('dname', __dirname)
|
||||
console.log(env.error)
|
||||
// console.log(env.parsed)
|
||||
export default defineConfig({
|
||||
plugins: [tsconfigPaths(), react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'~/app/_trpc/TrpcProvicer': path.resolve(__dirname,'./vitest.trpcProvider.mock')
|
||||
}
|
||||
},
|
||||
optimizeDeps: {
|
||||
include: [
|
||||
'@tanstack/react-query-next-experimental',
|
||||
],
|
||||
},
|
||||
ssr: {
|
||||
noExternal: [
|
||||
'@tanstack/react-query-next-experimental',
|
||||
],
|
||||
},
|
||||
test: {
|
||||
reporters: ['verbose'],
|
||||
coverage: { provider: 'v8'},
|
||||
projects: [
|
||||
{
|
||||
extends: true,
|
||||
test: {
|
||||
name: "server",
|
||||
setupFiles: [
|
||||
'./test/mocks/vitest.isAdmin.mock.ts',
|
||||
'./test/mocks/vitest.nextnavigation.mock.ts',
|
||||
'./test/mocks/vitest.drizzle.mock.ts'
|
||||
],
|
||||
include: ['./src/**/*.server.test.ts'],
|
||||
env: env.parsed
|
||||
}
|
||||
},
|
||||
{
|
||||
extends: true,
|
||||
test: {
|
||||
name: "client",
|
||||
setupFiles: [
|
||||
'./test/mocks/vitest.isAdmin.mock.ts',
|
||||
'./test/mocks/vitest.nextnavigation.mock.ts',
|
||||
'./test/mocks/vitest.drizzle.mock.ts'
|
||||
],
|
||||
include: ['./src/**/*.client.test.ts','./src/**/*.client.test.tsx'],
|
||||
environment: 'jsdom',
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user