testing setup
This commit is contained in:
234
package.json
234
package.json
@@ -1,110 +1,128 @@
|
|||||||
{
|
{
|
||||||
"name": "gregorlohaus.com",
|
"name": "gregorlohaus.com",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"check": "biome check .",
|
"check": "biome check .",
|
||||||
"check:unsafe": "biome check --write --unsafe .",
|
"check:unsafe": "biome check --write --unsafe .",
|
||||||
"check:write": "biome check --write .",
|
"check:write": "biome check --write .",
|
||||||
"db:generate": "drizzle-kit generate",
|
"db:generate": "drizzle-kit generate",
|
||||||
"db:migrate": "drizzle-kit migrate",
|
"db:migrate": "drizzle-kit migrate",
|
||||||
"db:push": "drizzle-kit push",
|
"db:push": "drizzle-kit push",
|
||||||
"db:studio": "drizzle-kit studio",
|
"db:studio": "drizzle-kit studio",
|
||||||
"dev": "next dev --turbo",
|
"dev": "next dev --turbo",
|
||||||
"preview": "next build && next start",
|
"preview": "next build && next start",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit",
|
||||||
},
|
"test": "vitest --coverage --typecheck"
|
||||||
"dependencies": {
|
},
|
||||||
"@clerk/nextjs": "^6.27.1",
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.7.2",
|
"@clerk/nextjs": "^6.27.1",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
"@electric-sql/pglite": "^0.3.7",
|
||||||
"@fortawesome/react-fontawesome": "^0.2.3",
|
"@fortawesome/fontawesome-svg-core": "^6.7.2",
|
||||||
"@gsap/react": "^2.1.2",
|
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
||||||
"@hookform/resolvers": "^5.2.0",
|
"@fortawesome/react-fontawesome": "^0.2.3",
|
||||||
"@neondatabase/serverless": "^1.0.1",
|
"@gsap/react": "^2.1.2",
|
||||||
"@radix-ui/react-accordion": "^1.2.11",
|
"@hookform/resolvers": "^5.2.0",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.14",
|
"@neondatabase/serverless": "^1.0.1",
|
||||||
"@radix-ui/react-aspect-ratio": "^1.1.7",
|
"@radix-ui/react-accordion": "^1.2.11",
|
||||||
"@radix-ui/react-avatar": "^1.1.10",
|
"@radix-ui/react-alert-dialog": "^1.1.14",
|
||||||
"@radix-ui/react-checkbox": "^1.3.2",
|
"@radix-ui/react-aspect-ratio": "^1.1.7",
|
||||||
"@radix-ui/react-collapsible": "^1.1.11",
|
"@radix-ui/react-avatar": "^1.1.10",
|
||||||
"@radix-ui/react-context-menu": "^2.2.15",
|
"@radix-ui/react-checkbox": "^1.3.2",
|
||||||
"@radix-ui/react-dialog": "^1.1.14",
|
"@radix-ui/react-collapsible": "^1.1.11",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
"@radix-ui/react-context-menu": "^2.2.15",
|
||||||
"@radix-ui/react-hover-card": "^1.1.14",
|
"@radix-ui/react-dialog": "^1.1.14",
|
||||||
"@radix-ui/react-label": "^2.1.7",
|
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||||
"@radix-ui/react-menubar": "^1.1.15",
|
"@radix-ui/react-hover-card": "^1.1.14",
|
||||||
"@radix-ui/react-navigation-menu": "^1.2.13",
|
"@radix-ui/react-label": "^2.1.7",
|
||||||
"@radix-ui/react-popover": "^1.1.14",
|
"@radix-ui/react-menubar": "^1.1.15",
|
||||||
"@radix-ui/react-progress": "^1.1.7",
|
"@radix-ui/react-navigation-menu": "^1.2.13",
|
||||||
"@radix-ui/react-radio-group": "^1.3.7",
|
"@radix-ui/react-popover": "^1.1.14",
|
||||||
"@radix-ui/react-scroll-area": "^1.2.9",
|
"@radix-ui/react-progress": "^1.1.7",
|
||||||
"@radix-ui/react-select": "^2.2.5",
|
"@radix-ui/react-radio-group": "^1.3.7",
|
||||||
"@radix-ui/react-separator": "^1.1.7",
|
"@radix-ui/react-scroll-area": "^1.2.9",
|
||||||
"@radix-ui/react-slider": "^1.3.5",
|
"@radix-ui/react-select": "^2.2.5",
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-separator": "^1.1.7",
|
||||||
"@radix-ui/react-switch": "^1.2.5",
|
"@radix-ui/react-slider": "^1.3.5",
|
||||||
"@radix-ui/react-tabs": "^1.1.12",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"@radix-ui/react-toggle": "^1.1.9",
|
"@radix-ui/react-switch": "^1.2.5",
|
||||||
"@radix-ui/react-toggle-group": "^1.1.10",
|
"@radix-ui/react-tabs": "^1.1.12",
|
||||||
"@radix-ui/react-tooltip": "^1.2.7",
|
"@radix-ui/react-toggle": "^1.1.9",
|
||||||
"@t3-oss/env-nextjs": "^0.12.0",
|
"@radix-ui/react-toggle-group": "^1.1.10",
|
||||||
"@tanstack/react-query": "^5.83.0",
|
"@radix-ui/react-tooltip": "^1.2.7",
|
||||||
"@tanstack/react-query-next-experimental": "^5.83.0",
|
"@t3-oss/env-nextjs": "^0.12.0",
|
||||||
"@trpc/client": "^11.4.3",
|
"@tanstack/react-query": "^5.83.0",
|
||||||
"@trpc/next": "^11.4.3",
|
"@tanstack/react-query-next-experimental": "^5.83.0",
|
||||||
"@trpc/react-query": "^11.4.3",
|
"@trpc/client": "^11.4.3",
|
||||||
"@trpc/server": "^11.4.3",
|
"@trpc/next": "^11.4.3",
|
||||||
"@uiw/react-md-editor": "^4.0.8",
|
"@trpc/react-query": "^11.4.3",
|
||||||
"class-variance-authority": "^0.7.1",
|
"@trpc/server": "^11.4.3",
|
||||||
"clsx": "^2.1.1",
|
"@uiw/react-md-editor": "^4.0.8",
|
||||||
"cmdk": "^1.1.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"date-fns": "^4.1.0",
|
"clsx": "^2.1.1",
|
||||||
"date-format": "^3.0.0",
|
"cmdk": "^1.1.1",
|
||||||
"drizzle-orm": "^0.41.0",
|
"date-fns": "^4.1.0",
|
||||||
"drizzle-zod": "^0.7.1",
|
"date-format": "^3.0.0",
|
||||||
"embla-carousel-react": "^8.6.0",
|
"drizzle-orm": "^0.44.5",
|
||||||
"glazejs": "^2.0.1",
|
"drizzle-zod": "^0.7.1",
|
||||||
"gsap": "^3.13.0",
|
"embla-carousel-react": "^8.6.0",
|
||||||
"input-otp": "^1.4.2",
|
"glazejs": "^2.0.1",
|
||||||
"lucide-react": "^0.503.0",
|
"gsap": "^3.13.0",
|
||||||
"next": "15.4.0-canary.17",
|
"input-otp": "^1.4.2",
|
||||||
"next-themes": "^0.4.6",
|
"lucide-react": "^0.503.0",
|
||||||
"postgres": "^3.4.7",
|
"next": "15.4.0-canary.17",
|
||||||
"react": "^19.1.1",
|
"next-themes": "^0.4.6",
|
||||||
"react-day-picker": "8.10.1",
|
"postgres": "^3.4.7",
|
||||||
"react-dom": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-hook-form": "^7.61.1",
|
"react-day-picker": "9.8.1",
|
||||||
"react-markdown": "^10.1.0",
|
"react-dom": "^19.1.1",
|
||||||
"react-resizable-panels": "^3.0.3",
|
"react-hook-form": "^7.61.1",
|
||||||
"recharts": "^2.15.4",
|
"react-markdown": "^10.1.0",
|
||||||
"rehype-highlight": "^7.0.2",
|
"react-resizable-panels": "^3.0.3",
|
||||||
"rehype-raw": "^7.0.0",
|
"recharts": "^2.15.4",
|
||||||
"server-only": "^0.0.1",
|
"rehype-highlight": "^7.0.2",
|
||||||
"sonner": "^2.0.6",
|
"rehype-raw": "^7.0.0",
|
||||||
"tailwind-merge": "^3.3.1",
|
"server-only": "^0.0.1",
|
||||||
"tailwindcss-motion": "^1.1.1",
|
"sonner": "^2.0.6",
|
||||||
"type-fest": "^4.41.0",
|
"tailwind-merge": "^3.3.1",
|
||||||
"vaul": "^1.1.2",
|
"tailwindcss-motion": "^1.1.1",
|
||||||
"zod": "^3.25.76"
|
"type-fest": "^4.41.0",
|
||||||
},
|
"vaul": "^1.1.2",
|
||||||
"devDependencies": {
|
"zod": "^3.25.76"
|
||||||
"@biomejs/biome": "1.9.4",
|
},
|
||||||
"@tailwindcss/postcss": "^4.1.11",
|
"devDependencies": {
|
||||||
"@types/node": "^20.19.9",
|
"@biomejs/biome": "1.9.4",
|
||||||
"@types/react": "^19.1.8",
|
"@swc/jest": "^0.2.39",
|
||||||
"@types/react-dom": "^19.1.6",
|
"@tailwindcss/postcss": "^4.1.11",
|
||||||
"drizzle-kit": "^0.30.6",
|
"@testing-library/dom": "^10.4.1",
|
||||||
"postcss": "^8.5.6",
|
"@testing-library/jest-dom": "^6.6.4",
|
||||||
"tailwindcss": "^4.1.11",
|
"@testing-library/react": "^16.3.0",
|
||||||
"tw-animate-css": "^1.3.6",
|
"@types/jest": "^30.0.0",
|
||||||
"typescript": "^5.8.3"
|
"@types/node": "^20.19.9",
|
||||||
},
|
"@types/react": "^19.1.8",
|
||||||
"ct3aMetadata": {
|
"@types/react-dom": "^19.1.6",
|
||||||
"initVersion": "7.39.3"
|
"@vitejs/plugin-react": "^5.0.0",
|
||||||
},
|
"@vitest/coverage-v8": "^3.2.4",
|
||||||
"packageManager": "pnpm@10.7.1"
|
"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 }>) {
|
export default async function AdminWrap({children,}: Readonly<{ children: React.ReactNode }>) {
|
||||||
if (await isAdmin()) {
|
if (await isAdmin()) {
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ export default function CalendarFormField<T extends FieldValues>(params: { contr
|
|||||||
disabled={(date) =>
|
disabled={(date) =>
|
||||||
date > new Date() || date < new Date("1900-01-01")
|
date > new Date() || date < new Date("1900-01-01")
|
||||||
}
|
}
|
||||||
initialFocus
|
|
||||||
captionLayout="dropdown"
|
captionLayout="dropdown"
|
||||||
/>
|
/>
|
||||||
</PopoverContent>
|
</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 type { Control, FieldValues, Path } from "react-hook-form";
|
||||||
import { FormField,FormControl, FormItem, FormLabel } from "~/components/ui/form";
|
import { FormField,FormControl, FormItem, FormLabel } from "~/components/ui/form";
|
||||||
import { Select, SelectContent, SelectTrigger, SelectValue } from "~/components/ui/select";
|
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 (
|
return (
|
||||||
<FormField
|
<FormField
|
||||||
control={params.control}
|
control={params.control}
|
||||||
@@ -12,10 +12,10 @@ export default function SelectFormField<T extends FieldValues>(params: { control
|
|||||||
<FormLabel>
|
<FormLabel>
|
||||||
{params.label}
|
{params.label}
|
||||||
</FormLabel>
|
</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>
|
<FormControl>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder={params.placeholder} />
|
<SelectValue placeholder={params.placeholder ? undefined : params.placeholder} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@@ -9,7 +9,7 @@ export default function TexttInputFormField<T extends FieldValues>(params: { con
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="flex flex-col">
|
<FormItem className="flex flex-col">
|
||||||
<FormLabel>{params.label}</FormLabel>
|
<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>
|
</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 }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
Description
|
{params.label}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<MDEditor
|
<MDEditor
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export default async function AdminSideBar() {
|
|||||||
<SimpleSidebarGroup lable="Projects">
|
<SimpleSidebarGroup lable="Projects">
|
||||||
<Link href={"/admin/project/create"}> Create Project </Link>
|
<Link href={"/admin/project/create"}> Create Project </Link>
|
||||||
<Link href={"/admin/project/techStack/create"}> Create Stack </Link>
|
<Link href={"/admin/project/techStack/create"}> Create Stack </Link>
|
||||||
|
<Link href={"/admin/project/list"}> Project List </Link>
|
||||||
</SimpleSidebarGroup>
|
</SimpleSidebarGroup>
|
||||||
<SimpleSidebarGroup lable="Blog">
|
<SimpleSidebarGroup lable="Blog">
|
||||||
<Link href={"/"}> Some Blog Action </Link>
|
<Link href={"/"}> Some Blog Action </Link>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export default function Page() {
|
|||||||
const {data} = trpc.category.select.useQuery({id: id})
|
const {data} = trpc.category.select.useQuery({id: id})
|
||||||
if (data !== undefined && data.length > 0) {
|
if (data !== undefined && data.length > 0) {
|
||||||
return (
|
return (
|
||||||
<CreateUpdateCvCategoryForm category={data[0]}/>
|
<CreateUpdateCvCategoryForm entity={data[0]}/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return (<></>)
|
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 { zodResolver } from '@hookform/resolvers/zod'
|
||||||
import { useForm } from 'react-hook-form'
|
import { useForm } from 'react-hook-form'
|
||||||
import { z } from "zod";
|
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 { 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 type { IterableElement } from 'type-fest'
|
||||||
import { entitySchemas, makeOnSuccess } from "~/lib/utils";
|
import { entitySchemas, makeOnSuccess } from "~/lib/utils";
|
||||||
import type { RouterOutputs } from "~/server/routers/_app";
|
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 { useState } from 'react';
|
||||||
import DependentText from '~/app/_components/DependentText';
|
import { SelectFormField, TextInputFormField } from '~/app/_components/Form/Fields';
|
||||||
export default function CreateUpdateCvCategoryForm(params:{className?:string,category?:IterableElement<RouterOutputs['category']['select']>,isUpdate?:boolean}) {
|
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 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>>({
|
const form = useForm<z.infer<typeof schemas.insert>>({
|
||||||
resolver: zodResolver(schemas.insert),
|
resolver: zodResolver(schemas.insert),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
id: params.category ? params.category.id : crypto.randomUUID(),
|
id: params.entity ? params.entity.id : crypto.randomUUID(),
|
||||||
name: params.category ? params.category.name : "",
|
name: params.entity ? params.entity.name : "",
|
||||||
layoutPosition: params.category ? params.category.layoutPosition : "col1"
|
layoutPosition: params.entity ? params.entity.layoutPosition : "col1"
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const createMutation = trpc.category.insert.useMutation({onSuccess: makeOnSuccess('create',form,setIsUpdate)})
|
let path = usePathname()
|
||||||
const updateMutation = trpc.category.update.useMutation({onSuccess: makeOnSuccess('update',form)})
|
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>) {
|
function onSubmit(values: z.infer<typeof schemas.insert>) {
|
||||||
isUpdate ?
|
id ?
|
||||||
updateMutation.mutate(values) :
|
updateMutation.mutate(values) :
|
||||||
createMutation.mutate(values)
|
createMutation.mutate(values)
|
||||||
}
|
}
|
||||||
//TODO use SelectFormField and TextInputFormField
|
|
||||||
return (
|
return (
|
||||||
<Card.Card className={params.className ? params.className : "w-5/6 lg:w-1/2"}>
|
<FormScaffold
|
||||||
<Card.CardHeader>
|
form={form}
|
||||||
<Card.CardTitle>
|
createMutation={createMutation}
|
||||||
<DependentText bool={isUpdate} true='Update Category' false='Create Category'/>
|
updateMutation={updateMutation}
|
||||||
</Card.CardTitle>
|
deleteMutation={deleteMutation}
|
||||||
</Card.CardHeader>
|
onSubmit={onSubmit}
|
||||||
<Card.CardContent>
|
title='Category'
|
||||||
<Form {...form}>
|
id={id}
|
||||||
<form
|
className={params.className}
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
>
|
||||||
className="space-y-8"
|
<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}>
|
||||||
<FormField
|
{schemas.insert.shape.layoutPosition.unwrap().unwrap().options.map((o) => (
|
||||||
control={form.control}
|
<SelectItem key={o} value={o}> {o} </SelectItem>
|
||||||
name="name"
|
))}
|
||||||
render={({ field }) => (
|
</SelectFormField>
|
||||||
<FormItem>
|
</FormScaffold>
|
||||||
<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>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ import { useGSAP } from '@gsap/react'
|
|||||||
import { Suspense, useRef } from "react";
|
import { Suspense, useRef } from "react";
|
||||||
import * as Card from '~/components/ui/card'
|
import * as Card from '~/components/ui/card'
|
||||||
import { useGsapContext } from "~/app/_providers/GsapProvicer";
|
import { useGsapContext } from "~/app/_providers/GsapProvicer";
|
||||||
import CollapsibleCvEntryForm from "../../entry/_components/CollapsibleForm";
|
import { CollapsibleForm } from "~/app/_components/Form/Components";
|
||||||
import CollapsibleCvCategoryForm from "../_components/CollapsibleForm";
|
import CreateUpdateCvEntryForm from "../../entry/_components/CreateUpdateForm";
|
||||||
import CreateUpdateCvCategoryForm from "../_components/CreateUpdateForm";
|
import CreateUpdateCvCategoryForm from "../_components/CreateUpdateForm";
|
||||||
export default function CvPage() {
|
export default function CvPage() {
|
||||||
const categories = trpc.category.select.useQuery({},{refetchInterval:1000});
|
const categories = trpc.category.select.useQuery({}, { refetchInterval: 1000 });
|
||||||
const entires = trpc.entry.select.useSuspenseQuery({},{refetchInterval:1000})
|
const entires = trpc.entry.select.useSuspenseQuery({}, { refetchInterval: 1000 })
|
||||||
const gsap = useGsapContext()
|
const gsap = useGsapContext()
|
||||||
const container = useRef<HTMLDivElement>(null);
|
const container = useRef<HTMLDivElement>(null);
|
||||||
useGSAP(() => {
|
useGSAP(() => {
|
||||||
@@ -34,31 +34,34 @@ export default function CvPage() {
|
|||||||
</Link>
|
</Link>
|
||||||
<Card.CardContent className="flex flex-row">
|
<Card.CardContent className="flex flex-row">
|
||||||
<div className="flex flex-col w-full">
|
<div className="flex flex-col w-full">
|
||||||
<CreateUpdateCvCategoryForm category={cat} className="w-full" />
|
<CreateUpdateCvCategoryForm entity={cat} className="w-full" />
|
||||||
<br />
|
<Card.Card className="w-full">
|
||||||
<span>Entries:</span>
|
<Card.CardHeader>
|
||||||
<Suspense fallback={(<></>)}>
|
<Card.CardTitle>
|
||||||
<div className="w-full">
|
Entries:
|
||||||
{
|
</Card.CardTitle>
|
||||||
entires[0].filter((e) => {return e.categoryId == cat.id}).length > 0 ? (
|
</Card.CardHeader>
|
||||||
<>
|
<Card.CardContent className="w-full">
|
||||||
{entires[0].filter((e) => {return e.categoryId == cat.id}).map((entry) => (
|
<Suspense fallback={(<></>)}>
|
||||||
<CollapsibleCvEntryForm key={entry.id} entry={entry}/>
|
{
|
||||||
))}
|
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" />
|
||||||
</div>
|
))}
|
||||||
</Suspense>
|
</>
|
||||||
<div className="flex flex-col w-full">
|
) : (<></>)
|
||||||
<CollapsibleCvEntryForm categoryId={cat.id} />
|
}
|
||||||
</div>
|
</Suspense>
|
||||||
|
<CollapsibleForm entityName="Entry" form={CreateUpdateCvEntryForm} />
|
||||||
|
</Card.CardContent>
|
||||||
|
</Card.Card>
|
||||||
</div>
|
</div>
|
||||||
</Card.CardContent>
|
</Card.CardContent>
|
||||||
</Card.Card>
|
</Card.Card>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
<CollapsibleCvCategoryForm />
|
<CollapsibleForm entityName="Category" form={CreateUpdateCvCategoryForm} />
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export default function Page() {
|
|||||||
const {data} = trpc.entry.select.useQuery({id: id})
|
const {data} = trpc.entry.select.useQuery({id: id})
|
||||||
if (data !== undefined && data.length > 0) {
|
if (data !== undefined && data.length > 0) {
|
||||||
return (
|
return (
|
||||||
<CreateUpdateCvEntryForm entry={data[0]}/>
|
<CreateUpdateCvEntryForm entity={data[0]}/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return (<></>)
|
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'
|
'use client'
|
||||||
import { zodResolver } from '@hookform/resolvers/zod'
|
import { zodResolver } from '@hookform/resolvers/zod'
|
||||||
import { useForm, type UseFormReturn } from 'react-hook-form'
|
import { useForm } from 'react-hook-form'
|
||||||
import { format } from 'date-fns'
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "~/components/ui/form";
|
import { SelectItem } from "~/components/ui/select";
|
||||||
import { Input } from "~/components/ui/input";
|
|
||||||
import { Select, SelectContent, SelectTrigger, SelectItem, SelectValue } from "~/components/ui/select";
|
|
||||||
import { trpc } from "~/app/_trpc/Client";
|
import { trpc } from "~/app/_trpc/Client";
|
||||||
import { Button } from "~/components/ui/button";
|
import { entitySchemas, ft, makeOnSuccess, tt } from "~/lib/utils";
|
||||||
import * as Card from '~/components/ui/card'
|
import { useRelationShip } from '~/lib/hooks';
|
||||||
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 { useTheme } from "next-themes"
|
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 type { RouterOutputs } from '~/server/routers/_app';
|
||||||
import DependentText from '~/app/_components/DependentText';
|
import { FormScaffold } from '~/app/_components/Form/Components';
|
||||||
import DependentFormMessaage from '~/app/_components/MutationFormMessage';
|
import { useState } from 'react';
|
||||||
import { useEffect, useState } from 'react';
|
import { SelectFormField, TextInputFormField, MdeFormField, CalenderFormField, BooleanFormField } from '~/app/_components/Form/Fields'
|
||||||
import SelectFormField from '~/app/_components/SelectFormField';
|
import { usePathname, useRouter } from 'next/navigation';
|
||||||
import TexttInputFormField from '~/app/_components/TextInputFormField';
|
export default function CreateUpdateCvEntryForm(params: { className?: string, entity?: IterableElement<RouterOutputs['entry']['select']>, isUpdate?: boolean }) {
|
||||||
import MdeFormField from '~/app/_components/MdeFormField';
|
const [id, setId] = useState<string | undefined>(params.entity ? params.entity.id : undefined)
|
||||||
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>()
|
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
const schemas = entitySchemas('cvEntry')
|
const schemas = entitySchemas('cvEntry')
|
||||||
const {data:categories,isSuccess: categoriesSuccess} = trpc.category.select.useQuery({})
|
const {data: categories,id:categoryId,name:categoryName,success:categoriesSuccess} = useRelationShip(trpc.category.select.useQuery({}),'name',id)
|
||||||
useEffect(() => {
|
|
||||||
if (isUpdate) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setCategoryId(categories?.at(0)?.id)
|
|
||||||
if (categories !== undefined && categories[0]?.name !== null) {
|
|
||||||
setCategoryName(categories[0]?.name)
|
|
||||||
}
|
|
||||||
},[categoriesSuccess])
|
|
||||||
let defaultValues = {
|
let defaultValues = {
|
||||||
id: params.entry ? params.entry.id : crypto.randomUUID(),
|
id: params.entity ? params.entity.id : crypto.randomUUID(),
|
||||||
title: params.entry ? params.entry.title : "",
|
title: params.entity ? params.entity.title : "",
|
||||||
description: params.entry ? params.entry.description : "",
|
description: params.entity ? params.entity.description : "",
|
||||||
categoryId: params.entry ? params.entry.categoryId : categoriesSuccess ? categories.at(0)?.id : "",
|
categoryId: params.entity ? params.entity.categoryId : categoriesSuccess ? categories?.at(0)?.id : "",
|
||||||
fromTime: params.entry ? ft(params.entry).fromTime : new Date(),
|
fromTime: params.entity ? ft(params.entity).fromTime : new Date(),
|
||||||
toTime: params.entry ? tt(params.entry).toTime : new Date(),
|
toTime: params.entity ? tt(params.entity).toTime : new Date(),
|
||||||
hideDates: params.entry ? params.entry.hideDates : false,
|
hideDates: params.entity ? params.entity.hideDates : false,
|
||||||
};
|
};
|
||||||
const form = useForm<z.infer<typeof schemas.insert>>({
|
const form = useForm<z.infer<typeof schemas.insert>>({
|
||||||
resolver: zodResolver(schemas.insert),
|
resolver: zodResolver(schemas.insert),
|
||||||
defaultValues: defaultValues,
|
defaultValues: defaultValues,
|
||||||
// values: params.entry ? ut(ct(tt(ff(params.entry)))) : defaultValues
|
|
||||||
})
|
})
|
||||||
const createMutation = trpc.entry.insert.useMutation({onSuccess: makeOnSuccess('create',form,setIsUpdate)})
|
let path = usePathname()
|
||||||
const updateMutation = trpc.entry.update.useMutation({onSuccess: makeOnSuccess('update',form)})
|
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>) {
|
function onSubmit(values: z.infer<typeof schemas.insert>) {
|
||||||
isUpdate ?
|
id ?
|
||||||
updateMutation.mutate(values) :
|
updateMutation.mutate(values) :
|
||||||
createMutation.mutate(values)
|
createMutation.mutate(values)
|
||||||
}
|
}
|
||||||
//TODO use SelectFormField and TextInputFormField
|
|
||||||
return (
|
return (
|
||||||
<Card.Card className={params.className ? params.className : "w-5/6 lg:w-1/2"}>
|
<FormScaffold
|
||||||
<Card.CardHeader>
|
form={form}
|
||||||
<Card.CardTitle>
|
createMutation={createMutation}
|
||||||
<DependentText bool={isUpdate} true='Update Entry' false='Create Entry' />
|
updateMutation={updateMutation}
|
||||||
</Card.CardTitle>
|
deleteMutation={deleteMutation}
|
||||||
</Card.CardHeader>
|
onSubmit={onSubmit}
|
||||||
<Card.CardContent>
|
title='Entry'
|
||||||
<Form {...form}>
|
id={id}
|
||||||
<form
|
className={params.className}
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
>
|
||||||
className="space-y-8"
|
<SelectFormField control={form.control} name='categoryId' label='Category' defaultValue={categoryId} placeholder={categoryName}>
|
||||||
>
|
{
|
||||||
<SelectFormField control={form.control} name='categoryId' label='Category' defaultValue={categoryId} placeholder={categoryName}>
|
categoriesSuccess ?
|
||||||
{
|
<>
|
||||||
categoriesSuccess ?
|
{categories.map((c) => {
|
||||||
<>
|
return (<SelectItem key={c.id} value={c.id}> {c.name} </SelectItem>)
|
||||||
{categories.map((c) => {
|
})}
|
||||||
return (<SelectItem key={c.id} value={c.id}> {c.name} </SelectItem>)
|
</> :
|
||||||
})}
|
<SelectItem key="abc" value="abcd" />
|
||||||
</> :
|
}
|
||||||
<SelectItem key="abc" value="abcd"/>
|
</SelectFormField>
|
||||||
}
|
<TextInputFormField control={form.control} name='title' label='Title' />
|
||||||
</SelectFormField>
|
<MdeFormField control={form.control} name='description' label='Description' dataColorMode={(theme as "dark" | "light") ? (theme as "dark" | "light") : "dark"} />
|
||||||
<TexttInputFormField control={form.control} name='title' label='Title'/>
|
<CalenderFormField control={form.control} name='fromTime' label='From Date' />
|
||||||
<MdeFormField control={form.control} name='description' label='Description' dataColorMode={(theme as "dark" | "light") ? (theme as "dark" | "light") : "dark"}/>
|
<CalenderFormField control={form.control} name='toTime' label='To Date' />
|
||||||
<CalendarFormField control={form.control} name='fromTime' label='From Date'/>
|
<BooleanFormField control={form.control} name='hideDates' label='Hide Dates' />
|
||||||
<CalendarFormField control={form.control} name='toTime' label='To Date'/>
|
</FormScaffold>
|
||||||
<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>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { zodResolver } from '@hookform/resolvers/zod'
|
||||||
import { useForm } from 'react-hook-form'
|
import { useForm } from 'react-hook-form'
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "~/components/ui/form";
|
import { SelectItem } from "~/components/ui/select";
|
||||||
import { Input } from "~/components/ui/input";
|
|
||||||
import { Select, SelectContent, SelectTrigger, SelectItem, SelectValue } from "~/components/ui/select";
|
|
||||||
import { trpc } from "~/app/_trpc/Client";
|
import { trpc } from "~/app/_trpc/Client";
|
||||||
import { Button } from "~/components/ui/button";
|
import type { IterableElement } from 'type-fest'
|
||||||
import * as Card from '~/components/ui/card'
|
|
||||||
import type { Entries, IterableElement } from 'type-fest'
|
|
||||||
import { entitySchemas, makeOnSuccess } from "~/lib/utils";
|
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 type { RouterOutputs } from '~/server/routers/_app';
|
||||||
import TexttInputFormField from '~/app/_components/TextInputFormField';
|
import { SelectFormField, TextInputFormField } from '~/app/_components/Form/Fields'
|
||||||
import SelectFormField from '~/app/_components/SelectFormField';
|
import { FormScaffold } from '~/app/_components/Form/Components';
|
||||||
import DependentFormMessaage from '~/app/_components/MutationFormMessage';
|
import { usePathname, useRouter } from 'next/navigation';
|
||||||
import DependentText from '~/app/_components/DependentText';
|
import { makeUseRelationShipWithNameIndex } from '~/lib/hooks';
|
||||||
export default function CreateUpdateProjectForm(params: { className?: string, project?: IterableElement<RouterOutputs['project']['select']>, isUpdate?: boolean }) {
|
export default function CreateUpdateProjectForm(params: { className?: string, entity?: IterableElement<RouterOutputs['project']['select']> }) {
|
||||||
const [isUpdate, setIsUpdate] = useState<boolean>(params.isUpdate ? params.isUpdate : (params.project ? true : false))
|
const [id, setId] = useState<string|undefined>(params.entity ? params.entity.id : undefined)
|
||||||
const [stackId, setstackId] = useState<string | undefined>()
|
|
||||||
const [stackName, setstackName] = useState<string | undefined>()
|
|
||||||
const schemas = entitySchemas('project')
|
const schemas = entitySchemas('project')
|
||||||
const { data: stacks, isSuccess: stacksSuccess } = trpc.techStack.select.useQuery({})
|
console.log('using trpc')
|
||||||
useEffect(() => {
|
const { data:stacks,id:stackId, name:stackName,success:stacksSuccess, error:stackError } = makeUseRelationShipWithNameIndex('stackItems')(trpc.techStack.select.useQuery({}),id,(items) => {return items ? items.join('-') : ""})
|
||||||
if (isUpdate) {
|
console.log(stackError)
|
||||||
return
|
|
||||||
}
|
|
||||||
setstackId(stacks?.at(0)?.id)
|
|
||||||
if (stacks !== undefined) {
|
|
||||||
setstackName(stacks.at(0)?.stackItems ? stacks.at(0)?.stackItems?.join("-") : "")
|
|
||||||
}
|
|
||||||
}, [stacksSuccess])
|
|
||||||
const form = useForm<z.infer<typeof schemas.insert>>({
|
const form = useForm<z.infer<typeof schemas.insert>>({
|
||||||
resolver: zodResolver(schemas.insert),
|
resolver: zodResolver(schemas.insert),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
id: params.project ? params.project.id : crypto.randomUUID(),
|
id: id ? id : crypto.randomUUID(),
|
||||||
stackId: params.project ? params.project.stackId : stacksSuccess ? stacks?.at(0)?.id : "",
|
stackId: params.entity ? params.entity.stackId : stacksSuccess ? stacks?.at(0)?.id : "",
|
||||||
releaseStatus: params.project ? params.project.releaseStatus : "unreleased",
|
releaseStatus: params.entity ? params.entity.releaseStatus : "unreleased",
|
||||||
releaseLink: params.project ? params.project.releaseLink : "",
|
releaseLink: params.entity ? params.entity.releaseLink : "",
|
||||||
sourceType: params.project ? params.project.sourceType : "open",
|
sourceType: params.entity ? params.entity.sourceType : "open",
|
||||||
sourceLink: params.project ? params.project.sourceLink : ""
|
sourceLink: params.entity ? params.entity.sourceLink : ""
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const createMutation = trpc.project.insert.useMutation({
|
let path = usePathname()
|
||||||
onSuccess: makeOnSuccess('create',form,setIsUpdate)
|
let router = useRouter()
|
||||||
})
|
const createMutation = trpc.project.insert.useMutation({ onSuccess: makeOnSuccess('create', form, setId) })
|
||||||
const updateMutation = trpc.project.update.useMutation({
|
const updateMutation = trpc.project.update.useMutation({ onSuccess: makeOnSuccess('update', form) })
|
||||||
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>) {
|
function onSubmit(values: z.infer<typeof schemas.insert>) {
|
||||||
params.project ?
|
id ?
|
||||||
updateMutation.mutate(values) :
|
updateMutation.mutate(values) :
|
||||||
createMutation.mutate(values)
|
createMutation.mutate(values)
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Card.Card className={params.className ? params.className : "w-5/6 lg:w-1/2"}>
|
<FormScaffold
|
||||||
<Card.CardHeader>
|
form={form}
|
||||||
<Card.CardTitle>
|
createMutation={createMutation}
|
||||||
<DependentText bool={isUpdate} true='Update Project' false='Create Project'/>
|
updateMutation={updateMutation}
|
||||||
</Card.CardTitle>
|
deleteMutation={deleteMutation}
|
||||||
</Card.CardHeader>
|
onSubmit={onSubmit}
|
||||||
<Card.CardContent>
|
title='Project'
|
||||||
<Form {...form}>
|
id={id}
|
||||||
<form
|
className={params.className}
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
>
|
||||||
className="space-y-8"
|
<SelectFormField control={form.control} name='stackId' label='Stack' defaultValue={stackId} placeholder={stackName} >
|
||||||
>
|
{
|
||||||
<SelectFormField control={form.control} name='stackId' label='Stack' defaultValue={stackId} placeholder={stackName} >
|
stacksSuccess ?
|
||||||
{
|
<>
|
||||||
stacksSuccess ?
|
{stacks?.map((stack) => {
|
||||||
<>
|
return (<SelectItem key={stack.id} value={stack.id}> {stack.stackItems ? stack.stackItems.join("-") : "Empty Stack"} </SelectItem>)
|
||||||
{stacks.map((stack) => {
|
})}
|
||||||
return (<SelectItem key={stack.id} value={stack.id}> {stack.stackItems ? stack.stackItems.join("-") : "Empty Stack"} </SelectItem>)
|
</> :
|
||||||
})}
|
<SelectItem key='placeholder' value="placeholder" />
|
||||||
</> :
|
}
|
||||||
<SelectItem key='placeholder' value="placeholder" />
|
</SelectFormField>
|
||||||
}
|
<TextInputFormField control={form.control} name='title' label='Title' />
|
||||||
</SelectFormField>
|
<SelectFormField control={form.control} name='sourceType' label='Source Type' defaultValue={'open'} placeholder='open' >
|
||||||
<TexttInputFormField control={form.control} name='title' label='Title' />
|
<SelectItem value="open"> open </SelectItem>
|
||||||
<SelectFormField control={form.control} name='sourceType' label='Source Type' defaultValue={'open'} placeholder='open' >
|
<SelectItem value="closed"> closed </SelectItem>
|
||||||
<SelectItem value="open"> open </SelectItem>
|
</SelectFormField>
|
||||||
<SelectItem value="closed"> closed </SelectItem>
|
<TextInputFormField control={form.control} label='Source Link' name='sourceLink' />
|
||||||
</SelectFormField>
|
<SelectFormField control={form.control} name='releaseStatus' label='Release Status' defaultValue={'unreleased'} placeholder='unreleased' >
|
||||||
<TexttInputFormField control={form.control} label='Source Link' name='sourceLink' />
|
<SelectItem value="released"> released </SelectItem>
|
||||||
<SelectFormField control={form.control} name='releaseStatus' label='Release Status' defaultValue={'unreleased'} placeholder='unreleased' >
|
<SelectItem value="unreleased"> unreleased </SelectItem>
|
||||||
<SelectItem value="released"> released </SelectItem>
|
</SelectFormField>
|
||||||
<SelectItem value="unreleased"> unreleased </SelectItem>
|
<TextInputFormField control={form.control} label='Release Link' name='releaseLink' />
|
||||||
</SelectFormField>
|
</FormScaffold>
|
||||||
<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>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
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 { zodResolver } from '@hookform/resolvers/zod'
|
||||||
import { useForm } from 'react-hook-form'
|
import { useForm } from 'react-hook-form'
|
||||||
import { z } from "zod";
|
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 { trpc } from "~/app/_trpc/Client";
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
import * as Card from '~/components/ui/card'
|
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 { stackItemEnum } from "~/server/db/schema";
|
||||||
import { ToggleGroup, ToggleGroupItem } from "~/components/ui/toggle-group";
|
|
||||||
import type { RouterOutputs } from '~/server/routers/_app';
|
import type { RouterOutputs } from '~/server/routers/_app';
|
||||||
import { entitySchemas } from '~/lib/utils';
|
import { entitySchemas, makeOnSuccess } from '~/lib/utils';
|
||||||
import { useState } from 'react';
|
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 schemas = entitySchemas('techStack')
|
||||||
const form = useForm<z.infer<typeof schemas.insert>>({
|
const form = useForm<z.infer<typeof schemas.insert>>({
|
||||||
resolver: zodResolver(schemas.insert),
|
resolver: zodResolver(schemas.insert),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
id: params.techStack ? params.techStack.id : crypto.randomUUID(),
|
id: params.entity ? params.entity.id : crypto.randomUUID(),
|
||||||
stackItems: params.techStack ? params.techStack.stackItems : [],
|
stackItems: params.entity ? params.entity.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])
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
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>) {
|
function onSubmit(values: z.infer<typeof schemas.insert>) {
|
||||||
params.techStack ?
|
params.entity ?
|
||||||
updateMutation.mutate(values) :
|
updateMutation.mutate(values) :
|
||||||
createMutation.mutate(values)
|
createMutation.mutate(values)
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Card.Card className={params.className ? params.className : "w-5/6 lg:w-1/2"}>
|
<FormScaffold
|
||||||
<Card.CardHeader>
|
form={form}
|
||||||
<Card.CardTitle>
|
createMutation={createMutation}
|
||||||
{params.techStack ? "Update" : "Create"} Tech Stack
|
updateMutation={updateMutation}
|
||||||
</Card.CardTitle>
|
deleteMutation={deleteMutation}
|
||||||
</Card.CardHeader>
|
onSubmit={onSubmit}
|
||||||
<Card.CardContent>
|
title='Entry'
|
||||||
<Form {...form}>
|
id={id}
|
||||||
<form
|
className={params.className}
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
>
|
||||||
className="space-y-8"
|
<MultiBooleanFormField control={form.control} name='stackItems' label='Stack Items' options={stackItemEnum.enumValues} defaultValues={params.entity?.stackItems} />
|
||||||
>
|
</FormScaffold>
|
||||||
<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>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,16 +12,16 @@ type CvCategoryProps = {
|
|||||||
children?: React.ReactElement<Parameters<typeof CvEntry>>
|
children?: React.ReactElement<Parameters<typeof CvEntry>>
|
||||||
}
|
}
|
||||||
export default function CvCategory(props:CvCategoryProps) {
|
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) {
|
if (query.data !== undefined) {
|
||||||
return (
|
return (
|
||||||
<Card className={cn(props.layout == "row" ? "w-full" : "","gsapan")}>
|
<Card className={cn(props.layout == "row" ? "w-full" : "","gsapan")}>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>
|
<CardTitle>
|
||||||
{query.data?.name}
|
{query.data[0].name}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</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")}>
|
<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) => (
|
{query.data?.cvEntry.map((entry) => (
|
||||||
<CvEntry className={props.layout == "row" ? "w-full lg:w-fit" : undefined} key={entry.id} initialData={entry}/>
|
<CvEntry className={props.layout == "row" ? "w-full lg:w-fit" : undefined} key={entry.id} initialData={entry}/>
|
||||||
|
|||||||
@@ -1,75 +1,213 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { ChevronLeft, ChevronRight } from "lucide-react"
|
import {
|
||||||
import { DayPicker } from "react-day-picker"
|
ChevronDownIcon,
|
||||||
|
ChevronLeftIcon,
|
||||||
|
ChevronRightIcon,
|
||||||
|
} from "lucide-react"
|
||||||
|
import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker"
|
||||||
|
|
||||||
import { cn } from "~/lib/utils"
|
import { cn } from "~/lib/utils"
|
||||||
import { buttonVariants } from "~/components/ui/button"
|
import { Button, buttonVariants } from "~/components/ui/button"
|
||||||
|
|
||||||
function Calendar({
|
function Calendar({
|
||||||
className,
|
className,
|
||||||
classNames,
|
classNames,
|
||||||
showOutsideDays = true,
|
showOutsideDays = true,
|
||||||
|
captionLayout = "label",
|
||||||
|
buttonVariant = "ghost",
|
||||||
|
formatters,
|
||||||
|
components,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DayPicker>) {
|
}: React.ComponentProps<typeof DayPicker> & {
|
||||||
|
buttonVariant?: React.ComponentProps<typeof Button>["variant"]
|
||||||
|
}) {
|
||||||
|
const defaultClassNames = getDefaultClassNames()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DayPicker
|
<DayPicker
|
||||||
showOutsideDays={showOutsideDays}
|
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={{
|
classNames={{
|
||||||
months: "flex flex-col sm:flex-row gap-2",
|
root: cn("w-fit", defaultClassNames.root),
|
||||||
month: "flex flex-col gap-4",
|
months: cn(
|
||||||
caption: "flex justify-center pt-1 relative items-center w-full",
|
"flex gap-4 flex-col md:flex-row relative",
|
||||||
caption_label: "text-sm font-medium",
|
defaultClassNames.months
|
||||||
nav: "flex items-center gap-1",
|
|
||||||
nav_button: cn(
|
|
||||||
buttonVariants({ variant: "outline" }),
|
|
||||||
"size-7 bg-transparent p-0 opacity-50 hover:opacity-100"
|
|
||||||
),
|
),
|
||||||
nav_button_previous: "absolute left-1",
|
month: cn("flex flex-col w-full gap-4", defaultClassNames.month),
|
||||||
nav_button_next: "absolute right-1",
|
nav: cn(
|
||||||
table: "w-full border-collapse space-x-1",
|
"flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between",
|
||||||
head_row: "flex",
|
defaultClassNames.nav
|
||||||
head_cell:
|
),
|
||||||
"text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]",
|
button_previous: cn(
|
||||||
row: "flex w-full mt-2",
|
buttonVariants({ variant: buttonVariant }),
|
||||||
cell: cn(
|
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
|
||||||
"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",
|
defaultClassNames.button_previous
|
||||||
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"
|
button_next: cn(
|
||||||
: "[&:has([aria-selected])]:rounded-md"
|
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(
|
day: cn(
|
||||||
buttonVariants({ variant: "ghost" }),
|
"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",
|
||||||
"size-8 p-0 font-normal aria-selected:opacity-100"
|
defaultClassNames.day
|
||||||
),
|
),
|
||||||
day_range_start:
|
range_start: cn(
|
||||||
"day-range-start aria-selected:bg-primary aria-selected:text-primary-foreground",
|
"rounded-l-md bg-accent",
|
||||||
day_range_end:
|
defaultClassNames.range_start
|
||||||
"day-range-end aria-selected:bg-primary aria-selected:text-primary-foreground",
|
),
|
||||||
day_selected:
|
range_middle: cn("rounded-none", defaultClassNames.range_middle),
|
||||||
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
|
range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end),
|
||||||
day_today: "bg-accent text-accent-foreground",
|
today: cn(
|
||||||
day_outside:
|
"bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
|
||||||
"day-outside text-muted-foreground aria-selected:text-muted-foreground",
|
defaultClassNames.today
|
||||||
day_disabled: "text-muted-foreground opacity-50",
|
),
|
||||||
day_range_middle:
|
outside: cn(
|
||||||
"aria-selected:bg-accent aria-selected:text-accent-foreground",
|
"text-muted-foreground aria-selected:text-muted-foreground",
|
||||||
day_hidden: "invisible",
|
defaultClassNames.outside
|
||||||
|
),
|
||||||
|
disabled: cn(
|
||||||
|
"text-muted-foreground opacity-50",
|
||||||
|
defaultClassNames.disabled
|
||||||
|
),
|
||||||
|
hidden: cn("invisible", defaultClassNames.hidden),
|
||||||
...classNames,
|
...classNames,
|
||||||
}}
|
}}
|
||||||
components={{
|
components={{
|
||||||
IconLeft: ({ className, ...props }) => (
|
Root: ({ className, rootRef, ...props }) => {
|
||||||
<ChevronLeft className={cn("size-4", className)} {...props} />
|
return (
|
||||||
),
|
<div
|
||||||
IconRight: ({ className, ...props }) => (
|
data-slot="calendar"
|
||||||
<ChevronRight className={cn("size-4", className)} {...props} />
|
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}
|
{...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 (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="drawer-header"
|
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}
|
{...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 * as schema from "~/server/db/schema";
|
||||||
import { PgTable } from "drizzle-orm/pg-core";
|
import { PgTable } from "drizzle-orm/pg-core";
|
||||||
import type { FieldValues, Path, UseFormReturn } from "react-hook-form";
|
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[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs))
|
return twMerge(clsx(inputs))
|
||||||
}
|
}
|
||||||
@@ -15,19 +16,19 @@ export type SchemaKeys<S> = {
|
|||||||
}[keyof S]
|
}[keyof S]
|
||||||
|
|
||||||
export type Pretty<T> = {
|
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({
|
const { createInsertSchema, createUpdateSchema } = createSchemaFactory({
|
||||||
coerce: {
|
coerce: {
|
||||||
date: true
|
date: true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export type DatesAreString<T extends {[x:string]: any}> = {
|
|
||||||
[K in keyof T] : T[K] extends Date ? string : T[K]
|
|
||||||
}
|
|
||||||
|
|
||||||
// type X = {
|
// type X = {
|
||||||
// foo: Date
|
// foo: Date
|
||||||
// bar: number
|
// bar: number
|
||||||
@@ -35,7 +36,8 @@ export type DatesAreString<T extends {[x:string]: any}> = {
|
|||||||
|
|
||||||
// type Y = DatesAreString<X>
|
// 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) {
|
switch (uc) {
|
||||||
case 'update':
|
case 'update':
|
||||||
return (data: T[]) => {
|
return (data: T[]) => {
|
||||||
@@ -46,22 +48,32 @@ export function makeOnSuccess<T extends FieldValues>(uc: 'update' | 'create', fo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
case 'create':
|
case 'create':
|
||||||
if (setIsUpdate !== undefined) {
|
if (setId !== undefined) {
|
||||||
return (data: T[]) => {
|
return (data: T[]) => {
|
||||||
form.setValue(("id" as Path<T>), data[0] ? data[0].id : "");
|
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)
|
export function entitySchemas<T extends SchemaKeys<Schema>>(table: T)
|
||||||
// : {
|
: {
|
||||||
// insert: Pretty<BuildSchema<'insert', Schema[T]['_']['columns'], undefined>>
|
insert: Pretty<BuildSchema<'insert', Schema[T]['_']['columns'], undefined>>
|
||||||
// update: Pretty<BuildSchema<'update', Schema[T]['_']['columns'], undefined>>
|
update: Pretty<BuildSchema<'update', Schema[T]['_']['columns'], undefined>>
|
||||||
// select: Pretty<BuildSchema<'select', Schema[T]['_']['columns'], undefined>>
|
select: Pretty<BuildSchema<'select', Schema[T]['_']['columns'], undefined>>
|
||||||
// }
|
} {
|
||||||
{
|
|
||||||
const insertSchema = createInsertSchema<Schema[T]>(schema[table]);
|
const insertSchema = createInsertSchema<Schema[T]>(schema[table]);
|
||||||
const updateSchema = createUpdateSchema<Schema[T]>(schema[table]);
|
const updateSchema = createUpdateSchema<Schema[T]>(schema[table]);
|
||||||
const selectSchema = createSelectSchema<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;
|
const { input } = opts;
|
||||||
console.log(input)
|
|
||||||
if (input === undefined) {
|
if (input === undefined) {
|
||||||
throw new TRPCError({ message: "no update input", code: "BAD_REQUEST" })
|
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" })
|
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) => {
|
insert: publicProcedure.input(schemas.insert).mutation(async (opts) => {
|
||||||
let admin = await isAdmin();
|
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 : techStack } = trpcCrudRouterFromDrizzleEntity('techStack')
|
||||||
const { router : category } = trpcCrudRouterFromDrizzleEntity('cvCategory')
|
const { router : category } = trpcCrudRouterFromDrizzleEntity('cvCategory')
|
||||||
const { router : entry } = trpcCrudRouterFromDrizzleEntity('cvEntry')
|
const { router : entry } = trpcCrudRouterFromDrizzleEntity('cvEntry')
|
||||||
const root = {}
|
|
||||||
export const trpcRouter = router({
|
export const trpcRouter = router({
|
||||||
project: project,
|
project: project,
|
||||||
techStack: techStack,
|
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