diff --git a/.phase3-state.json b/.phase3-state.json index 2ea4cc3..9ec1ffb 100644 --- a/.phase3-state.json +++ b/.phase3-state.json @@ -2,8 +2,9 @@ "completed_features": [ "toast-notifications", "logout-everywhere", - "empty-loading-states" + "empty-loading-states", + "time-entries-search-filter" ], - "current_feature": "time-entries-search-filter", + "current_feature": "user-profile-page", "started_at": "2026-05-23T04:57:10.921624" } \ No newline at end of file diff --git a/GENERATION_LOG.md b/GENERATION_LOG.md index e4cf124..e184827 100644 --- a/GENERATION_LOG.md +++ b/GENERATION_LOG.md @@ -394,3 +394,22 @@ src/routes/customers.ts(22,36): error TS7006: Parameter - `04:59:09` **INFO** wrote 8237 chars in 66.9s (attempt 1) - `04:59:09` **INFO** Running tsc --noEmit on api… - `04:59:10` **INFO** tsc clean ✓ +- `04:59:10` **INFO** Committed feature time-entries-search-filter +- `04:59:10` **INFO** Pushed: rc=0 + +## Phase-3 Feature: user-profile-page (2026-05-23 04:59:10) + +- `04:59:10` **INFO** Description: User-Profile-Page mit Name/Email/Theme +- `04:59:10` **INFO** Generating apps/api/src/routes/users.ts (Fastify-Plugin für /api/users. GET /me (aktueller User), PATCH /me (up…) +- `04:59:24` **INFO** wrote 1399 chars in 13.6s (attempt 1) +- `04:59:24` **INFO** Generating apps/web/src/pages/Profile.tsx (Profile-Page. Liest current user via api.getMe(). Form mit Name (editi…) +- `04:59:57` **INFO** wrote 3841 chars in 32.8s (attempt 1) +- `04:59:57` **INFO** Running tsc --noEmit on api… +- `04:59:58` **WARN** tsc errors: +src/routes/users.ts(21,34): error TS2339: Property 'id' does not exist on type 'string | object | Buffer'. + Property 'id' does not exist on type 'string'. +src/routes/users.ts(41,34): error TS2339: Property 'id' does not exist on type 'string | object | Buffer'. + Property 'id' does not exist on type 'string'. +undefined +/home/dark/Developer/EmberClone/apps/api: + ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL  Command failed with exit code 2: tsc --noEmit -p tsconfig.json diff --git a/apps/api/src/routes/users.ts b/apps/api/src/routes/users.ts new file mode 100644 index 0000000..a1a05c8 --- /dev/null +++ b/apps/api/src/routes/users.ts @@ -0,0 +1,60 @@ +import { FastifyInstance } from "fastify" +import { db } from "../db" +import { users } from "../db/schema" +import { eq } from "drizzle-orm" +import { z } from "zod" + +const UserUpdateSchema = z.object({ + name: z.string().min(1).optional() +}) + +export default async function userRoutes(fastify: FastifyInstance) { + fastify.addHook("preHandler", async (request, reply) => { + try { + await request.jwtVerify() + } catch (err) { + return reply.code(401).send({ message: "Unauthorized" }) + } + }) + + fastify.get("/me", async (request, reply) => { + const userId = request.user?.id + + if (!userId) { + return reply.code(401).send({ message: "User ID not found in token" }) + } + + const [user] = await db + .select() + .from(users) + .where(eq(users.id, userId)) + .limit(1) + + if (!user) { + return reply.code(404).send({ message: "User not found" }) + } + + return user + }) + + fastify.patch("/me", async (request, reply) => { + const userId = request.user?.id + if (!userId) { + return reply.code(401).send({ message: "User ID not found in token" }) + } + + const body = UserUpdateSchema.parse(request.body) + + const [user] = await db + .update(users) + .set(body) + .where(eq(users.id, userId)) + .returning() + + if (!user) { + return reply.code(404).send({ message: "User not found" }) + } + + return user + }) +} \ No newline at end of file diff --git a/apps/web/src/pages/Profile.tsx b/apps/web/src/pages/Profile.tsx new file mode 100644 index 0000000..7239cb5 --- /dev/null +++ b/apps/web/src/pages/Profile.tsx @@ -0,0 +1,114 @@ +import React, { useState, useEffect } from 'react'; +import { useQuery, useMutation } from '@tanstack/react-query'; +import { api } from '../lib/api'; +import { useToast } from '../components/Toast'; + +export default function Profile() { + const toast = useToast(); + const [name, setName] = useState(''); + + const { data: user, isLoading, refetch } = useQuery({ + queryKey: ['me'], + queryFn: () => api.getMe(), + }); + + const updateMutation = useMutation({ + mutationFn: async (newName: string) => { + return api.updateProfile({ name: newName }); + }, + onSuccess: async () => { + toast.success('Profil erfolgreich aktualisiert'); + await refetch(); + }, + onError: () => { + toast.error('Fehler beim Aktualisieren des Profils'); + }, + }); + + useEffect(() => { + if (user?.name) { + setName(user.name); + } + }, [user]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + updateMutation.mutate(name); + }; + + if (isLoading) { + return ( +
+
+
+ ); + } + + if (!user) { + return ( +
+

Keine Benutzerdaten gefunden.

+
+ ); + } + + return ( +
+
+
+

Mein Profil

+
+ +
+
+ {/* Name Field */} +
+ + setName(e.target.value)} + className="w-full px-3 py-2 border border-slate-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all" + placeholder="Dein Name" + /> +
+ + {/* Email Field (Readonly) */} +
+ + +
+ + {/* Role Badge */} +
+ +
+ + {user.role.toUpperCase()} + +
+
+
+ +
+ +
+
+
+
+ ); +} \ No newline at end of file