Add Child functionality

This commit is contained in:
Philip
2025-04-13 08:43:32 +02:00
parent 72762cb02c
commit fe790945fc
17 changed files with 2013 additions and 172 deletions

1479
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -14,8 +14,10 @@
"@prisma/client": "^6.5.0", "@prisma/client": "^6.5.0",
"@radix-ui/react-label": "^2.1.2", "@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-navigation-menu": "^1.2.5", "@radix-ui/react-navigation-menu": "^1.2.5",
"@radix-ui/react-popover": "^1.1.7",
"@radix-ui/react-select": "^2.1.7",
"@radix-ui/react-separator": "^1.1.2", "@radix-ui/react-separator": "^1.1.2",
"@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-slot": "^1.2.0",
"@tanstack/react-query": "^5.72.0", "@tanstack/react-query": "^5.72.0",
"@trpc/client": "^11.0.2", "@trpc/client": "^11.0.2",
"@trpc/next": "^11.0.2", "@trpc/next": "^11.0.2",
@@ -24,13 +26,17 @@
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"date-fns": "^4.1.0",
"lucide-react": "^0.487.0", "lucide-react": "^0.487.0",
"next": "15.2.4", "next": "15.2.4",
"next-auth": "^4.24.11", "next-auth": "^4.24.11",
"next-themes": "^0.4.6",
"prisma": "^6.5.0", "prisma": "^6.5.0",
"react": "^19.0.0", "react": "^19.0.0",
"react-day-picker": "^8.10.1",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-hook-form": "^7.55.0", "react-hook-form": "^7.55.0",
"sonner": "^2.0.3",
"tailwind-merge": "^3.1.0", "tailwind-merge": "^3.1.0",
"tw-animate-css": "^1.2.5", "tw-animate-css": "^1.2.5",
"zod": "^3.24.2" "zod": "^3.24.2"

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Child" ADD COLUMN "gender" TEXT;

View File

