fix: Redesign ProfileModal with Hallmark design tokens and safe array fallbacks
This commit is contained in:
Vendored
+2
-2
@@ -11,8 +11,8 @@
|
|||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700&family=JetBrains+Mono:wght@400;500;600;700&family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700&family=JetBrains+Mono:wght@400;500;600;700&family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||||||
<title>Schichtplaner — Veranstaltungsschichtpläne</title>
|
<title>Schichtplaner — Veranstaltungsschichtpläne</title>
|
||||||
<script type="module" crossorigin src="/assets/index-CgCRSgos.js"></script>
|
<script type="module" crossorigin src="/assets/index-CaFJNwPw.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-BIEZThrv.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-BpS4KT-L.css">
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-paper text-main font-sans antialiased min-h-screen">
|
<body class="bg-paper text-main font-sans antialiased min-h-screen">
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { X, User, Award, Check, AlertCircle } from 'lucide-react';
|
import { X, User, Award, Check, AlertCircle, CheckCircle2 } from 'lucide-react';
|
||||||
import { apiFetch } from '../api/client';
|
import { apiFetch } from '../api/client';
|
||||||
import { getRandomSkillColor } from '../utils/colors';
|
import { getRandomSkillColor } from '../utils/colors';
|
||||||
|
|
||||||
export default function ProfileModal({ user, skills, onClose, onUserUpdated, onRefreshSkills }) {
|
export default function ProfileModal({ user, skills = [], onClose, onUserUpdated, onRefreshSkills }) {
|
||||||
|
const safeSkills = Array.isArray(skills) ? skills : [];
|
||||||
|
const safeUserSkills = user?.skills ? (Array.isArray(user.skills) ? user.skills : []) : [];
|
||||||
|
|
||||||
const [displayName, setDisplayName] = useState(
|
const [displayName, setDisplayName] = useState(
|
||||||
user?.display_name || localStorage.getItem('guest_display_name') || ''
|
user?.display_name || user?.username || localStorage.getItem('guest_display_name') || ''
|
||||||
);
|
);
|
||||||
const [selectedSkillIds, setSelectedSkillIds] = useState(
|
const [selectedSkillIds, setSelectedSkillIds] = useState(
|
||||||
user?.skills ? user.skills.map(s => s.id) : []
|
safeUserSkills.map(s => (typeof s === 'object' ? s.id : s))
|
||||||
);
|
);
|
||||||
|
|
||||||
// New skill fields
|
// New skill fields
|
||||||
@@ -38,6 +41,7 @@ export default function ProfileModal({ user, skills, onClose, onUserUpdated, onR
|
|||||||
});
|
});
|
||||||
setSuccess('Profil und Anzeigename erfolgreich aktualisiert!');
|
setSuccess('Profil und Anzeigename erfolgreich aktualisiert!');
|
||||||
if (onUserUpdated) onUserUpdated(updatedUser);
|
if (onUserUpdated) onUserUpdated(updatedUser);
|
||||||
|
setTimeout(() => onClose(), 800);
|
||||||
} else {
|
} else {
|
||||||
localStorage.setItem('guest_display_name', displayName.trim());
|
localStorage.setItem('guest_display_name', displayName.trim());
|
||||||
window.dispatchEvent(new Event('guest_name_updated'));
|
window.dispatchEvent(new Event('guest_name_updated'));
|
||||||
@@ -79,7 +83,7 @@ export default function ProfileModal({ user, skills, onClose, onUserUpdated, onR
|
|||||||
setNewSkillDesc('');
|
setNewSkillDesc('');
|
||||||
setNewSkillColor(getRandomSkillColor());
|
setNewSkillColor(getRandomSkillColor());
|
||||||
setSuccess('Neue Fähigkeit erfolgreich angelegt!');
|
setSuccess('Neue Fähigkeit erfolgreich angelegt!');
|
||||||
onRefreshSkills();
|
if (onRefreshSkills) onRefreshSkills();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message || 'Erstellen der Fähigkeit fehlgeschlagen.');
|
setError(err.message || 'Erstellen der Fähigkeit fehlgeschlagen.');
|
||||||
} finally {
|
} finally {
|
||||||
@@ -88,44 +92,51 @@ export default function ProfileModal({ user, skills, onClose, onUserUpdated, onR
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-slate-950/70 backdrop-blur-sm overflow-y-auto">
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/70 backdrop-blur-sm overflow-y-auto animate-in fade-in duration-200">
|
||||||
<div className="bg-slate-900 w-full max-w-lg rounded-2xl border border-slate-800 p-6 shadow-xl relative my-8">
|
<div className="bg-surface w-full max-w-lg rounded-sm border border-grid p-6 shadow-2xl relative my-8 text-main font-sans">
|
||||||
|
{/* Close Button */}
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="absolute top-4 right-4 p-2 text-slate-400 hover:text-white rounded-lg transition"
|
className="absolute top-4 right-4 p-2 text-muted hover:text-main rounded-sm transition"
|
||||||
|
title="Schließen"
|
||||||
>
|
>
|
||||||
<X className="w-5 h-5" />
|
<X className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className="flex items-center gap-3 mb-6">
|
{/* Modal Header */}
|
||||||
<div className="w-10 h-10 rounded-xl bg-slate-800 text-slate-200 border border-slate-700 flex items-center justify-center">
|
<div className="flex items-center gap-3 mb-6 pb-4 border-b border-grid font-mono">
|
||||||
|
<div className="w-10 h-10 rounded-sm bg-subtle text-amber-500 border border-grid flex items-center justify-center font-bold">
|
||||||
<User className="w-5 h-5" />
|
<User className="w-5 h-5" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-bold text-lg text-white">Profil & Einstellungen</h3>
|
<h3 className="font-serif font-bold text-xl uppercase tracking-tight text-main">
|
||||||
<p className="text-xs text-slate-400">Anzeigenamen und Qualifikationen verwalten</p>
|
{user ? 'Profil & Qualifikationen' : 'Gast-Name anpassen'}
|
||||||
|
</h3>
|
||||||
|
<p className="text-xs text-muted">
|
||||||
|
{user ? 'Anzeigename & persönliche Qualifikationen verwalten' : 'Passe deinen Namen für Schichteintragungen an'}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="mb-4 p-3 rounded-xl bg-red-500/10 border border-red-500/20 text-red-400 text-xs flex items-center gap-2">
|
<div className="mb-4 p-3 rounded-sm bg-red-500/10 border border-red-500/20 text-red-400 text-xs font-mono flex items-center gap-2">
|
||||||
<AlertCircle className="w-4 h-4 shrink-0" />
|
<AlertCircle className="w-4 h-4 shrink-0" />
|
||||||
<span>{error}</span>
|
<span>{error}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{success && (
|
{success && (
|
||||||
<div className="mb-4 p-3 rounded-xl bg-emerald-500/10 border border-emerald-500/20 text-emerald-400 text-xs flex items-center gap-2">
|
<div className="mb-4 p-3 rounded-sm bg-emerald-500/10 border border-emerald-500/20 text-emerald-400 text-xs font-mono flex items-center gap-2">
|
||||||
<Check className="w-4 h-4 shrink-0" />
|
<CheckCircle2 className="w-4 h-4 shrink-0" />
|
||||||
<span>{success}</span>
|
<span>{success}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<form onSubmit={handleSaveProfile} className="space-y-5">
|
<form onSubmit={handleSaveProfile} className="space-y-5 font-mono text-xs">
|
||||||
{/* Display Name Edit Field */}
|
{/* Display Name Edit Field */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-semibold text-slate-300 mb-1">
|
<label className="block text-xs font-bold text-main mb-1 uppercase">
|
||||||
Dein Anzeigename (wird in allen Schichtlisten angezeigt)
|
Dein Anzeigename
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -133,88 +144,90 @@ export default function ProfileModal({ user, skills, onClose, onUserUpdated, onR
|
|||||||
value={displayName}
|
value={displayName}
|
||||||
onChange={(e) => setDisplayName(e.target.value)}
|
onChange={(e) => setDisplayName(e.target.value)}
|
||||||
placeholder="z. B. Alex Muster"
|
placeholder="z. B. Alex Muster"
|
||||||
className="w-full px-3.5 py-2.5 rounded-xl bg-slate-950 border border-slate-800 text-white text-sm focus:outline-none focus:border-slate-600"
|
className="w-full px-3.5 py-2.5 rounded-sm bg-subtle border border-grid text-main text-sm focus:outline-none focus:border-muted font-sans"
|
||||||
/>
|
/>
|
||||||
<p className="text-[11px] text-slate-500 mt-1">
|
<p className="text-[11px] text-muted font-sans mt-1">
|
||||||
Änderungen werden sofort in allen von dir gebuchten Schichten wirksam.
|
Wird in allen Schichtplänen bei deinen Eintragungen angezeigt.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* User Skills Selection */}
|
{/* User Skills Selection (Only for logged-in users) */}
|
||||||
<div className="space-y-2">
|
{user && (
|
||||||
<label className="block text-xs font-semibold text-slate-300">
|
<div className="space-y-2 pt-2 border-t border-grid">
|
||||||
|
<label className="block text-xs font-bold text-main uppercase">
|
||||||
Deine Qualifikationen & Fähigkeiten
|
Deine Qualifikationen & Fähigkeiten
|
||||||
</label>
|
</label>
|
||||||
{skills.length === 0 ? (
|
{safeSkills.length === 0 ? (
|
||||||
<p className="text-xs text-slate-500 italic">Noch keine Fähigkeiten definiert.</p>
|
<p className="text-xs text-muted italic font-sans">[ Noch keine Qualifikationen im System angelegt ]</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||||
{skills.map((s) => {
|
{safeSkills.map((s) => {
|
||||||
const isSelected = selectedSkillIds.includes(s.id);
|
const isSelected = selectedSkillIds.includes(s.id);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={s.id}
|
key={s.id}
|
||||||
onClick={() => toggleSkill(s.id)}
|
onClick={() => toggleSkill(s.id)}
|
||||||
className={`p-2.5 rounded-xl border cursor-pointer transition flex items-center justify-between ${
|
className={`p-2.5 rounded-sm border cursor-pointer transition flex items-center justify-between font-mono ${
|
||||||
isSelected
|
isSelected
|
||||||
? 'bg-slate-800 border-slate-700 text-white'
|
? 'bg-surface-hover border-grid text-main font-bold shadow-sm'
|
||||||
: 'bg-slate-950/60 border-slate-800/80 text-slate-400 hover:border-slate-700'
|
: 'bg-subtle border-grid/60 text-muted hover:text-main hover:border-grid'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="w-2.5 h-2.5 rounded-full" style={{ backgroundColor: s.color }}></span>
|
<span className="w-2.5 h-2.5 rounded-full shrink-0" style={{ backgroundColor: s.color }}></span>
|
||||||
<span className="text-xs font-medium">{s.name}</span>
|
<span className="text-xs truncate">{s.name}</span>
|
||||||
</div>
|
</div>
|
||||||
{isSelected && <Check className="w-3.5 h-3.5 text-emerald-400" />}
|
{isSelected && <Check className="w-3.5 h-3.5 text-emerald-400 shrink-0" />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="pt-2 flex justify-end">
|
<div className="pt-3 flex justify-end">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={savingProfile}
|
disabled={savingProfile}
|
||||||
className="px-5 py-2.5 rounded-xl text-xs font-bold bg-slate-100 hover:bg-white text-slate-950 transition shadow-sm disabled:opacity-50"
|
className="px-5 py-2.5 rounded-sm text-xs font-bold bg-indigo-600 hover:bg-indigo-500 text-white transition shadow-sm disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{savingProfile ? 'Speichern...' : 'Profil speichern'}
|
{savingProfile ? '[ SPEICHERN... ]' : '[ PROFIL SPEICHERN ]'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{/* Add New Skill Section */}
|
{/* Add New Skill Section (For Admins / Staff) */}
|
||||||
<form onSubmit={handleCreateSkill} className="border-t border-slate-800 mt-6 pt-5 space-y-3">
|
{user && (user.is_admin_user || user.is_staff || user.is_superuser) && (
|
||||||
<h4 className="font-bold text-xs text-slate-400 uppercase tracking-wider flex items-center gap-1.5">
|
<form onSubmit={handleCreateSkill} className="border-t border-grid mt-6 pt-5 space-y-3 font-mono text-xs">
|
||||||
<Award className="w-3.5 h-3.5" /> Neue Qualifikation im System anlegen
|
<h4 className="font-bold text-xs text-muted uppercase tracking-wider flex items-center gap-1.5">
|
||||||
|
<Award className="w-3.5 h-3.5 text-amber-500" /> Neue Qualifikation im System anlegen
|
||||||
</h4>
|
</h4>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2">
|
<div className="grid grid-cols-1 sm:grid-cols-12 gap-2">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
required
|
required
|
||||||
placeholder="Fähigkeit (z. B. Ersthelfer)"
|
placeholder="Fähigkeit (z. B. Ersthelfer)"
|
||||||
value={newSkillName}
|
value={newSkillName}
|
||||||
onChange={(e) => setNewSkillName(e.target.value)}
|
onChange={(e) => setNewSkillName(e.target.value)}
|
||||||
className="sm:col-span-2 px-3 py-2 rounded-xl bg-slate-950 border border-slate-800 text-white text-xs"
|
className="sm:col-span-7 px-3 py-2 rounded-sm bg-subtle border border-grid text-main text-xs font-sans"
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
type="color"
|
type="color"
|
||||||
value={newSkillColor}
|
value={newSkillColor}
|
||||||
onChange={(e) => setNewSkillColor(e.target.value)}
|
onChange={(e) => setNewSkillColor(e.target.value)}
|
||||||
className="w-full h-8 rounded-xl border-0 cursor-pointer bg-slate-950 p-1"
|
className="sm:col-span-2 h-8 w-full rounded-sm border border-grid cursor-pointer bg-subtle p-0.5"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<div className="flex justify-end">
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={creatingSkill}
|
disabled={creatingSkill}
|
||||||
className="px-3.5 py-1.5 rounded-xl text-xs font-semibold bg-slate-800 hover:bg-slate-700 text-slate-200 border border-slate-700 transition"
|
className="sm:col-span-3 px-3 py-1.5 rounded-sm text-xs font-bold bg-surface hover:bg-surface-hover text-main border border-grid transition"
|
||||||
>
|
>
|
||||||
{creatingSkill ? 'Anlegen...' : 'Fähigkeit hinzufügen'}
|
{creatingSkill ? '[ ANLEGEN... ]' : '[ ANLEGEN ]'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user