add onboarding flow with form, validation, db persistence

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-11 11:08:01 +01:00
parent e39d758d39
commit 780e9816c1
12 changed files with 365 additions and 46 deletions

View File

@@ -13,7 +13,21 @@
"linter": { "linter": {
"enabled": true, "enabled": true,
"rules": { "rules": {
"recommended": true "recommended": true,
"a11y": {
"noLabelWithoutControl": "off"
} }
} }
},
"overrides": [
{
"includes": ["src/routeTree.gen.ts"],
"linter": {
"enabled": false
},
"formatter": {
"enabled": false
}
}
]
} }

View File

@@ -7,8 +7,10 @@
"dependencies": { "dependencies": {
"@electric-sql/pglite": "^0.3.16", "@electric-sql/pglite": "^0.3.16",
"@tailwindcss/vite": "^4.2.1", "@tailwindcss/vite": "^4.2.1",
"@tanstack/react-form": "^1.28.4",
"@tanstack/react-router": "^1.166.7", "@tanstack/react-router": "^1.166.7",
"@tanstack/router-plugin": "^1.166.7", "@tanstack/router-plugin": "^1.166.7",
"@tanstack/zod-form-adapter": "^0.42.1",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"lucide-react": "^0.577.0", "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=="], "@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/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-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=="], "@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/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/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=="], "@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/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/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=="], "@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=="], "@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=="], "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=="], "log-update/slice-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],

View File

@@ -15,8 +15,10 @@
"dependencies": { "dependencies": {
"@electric-sql/pglite": "^0.3.16", "@electric-sql/pglite": "^0.3.16",
"@tailwindcss/vite": "^4.2.1", "@tailwindcss/vite": "^4.2.1",
"@tanstack/react-form": "^1.28.4",
"@tanstack/react-router": "^1.166.7", "@tanstack/react-router": "^1.166.7",
"@tanstack/router-plugin": "^1.166.7", "@tanstack/router-plugin": "^1.166.7",
"@tanstack/zod-form-adapter": "^0.42.1",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"lucide-react": "^0.577.0", "lucide-react": "^0.577.0",

View File

@@ -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 (
<div className="flex min-h-[80vh] flex-col items-center justify-center">
<div className="w-full max-w-md space-y-6">
<div className="space-y-2 text-center">
<h1 className="text-2xl font-bold">Willkommen bei TherapyFinder</h1>
<p className="text-muted-foreground">
Erzähl uns ein wenig über dich, damit wir dich auf deinem Weg zur
Therapie unterstützen können.
</p>
</div>
<form
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
form.handleSubmit();
}}
className="space-y-4"
>
<form.Field name="name">
{(field) => (
<div className="space-y-1">
<Label htmlFor="name">Name</Label>
<Input
id="name"
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
placeholder="Max Mustermann"
/>
<FieldErrors errors={field.state.meta.errors} />
</div>
)}
</form.Field>
<div className="grid grid-cols-2 gap-4">
<form.Field name="plz">
{(field) => (
<div className="space-y-1">
<Label htmlFor="plz">PLZ</Label>
<Input
id="plz"
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
placeholder="10115"
inputMode="numeric"
maxLength={5}
/>
<FieldErrors errors={field.state.meta.errors} />
</div>
)}
</form.Field>
<form.Field name="ort">
{(field) => (
<div className="space-y-1">
<Label htmlFor="ort">Ort</Label>
<Input
id="ort"
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
placeholder="Berlin"
/>
<FieldErrors errors={field.state.meta.errors} />
</div>
)}
</form.Field>
</div>
<form.Field name="krankenkasse">
{(field) => (
<div className="space-y-1">
<Label htmlFor="krankenkasse">Krankenkasse</Label>
<Input
id="krankenkasse"
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
placeholder="TK"
/>
<FieldErrors errors={field.state.meta.errors} />
</div>
)}
</form.Field>
<form.Field name="aktueller_schritt">
{(field) => (
<div className="space-y-1">
<Label htmlFor="aktueller_schritt">Aktueller Schritt</Label>
<select
id="aktueller_schritt"
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) =>
field.handleChange(e.target.value as ProzessSchritt)
}
className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm"
>
{PROZESS_SCHRITTE.map((schritt) => (
<option key={schritt.key} value={schritt.key}>
{schritt.label}
</option>
))}
</select>
<FieldErrors errors={field.state.meta.errors} />
</div>
)}
</form.Field>
<Button type="submit" className="w-full">
Weiter
</Button>
</form>
</div>
</div>
);
}
function FieldErrors({
errors,
}: {
errors: ReadonlyArray<string | undefined | { message: string }>;
}) {
const messages = errors
.filter((e): e is string | { message: string } => e != null)
.map((e) => (typeof e === "string" ? e : e.message));
if (messages.length === 0) return null;
return <p className="text-sm text-destructive">{messages.join(", ")}</p>;
}

View File

@@ -0,0 +1,2 @@
export { OnboardingForm } from "./components/onboarding-form";
export { type OnboardingData, onboardingSchema } from "./schema";

View File

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

View File

@@ -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<typeof onboardingSchema>;

View File

