testing setup

This commit is contained in:
2025-08-29 14:19:38 +02:00
parent e74b30e04c
commit 869cc07fdd
46 changed files with 6710 additions and 1460 deletions

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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()) {

View File

@@ -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>

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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'

View 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>
)}
/>
)
}

View 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>
)}
/>
)
}

View 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>
)}
/>
)
}

View 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>
)}
/>
)
}

View File

@@ -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>

View File

@@ -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>
)}
/>

View 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'

View File

@@ -9,7 +9,7 @@ export default function MdeFormField<T extends FieldValues>(params: { control: C
render={({ field }) => (
<FormItem>
<FormLabel>
Description
{params.label}
</FormLabel>
<FormControl>
<MDEditor

View File

@@ -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>

View File

@@ -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 (<></>)

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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>

View File

@@ -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 (<></>)

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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)
})

View File

@@ -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>
)
}

View 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>
)
}

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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}/>

View File

@@ -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 }

View File

@@ -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
View 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}
}
}

View File

@@ -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]);

View File

@@ -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();

View 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)
})
}
})

View File

@@ -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,

View 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
}
})

View File

@@ -0,0 +1,9 @@
import { vi } from 'vitest'
vi.mock('~/app/actions', () => {
return {
isAdmin: vi.fn(() => {
console.log("_____idAdminMockCall_____")
return true
})
}
})

View 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(() => {})
}
})

View 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>
)}

View 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
View 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',
}
}
]
},
})