feat(profil): eigenes Profilbild hochladen
Jeder setzt sein Bild unter "Mein Konto", Vorgesetzte und Administratoren auch fuer die Benutzer, die sie verwalten. Wo bisher die Initialen standen, steht jetzt das Bild - Seitenleiste, Kopfzeile, Team, Dashboard, Reports, Projekte, Kunden und die Buchungsliste. Ohne Bild bleiben die Initialen. Der Browser verkleinert das Bild vor dem Hochladen auf ein Quadrat von 256 px und schneidet mittig zu (lib/imageCompress.ts): aus einem 4-MB-Foto werden wenige Kilobyte, und der Server muss kein Bild verarbeiten. Gespeichert wird es als Data-URL in pulse_users.avatar (LONGTEXT). Kein Upload-Verzeichnis, das gesichert und beim Deployment am Leben gehalten werden muesste. Geprueft wird serverseitig in lib/avatar.js: nur PNG, JPEG, GIF, WebP und AVIF als Data-URL, hoechstens 2 MB. SVG ist bewusst nicht erlaubt, es kann Skripte tragen. Externe http(s)-Adressen ebenso wenig - sonst laedt jeder Seitenaufruf ein Bild von einem fremden Server und verraet, wer wann mit Pulse arbeitet. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
2d3d843988
commit
9576d18abc
@@ -1,6 +1,7 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Modal } from '../ui/Modal';
|
||||
import { Avatar, Button, Field, inputClass } from '../ui/controls';
|
||||
import { Button, Field, inputClass } from '../ui/controls';
|
||||
import { AvatarPicker } from '../ui/AvatarPicker';
|
||||
import { useAuth } from '../../lib/auth';
|
||||
import { authApi } from '../../lib/api';
|
||||
import { useToast } from '../Toast';
|
||||
@@ -8,7 +9,7 @@ import { ROLE_LABELS } from '../../types';
|
||||
|
||||
const COLORS = ['#6ba04a', '#0ea5e9', '#8b5cf6', '#f97316', '#14b8a6', '#e11d48', '#eab308', '#6366f1', '#10b981', '#ec4899'];
|
||||
|
||||
/** Mein Konto: Farbe, Passwort, Abmelden - fuer jede Rolle. */
|
||||
/** Mein Konto: Profilbild, Farbe, Passwort, Abmelden - fuer jede Rolle. */
|
||||
export const AccountDialog: React.FC<{ onClose: () => void }> = ({ onClose }) => {
|
||||
const { user, updateUser, logout, settings } = useAuth();
|
||||
const { addToast } = useToast();
|
||||
@@ -27,6 +28,21 @@ export const AccountDialog: React.FC<{ onClose: () => void }> = ({ onClose }) =>
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* Das Bild wird sofort gespeichert, nicht erst beim Schliessen: der Dialog
|
||||
* hat keinen Speichern-Knopf, und eine halb uebernommene Aenderung waere
|
||||
* schlimmer als eine, die sofort steht.
|
||||
*/
|
||||
const changeAvatar = async (avatar: string) => {
|
||||
try {
|
||||
const { user: saved } = await authApi.updateMe({ avatar: avatar || null });
|
||||
updateUser(saved);
|
||||
addToast(avatar ? 'Profilbild gespeichert.' : 'Profilbild entfernt.', 'success');
|
||||
} catch (err) {
|
||||
addToast(err instanceof Error ? err.message : 'Speichern fehlgeschlagen.', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
const changePassword = async (event: React.FormEvent) => {
|
||||
event.preventDefault();
|
||||
if (next !== repeat) return addToast('Die neuen Passwörter stimmen nicht überein.', 'error');
|
||||
@@ -46,8 +62,8 @@ export const AccountDialog: React.FC<{ onClose: () => void }> = ({ onClose }) =>
|
||||
|
||||
return (
|
||||
<Modal title="Mein Konto" subtitle={`${ROLE_LABELS[user.role]} · ${user.username}`} onClose={onClose}>
|
||||
<div className="flex items-center gap-4">
|
||||
<Avatar name={user.name} color={user.color} id={user.id} size={56} external={user.is_external} />
|
||||
<div className="flex flex-wrap items-center gap-4">
|
||||
<AvatarPicker value={user.avatar ?? ''} onChange={changeAvatar} name={user.name} color={user.color} id={user.id} size={72} />
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-lg font-extrabold text-ink-900">{user.name}</p>
|
||||
<p className="truncate text-sm text-ink-500">{user.email || '–'}</p>
|
||||
|
||||
Reference in New Issue
Block a user