@@ -8,52 +8,70 @@
// You should NOT make any changes in this file as it will be overwritten. // You should NOT make any changes in this file as it will be overwritten.
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root' import { Route as rootRouteImport } from "./routes/__root";
import { Route as IndexRouteImport } from './routes/index' import { Route as IndexRouteImport } from "./routes/index";
import { Route as OnboardingIndexRouteImport } from "./routes/onboarding/index";
const IndexRoute = IndexRouteImport.update({ const IndexRoute = IndexRouteImport.update({
id: '/', id: "/",
path: '/', path: "/",
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
} as any) } as any);
const OnboardingIndexRoute = OnboardingIndexRouteImport.update({
id: "/onboarding/",
path: "/onboarding/",
getParentRoute: () => rootRouteImport,
} as any);
export interface FileRoutesByFullPath { export interface FileRoutesByFullPath {
'/': typeof IndexRoute "/": typeof IndexRoute;
"/onboarding/": typeof OnboardingIndexRoute;
} }
export interface FileRoutesByTo { export interface FileRoutesByTo {
'/': typeof IndexRoute "/": typeof IndexRoute;
"/onboarding": typeof OnboardingIndexRoute;
} }
export interface FileRoutesById { export interface FileRoutesById {
__root__: typeof rootRouteImport __root__: typeof rootRouteImport;
'/': typeof IndexRoute "/": typeof IndexRoute;
"/onboarding/": typeof OnboardingIndexRoute;
} }
export interface FileRouteTypes { export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath fileRoutesByFullPath: FileRoutesByFullPath;
fullPaths: '/' fullPaths: "/" | "/onboarding/";
fileRoutesByTo: FileRoutesByTo fileRoutesByTo: FileRoutesByTo;
to: '/' to: "/" | "/onboarding";
id: '__root__' | '/' id: "__root__" | "/" | "/onboarding/";
fileRoutesById: FileRoutesById fileRoutesById: FileRoutesById;
} }
export interface RootRouteChildren { export interface RootRouteChildren {
IndexRoute: typeof IndexRoute IndexRoute: typeof IndexRoute;
OnboardingIndexRoute: typeof OnboardingIndexRoute;
} }
declare module '@tanstack/react-router' { declare module "@tanstack/react-router" {
interface FileRoutesByPath { interface FileRoutesByPath {
'/': { "/": {
id: '/' id: "/";
path: '/' path: "/";
fullPath: '/' fullPath: "/";
preLoaderRoute: typeof IndexRouteImport preLoaderRoute: typeof IndexRouteImport;
parentRoute: typeof rootRouteImport parentRoute: typeof rootRouteImport;
} };
"/onboarding/": {
id: "/onboarding/";
path: "/onboarding";
fullPath: "/onboarding/";
preLoaderRoute: typeof OnboardingIndexRouteImport;
parentRoute: typeof rootRouteImport;
};
} }
} }
const rootRouteChildren: RootRouteChildren = { const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute, IndexRoute: IndexRoute,
} OnboardingIndexRoute: OnboardingIndexRoute,
};
export const routeTree = rootRouteImport export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren) ._addFileChildren(rootRouteChildren)
._addFileTypes<FileRouteTypes>() ._addFileTypes<FileRouteTypes>();

View File

@@ -1,15 +1,14 @@
import { createFileRoute } from "@tanstack/react-router" import { createFileRoute, redirect } from "@tanstack/react-router";
import { Button } from "@/shared/components/ui/button" import { getDb } from "@/shared/db/client";
export const Route = createFileRoute("/")({ export const Route = createFileRoute("/")({
component: IndexPage, beforeLoad: async () => {
}) const db = await getDb();
const result = await db.query("SELECT id FROM nutzer LIMIT 1");
function IndexPage() { if (result.rows.length === 0) {
return ( throw redirect({ to: "/onboarding" });
<div className="flex min-h-screen flex-col items-center justify-center gap-4">
<h1 className="text-2xl font-bold">TherapyFinder</h1>
<Button>Get Started</Button>
</div>
)
} }
// /prozess route will be added in a future task
throw redirect({ to: "/onboarding" as string });
},
});

View File

@@ -0,0 +1,6 @@
import { createFileRoute } from "@tanstack/react-router";
import { OnboardingForm } from "@/features/onboarding/components/onboarding-form";
export const Route = createFileRoute("/onboarding/")({
component: () => <OnboardingForm />,
});

View File

@@ -0,0 +1,21 @@
import { type ComponentProps, forwardRef } from "react";
import { cn } from "@/shared/lib/utils";
const Input = forwardRef<HTMLInputElement, ComponentProps<"input">>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className,
)}
ref={ref}
{...props}
/>
);
},
);
Input.displayName = "Input";
export { Input };

View File

@@ -0,0 +1,20 @@
import { type ComponentProps, forwardRef } from "react";
import { cn } from "@/shared/lib/utils";
const Label = forwardRef<HTMLLabelElement, ComponentProps<"label">>(
({ className, ...props }, ref) => {
return (
<label
ref={ref}
className={cn(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
className,
)}
{...props}
/>
);
},
);
Label.displayName = "Label";
export { Label };