diff --git a/biome.json b/biome.json index 09c7a78..689af8f 100644 --- a/biome.json +++ b/biome.json @@ -13,7 +13,21 @@ "linter": { "enabled": true, "rules": { - "recommended": true + "recommended": true, + "a11y": { + "noLabelWithoutControl": "off" + } } - } + }, + "overrides": [ + { + "includes": ["src/routeTree.gen.ts"], + "linter": { + "enabled": false + }, + "formatter": { + "enabled": false + } + } + ] } diff --git a/bun.lock b/bun.lock index a84d24e..aa056b8 100644 --- a/bun.lock +++ b/bun.lock @@ -7,8 +7,10 @@ "dependencies": { "@electric-sql/pglite": "^0.3.16", "@tailwindcss/vite": "^4.2.1", + "@tanstack/react-form": "^1.28.4", "@tanstack/react-router": "^1.166.7", "@tanstack/router-plugin": "^1.166.7", + "@tanstack/zod-form-adapter": "^0.42.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.577.0", @@ -564,8 +566,16 @@ "@tailwindcss/vite": ["@tailwindcss/vite@4.2.1", "", { "dependencies": { "@tailwindcss/node": "4.2.1", "@tailwindcss/oxide": "4.2.1", "tailwindcss": "4.2.1" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-TBf2sJjYeb28jD2U/OhwdW0bbOsxkWPwQ7SrqGf9sVcoYwZj7rkXljroBO9wKBut9XnmQLXanuDUeqQK0lGg/w=="], + "@tanstack/devtools-event-client": ["@tanstack/devtools-event-client@0.4.1", "", {}, "sha512-GRxmPw4OHZ2oZeIEUkEwt/NDvuEqzEYRAjzUVMs+I0pd4C7k1ySOiuJK2CqF+K/yEAR3YZNkW3ExrpDarh9Vwg=="], + + "@tanstack/form-core": ["@tanstack/form-core@1.28.4", "", { "dependencies": { "@tanstack/devtools-event-client": "^0.4.0", "@tanstack/pacer-lite": "^0.1.1", "@tanstack/store": "^0.9.1" } }, "sha512-2eox5ePrJ6kvA1DXD5QHk/GeGr3VFZ0uYR63UgQOe7bUg6h1JfXaIMqTjZK9sdGyE4oRNqFpoW54H0pZM7nObQ=="], + "@tanstack/history": ["@tanstack/history@1.161.4", "", {}, "sha512-Kp/WSt411ZWYvgXy6uiv5RmhHrz9cAml05AQPrtdAp7eUqvIDbMGPnML25OKbzR3RJ1q4wgENxDTvlGPa9+Mww=="], + "@tanstack/pacer-lite": ["@tanstack/pacer-lite@0.1.1", "", {}, "sha512-y/xtNPNt/YeyoVxE/JCx+T7yjEzpezmbb+toK8DDD1P4m7Kzs5YR956+7OKexG3f8aXgC3rLZl7b1V+yNUSy5w=="], + + "@tanstack/react-form": ["@tanstack/react-form@1.28.4", "", { "dependencies": { "@tanstack/form-core": "1.28.4", "@tanstack/react-store": "^0.9.1" }, "peerDependencies": { "react": "^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-ZGBwl9JM2u0kol7jAWpqAkr2JSHfXJaLPsFDZWPf+ewpVkwngTTW/rGgtoDe5uVpHoDIpOhzpPCAh6O1SjGEOg=="], + "@tanstack/react-router": ["@tanstack/react-router@1.166.7", "", { "dependencies": { "@tanstack/history": "1.161.4", "@tanstack/react-store": "^0.9.1", "@tanstack/router-core": "1.166.7", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-LLcXu2nrCn2WL+w0YAbg3CRZIIO2cYVSC3y+ZYlFBxBs4hh8eoNP1EWFvRLZGCFYpqON7x6qUf1u0W7tH0cJJw=="], "@tanstack/react-store": ["@tanstack/react-store@0.9.2", "", { "dependencies": { "@tanstack/store": "0.9.2", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Vt5usJE5sHG/cMechQfmwvwne6ktGCELe89Lmvoxe3LKRoFrhPa8OCKWs0NliG8HTJElEIj7PLtaBQIcux5pAQ=="], @@ -582,6 +592,8 @@ "@tanstack/virtual-file-routes": ["@tanstack/virtual-file-routes@1.161.4", "", {}, "sha512-42WoRePf8v690qG8yGRe/YOh+oHni9vUaUUfoqlS91U2scd3a5rkLtVsc6b7z60w3RogH0I00vdrC5AaeiZ18w=="], + "@tanstack/zod-form-adapter": ["@tanstack/zod-form-adapter@0.42.1", "", { "dependencies": { "@tanstack/form-core": "0.42.1" }, "peerDependencies": { "zod": "^3.x" } }, "sha512-hPRM0lawVKP64yurW4c6KHZH6altMo2MQN14hfi+GMBTKjO9S7bW1x5LPZ5cayoJE3mBvdlahpSGT5rYZtSbXQ=="], + "@testing-library/dom": ["@testing-library/dom@10.4.1", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg=="], "@testing-library/jest-dom": ["@testing-library/jest-dom@6.9.1", "", { "dependencies": { "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", "css.escape": "^1.5.1", "dom-accessibility-api": "^0.6.3", "picocolors": "^1.1.1", "redent": "^3.0.0" } }, "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA=="], @@ -1408,6 +1420,8 @@ "@tanstack/router-plugin/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "@tanstack/zod-form-adapter/@tanstack/form-core": ["@tanstack/form-core@0.42.1", "", { "dependencies": { "@tanstack/store": "^0.7.0" } }, "sha512-jTU0jyHqFceujdtPNv3jPVej1dTqBwa8TYdIyWB5BCwRVUBZEp1PiYEBkC9r92xu5fMpBiKc+JKud3eeVjuMiA=="], + "@testing-library/dom/aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="], "@testing-library/dom/dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="], @@ -1444,6 +1458,8 @@ "@rollup/plugin-node-resolve/@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + "@tanstack/zod-form-adapter/@tanstack/form-core/@tanstack/store": ["@tanstack/store@0.7.7", "", {}, "sha512-xa6pTan1bcaqYDS9BDpSiS63qa6EoDkPN9RsRaxHuDdVDNntzq3xNwR5YKTU/V3SkSyC9T4YVOPh2zRQN0nhIQ=="], + "filelist/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], "log-update/slice-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], diff --git a/package.json b/package.json index 94894d5..c3c9ce1 100644 --- a/package.json +++ b/package.json @@ -15,8 +15,10 @@ "dependencies": { "@electric-sql/pglite": "^0.3.16", "@tailwindcss/vite": "^4.2.1", + "@tanstack/react-form": "^1.28.4", "@tanstack/react-router": "^1.166.7", "@tanstack/router-plugin": "^1.166.7", + "@tanstack/zod-form-adapter": "^0.42.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.577.0", diff --git a/src/features/onboarding/components/onboarding-form.tsx b/src/features/onboarding/components/onboarding-form.tsx new file mode 100644 index 0000000..61c3361 --- /dev/null +++ b/src/features/onboarding/components/onboarding-form.tsx @@ -0,0 +1,172 @@ +import { useForm } from "@tanstack/react-form"; +import { useNavigate } from "@tanstack/react-router"; +import { Button } from "@/shared/components/ui/button"; +import { Input } from "@/shared/components/ui/input"; +import { Label } from "@/shared/components/ui/label"; +import type { ProzessSchritt } from "@/shared/db/schema"; +import { dbExec } from "@/shared/hooks/use-db"; +import { PROZESS_SCHRITTE } from "@/shared/lib/constants"; +import type { OnboardingData } from "../schema"; +import { onboardingSchema } from "../schema"; + +export function OnboardingForm() { + const navigate = useNavigate(); + + const form = useForm({ + defaultValues: { + name: "", + plz: "", + ort: "", + krankenkasse: "", + aktueller_schritt: "neu" as ProzessSchritt, + } satisfies OnboardingData, + validators: { + onSubmit: onboardingSchema, + }, + onSubmit: async ({ value }) => { + await dbExec( + `INSERT INTO nutzer (name, plz, ort, krankenkasse, aktueller_schritt) + VALUES ($1, $2, $3, $4, $5)`, + [ + value.name, + value.plz, + value.ort, + value.krankenkasse, + value.aktueller_schritt, + ], + ); + navigate({ to: "/prozess" as string }); + }, + }); + + return ( +
+ Erzähl uns ein wenig über dich, damit wir dich auf deinem Weg zur + Therapie unterstützen können. +
+{messages.join(", ")}
; +} diff --git a/src/features/onboarding/index.ts b/src/features/onboarding/index.ts new file mode 100644 index 0000000..9f03026 --- /dev/null +++ b/src/features/onboarding/index.ts @@ -0,0 +1,2 @@ +export { OnboardingForm } from "./components/onboarding-form"; +export { type OnboardingData, onboardingSchema } from "./schema"; diff --git a/src/features/onboarding/schema.test.ts b/src/features/onboarding/schema.test.ts new file mode 100644 index 0000000..bfb3c75 --- /dev/null +++ b/src/features/onboarding/schema.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, it } from "vitest"; +import { onboardingSchema } from "./schema"; + +describe("onboardingSchema", () => { + it("accepts valid data", () => { + const result = onboardingSchema.safeParse({ + name: "Max Mustermann", + plz: "10115", + ort: "Berlin", + krankenkasse: "TK", + aktueller_schritt: "neu", + }); + expect(result.success).toBe(true); + }); + + it("rejects invalid PLZ", () => { + const result = onboardingSchema.safeParse({ + name: "Max", + plz: "123", + ort: "Berlin", + krankenkasse: "TK", + aktueller_schritt: "neu", + }); + expect(result.success).toBe(false); + }); + + it("rejects empty name", () => { + const result = onboardingSchema.safeParse({ + name: "", + plz: "10115", + ort: "Berlin", + krankenkasse: "TK", + aktueller_schritt: "neu", + }); + expect(result.success).toBe(false); + }); +}); diff --git a/src/features/onboarding/schema.ts b/src/features/onboarding/schema.ts new file mode 100644 index 0000000..44a3a3c --- /dev/null +++ b/src/features/onboarding/schema.ts @@ -0,0 +1,12 @@ +import { z } from "zod"; +import { prozessSchrittEnum } from "@/shared/db/schema"; + +export const onboardingSchema = z.object({ + name: z.string().min(1, "Bitte gib deinen Namen ein."), + plz: z.string().regex(/^\d{5}$/, "Bitte gib eine gültige PLZ ein."), + ort: z.string().min(1, "Bitte gib deinen Ort ein."), + krankenkasse: z.string().min(1, "Bitte gib deine Krankenkasse ein."), + aktueller_schritt: prozessSchrittEnum, +}); + +export type OnboardingData = z.infer