Enhance dashboard functionality by integrating child selection and displaying related measurements, vaccinations, and toothing data. Update Prisma schema to use UUIDs for IDs and add new API endpoint to fetch child details by ID.

This commit is contained in:
Philip
2025-04-14 21:31:21 +02:00
parent 9a3692d147
commit 84a2b3bf0d
15 changed files with 1596 additions and 87 deletions

1
.gitignore vendored
View File

@@ -40,3 +40,4 @@ yarn-error.log*
*.tsbuildinfo *.tsbuildinfo
next-env.d.ts next-env.d.ts
.cursor/mcp.json .cursor/mcp.json
.cursor/rules/generalprompt.mdc

725
package-lock.json generated
View File

@@ -17,6 +17,7 @@
"@radix-ui/react-select": "^2.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.2.0", "@radix-ui/react-slot": "^1.2.0",
"@radix-ui/react-tabs": "^1.1.4",
"@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",
@@ -35,6 +36,7 @@
"react-day-picker": "^8.10.1", "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",
"recharts": "^2.15.2",
"sonner": "^2.0.3", "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",
@@ -2277,6 +2279,203 @@
} }
} }
}, },
"node_modules/@radix-ui/react-roving-focus": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.3.tgz",
"integrity": "sha512-ufbpLUjZiOg4iYgb2hQrWXEPYX6jOLBbR27bDyAff5GYMRrCzcze8lukjuXVUQvJ6HZe8+oL+hhswDcjmcgVyg==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.2",
"@radix-ui/react-collection": "1.1.3",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-direction": "1.1.1",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-primitive": "2.0.3",
"@radix-ui/react-use-callback-ref": "1.1.1",
"@radix-ui/react-use-controllable-state": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/primitive": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz",
"integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==",
"license": "MIT"
},
"node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-collection": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.3.tgz",
"integrity": "sha512-mM2pxoQw5HJ49rkzwOs7Y6J4oYH22wS8BfK2/bBxROlI4xuR0c4jEenQP63LlTlDkO6Buj2Vt+QYAYcOgqtrXA==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-primitive": "2.0.3",
"@radix-ui/react-slot": "1.2.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-context": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
"integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-direction": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
"integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-id": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
"integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-primitive": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.3.tgz",
"integrity": "sha512-Pf/t/GkndH7CQ8wE2hbkXA+WyZ83fhQQn5DDmwDiDo6AwN/fhaH8oqZ0jRjMrO2iaMhDi6P1HRx6AZwyMinY1g==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-slot": "1.2.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
"integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-use-controllable-state": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.1.tgz",
"integrity": "sha512-YnEXIy8/ga01Y1PN0VfaNH//MhA91JlEGVBDxDzROqwrAtG5Yr2QGEPz8A/rJA3C7ZAHryOYGaUv8fLSW2H/mg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-callback-ref": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-use-layout-effect": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
"integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-select": { "node_modules/@radix-ui/react-select": {
"version": "2.1.7", "version": "2.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.1.7.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.1.7.tgz",
@@ -2625,6 +2824,200 @@
} }
} }
}, },
"node_modules/@radix-ui/react-tabs": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.4.tgz",
"integrity": "sha512-fuHMHWSf5SRhXke+DbHXj2wVMo+ghVH30vhX3XVacdXqDl+J4XWafMIGOOER861QpBx1jxgwKXL2dQnfrsd8MQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-direction": "1.1.1",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-presence": "1.1.3",
"@radix-ui/react-primitive": "2.0.3",
"@radix-ui/react-roving-focus": "1.1.3",
"@radix-ui/react-use-controllable-state": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/primitive": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz",
"integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==",
"license": "MIT"
},
"node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-context": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
"integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-direction": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
"integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-id": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
"integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-presence": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.3.tgz",
"integrity": "sha512-IrVLIhskYhH3nLvtcBLQFZr61tBG7wx7O3kEmdzcYwRGAEBmBicGGL7ATzNgruYJ3xBTbuzEEq9OXJM3PAX3tA==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-primitive": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.3.tgz",
"integrity": "sha512-Pf/t/GkndH7CQ8wE2hbkXA+WyZ83fhQQn5DDmwDiDo6AwN/fhaH8oqZ0jRjMrO2iaMhDi6P1HRx6AZwyMinY1g==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-slot": "1.2.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
"integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-use-controllable-state": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.1.tgz",
"integrity": "sha512-YnEXIy8/ga01Y1PN0VfaNH//MhA91JlEGVBDxDzROqwrAtG5Yr2QGEPz8A/rJA3C7ZAHryOYGaUv8fLSW2H/mg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-callback-ref": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-use-layout-effect": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
"integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-callback-ref": { "node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz",
@@ -3174,6 +3567,69 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/@types/d3-array": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz",
"integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==",
"license": "MIT"
},
"node_modules/@types/d3-color": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
"license": "MIT"
},
"node_modules/@types/d3-ease": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
"integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
"license": "MIT"
},
"node_modules/@types/d3-interpolate": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
"integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
"license": "MIT",
"dependencies": {
"@types/d3-color": "*"
}
},
"node_modules/@types/d3-path": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
"integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
"license": "MIT"
},
"node_modules/@types/d3-scale": {
"version": "4.0.9",
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
"integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
"license": "MIT",
"dependencies": {
"@types/d3-time": "*"
}
},
"node_modules/@types/d3-shape": {
"version": "3.1.7",
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz",
"integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==",
"license": "MIT",
"dependencies": {
"@types/d3-path": "*"
}
},
"node_modules/@types/d3-time": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
"integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
"license": "MIT"
},
"node_modules/@types/d3-timer": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
"license": "MIT"
},
"node_modules/@types/estree": { "node_modules/@types/estree": {
"version": "1.0.7", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
@@ -4298,9 +4754,129 @@
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/d3-array": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
"license": "ISC",
"dependencies": {
"internmap": "1 - 2"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-color": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-ease": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-format": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
"integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-interpolate": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
"license": "ISC",
"dependencies": {
"d3-color": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-path": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
"integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-scale": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
"license": "ISC",
"dependencies": {
"d3-array": "2.10.0 - 3",
"d3-format": "1 - 3",
"d3-interpolate": "1.2.0 - 3",
"d3-time": "2.1.1 - 3",
"d3-time-format": "2 - 4"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-shape": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
"integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
"license": "ISC",
"dependencies": {
"d3-path": "^3.1.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-time": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
"integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
"license": "ISC",
"dependencies": {
"d3-array": "2 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-time-format": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
"integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
"license": "ISC",
"dependencies": {
"d3-time": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-timer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/damerau-levenshtein": { "node_modules/damerau-levenshtein": {
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
@@ -4389,6 +4965,12 @@
} }
} }
}, },
"node_modules/decimal.js-light": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
"integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
"license": "MIT"
},
"node_modules/deep-is": { "node_modules/deep-is": {
"version": "0.1.4", "version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@@ -4466,6 +5048,16 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/dom-helpers": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.8.7",
"csstype": "^3.0.2"
}
},
"node_modules/dunder-proto": { "node_modules/dunder-proto": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -5167,6 +5759,12 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/eventemitter3": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
"license": "MIT"
},
"node_modules/fast-deep-equal": { "node_modules/fast-deep-equal": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -5174,6 +5772,15 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/fast-equals": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.2.2.tgz",
"integrity": "sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw==",
"license": "MIT",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/fast-glob": { "node_modules/fast-glob": {
"version": "3.3.1", "version": "3.3.1",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz",
@@ -5781,6 +6388,15 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/internmap": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/is-array-buffer": { "node_modules/is-array-buffer": {
"version": "3.0.5", "version": "3.0.5",
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
@@ -6240,7 +6856,6 @@
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/js-yaml": { "node_modules/js-yaml": {
@@ -6605,6 +7220,12 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"license": "MIT"
},
"node_modules/lodash.merge": { "node_modules/lodash.merge": {
"version": "4.6.2", "version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@@ -6616,7 +7237,6 @@
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0" "js-tokens": "^3.0.0 || ^4.0.0"
@@ -7409,7 +8029,6 @@
"version": "15.8.1", "version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"loose-envify": "^1.4.0", "loose-envify": "^1.4.0",
@@ -7503,7 +8122,6 @@
"version": "16.13.1", "version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/react-remove-scroll": { "node_modules/react-remove-scroll": {
@@ -7553,6 +8171,21 @@
} }
} }
}, },
"node_modules/react-smooth": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz",
"integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==",
"license": "MIT",
"dependencies": {
"fast-equals": "^5.0.1",
"prop-types": "^15.8.1",
"react-transition-group": "^4.4.5"
},
"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"
}
},
"node_modules/react-style-singleton": { "node_modules/react-style-singleton": {
"version": "2.2.3", "version": "2.2.3",
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
@@ -7575,6 +8208,22 @@
} }
} }
}, },
"node_modules/react-transition-group": {
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
"integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
"license": "BSD-3-Clause",
"dependencies": {
"@babel/runtime": "^7.5.5",
"dom-helpers": "^5.0.1",
"loose-envify": "^1.4.0",
"prop-types": "^15.6.2"
},
"peerDependencies": {
"react": ">=16.6.0",
"react-dom": ">=16.6.0"
}
},
"node_modules/readable-stream": { "node_modules/readable-stream": {
"version": "3.6.2", "version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
@@ -7589,6 +8238,44 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/recharts": {
"version": "2.15.2",
"resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.2.tgz",
"integrity": "sha512-xv9lVztv3ingk7V3Jf05wfAZbM9Q2umJzu5t/cfnAK7LUslNrGT7LPBr74G+ok8kSCeFMaePmWMg0rcYOnczTw==",
"license": "MIT",
"dependencies": {
"clsx": "^2.0.0",
"eventemitter3": "^4.0.1",
"lodash": "^4.17.21",
"react-is": "^18.3.1",
"react-smooth": "^4.0.4",
"recharts-scale": "^0.4.4",
"tiny-invariant": "^1.3.1",
"victory-vendor": "^36.6.8"
},
"engines": {
"node": ">=14"
},
"peerDependencies": {
"react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/recharts-scale": {
"version": "0.4.5",
"resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz",
"integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==",
"license": "MIT",
"dependencies": {
"decimal.js-light": "^2.4.1"
}
},
"node_modules/recharts/node_modules/react-is": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
"license": "MIT"
},
"node_modules/reflect.getprototypeof": { "node_modules/reflect.getprototypeof": {
"version": "1.0.10", "version": "1.0.10",
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
@@ -8338,6 +9025,12 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/tiny-invariant": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
"license": "MIT"
},
"node_modules/tinyglobby": { "node_modules/tinyglobby": {
"version": "0.2.12", "version": "0.2.12",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.12.tgz", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.12.tgz",
@@ -8641,6 +9334,28 @@
"uuid": "dist/bin/uuid" "uuid": "dist/bin/uuid"
} }
}, },
"node_modules/victory-vendor": {
"version": "36.9.2",
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz",
"integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==",
"license": "MIT AND ISC",
"dependencies": {
"@types/d3-array": "^3.0.3",
"@types/d3-ease": "^3.0.0",
"@types/d3-interpolate": "^3.0.1",
"@types/d3-scale": "^4.0.2",
"@types/d3-shape": "^3.1.0",
"@types/d3-time": "^3.0.0",
"@types/d3-timer": "^3.0.0",
"d3-array": "^3.1.6",
"d3-ease": "^3.0.1",
"d3-interpolate": "^3.0.1",
"d3-scale": "^4.0.2",
"d3-shape": "^3.1.0",
"d3-time": "^3.0.0",
"d3-timer": "^3.0.1"
}
},
"node_modules/webidl-conversions": { "node_modules/webidl-conversions": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",

View File

@@ -18,6 +18,7 @@
"@radix-ui/react-select": "^2.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.2.0", "@radix-ui/react-slot": "^1.2.0",
"@radix-ui/react-tabs": "^1.1.4",
"@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",
@@ -36,6 +37,7 @@
"react-day-picker": "^8.10.1", "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",
"recharts": "^2.15.2",
"sonner": "^2.0.3", "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",

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Measurement" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;

View File

@@ -22,7 +22,7 @@ model User {
} }
model Child { model Child {
id Int @id @default(autoincrement()) id String @id @default(uuid())
name String name String
birthDate DateTime birthDate DateTime
gender String? // can be "male", "female", "diverse", or "unknown" gender String? // can be "male", "female", "diverse", or "unknown"
@@ -35,26 +35,27 @@ model Child {
} }
model Measurement { model Measurement {
id Int @id @default(autoincrement()) id String @id @default(uuid())
childId Int childId String
child Child @relation(fields: [childId], references: [id]) child Child @relation(fields: [childId], references: [id])
date DateTime date DateTime
weightKg Float? weightKg Float?
heightCm Float? heightCm Float?
createdAt DateTime @default(now())
} }
model ToothStatus { model ToothStatus {
id Int @id @default(autoincrement()) id String @id @default(uuid())
childId Int childId String
child Child @relation(fields: [childId], references: [id]) child Child @relation(fields: [childId], references: [id])
toothLabel String // z.B. "oben links 1" oder "Zahn 61" toothLabel String // z. B. "oben links 1" oder "Zahn 61"
date DateTime date DateTime
status String // z.B. "durchgebrochen", "locker", "fehlend" status String // z. B. "durchgebrochen", "locker", "fehlend"
} }
model Vaccine { model Vaccine {
id Int @id @default(autoincrement()) id String @id @default(uuid())
childId Int childId String
child Child @relation(fields: [childId], references: [id]) child Child @relation(fields: [childId], references: [id])
name String name String
date DateTime date DateTime

View File

@@ -0,0 +1,34 @@
import { getAuthSession } from "@/lib/auth"
import { redirect } from "next/navigation"
import { Header } from "@/components/layout/header"
import { Footer } from "@/components/layout/footer"
import { ChildDetailContent } from "@/components/child/child-detail-content"
import { notFound } from "next/navigation"
export default async function ChildDetailPage({
params,
}: {
params: { childId: string }
}) {
const session = await getAuthSession()
if (!session?.user) redirect("/login")
const { childId } = params;
if (!childId) {
notFound();
}
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-4xl mx-auto space-y-6">
<ChildDetailContent childId={childId} />
</div>
</main>
<Footer />
</>
)
}

View File

@@ -7,6 +7,7 @@ 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"; import { Toaster } from "sonner";
import { SessionProvider } from "next-auth/react"
const queryClient = new QueryClient() const queryClient = new QueryClient()
const trpcClient = trpc.createClient({ const trpcClient = trpc.createClient({
@@ -31,12 +32,14 @@ export default function RootLayout({ children }: { children: ReactNode }) {
return ( return (
<html lang="de"> <html lang="de">
<body className={`${geistSans.variable} ${geistMono.variable} font-sans`}> <body className={`${geistSans.variable} ${geistMono.variable} font-sans`}>
<trpc.Provider client={trpcClient} queryClient={queryClient}> <SessionProvider>
<QueryClientProvider client={queryClient}> <trpc.Provider client={trpcClient} queryClient={queryClient}>
{children} <QueryClientProvider client={queryClient}>
<Toaster /> {children}
</QueryClientProvider> <Toaster />
</trpc.Provider> </QueryClientProvider>
</trpc.Provider>
</SessionProvider>
</body> </body>
</html> </html>
) )

View File

@@ -0,0 +1,19 @@
"use client"
import { signOut } from "next-auth/react"
import { Button } from "@/components/ui/button"
import { LogOut } from "lucide-react"
export function LogoutButton() {
return (
<Button
variant="ghost"
size="sm"
onClick={() => signOut({ callbackUrl: "/" })}
className="flex items-center gap-2"
>
<LogOut className="h-4 w-4" />
<span>Abmelden</span>
</Button>
)
}

View File

@@ -0,0 +1,517 @@
"use client";
import { useState } from "react";
import { format, differenceInMonths } from "date-fns";
import { de } from "date-fns/locale";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { toast } from "sonner";
import { Card, CardContent } from "@/components/ui/card";
import { trpc } from "@/utils/trpc";
import {
LineChart,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
Legend,
} from "recharts";
import { Skeleton } from "@/components/ui/skeleton";
import type { TRPCClientErrorLike } from "@trpc/client";
import type { AppRouter } from "@/server/api/root";
import { Calendar } from "@/components/ui/calendar";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Button } from "@/components/ui/button";
import { CalendarIcon, Baby, Syringe, Activity, Plus, Stethoscope, ChevronDown, ChevronUp } from "lucide-react";
import { cn } from "@/lib/utils";
import { Input } from "@/components/ui/input";
const measurementFormSchema = z.object({
date: z.date({
required_error: "Bitte wähle ein Datum",
}),
weightKg: z.string().refine((val) => !isNaN(Number(val)) && Number(val) > 0, {
message: "Bitte gib ein gültiges Gewicht ein",
}),
heightCm: z.string().refine((val) => !isNaN(Number(val)) && Number(val) > 0, {
message: "Bitte gib eine gültige Größe ein",
}),
});
type MeasurementFormValues = z.infer<typeof measurementFormSchema>;
interface ChildDetailContentProps {
childId: string;
}
export function ChildDetailContent({ childId }: ChildDetailContentProps) {
const [isLoading, setIsLoading] = useState(false);
const [date, setDate] = useState<Date | undefined>(new Date());
const [expandedSections, setExpandedSections] = useState({
overview: true,
measurements: false,
vaccinations: false,
toothing: false,
});
// Define error handler within component scope
const handleAddMeasurement = (err: TRPCClientErrorLike<AppRouter>) => {
toast.error("Fehler", {
description: err.message
});
setIsLoading(false);
};
// Fetch child data
const { data: child, isLoading: isLoadingChild } = trpc.child.getById.useQuery({ id: childId });
// Fetch measurements
const { data: measurements, isLoading: isLoadingMeasurements, refetch: refetchMeasurements } = trpc.measurement.getByChildId.useQuery(
{ childId },
{ enabled: !!childId }
);
// Add measurement mutation
const addMeasurement = trpc.measurement.add.useMutation({
onSuccess: () => {
toast.success("Messung hinzugefügt", {
description: "Die Messung wurde erfolgreich hinzugefügt."
});
form.reset();
setDate(new Date());
refetchMeasurements();
setIsLoading(false);
},
onError: handleAddMeasurement,
});
// Form setup
const form = useForm<MeasurementFormValues>({
resolver: zodResolver(measurementFormSchema),
defaultValues: {
date: new Date(),
weightKg: "",
heightCm: "",
},
});
const onSubmit = form.handleSubmit((values: MeasurementFormValues) => {
setIsLoading(true);
// Create a new date at the start of the selected day in local timezone
const selectedDate = date || new Date();
const localDate = new Date(
selectedDate.getFullYear(),
selectedDate.getMonth(),
selectedDate.getDate(),
12, // Set to noon to avoid any timezone issues
0,
0,
0
);
addMeasurement.mutate({
childId,
date: localDate.toISOString(),
weightKg: parseFloat(values.weightKg),
heightCm: parseFloat(values.heightCm),
});
});
const toggleSection = (section: keyof typeof expandedSections) => {
setExpandedSections(prev => ({
...prev,
[section]: !prev[section]
}));
};
if (isLoadingChild || isLoadingMeasurements) {
return (
<div className="space-y-4">
<Skeleton className="h-8 w-1/3" />
<Skeleton className="h-32 w-full" />
<Skeleton className="h-64 w-full" />
</div>
);
}
if (!child) {
return (
<div className="text-center py-10">
<h2 className="text-xl font-semibold text-zinc-800">Kind nicht gefunden</h2>
<p className="text-zinc-500 mt-2">Das angeforderte Kind konnte nicht gefunden werden.</p>
</div>
);
}
const birthDate = new Date(child.birthDate);
const ageInMonths = differenceInMonths(new Date(), birthDate);
// Prepare data for charts
const measurementData = measurements
?.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime())
.map((m) => ({
date: format(new Date(m.date), "dd.MM.yyyy", { locale: de }),
weight: m.weightKg,
height: m.heightCm,
})) || [];
return (
<div className="space-y-6">
<Card className="overflow-hidden">
<div className="bg-gradient-to-r from-rose-100 to-rose-200 p-6">
<div className="flex items-center gap-4">
<div className="bg-white/80 p-3 rounded-full shadow-sm">
<Baby className="h-8 w-8 text-rose-600" />
</div>
<div>
<h2 className="text-2xl font-bold text-rose-900">{child.name}</h2>
<p className="text-rose-700">
{ageInMonths} Monate {format(new Date(child.birthDate), "PPP", { locale: de })}
</p>
</div>
</div>
</div>
<CardContent className="p-6">
<div className="grid grid-cols-1 sm:grid-cols-3 gap-6">
<div className="bg-white p-4 rounded-lg shadow-sm border border-rose-100">
<div className="flex items-center gap-2 mb-2">
<CalendarIcon className="h-5 w-5 text-rose-500" />
<h3 className="font-medium text-gray-700">Geburtsdatum</h3>
</div>
<p className="text-lg">{format(new Date(child.birthDate), "PPP", { locale: de })}</p>
</div>
<div className="bg-white p-4 rounded-lg shadow-sm border border-rose-100">
<div className="flex items-center gap-2 mb-2">
<Activity className="h-5 w-5 text-rose-500" />
<h3 className="font-medium text-gray-700">Alter</h3>
</div>
<p className="text-lg">{ageInMonths} Monate</p>
</div>
<div className="bg-white p-4 rounded-lg shadow-sm border border-rose-100">
<div className="flex items-center gap-2 mb-2">
<Stethoscope className="h-5 w-5 text-rose-500" />
<h3 className="font-medium text-gray-700">Entwicklung</h3>
</div>
<p className="text-lg">Normal</p>
</div>
</div>
</CardContent>
</Card>
{/* Overview Section */}
<div className="border rounded-lg overflow-hidden bg-white shadow-sm">
<button
onClick={() => toggleSection('overview')}
className="w-full flex items-center justify-between p-4 bg-gray-50 hover:bg-gray-100 transition-colors"
>
<div className="flex items-center gap-2">
<Activity className="h-5 w-5 text-gray-600" />
<h2 className="text-lg font-medium">Übersicht</h2>
</div>
{expandedSections.overview ? <ChevronUp className="h-5 w-5" /> : <ChevronDown className="h-5 w-5" />}
</button>
{expandedSections.overview && (
<div className="p-4 space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<h3 className="text-lg font-medium">Letzte Messungen</h3>
{measurements && measurements.length > 0 ? (
<div className="bg-white rounded-md p-4 border">
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-sm text-gray-500">Gewicht</p>
<p className="text-xl font-semibold">{measurements[0].weightKg} kg</p>
<p className="text-xs text-gray-500">
{format(new Date(measurements[0].date), "dd.MM.yyyy", { locale: de })}
</p>
</div>
<div>
<p className="text-sm text-gray-500">Größe</p>
<p className="text-xl font-semibold">{measurements[0].heightCm} cm</p>
<p className="text-xs text-gray-500">
{format(new Date(measurements[0].date), "dd.MM.yyyy", { locale: de })}
</p>
</div>
</div>
</div>
) : (
<p className="text-muted-foreground">Noch keine Messungen aufgezeichnet.</p>
)}
</div>
<div className="space-y-2">
<h3 className="text-lg font-medium">Nächste Impfung</h3>
<div className="bg-white rounded-md p-4 border">
<p className="text-muted-foreground">Keine anstehenden Impfungen.</p>
</div>
</div>
</div>
</div>
)}
</div>
{/* Measurements Section */}
<div className="border rounded-lg overflow-hidden bg-white shadow-sm">
<button
onClick={() => toggleSection('measurements')}
className="w-full flex items-center justify-between p-4 bg-gray-50 hover:bg-gray-100 transition-colors"
>
<div className="flex items-center gap-2">
<Baby className="h-5 w-5 text-gray-600" />
<h2 className="text-lg font-medium">Messungen</h2>
</div>
{expandedSections.measurements ? <ChevronUp className="h-5 w-5" /> : <ChevronDown className="h-5 w-5" />}
</button>
{expandedSections.measurements && (
<div className="p-4 space-y-4">
<form onSubmit={onSubmit} className="space-y-4 mb-6">
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div className="space-y-2">
<label htmlFor="date" className="text-sm font-medium text-gray-700">
Messdatum
</label>
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
className={cn(
"w-full justify-start text-left font-normal",
!date && "text-muted-foreground"
)}
>
<CalendarIcon className="mr-2 h-4 w-4" />
{date ? (
format(date, "PPP", { locale: de })
) : (
<span>Datum auswählen</span>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
selected={date}
onSelect={setDate}
initialFocus
locale={de}
/>
</PopoverContent>
</Popover>
</div>
<div className="space-y-2">
<label htmlFor="weightKg" className="text-sm font-medium text-gray-700">
Gewicht (kg)
</label>
<Input
type="number"
id="weightKg"
step="0.1"
className="w-full"
{...form.register("weightKg")}
disabled={isLoading}
/>
{form.formState.errors.weightKg && (
<p className="text-sm text-red-600">{form.formState.errors.weightKg.message}</p>
)}
</div>
<div className="space-y-2">
<label htmlFor="heightCm" className="text-sm font-medium text-gray-700">
Größe (cm)
</label>
<Input
type="number"
id="heightCm"
step="0.1"
className="w-full"
{...form.register("heightCm")}
disabled={isLoading}
/>
{form.formState.errors.heightCm && (
<p className="text-sm text-red-600">{form.formState.errors.heightCm.message}</p>
)}
</div>
</div>
<Button
type="submit"
disabled={isLoading}
className="w-full sm:w-auto bg-rose-600 text-white hover:bg-rose-700"
>
{isLoading ? "Wird hinzugefügt..." : "Messung hinzufügen"}
</Button>
</form>
{measurements && measurements.length > 0 ? (
<div className="space-y-8">
<div>
<h3 className="text-lg font-medium mb-2">Gewicht</h3>
<div className="h-[300px] bg-white rounded-md p-4 border">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={measurementData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="date" />
<YAxis />
<Tooltip />
<Legend />
<Line
type="monotone"
dataKey="weight"
stroke="#8884d8"
name="Gewicht (kg)"
activeDot={{ r: 8 }}
/>
</LineChart>
</ResponsiveContainer>
</div>
</div>
<div>
<h3 className="text-lg font-medium mb-2">Größe</h3>
<div className="h-[300px] bg-white rounded-md p-4 border">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={measurementData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="date" />
<YAxis />
<Tooltip />
<Legend />
<Line
type="monotone"
dataKey="height"
stroke="#82ca9d"
name="Größe (cm)"
activeDot={{ r: 8 }}
/>
</LineChart>
</ResponsiveContainer>
</div>
</div>
</div>
) : (
<p className="text-muted-foreground">Noch keine Messungen aufgezeichnet.</p>
)}
</div>
)}
</div>
{/* Vaccinations Section */}
<div className="border rounded-lg overflow-hidden bg-white shadow-sm">
<button
onClick={() => toggleSection('vaccinations')}
className="w-full flex items-center justify-between p-4 bg-gray-50 hover:bg-gray-100 transition-colors"
>
<div className="flex items-center gap-2">
<Syringe className="h-5 w-5 text-gray-600" />
<h2 className="text-lg font-medium">Impfungen</h2>
</div>
{expandedSections.vaccinations ? <ChevronUp className="h-5 w-5" /> : <ChevronDown className="h-5 w-5" />}
</button>
{expandedSections.vaccinations && (
<div className="p-4 space-y-4">
<div className="flex justify-end">
<Button size="sm" className="bg-rose-600 text-white hover:bg-rose-700">
<Plus className="h-4 w-4 mr-2" />
Impfung hinzufügen
</Button>
</div>
<div className="space-y-4">
<p className="text-muted-foreground">Noch keine Impfungen aufgezeichnet.</p>
<div className="bg-white rounded-md p-4 border">
<h3 className="text-lg font-medium mb-2">Empfohlene Impfungen</h3>
<ul className="space-y-2">
<li className="flex items-center justify-between">
<span>6-fach Impfung</span>
<span className="text-sm text-gray-500">2 Monate</span>
</li>
<li className="flex items-center justify-between">
<span>Pneumokokken</span>
<span className="text-sm text-gray-500">2 Monate</span>
</li>
<li className="flex items-center justify-between">
<span>Rotaviren</span>
<span className="text-sm text-gray-500">6 Wochen</span>
</li>
</ul>
</div>
</div>
</div>
)}
</div>
{/* Toothing Section */}
<div className="border rounded-lg overflow-hidden bg-white shadow-sm">
<button
onClick={() => toggleSection('toothing')}
className="w-full flex items-center justify-between p-4 bg-gray-50 hover:bg-gray-100 transition-colors"
>
<div className="flex items-center gap-2">
<Stethoscope className="h-5 w-5 text-gray-600" />
<h2 className="text-lg font-medium">Zahnung</h2>
</div>
{expandedSections.toothing ? <ChevronUp className="h-5 w-5" /> : <ChevronDown className="h-5 w-5" />}
</button>
{expandedSections.toothing && (
<div className="p-4 space-y-4">
<div className="flex justify-end">
<Button size="sm" className="bg-rose-600 text-white hover:bg-rose-700">
<Plus className="h-4 w-4 mr-2" />
Zahn hinzufügen
</Button>
</div>
<div className="space-y-4">
<p className="text-muted-foreground">Noch keine Zähne aufgezeichnet.</p>
<div className="bg-white rounded-md p-4 border">
<h3 className="text-lg font-medium mb-2">Zahnungsplan</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<h4 className="font-medium">Oberkiefer</h4>
<ul className="space-y-2 mt-2">
<li className="flex items-center justify-between">
<span>Schneidezähne</span>
<span className="text-sm text-gray-500">6-10 Monate</span>
</li>
<li className="flex items-center justify-between">
<span>Seitenzähne</span>
<span className="text-sm text-gray-500">12-16 Monate</span>
</li>
<li className="flex items-center justify-between">
<span>Backenzähne</span>
<span className="text-sm text-gray-500">16-20 Monate</span>
</li>
</ul>
</div>
<div>
<h4 className="font-medium">Unterkiefer</h4>
<ul className="space-y-2 mt-2">
<li className="flex items-center justify-between">
<span>Schneidezähne</span>
<span className="text-sm text-gray-500">5-8 Monate</span>
</li>
<li className="flex items-center justify-between">
<span>Seitenzähne</span>
<span className="text-sm text-gray-500">10-14 Monate</span>
</li>
<li className="flex items-center justify-between">
<span>Backenzähne</span>
<span className="text-sm text-gray-500">14-18 Monate</span>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
)}
</div>
</div>
);
}

View File

@@ -4,28 +4,54 @@ import { format, differenceInMonths } from "date-fns";
import { de } from "date-fns/locale"; import { de } from "date-fns/locale";
import Link from "next/link"; import Link from "next/link";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Plus, Baby, Syringe, Calendar, Clock } from "lucide-react"; import { Plus, Baby, Activity, Calendar } from "lucide-react";
import { trpc } from "@/utils/trpc"; import { trpc } from "@/utils/trpc";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { useState } from "react";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
export function DashboardContent() { export function DashboardContent() {
const { data: children, isLoading } = trpc.child.getAllByUser.useQuery(); const { data: children, isLoading } = trpc.child.getAllByUser.useQuery();
const [selectedChildId, setSelectedChildId] = useState<string | null>(null);
// Get measurements for the selected child
const { data: measurements } = trpc.measurement.getByChildId.useQuery(
{ childId: selectedChildId || "" },
{ enabled: !!selectedChildId }
);
// Get vaccinations for the selected child - using child router as a fallback
const { data: vaccinations } = trpc.child.getById.useQuery(
{ id: selectedChildId || "" },
{ enabled: !!selectedChildId }
);
// Get toothing data for the selected child - using child router as a fallback
const { data: toothing } = trpc.child.getById.useQuery(
{ id: selectedChildId || "" },
{ enabled: !!selectedChildId }
);
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div className="flex justify-between items-center">
<h1 className="text-2xl font-bold text-zinc-800">Willkommen bei Bambino</h1>
<Link href="/dashboard/add-child">
<Button variant="default" size="sm" className="bg-rose-500 hover:bg-rose-600">
<Plus className="h-4 w-4 mr-1" />
Kind hinzufügen
</Button>
</Link>
</div>
<div className="grid gap-6 grid-cols-1 md:grid-cols-2"> <div className="grid gap-6 grid-cols-1 md:grid-cols-2">
<Card className="bg-white/80 border-zinc-200 shadow-sm hover:shadow-md transition-shadow"> <Card className="bg-white border-zinc-200 shadow-sm hover:shadow-md transition-shadow">
<CardHeader className="flex flex-row items-center justify-between pb-2"> <CardHeader className="pb-2">
<div> <div className="flex items-center gap-2">
<CardTitle className="text-xl font-semibold text-zinc-800">Kinder</CardTitle> <Baby className="h-5 w-5 text-rose-500" />
<CardDescription className="text-zinc-500">Verwalte deine Kinder</CardDescription> <CardTitle className="text-xl font-semibold text-zinc-800">Deine Kinder</CardTitle>
</div> </div>
<Link href="/dashboard/add-child"> <CardDescription className="text-zinc-500">Übersicht deiner Kinder</CardDescription>
<Button variant="default" size="sm" className="bg-rose-500 hover:bg-rose-600">
<Plus className="h-4 w-4 mr-1" />
Kind hinzufügen
</Button>
</Link>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{isLoading ? ( {isLoading ? (
@@ -40,82 +66,150 @@ export function DashboardContent() {
const ageInMonths = differenceInMonths(new Date(), birthDate); const ageInMonths = differenceInMonths(new Date(), birthDate);
return ( return (
<div <Link
href={`/dashboard/child/${child.id}`}
key={child.id} key={child.id}
className="flex items-center p-4 rounded-lg bg-white border border-zinc-100 hover:bg-rose-50/50 hover:border-rose-100 transition-all"
> >
<div className="flex-shrink-0 mr-4"> <div
<div className="w-12 h-12 rounded-full bg-rose-100 flex items-center justify-center"> className="flex items-center p-4 rounded-lg bg-white border border-zinc-100 hover:bg-rose-50/50 hover:border-rose-100 transition-all cursor-pointer"
<Baby className="h-6 w-6 text-rose-500" /> >
</div> <div className="flex-shrink-0 mr-4">
</div> <div className="w-12 h-12 rounded-full bg-rose-100 flex items-center justify-center">
<div className="flex-grow"> <Baby className="h-6 w-6 text-rose-500" />
<h3 className="font-medium text-lg text-zinc-800">{child.name}</h3>
<div className="flex flex-wrap gap-x-4 mt-1 text-sm text-zinc-500">
<div className="flex items-center">
<Calendar className="h-3.5 w-3.5 mr-1 text-zinc-400" />
<span>Geboren: {format(birthDate, "dd.MM.yyyy", { locale: de })}</span>
</div>
<div className="flex items-center">
<Clock className="h-3.5 w-3.5 mr-1 text-zinc-400" />
<span>Alter: {ageInMonths} Monate</span>
</div> </div>
</div> </div>
<div className="flex-grow">
<h3 className="font-medium text-zinc-800">{child.name}</h3>
<p className="text-sm text-zinc-500">
{format(birthDate, "PPP", { locale: de })} {ageInMonths} Monate
</p>
</div>
</div> </div>
</div> </Link>
); );
})} })}
</div> </div>
) : ( ) : (
<div className="flex flex-col items-center justify-center py-8 text-center"> <div className="text-center py-6">
<div className="w-16 h-16 rounded-full bg-rose-100 flex items-center justify-center mb-4"> <p className="text-zinc-500">Du hast noch keine Kinder hinzugefügt.</p>
<Baby className="h-8 w-8 text-rose-500" />
</div>
<p className="text-zinc-500 mb-2">Du hast noch keine Kinder hinzugefügt.</p>
<Link href="/dashboard/add-child">
<Button variant="outline" size="sm" className="border-rose-200 text-rose-600 hover:bg-rose-50">
<Plus className="h-4 w-4 mr-1" />
Kind hinzufügen
</Button>
</Link>
</div> </div>
)} )}
</CardContent> </CardContent>
</Card> </Card>
<Card className="bg-white/80 border-zinc-200 shadow-sm hover:shadow-md transition-shadow"> <Card className="bg-white border-zinc-200 shadow-sm hover:shadow-md transition-shadow">
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<div> <div className="flex items-center gap-2">
<CardTitle className="text-xl font-semibold text-zinc-800">Impfungen</CardTitle> <Calendar className="h-5 w-5 text-blue-500" />
<CardDescription className="text-zinc-500">Verfolge Impftermine</CardDescription> <CardTitle className="text-xl font-semibold text-zinc-800">Anstehende Termine</CardTitle>
</div> </div>
<CardDescription className="text-zinc-500">Wichtige Termine und Erinnerungen</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="flex flex-col items-center justify-center py-8 text-center"> <div className="flex flex-col items-center justify-center py-8 text-center">
<div className="w-16 h-16 rounded-full bg-blue-100 flex items-center justify-center mb-4"> <p className="text-zinc-500 mb-2">Keine anstehenden Termine.</p>
<Syringe className="h-8 w-8 text-blue-500" />
</div>
<p className="text-zinc-500 mb-2">Noch keine Impfungen eingetragen.</p>
<Button variant="outline" size="sm" className="border-blue-200 text-blue-600 hover:bg-blue-50" disabled>
<Plus className="h-4 w-4 mr-1" />
Impfung hinzufügen
</Button>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
<Card className="bg-white/80 border-zinc-200 shadow-sm hover:shadow-md transition-shadow"> <Card className="bg-white border-zinc-200 shadow-sm hover:shadow-md transition-shadow">
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<div> <div className="flex items-center gap-2">
<Activity className="h-5 w-5 text-green-500" />
<CardTitle className="text-xl font-semibold text-zinc-800">Entwicklung</CardTitle> <CardTitle className="text-xl font-semibold text-zinc-800">Entwicklung</CardTitle>
<CardDescription className="text-zinc-500">Verfolge die Entwicklung deines Kindes</CardDescription>
</div> </div>
<CardDescription className="text-zinc-500">Übersicht der Entwicklung deiner Kinder</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="flex flex-col items-center justify-center py-8 text-center"> {isLoading ? (
<p className="text-zinc-500 mb-2">Entwicklungsdaten werden bald verfügbar sein.</p> <div className="space-y-4">
</div> <div className="h-32 w-full rounded-lg bg-zinc-200/50 animate-pulse"></div>
</div>
) : children && children.length > 0 ? (
<div className="space-y-6">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-zinc-700">Kind auswählen:</span>
<Select
value={selectedChildId || ""}
onValueChange={setSelectedChildId}
>
<SelectTrigger className="w-[200px]">
<SelectValue placeholder="Wähle ein Kind" />
</SelectTrigger>
<SelectContent>
{children.map((child) => (
<SelectItem key={child.id} value={child.id.toString()}>
{child.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{selectedChildId ? (
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{/* Measurements Summary */}
<div className="bg-zinc-50 p-4 rounded-lg border border-zinc-200">
<h3 className="font-medium text-zinc-800 mb-2">Messungen</h3>
{measurements && measurements.length > 0 ? (
<div>
<p className="text-sm text-zinc-600">
Letzte Messung: {format(new Date(measurements[0].date), "dd.MM.yyyy", { locale: de })}
</p>
<div className="mt-2 grid grid-cols-2 gap-2">
<div>
<p className="text-xs text-zinc-500">Gewicht</p>
<p className="font-medium">{measurements[0].weightKg} kg</p>
</div>
<div>
<p className="text-xs text-zinc-500">Größe</p>
<p className="font-medium">{measurements[0].heightCm} cm</p>
</div>
</div>
</div>
) : (
<p className="text-sm text-zinc-500">Keine Messungen vorhanden</p>
)}
</div>
{/* Vaccinations Summary */}
<div className="bg-zinc-50 p-4 rounded-lg border border-zinc-200">
<h3 className="font-medium text-zinc-800 mb-2">Impfungen</h3>
{vaccinations ? (
<div>
<p className="text-sm text-zinc-600">
Impfungen werden bald verfügbar sein
</p>
</div>
) : (
<p className="text-sm text-zinc-500">Keine Impfungen vorhanden</p>
)}
</div>
{/* Toothing Summary */}
<div className="bg-zinc-50 p-4 rounded-lg border border-zinc-200">
<h3 className="font-medium text-zinc-800 mb-2">Zahnung</h3>
{toothing ? (
<div>
<p className="text-sm text-zinc-600">
Zahnungsdaten werden bald verfügbar sein
</p>
</div>
) : (
<p className="text-sm text-zinc-500">Keine Zahnungsdaten vorhanden</p>
)}
</div>
</div>
) : (
<p className="text-zinc-500">Bitte wähle ein Kind aus, um Entwicklungsdaten zu sehen.</p>
)}
</div>
) : (
<div className="flex flex-col items-center justify-center py-8 text-center">
<p className="text-zinc-500 mb-2">Füge ein Kind hinzu, um Entwicklungsdaten zu verfolgen.</p>
</div>
)}
</CardContent> </CardContent>
</Card> </Card>
</div> </div>

View File

@@ -2,8 +2,12 @@
import Link from "next/link" import Link from "next/link"
import { Baby } from "lucide-react" import { Baby } from "lucide-react"
import { useSession } from "next-auth/react"
import { LogoutButton } from "@/components/auth/logout-button"
export function Header() { export function Header() {
const { data: session } = useSession()
return ( return (
<header className="w-full px-6 py-4 border-b bg-white/70 backdrop-blur-md shadow-sm"> <header className="w-full px-6 py-4 border-b bg-white/70 backdrop-blur-md shadow-sm">
<div className="max-w-5xl mx-auto flex items-center justify-between"> <div className="max-w-5xl mx-auto flex items-center justify-between">
@@ -12,11 +16,24 @@ export function Header() {
Bambino Bambino
</Link> </Link>
{/* Später Login- oder Sprachmenü */} <nav className="text-sm text-zinc-600 hidden sm:flex items-center gap-4">
<nav className="text-sm text-zinc-600 hidden sm:block"> {session ? (
<Link href="/register" className="hover:underline"> <>
Registrierung <Link href="/app" className="hover:underline">
</Link> Dashboard
</Link>
<LogoutButton />
</>
) : (
<>
<Link href="/login" className="hover:underline">
Login
</Link>
<Link href="/register" className="hover:underline">
Registrierung
</Link>
</>
)}
</nav> </nav>
</div> </div>
</header> </header>

View File

@@ -0,0 +1,55 @@
"use client"
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "@/lib/utils"
const Tabs = TabsPrimitive.Root
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-9 items-center justify-center rounded-lg bg-zinc-100 p-1 text-zinc-500",
className
)}
{...props}
/>
))
TabsList.displayName = TabsPrimitive.List.displayName
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-white transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-400 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-white data-[state=active]:text-zinc-950 data-[state=active]:shadow",
className
)}
{...props}
/>
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-400 focus-visible:ring-offset-2",
className
)}
{...props}
/>
))
TabsContent.displayName = TabsPrimitive.Content.displayName
export { Tabs, TabsList, TabsTrigger, TabsContent }

View File

@@ -1,12 +1,12 @@
import { router } from "@/server/trpc" import { router } from "@/server/trpc"
import { childRouter } from "./routers/child" import { childRouter } from "./routers/child"
import { authRouter } from "./routers/auth" import { authRouter } from "./routers/auth"
import { measurementRouter } from "./routers/measurement"
export const appRouter = router({ export const appRouter = router({
child: childRouter, child: childRouter,
auth: authRouter, auth: authRouter,
measurement: measurementRouter,
}) })
// Export type helper // Export type helper

View File

@@ -45,4 +45,16 @@ export const childRouter = router({
return ctx.prisma.child.create({ data }); return ctx.prisma.child.create({ data });
}), }),
getById: protectedProcedure
.input(z.object({ id: z.string() }))
.query(async ({ ctx, input }) => {
const child = await ctx.prisma.child.findFirst({
where: {
id: input.id,
userId: ctx.session.user.id,
},
})
return child
}),
}); });

View File

@@ -0,0 +1,37 @@
import { z } from "zod";
import { protectedProcedure, router } from "@/server/trpc";
export const measurementRouter = router({
getByChildId: protectedProcedure
.input(z.object({ childId: z.string() }))
.query(async ({ ctx, input }) => {
return ctx.prisma.measurement.findMany({
where: {
childId: input.childId,
},
orderBy: {
date: "desc",
},
});
}),
add: protectedProcedure
.input(
z.object({
childId: z.string(),
date: z.string().refine((date) => !isNaN(Date.parse(date)), "Invalid date"),
weightKg: z.number().optional(),
heightCm: z.number().optional(),
})
)
.mutation(async ({ ctx, input }) => {
return ctx.prisma.measurement.create({
data: {
childId: input.childId,
date: new Date(input.date),
weightKg: input.weightKg,
heightCm: input.heightCm,
},
});
}),
});