@@ -25,6 +25,7 @@ model Child {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
name String name String
birthDate DateTime birthDate DateTime
gender String? // can be "male", "female", "diverse", or "unknown"
userId String userId String
user User @relation(fields: [userId], references: [id]) user User @relation(fields: [userId], references: [id])
measurements Measurement[] measurements Measurement[]

52
src/app/app/page.tsx Normal file
View File

@@ -0,0 +1,52 @@
import { getAuthSession } from "@/lib/auth"
import { redirect } from "next/navigation"
import { Header } from "@/components/layout/header"
import { Footer } from "@/components/layout/footer"
import Link from "next/link"
import { Button } from "@/components/ui/button"
import { Plus } from "lucide-react"
export default async function DashboardPage() {
const session = await getAuthSession()
if (!session?.user) redirect("/login")
return (
<>
<Header />
<main className="min-h-[calc(100vh-160px)] px-6 py-10 bg-gradient-to-br from-rose-100 to-rose-50">
<div className="max-w-3xl mx-auto space-y-6">
<h1 className="text-3xl font-bold text-zinc-800">
Willkommen zurück, {session.user.name ?? "Elternteil"}! 👶
</h1>
<p className="text-zinc-600">
Dein digitales Eltern-Dashboard. Hier findest du alle Infos rund um dein Kind:
Wachstum, Impfungen, Zähne & mehr.
</p>
<div className="grid gap-4 grid-cols-1 md:grid-cols-2">
<div className="rounded-xl bg-white/70 shadow p-4 border border-zinc-200">
<div className="flex items-center justify-between mb-2">
<h2 className="font-semibold text-zinc-700">Kinder</h2>
<Link href="/dashboard/add-child">
<Button variant="ghost" size="sm">
<Plus className="h-4 w-4 mr-1" />
Kind hinzufügen
</Button>
</Link>
</div>
<p className="text-sm text-zinc-500">Du hast noch keine Kinder hinzugefügt.</p>
</div>
<div className="rounded-xl bg-white/70 shadow p-4 border border-zinc-200">
<h2 className="font-semibold text-zinc-700">Impfungen</h2>
<p className="text-sm text-zinc-500">Noch keine Einträge vorhanden.</p>
</div>
</div>
</div>
</main>
<Footer />
</>
)
}

View File

@@ -0,0 +1,191 @@
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { CalendarIcon } from "lucide-react";
import { format } from "date-fns";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Calendar } from "@/components/ui/calendar";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { trpc } from "@/utils/trpc";
import { Header } from "@/components/layout/header";
import { Footer } from "@/components/layout/footer";
const formSchema = z.object({
name: z.string().min(1, "Name wird benötigt"),
birthDate: z.date({
required_error: "Bitte wähle ein Geburtsdatum",
}),
gender: z.enum(["male", "female"], {
required_error: "Bitte wähle das Geschlecht",
}),
});
type FormValues = z.infer<typeof formSchema>;
export default function AddChildPage() {
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
name: "",
gender: "male",
},
});
const addChild = trpc.child.add.useMutation({
onSuccess: () => {
toast.success("Kind hinzugefügt", {
description: "Das Kind wurde erfolgreich hinzugefügt."
});
form.reset();
router.push("/app");
router.refresh();
},
onError: (err) => {
toast.error("Fehler", {
description: err.message
});
setIsLoading(false);
},
});
function onSubmit(values: FormValues) {
setIsLoading(true);
addChild.mutate({
name: values.name,
birthDate: values.birthDate.toISOString(),
gender: values.gender,
});
}
return (
<>
<Header />
<main className="min-h-[calc(100vh-160px)] px-6 py-10 bg-gradient-to-br from-rose-100 to-rose-50">
<div className="max-w-md mx-auto">
<h1 className="text-2xl font-bold text-zinc-800 mb-6">
Kind hinzufügen
</h1>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input className="bg-white" placeholder="Name des Kindes" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="birthDate"
render={({ field }) => (
<FormItem className="flex flex-col">
<FormLabel>Geburtsdatum</FormLabel>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<div className="flex h-9 w-full rounded-md border bg-white px-3 py-1 text-sm shadow-xs transition-[color,box-shadow] focus-within:border-ring focus-within:ring-ring/50 focus-within:ring-[3px]">
<Input
type="text"
className="border-0 bg-transparent p-0 focus-visible:ring-0 focus-visible:ring-offset-0"
placeholder="TT.MM.JJJJ"
value={field.value ? format(field.value, "dd.MM.yyyy") : ""}
onChange={(e) => {
const date = new Date(e.target.value);
if (!isNaN(date.getTime())) {
field.onChange(date);
}
}}
/>
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
</div>
</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
/>
</PopoverContent>
</Popover>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="gender"
render={({ field }) => (
<FormItem>
<FormLabel>Geschlecht</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<FormControl>
<SelectTrigger className="bg-white">
<SelectValue placeholder="Geschlecht auswählen" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="male">Bub</SelectItem>
<SelectItem value="female">Mädchen</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" className="w-full" disabled={isLoading}>
{isLoading ? "Wird hinzugefügt..." : "Kind hinzufügen"}
</Button>
</form>
</Form>
</div>
</main>
<Footer />
</>
);
}

View File

@@ -6,6 +6,7 @@ import { trpc } from "@/utils/trpc"
import { QueryClient, QueryClientProvider } from "@tanstack/react-query" import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { ReactNode } from "react" import { ReactNode } from "react"
import { httpBatchLink } from "@trpc/client" import { httpBatchLink } from "@trpc/client"
import { Toaster } from "sonner";
const queryClient = new QueryClient() const queryClient = new QueryClient()
const trpcClient = trpc.createClient({ const trpcClient = trpc.createClient({
@@ -29,10 +30,11 @@ const geistMono = Geist_Mono({
export default function RootLayout({ children }: { children: ReactNode }) { export default function RootLayout({ children }: { children: ReactNode }) {
return ( return (
<html lang="de"> <html lang="de">
<body> <body className={`${geistSans.variable} ${geistMono.variable} font-sans`}>
<trpc.Provider client={trpcClient} queryClient={queryClient}> <trpc.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
{children} {children}
<Toaster />
</QueryClientProvider> </QueryClientProvider>
</trpc.Provider> </trpc.Provider>
</body> </body>

View File

@@ -1,5 +1,5 @@
"use client" import { getAuthSession } from "@/lib/auth"
import { redirect } from "next/navigation"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card" import { Card, CardContent } from "@/components/ui/card"
import { Separator } from "@/components/ui/separator" import { Separator } from "@/components/ui/separator"
@@ -8,11 +8,16 @@ import Link from "next/link"
import { Header } from "@/components/layout/header" import { Header } from "@/components/layout/header"
import { Footer } from "@/components/layout/footer" import { Footer } from "@/components/layout/footer"
export default function Home() { export default async function Home() {
const session = await getAuthSession()
if (session?.user) {
redirect("/app")
}
return ( return (
<> <>
<Header /> <Header />
<main className="flex flex-col items-center justify-center min-h-[calc(100vh-160px)] bg-gradient-to-br from-rose-100 to-rose-50 px-4"> <main className="flex flex-col items-center justify-center min-h-[calc(100vh-160px)] bg-gradient-to-br from-rose-100 to-rose-50 px-4">
<Card className="max-w-md w-full shadow-xl rounded-2xl text-center bg-white/80 backdrop-blur-md border border-zinc-200 mt-10"> <Card className="max-w-md w-full shadow-xl rounded-2xl text-center bg-white/80 backdrop-blur-md border border-zinc-200 mt-10">
<CardContent className="p-8 space-y-6"> <CardContent className="p-8 space-y-6">

View File

@@ -0,0 +1,75 @@
"use client"
import * as React from "react"
import { ChevronLeft, ChevronRight } from "lucide-react"
import { DayPicker } from "react-day-picker"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
function Calendar({
className,
classNames,
showOutsideDays = true,
...props
}: React.ComponentProps<typeof DayPicker>) {
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn("p-3", className)}
classNames={{
months: "flex flex-col sm:flex-row gap-2",
month: "flex flex-col gap-4",
caption: "flex justify-center pt-1 relative items-center w-full",
caption_label: "text-sm font-medium",
nav: "flex items-center gap-1",
nav_button: cn(
buttonVariants({ variant: "outline" }),
"size-7 bg-transparent p-0 opacity-50 hover:opacity-100"
),
nav_button_previous: "absolute left-1",
nav_button_next: "absolute right-1",
table: "w-full border-collapse space-x-1",
head_row: "flex",
head_cell:
"text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]",
row: "flex w-full mt-2",
cell: cn(
"relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-range-end)]:rounded-r-md",
props.mode === "range"
? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
: "[&:has([aria-selected])]:rounded-md"
),
day: cn(
buttonVariants({ variant: "ghost" }),
"size-8 p-0 font-normal aria-selected:opacity-100"
),
day_range_start:
"day-range-start aria-selected:bg-primary aria-selected:text-primary-foreground",
day_range_end:
"day-range-end aria-selected:bg-primary aria-selected:text-primary-foreground",
day_selected:
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
day_today: "bg-accent text-accent-foreground",
day_outside:
"day-outside text-muted-foreground aria-selected:text-muted-foreground",
day_disabled: "text-muted-foreground opacity-50",
day_range_middle:
"aria-selected:bg-accent aria-selected:text-accent-foreground",
day_hidden: "invisible",
...classNames,
}}
components={{
IconLeft: ({ className, ...props }) => (
<ChevronLeft className={cn("size-4", className)} {...props} />
),
IconRight: ({ className, ...props }) => (
<ChevronRight className={cn("size-4", className)} {...props} />
),
}}
{...props}
/>
)
}
export { Calendar }

View File

@@ -0,0 +1,48 @@
"use client"
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import { cn } from "@/lib/utils"
function Popover({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
return <PopoverPrimitive.Root data-slot="popover" {...props} />
}
function PopoverTrigger({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
}
function PopoverContent({
className,
align = "center",
sideOffset = 4,
...props
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
return (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
data-slot="popover-content"
align={align}
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
)
}
function PopoverAnchor({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
}
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }

View File

@@ -0,0 +1,185 @@
"use client"
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Select({
...props
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
return <SelectPrimitive.Root data-slot="select" {...props} />
}
function SelectGroup({
...props
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
return <SelectPrimitive.Group data-slot="select-group" {...props} />
}
function SelectValue({
...props
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
return <SelectPrimitive.Value data-slot="select-value" {...props} />
}
function SelectTrigger({
className,
size = "default",
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
size?: "sm" | "default"
}) {
return (
<SelectPrimitive.Trigger
data-slot="select-trigger"
data-size={size}
className={cn(
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDownIcon className="size-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
)
}
function SelectContent({
className,
children,
position = "popper",
...props
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
data-slot="select-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
)
}
function SelectLabel({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
return (
<SelectPrimitive.Label
data-slot="select-label"
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
{...props}
/>
)
}
function SelectItem({
className,
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
return (
<SelectPrimitive.Item
data-slot="select-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className
)}
{...props}
>
<span className="absolute right-2 flex size-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
)
}
function SelectSeparator({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
return (
<SelectPrimitive.Separator
data-slot="select-separator"
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
{...props}
/>
)
}
function SelectScrollUpButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
return (
<SelectPrimitive.ScrollUpButton
data-slot="select-scroll-up-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUpIcon className="size-4" />
</SelectPrimitive.ScrollUpButton>
)
}
function SelectScrollDownButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
return (
<SelectPrimitive.ScrollDownButton
data-slot="select-scroll-down-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDownIcon className="size-4" />
</SelectPrimitive.ScrollDownButton>
)
}
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
}

View File

@@ -0,0 +1,25 @@
"use client"
import { useTheme } from "next-themes"
import { Toaster as Sonner, ToasterProps } from "sonner"
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme()
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
style={
{
"--normal-bg": "var(--popover)",
"--normal-text": "var(--popover-foreground)",
"--normal-border": "var(--border)",
} as React.CSSProperties
}
{...props}
/>
)
}
export { Toaster }

View File

@@ -1,8 +1,37 @@
import { router, publicProcedure } from "@/server/trpc" import { z } from "zod";
import { prisma } from "../../../lib/prisma" import { protectedProcedure, router } from "@/server/trpc";
import type { Prisma } from "@prisma/client";
export const childRouter = router({ export const childRouter = router({
getAll: publicProcedure.query(async () => { getAll: protectedProcedure.query(async ({ ctx }) => {
return prisma.child.findMany() return ctx.prisma.child.findMany({
where: {
userId: ctx.session.user.id
}
});
}), }),
add: protectedProcedure
.input(
z.object({
name: z.string().min(1, "Name is required"),
birthDate: z.string().refine((date) => !isNaN(Date.parse(date)), "Invalid date"),
gender: z.enum(["male", "female", "diverse", "unknown"]).optional(),
}) })
)
.mutation(async ({ ctx, input }) => {
const { name, birthDate, gender } = input;
const userId = ctx.session.user.id;
const data: Prisma.ChildCreateInput = {
name,
birthDate: new Date(birthDate),
gender,
user: {
connect: { id: userId }
}
};
return ctx.prisma.child.create({ data });
}),
});

View File

@@ -29,6 +29,15 @@ export const authOptions: NextAuthOptions = {
}, },
}), }),
], ],
callbacks: {
session: ({ session, token }) => ({
...session,
user: {
...session.user,
id: token.sub,
},
}),
},
session: { session: {
strategy: "jwt", strategy: "jwt",
}, },

View File

@@ -1,9 +1,11 @@
import { getAuthSession } from "@/lib/auth" import { getAuthSession } from "@/lib/auth"
import { prisma } from "@/lib/prisma"
export const createContext = async () => { export const createContext = async () => {
const session = await getAuthSession() const session = await getAuthSession()
return { return {
session, session,
prisma,
} }
} }

View File

@@ -1,4 +1,4 @@
import { initTRPC } from "@trpc/server" import { initTRPC, TRPCError } from "@trpc/server"
import { ZodError } from "zod" import { ZodError } from "zod"
import type { Context } from "./context" import type { Context } from "./context"
@@ -13,5 +13,21 @@ const t = initTRPC.context<Context>().create({
}, },
}) })
const isAuthenticated = t.middleware(async ({ ctx, next }) => {
if (!ctx.session?.user) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You must be logged in to access this resource",
})
}
return next({
ctx: {
...ctx,
session: { ...ctx.session, user: ctx.session.user },
},
})
})
export const router = t.router export const router = t.router
export const publicProcedure = t.procedure export const publicProcedure = t.procedure
export const protectedProcedure = t.procedure.use(isAuthenticated)

12
src/types/next-auth.d.ts vendored Normal file
View File

@@ -0,0 +1,12 @@
import 'next-auth';
declare module 'next-auth' {
interface Session {
user: {
id: string;
email?: string | null;
name?: string | null;
image?: string | null;
};
}
}