feat: integrate user profile pictures with circular avatar display and upload in settings
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 57s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 57s
This commit is contained in:
1 parent
8b043c4809
commit
444442fdf0
13 files changed
+367
-65
No files matched your search
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import {
|
||||
User as UserIcon,
|
||||
Bell,
|
||||
@@ -15,10 +15,13 @@ import {
|
||||
Send,
|
||||
AlertCircle,
|
||||
Palette,
|
||||
Upload,
|
||||
Camera,
|
||||
} from 'lucide-react';
|
||||
import { APIKey, User } from '../../lib/types';
|
||||
import { api, triggerHaptic } from '../../lib/api';
|
||||
import { AVAILABLE_THEMES, ThemeId } from '../../lib/theme';
|
||||
import { UserAvatar, resizeImageToDataUrl } from '../../components/UserAvatar';
|
||||
|
||||
interface UserSettingsViewProps {
|
||||
currentUser: User;
|
||||
@@ -42,9 +45,12 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
|
||||
// --- Profile State ---
|
||||
const [displayName, setDisplayName] = useState(currentUser.displayName || '');
|
||||
const [email, setEmail] = useState(currentUser.email || '');
|
||||
const [avatarUrl, setAvatarUrl] = useState(currentUser.avatarUrl || '');
|
||||
const [newPassword, setNewPassword] = useState('');
|
||||
const [profileMsg, setProfileMsg] = useState<{ type: 'ok' | 'err'; text: string } | null>(null);
|
||||
const [savingProfile, setSavingProfile] = useState(false);
|
||||
const [isProcessingAvatar, setIsProcessingAvatar] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// --- Multi-Channel Alarms State ---
|
||||
const [alarmChannels, setAlarmChannels] = useState<string[]>(
|
||||
@@ -69,6 +75,7 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
|
||||
useEffect(() => {
|
||||
setDisplayName(currentUser.displayName || '');
|
||||
setEmail(currentUser.email || '');
|
||||
setAvatarUrl(currentUser.avatarUrl || '');
|
||||
setAlarmChannels(Array.isArray(currentUser.alarmChannels) ? currentUser.alarmChannels : []);
|
||||
setAlarmEmail(currentUser.alarmEmail || currentUser.email || '');
|
||||
setDiscordWebhookUrl(currentUser.discordWebhookUrl || '');
|
||||
@@ -95,6 +102,34 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
// --- Handlers: Avatar ---
|
||||
const handleAvatarFileSelected = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
setIsProcessingAvatar(true);
|
||||
setProfileMsg(null);
|
||||
try {
|
||||
const dataUrl = await resizeImageToDataUrl(file, 256);
|
||||
setAvatarUrl(dataUrl);
|
||||
triggerHaptic(10);
|
||||
} catch (err: any) {
|
||||
setProfileMsg({ type: 'err', text: err.message || 'Fehler beim Laden des Bildes' });
|
||||
} finally {
|
||||
setIsProcessingAvatar(false);
|
||||
if (fileInputRef.current) fileInputRef.current.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectPresetAvatar = (preset: string) => {
|
||||
setAvatarUrl(preset);
|
||||
triggerHaptic(10);
|
||||
};
|
||||
|
||||
const handleRemoveAvatar = () => {
|
||||
setAvatarUrl('');
|
||||
triggerHaptic(10);
|
||||
};
|
||||
|
||||
// --- Handlers: Profile ---
|
||||
const handleSaveProfile = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
@@ -104,6 +139,7 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
|
||||
const patch: any = {
|
||||
displayName: displayName.trim(),
|
||||
email: email.trim(),
|
||||
avatarUrl: avatarUrl.trim(),
|
||||
};
|
||||
if (newPassword.trim()) {
|
||||
patch.password = newPassword.trim();
|
||||
@@ -358,7 +394,88 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSaveProfile} className="space-y-4">
|
||||
<form onSubmit={handleSaveProfile} className="space-y-5">
|
||||
{/* Avatar Management */}
|
||||
<div className="p-3.5 bg-panel-elevated/40 border border-border rounded-box space-y-3">
|
||||
<label className="block text-muted font-medium">Profilbild</label>
|
||||
<div className="flex flex-col sm:flex-row items-center sm:items-start gap-4">
|
||||
<div className="relative group shrink-0">
|
||||
<UserAvatar
|
||||
user={{ displayName, email, avatarUrl }}
|
||||
size="xl"
|
||||
className="border-2 border-border/80 shadow-md ring-2 ring-primary/20"
|
||||
/>
|
||||
{isProcessingAvatar && (
|
||||
<div className="absolute inset-0 bg-base-100/70 rounded-full flex items-center justify-center text-[10px] font-medium text-main">
|
||||
Lädt…
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 space-y-2.5 text-center sm:text-left">
|
||||
<div className="flex flex-wrap items-center justify-center sm:justify-start gap-2">
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleAvatarFileSelected}
|
||||
className="hidden"
|
||||
id="avatar-upload-input"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={isProcessingAvatar}
|
||||
className="btn-action h-8 px-3 text-xs flex items-center gap-1.5"
|
||||
>
|
||||
<Upload className="w-3.5 h-3.5" />
|
||||
<span>Bild hochladen</span>
|
||||
</button>
|
||||
|
||||
{avatarUrl && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleRemoveAvatar}
|
||||
className="btn-ghost h-8 px-2.5 text-xs text-status-err hover:bg-status-err/10 flex items-center gap-1"
|
||||
title="Profilbild entfernen"
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
<span>Entfernen</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Emoji Presets */}
|
||||
<div className="space-y-1">
|
||||
<span className="text-[11px] text-muted">Oder schnelles Emoji wählen:</span>
|
||||
<div className="flex flex-wrap items-center justify-center sm:justify-start gap-1.5 pt-0.5">
|
||||
{['🦊', '🐱', '🐶', '🐼', '🦁', '🦉', '🚀', '☕', '⚡', '🎨', '🌟', '🎯', '💡'].map(
|
||||
(preset) => (
|
||||
<button
|
||||
key={preset}
|
||||
type="button"
|
||||
onClick={() => handleSelectPresetAvatar(preset)}
|
||||
className={`w-7 h-7 rounded-full text-sm flex items-center justify-center transition-transform hover:scale-110 active:scale-95 ${
|
||||
avatarUrl === preset
|
||||
? 'bg-primary/20 border-2 border-primary scale-105'
|
||||
: 'bg-panel hover:bg-panel-hover border border-border'
|
||||
}`}
|
||||
title={`Emoji ${preset}`}
|
||||
>
|
||||
{preset}
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-[10px] text-faint">
|
||||
Wird überall kreisförmig dargestellt (z. B. Menüleiste oben, Freigabelisten, Aufgaben).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-muted font-medium mb-1">Anzeigename</label>
|
||||
|
||||
Reference in new issue
Block a user