feat(user-profile-page): User-Profile-Page mit Name/Email/Theme [tsc:fail]
This commit is contained in:
parent
6e349b7a56
commit
93eeba9134
@ -2,8 +2,9 @@
|
|||||||
"completed_features": [
|
"completed_features": [
|
||||||
"toast-notifications",
|
"toast-notifications",
|
||||||
"logout-everywhere",
|
"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"
|
"started_at": "2026-05-23T04:57:10.921624"
|
||||||
}
|
}
|
||||||
@ -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** wrote 8237 chars in 66.9s (attempt 1)
|
||||||
- `04:59:09` **INFO** Running tsc --noEmit on api…
|
- `04:59:09` **INFO** Running tsc --noEmit on api…
|
||||||
- `04:59:10` **INFO** tsc clean ✓
|
- `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<ArrayBufferLike>'.
|
||||||
|
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<ArrayBufferLike>'.
|
||||||
|
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
|
||||||
|
|||||||
60
apps/api/src/routes/users.ts
Normal file
60
apps/api/src/routes/users.ts
Normal file
@ -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
|
||||||
|
})
|
||||||
|
}
|
||||||
114
apps/web/src/pages/Profile.tsx
Normal file
114
apps/web/src/pages/Profile.tsx
Normal file
@ -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 (
|
||||||
|
<div className="flex items-center justify-center min-h-[400px]">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-slate-900"></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return (
|
||||||
|
<div className="text-center py-20">
|
||||||
|
<p className="text-slate-500">Keine Benutzerdaten gefunden.</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-2xl mx-auto py-10 px-4">
|
||||||
|
<div className="bg-white border border-slate-200 rounded-xl shadow-sm overflow-hidden">
|
||||||
|
<div className="px-6 py-4 border-b border-slate-200 bg-slate-50/50">
|
||||||
|
<h1 className="text-xl font-semibold text-slate-800">Mein Profil</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="p-6 space-y-6">
|
||||||
|
<div className="grid grid-cols-1 gap-6">
|
||||||
|
{/* Name Field */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium text-slate-700">Vollständiger Name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Email Field (Readonly) */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium text-slate-700">E-Mail Adresse</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
value={user.email}
|
||||||
|
readOnly
|
||||||
|
className="w-full px-3 py-2 border border-slate-200 bg-slate-50 text-slate-500 rounded-md cursor-not-allowed"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Role Badge */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium text-slate-700">Benutzerrolle</label>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span className={`
|
||||||
|
inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
|
||||||
|
${user.role === 'admin' ? 'bg-purple-100 text-purple-800' : 'bg-blue-100 text-blue-800'}
|
||||||
|
`}>
|
||||||
|
{user.role.toUpperCase()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-4 flex justify-end">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={updateMutation.isPending}
|
||||||
|
className="px-4 py-2 bg-slate-900 text-white rounded-md hover:bg-slate-800 disabled:opacity-50 transition-colors font-medium"
|
||||||
|
>
|
||||||
|
{updateMutation.isPending ? 'Speichert...' : 'Profil speichern'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user