feat(notifications): implement notification system with CRUD operations and UI integration
All checks were successful
Build & Push Docker Image / build (push) Successful in 6m27s

This commit is contained in:
2026-03-02 16:45:53 +01:00
parent 304349fce8
commit c13090bc80
16 changed files with 626 additions and 20 deletions

View File

@@ -4,7 +4,7 @@ import {
Users, Shield, Search, Trash2, ChevronDown, Loader2,
MoreVertical, Key, UserCheck, UserX, UserPlus, Mail, Lock, User,
Upload, X as XIcon, Image, Type, Palette, Send, Copy, Clock, Check,
ShieldCheck, Globe,
ShieldCheck, Globe, Link as LinkIcon,
} from 'lucide-react';
import { useAuth } from '../contexts/AuthContext';
import { useLanguage } from '../contexts/LanguageContext';
@@ -16,7 +16,7 @@ import toast from 'react-hot-toast';
export default function Admin() {
const { user } = useAuth();
const { t, language } = useLanguage();
const { appName, hasLogo, logoUrl, defaultTheme, registrationMode, refreshBranding } = useBranding();
const { appName, hasLogo, logoUrl, defaultTheme, registrationMode, imprintUrl, privacyUrl, refreshBranding } = useBranding();
const navigate = useNavigate();
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
@@ -43,6 +43,10 @@ export default function Admin() {
const logoInputRef = useRef(null);
const [editDefaultTheme, setEditDefaultTheme] = useState('');
const [savingDefaultTheme, setSavingDefaultTheme] = useState(false);
const [editImprintUrl, setEditImprintUrl] = useState('');
const [savingImprintUrl, setSavingImprintUrl] = useState(false);
const [editPrivacyUrl, setEditPrivacyUrl] = useState('');
const [savingPrivacyUrl, setSavingPrivacyUrl] = useState(false);
useEffect(() => {
if (user?.role !== 'admin') {
@@ -61,6 +65,14 @@ export default function Admin() {
setEditDefaultTheme(defaultTheme || 'dark');
}, [defaultTheme]);
useEffect(() => {
setEditImprintUrl(imprintUrl || '');
}, [imprintUrl]);
useEffect(() => {
setEditPrivacyUrl(privacyUrl || '');
}, [privacyUrl]);
const fetchUsers = async () => {
try {
const res = await api.get('/admin/users');
@@ -237,6 +249,32 @@ export default function Admin() {
}
};
const handleImprintUrlSave = async () => {
setSavingImprintUrl(true);
try {
await api.put('/branding/imprint-url', { imprintUrl: editImprintUrl.trim() });
toast.success(t('admin.imprintUrlSaved'));
refreshBranding();
} catch {
toast.error(t('admin.imprintUrlFailed'));
} finally {
setSavingImprintUrl(false);
}
};
const handlePrivacyUrlSave = async () => {
setSavingPrivacyUrl(true);
try {
await api.put('/branding/privacy-url', { privacyUrl: editPrivacyUrl.trim() });
toast.success(t('admin.privacyUrlSaved'));
refreshBranding();
} catch {
toast.error(t('admin.privacyUrlFailed'));
} finally {
setSavingPrivacyUrl(false);
}
};
const filteredUsers = users.filter(u =>
(u.display_name || u.name).toLowerCase().includes(search.toLowerCase()) ||
u.email.toLowerCase().includes(search.toLowerCase())
@@ -381,6 +419,59 @@ export default function Admin() {
</button>
</div>
</div>
{/* Legal links */}
<div className="mt-6 pt-6 border-t border-th-border">
<div className="flex items-center gap-2 mb-1">
<LinkIcon size={16} className="text-th-accent" />
<label className="block text-sm font-medium text-th-text">{t('admin.legalLinksTitle')}</label>
</div>
<p className="text-xs text-th-text-s mb-4">{t('admin.legalLinksDesc')}</p>
<div className="grid gap-4 sm:grid-cols-2">
{/* Imprint */}
<div>
<label className="block text-xs font-medium text-th-text-s mb-1">{t('admin.imprintUrl')}</label>
<div className="flex items-center gap-2">
<input
type="url"
value={editImprintUrl}
onChange={e => setEditImprintUrl(e.target.value)}
className="input-field text-sm flex-1"
placeholder="https://example.com/imprint"
maxLength={500}
/>
<button
onClick={handleImprintUrlSave}
disabled={savingImprintUrl || editImprintUrl === (imprintUrl || '')}
className="btn-primary text-sm px-4 flex-shrink-0"
>
{savingImprintUrl ? <Loader2 size={14} className="animate-spin" /> : t('common.save')}
</button>
</div>
</div>
{/* Privacy Policy */}
<div>
<label className="block text-xs font-medium text-th-text-s mb-1">{t('admin.privacyUrl')}</label>
<div className="flex items-center gap-2">
<input
type="url"
value={editPrivacyUrl}
onChange={e => setEditPrivacyUrl(e.target.value)}
className="input-field text-sm flex-1"
placeholder="https://example.com/privacy"
maxLength={500}
/>
<button
onClick={handlePrivacyUrlSave}
disabled={savingPrivacyUrl || editPrivacyUrl === (privacyUrl || '')}
className="btn-primary text-sm px-4 flex-shrink-0"
>
{savingPrivacyUrl ? <Loader2 size={14} className="animate-spin" /> : t('common.save')}
</button>
</div>
</div>
</div>
</div>
</div>
{/* Registration Mode */}