add onboarding flow with form, validation, db persistence
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
16
biome.json
16
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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
16
bun.lock
16
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=="],
|
||||
|
||||
@@ -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",
|
||||
|
||||
172
src/features/onboarding/components/onboarding-form.tsx
Normal file
172
src/features/onboarding/components/onboarding-form.tsx
Normal 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>;
|
||||
}
|
||||
2
src/features/onboarding/index.ts
Normal file
2
src/features/onboarding/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { OnboardingForm } from "./components/onboarding-form";
|
||||
export { type OnboardingData, onboardingSchema } from "./schema";
|
||||
37
src/features/onboarding/schema.test.ts
Normal file
37
src/features/onboarding/schema.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
12
src/features/onboarding/schema.ts
Normal file
12
src/features/onboarding/schema.ts
Normal 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>;
|
||||
@@ -8,52 +8,70 @@
|
||||
// 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.
|
||||
|
||||
import { Route as rootRouteImport } from './routes/__root'
|
||||
import { Route as IndexRouteImport } from './routes/index'
|
||||
import { Route as rootRouteImport } from "./routes/__root";
|
||||
import { Route as IndexRouteImport } from "./routes/index";
|
||||
import { Route as OnboardingIndexRouteImport } from "./routes/onboarding/index";
|
||||
|
||||
const IndexRoute = IndexRouteImport.update({
|
||||
id: '/',
|
||||
path: '/',
|
||||
id: "/",
|
||||
path: "/",
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
} as any);
|
||||
const OnboardingIndexRoute = OnboardingIndexRouteImport.update({
|
||||
id: "/onboarding/",
|
||||
path: "/onboarding/",
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any);
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
'/': typeof IndexRoute
|
||||
"/": typeof IndexRoute;
|
||||
"/onboarding/": typeof OnboardingIndexRoute;
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
'/': typeof IndexRoute
|
||||
"/": typeof IndexRoute;
|
||||
"/onboarding": typeof OnboardingIndexRoute;
|
||||
}
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport
|
||||
'/': typeof IndexRoute
|
||||
__root__: typeof rootRouteImport;
|
||||
"/": typeof IndexRoute;
|
||||
"/onboarding/": typeof OnboardingIndexRoute;
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fullPaths: '/'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to: '/'
|
||||
id: '__root__' | '/'
|
||||
fileRoutesById: FileRoutesById
|
||||
fileRoutesByFullPath: FileRoutesByFullPath;
|
||||
fullPaths: "/" | "/onboarding/";
|
||||
fileRoutesByTo: FileRoutesByTo;
|
||||
to: "/" | "/onboarding";
|
||||
id: "__root__" | "/" | "/onboarding/";
|
||||
fileRoutesById: FileRoutesById;
|
||||
}
|
||||
export interface RootRouteChildren {
|
||||
IndexRoute: typeof IndexRoute
|
||||
IndexRoute: typeof IndexRoute;
|
||||
OnboardingIndexRoute: typeof OnboardingIndexRoute;
|
||||
}
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
declare module "@tanstack/react-router" {
|
||||
interface FileRoutesByPath {
|
||||
'/': {
|
||||
id: '/'
|
||||
path: '/'
|
||||
fullPath: '/'
|
||||
preLoaderRoute: typeof IndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
"/": {
|
||||
id: "/";
|
||||
path: "/";
|
||||
fullPath: "/";
|
||||
preLoaderRoute: typeof IndexRouteImport;
|
||||
parentRoute: typeof rootRouteImport;
|
||||
};
|
||||
"/onboarding/": {
|
||||
id: "/onboarding/";
|
||||
path: "/onboarding";
|
||||
fullPath: "/onboarding/";
|
||||
preLoaderRoute: typeof OnboardingIndexRouteImport;
|
||||
parentRoute: typeof rootRouteImport;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
IndexRoute: IndexRoute,
|
||||
}
|
||||
OnboardingIndexRoute: OnboardingIndexRoute,
|
||||
};
|
||||
export const routeTree = rootRouteImport
|
||||
._addFileChildren(rootRouteChildren)
|
||||
._addFileTypes<FileRouteTypes>()
|
||||
._addFileTypes<FileRouteTypes>();
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
import { Button } from "@/shared/components/ui/button"
|
||||
import { createFileRoute, redirect } from "@tanstack/react-router";
|
||||
import { getDb } from "@/shared/db/client";
|
||||
|
||||
export const Route = createFileRoute("/")({
|
||||
component: IndexPage,
|
||||
})
|
||||
|
||||
function IndexPage() {
|
||||
return (
|
||||
<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>
|
||||
)
|
||||
beforeLoad: async () => {
|
||||
const db = await getDb();
|
||||
const result = await db.query("SELECT id FROM nutzer LIMIT 1");
|
||||
if (result.rows.length === 0) {
|
||||
throw redirect({ to: "/onboarding" });
|
||||
}
|
||||
// /prozess route will be added in a future task
|
||||
throw redirect({ to: "/onboarding" as string });
|
||||
},
|
||||
});
|
||||
|
||||
6
src/routes/onboarding/index.tsx
Normal file
6
src/routes/onboarding/index.tsx
Normal 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 />,
|
||||
});
|
||||
21
src/shared/components/ui/input.tsx
Normal file
21
src/shared/components/ui/input.tsx
Normal 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 };
|
||||
20
src/shared/components/ui/label.tsx
Normal file
20
src/shared/components/ui/label.tsx
Normal 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 };
|
||||
Reference in New Issue
Block a user