import { useState, useEffect, useRef, useCallback } from 'react';
import { api, S } from './lib.js';
import { TOOLS, getGroupedTools } from './toolRegistry.js';
import CalendarWidget, { CalendarSettings } from './calendar.jsx';
import { DateiSettings } from './tools/dateien.jsx';
import { useConfirm } from './confirm.jsx';
import { HomeIcon, AppsIcon, AdminIcon, MoreIcon, UserIcon, LogOutIcon, ChevronIcon, UpdateIcon, TrashIcon } from './icons.jsx';
const ICONS = ['๐','๐','๐','๐','๐ ','๐ง','๐','๐พ','๐ฅ','๐ก','๐','๐','๐ฏ','โก','๐ ','๐ง','๐ฑ','๐ฎ','๐','๐ก'];
// โโ Responsive Hook โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function useIsMobile() {
const [mobile, setMobile] = useState(window.innerWidth < 768);
useEffect(() => {
// Nur auf Breite reagieren, nicht auf Keyboard-Open (Hรถhenรคnderung)
const fn = () => {
const isMob = window.innerWidth < 768;
setMobile(prev => prev === isMob ? prev : isMob);
};
window.addEventListener('resize', fn);
return () => window.removeEventListener('resize', fn);
}, []);
return mobile;
}
// โโ Toast โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function Toast({ msg, type }) {
if (!msg) return null;
const c = type === 'error' ? '#ff6b9d' : '#4ecdc4';
return (
{type==='error'?'โ ':'โ '}{msg}
);
}
// โโ Modal โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function Modal({ title, onClose, children }) {
return (
=768 ? 'center' : 'flex-end',
justifyContent:'center', padding: window.innerWidth>=768 ? 24 : 0 }}
onClick={e=>e.target===e.currentTarget&&onClose()}>
=768 ? 16 : '16px 16px 0 0',
padding:'20px 20px 40px', width:'100%',
maxWidth:600, maxHeight:'85vh', overflowY:'auto',
boxShadow: window.innerWidth>=768 ? '0 24px 80px rgba(0,0,0,0.6)' : 'none' }}>
{window.innerWidth < 768 &&
}
{title}
{children}
);
}
// โโ PWA Splash Screen โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function SplashScreen({ onDone }) {
const [fade, setFade] = useState(false);
useEffect(() => {
const t1 = setTimeout(() => setFade(true), 1200);
const t2 = setTimeout(() => onDone(), 1700);
return () => { clearTimeout(t1); clearTimeout(t2); };
}, []);
return (
);
}
// โโ Login โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function Login({ onLogin }) {
const [u,setU]=useState(''); const [p,setP]=useState('');
const [err,setErr]=useState(''); const [busy,setBusy]=useState(false);
const go = async () => {
setErr(''); setBusy(true);
try { const d=await api('/auth/login',{body:{username:u,password:p}}); localStorage.setItem('sk_token',d.token); onLogin(d.user); }
catch(e) { setErr(e.message); } finally { setBusy(false); }
};
return (
{[['BENUTZERNAME',u,setU,'text'],['PASSWORT',p,setP,'password']].map(([l,v,s,t])=>(
s(e.target.value)}
onKeyDown={e=>e.key==='Enter'&&go()}
style={{ ...S.inp, fontSize:16, padding:'13px 14px', borderRadius:10,
border:'1px solid rgba(255,255,255,0.12)' }}
autoCapitalize="none" autoCorrect="off" />
))}
{err &&
โ {err}
}
);
}
// โโ Update Modal โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function UpdateModal({ data, onClose }) {
return (
Installiert: {data.currentVersion}
{' โ '}{data.latestVersion}
{(data.releases||[]).map((r,i)=>(
{r.name||r.version}
{r.publishedAt?new Date(r.publishedAt).toLocaleDateString('de-DE'):''}
{r.body}
))}
Update: docker compose up -d --build
);
}
// โโ Desktop Sidebar โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function Sidebar({ active, setActive, user, onLogout, updateInfo, onUpdateClick }) {
const [appsOpen, setAppsOpen] = useState(true);
const [collapsed, setCollapsed] = useState(false);
const NavBtn = ({ item, sub=false }) => {
const ic = active===item.id ? '#4ecdc4' : 'rgba(255,255,255,0.55)';
return (
);
};
return (
);
}
// โโ Mobile Bottom Navigation โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function BottomNav({ active, setActive, user, onLogout, updateInfo, onUpdateClick }) {
const [menuOpen, setMenuOpen] = useState(false);
const [appsOpen, setAppsOpen] = useState(false);
const NAV_H = 56;
const MI = ({Icon, icon, label, onClick, color='rgba(255,255,255,0.85)'}) => {
const Ic = Icon;
return (
);
};
const Sheet = ({open, onClose, children}) => {
if (!open) return null;
const isDesktop = window.innerWidth >= 768;
if (isDesktop) return (
e.stopPropagation()}>
{children}
);
return (
e.stopPropagation()}>
{children}
);
};
const mainNav = [
{ id:'dashboard', Icon:HomeIcon, label:'Home' },
{ id:'_apps', Icon:AppsIcon, label:'Apps' },
{ id:'admin', Icon:AdminIcon, label:'Einst.' },
{ id:'_mehr', Icon:MoreIcon, label:'Mehr' },
];
return (
<>
{/* Apps Sheet mit Gruppenรผberschriften */}
setAppsOpen(false)}>
{getGroupedTools().map(([group, tools]) => (
{group.toUpperCase()}
{tools.map(t => (
{ setActive(t.id); setAppsOpen(false); }} />
))}
))}
{/* Mehr Sheet */}
setMenuOpen(false)}>
{updateInfo?.hasUpdate && {setMenuOpen(false);onUpdateClick();}} color="#ffe66d"/>}
{}} color="rgba(255,255,255,0.35)"/>
{onLogout();setMenuOpen(false);}} color="#ff6b9d"/>
{/* Bottom Bar */}
>
);
}
// โโ Dashboard Widgets โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function QuickLinks({ toast, mobile }) {
const { confirm, ConfirmDialog } = useConfirm();
const [links, setLinks] = useState([]);
const [editMode, setEdit] = useState(false);
const [showForm, setForm] = useState(false);
const [form, setF] = useState({ title:'', url:'', icon:'๐' });
const [editId, setEditId] = useState(null);
useEffect(() => { api('/dashboard/links').then(setLinks).catch(() => {}); }, []);
const getFavicon = url => {
try { return `https://www.google.com/s2/favicons?sz=64&domain=${new URL(url).hostname}`; }
catch { return null; }
};
const save = async () => {
if (!form.title.trim() || !form.url.trim()) { toast('Titel und URL erforderlich', 'error'); return; }
let url = form.url.trim();
if (!/^https?:\/\//i.test(url)) url = 'https://' + url;
// Auto favicon if user left default
const icon = form.icon === '๐' ? (getFavicon(url) || '๐') : form.icon;
try {
if (editId) {
const u = await api(`/dashboard/links/${editId}`, { method:'PUT', body:{...form, icon, url} });
setLinks(p => p.map(l => l.id===editId ? u : l));
} else {
const n = await api('/dashboard/links', { body:{...form, icon, url} });
setLinks(p => [...p, n]);
}
toast(editId ? 'Aktualisiert' : 'Gespeichert');
setF({ title:'', url:'', icon:'๐' }); setForm(false); setEditId(null);
} catch(e) { toast(e.message, 'error'); }
};
const del = async id => {
if (!await confirm('Link wirklich lรถschen?')) return;
try { await api(`/dashboard/links/${id}`, { method:'DELETE' }); setLinks(p => p.filter(l => l.id!==id)); toast('Gelรถscht'); }
catch(e) { toast(e.message, 'error'); }
};
return (
{/* Link-Grid */}
{links.length === 0 && !editMode
?
Links kรถnnen unter Einstellungen โ Dashboard hinzugefรผgt werden.
:
}
{/* Formular */}
{showForm && (
{/* Icon-Picker */}
ICON
{ICONS.map(ic => (
))}
)}
);
}
function TodoList({ toast }) {
const [items, setItems] = useState([]);
const [text, setText] = useState('');
const [isOpen, setIsOpen] = useState(false);
useEffect(() => { api('/dashboard/todos').then(setItems).catch(() => {}); }, []);
const add = async () => {
if (!text.trim()) return;
try { const n = await api('/dashboard/todos', { body:{ text:text.trim() } }); setItems(p => [...p, n]); setText(''); }
catch(e) { toast(e.message, 'error'); }
};
const toggle = async item => {
try { const u = await api(`/dashboard/todos/${item.id}`, { method:'PUT', body:{ done:!item.done } }); setItems(p => p.map(i => i.id===item.id ? u : i)); }
catch(e) { toast(e.message, 'error'); }
};
const del = async id => {
try { await api(`/dashboard/todos/${id}`, { method:'DELETE' }); setItems(p => p.filter(i => i.id!==id)); }
catch(e) { toast(e.message, 'error'); }
};
const open = items.filter(i => !i.done);
const done = items.filter(i => i.done);
return (
{isOpen && <>
setText(e.target.value)}
onKeyDown={e => e.key==='Enter' && add()}
style={{ ...S.inp, flex:1, fontSize:16 }} placeholder="Neue Aufgabeโฆ" />
{open.length === 0 && done.length === 0 && (
Noch keine Aufgaben.
)}
{open.map(item => (
))}
{done.length > 0 && (
<>
ERLEDIGT
{done.map(item => (
toggle(item)} style={{
width:22, height:22, borderRadius:5, flexShrink:0, cursor:'pointer',
border:'1.5px solid rgba(78,205,196,0.4)', background:'rgba(78,205,196,0.15)',
color:'#4ecdc4', fontSize:12, lineHeight:1,
}}>โ
{item.text}
del(item.id)} style={{ background:'transparent', border:'none', color:'rgba(255,255,255,0.5)', cursor:'pointer', fontSize:18 }}>โ
))}
>
)}
>}
);
}
function Notepad({ toast }) {
const [content, setContent] = useState('');
const [saved, setSaved] = useState(true);
const [saveTime, setSaveTime] = useState(null);
const [isOpen, setIsOpen] = useState(false);
const tmr = useRef(null);
useEffect(() => {
api('/dashboard/note').then(d => { setContent(d.content || ''); setSaveTime(d.updated_at); }).catch(() => {});
}, []);
const change = val => {
setContent(val); setSaved(false); clearTimeout(tmr.current);
tmr.current = setTimeout(async () => {
try { const r = await api('/dashboard/note', { method:'PUT', body:{ content:val } }); setSaved(true); setSaveTime(r.updated_at); }
catch(e) { toast(e.message, 'error'); }
}, 900);
};
return (
setIsOpen(v=>!v)} style={{width:'100%',background:'transparent',border:'none',
display:'flex',justifyContent:'space-between',alignItems:'center',cursor:'pointer',padding:0}}>
NOTIZEN
โถ
{isOpen &&
{saved ? (saveTime ? `โ ${new Date(saveTime).toLocaleTimeString('de-DE', { hour:'2-digit', minute:'2-digit' })}` : 'โ') : 'โฆ'}
}
{isOpen &&
);
}
// โโ Bestell-Statistik Widget โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function BestellStats() {
const [stats, setStats] = useState(null);
const [open, setOpen] = useState(false);
const [loaded, setLoaded] = useState(false);
const load = () => {
if (loaded) return;
api('/dashboard/stats').then(s => { setStats(s); setLoaded(true); }).catch(() => {});
};
const toggle = () => { setOpen(v => !v); if (!open) load(); };
const Row = ({label, val, color='#fff'}) => (
{label}
{val}
);
return (
3D-DRUCK STATISTIK
โถ
{open && (
{!stats ? (
Lรคdtโฆ
) : (
<>
>
)}
)}
);
}
function Dashboard({ toast, mobile }) {
return (
Dashboard
{new Date().toLocaleDateString('de-DE', { weekday:'long', day:'numeric', month:'long' })}
);
}
// โโ Admin Panel โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// Sec muss auรerhalb von AdminPanel definiert sein damit Inputs den Fokus behalten
const Sec = ({title, children}) => (
);
// โโ Account-Lรถschung โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function DeleteAccountSection({ toast, user }) {
const [pw, setPw] = useState('');
const [busy, setBusy] = useState(false);
const { confirm, ConfirmDialog } = useConfirm();
const doDelete = async () => {
if (!pw) { toast('Passwort eingeben', 'error'); return; }
if (!await confirm(`Account "${user.username}" und alle Daten wirklich lรถschen? Das kann nicht rรผckgรคngig gemacht werden.`)) return;
setBusy(true);
try {
await api('/auth/delete-account', { method:'DELETE', body:{ password:pw } });
localStorage.removeItem('sk_token');
window.location.reload();
} catch(e) { toast(e.message, 'error'); } finally { setBusy(false); }
};
return (
setPw(e.target.value)}/>
{busy ? 'โฆ' : 'โ Account unwiderruflich lรถschen'}
);
}
// โโ QuickLinks Einstellungen โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function QuickLinksSettings({ toast }) {
const [links, setLinks] = useState([]);
const [showForm, setForm] = useState(false);
const [form, setF] = useState({ title:'', url:'', icon:'๐' });
const [editId, setEditId] = useState(null);
const { confirm, ConfirmDialog } = useConfirm();
useEffect(() => { api('/dashboard/links').then(setLinks).catch(() => {}); }, []);
const getFavicon = url => {
try { return `https://www.google.com/s2/favicons?sz=64&domain=${new URL(url).hostname}`; }
catch { return null; }
};
const save = async () => {
if (!form.title.trim() || !form.url.trim()) { toast('Titel und URL erforderlich', 'error'); return; }
let url = form.url.trim();
if (!/^https?:\/\//i.test(url)) url = 'https://' + url;
const icon = form.icon === '๐' ? (getFavicon(url) || '๐') : form.icon;
try {
if (editId) {
const u = await api(`/dashboard/links/${editId}`, { method:'PUT', body:{...form, icon, url} });
setLinks(p => p.map(l => l.id===editId ? u : l));
} else {
const n = await api('/dashboard/links', { body:{...form, icon, url} });
setLinks(p => [...p, n]);
}
toast(editId ? 'Aktualisiert' : 'Gespeichert');
setF({ title:'', url:'', icon:'๐' }); setForm(false); setEditId(null);
} catch(e) { toast(e.message, 'error'); }
};
const del = async id => {
if (!await confirm('Link wirklich lรถschen?')) return;
try { await api(`/dashboard/links/${id}`, { method:'DELETE' }); setLinks(p => p.filter(l => l.id!==id)); toast('Gelรถscht'); }
catch(e) { toast(e.message, 'error'); }
};
return (
{/* Link-Liste */}
{links.length > 0 && (
{links.map(l => (
{l.icon && l.icon.startsWith('http')
?
e.target.style.display='none'}/>
: l.icon}
{ setF({ title:l.title, url:l.url, icon:l.icon }); setEditId(l.id); setForm(true); }}
style={S.btn('#4ecdc4', true)}>โ
del(l.id)} style={S.btn('#ff6b9d', true)}>โ
))}
)}
{/* Formular */}
{showForm ? (
ICON
{['๐','๐','๐','๐','๐ ','๐ง','๐','๐พ','๐ฅ','๐ก','๐','๐','๐ฏ','โก','๐ ','๐ง','๐ฑ','๐ฎ','๐','๐ก'].map(ic => (
setF(f => ({...f, icon:ic}))} style={{
width:34, height:34, borderRadius:7, fontSize:17, cursor:'pointer',
border:`1px solid ${form.icon===ic ? '#4ecdc4' : 'rgba(255,255,255,0.1)'}`,
background: form.icon===ic ? 'rgba(78,205,196,0.15)' : 'rgba(255,255,255,0.04)',
}}>{ic}
))}
setF(f => ({...f, title:e.target.value}))} placeholder="Mein Link"/>
setF(f => ({...f, url:e.target.value}))} placeholder="https://โฆ" autoCapitalize="none"/>
{editId ? 'โ Aktualisieren' : 'โ Speichern'}
{ setForm(false); setEditId(null); setF({ title:'', url:'', icon:'๐' }); }}
style={{ ...S.btn('#ff6b9d'), flex:1, textAlign:'center', padding:'10px 0' }}>Abbrechen
) : (
{ setForm(true); setEditId(null); setF({ title:'', url:'', icon:'๐' }); }}
style={{ ...S.btn('#4ecdc4'), width:'100%', textAlign:'center', padding:'10px 0' }}>
+ Link hinzufรผgen
)}
);
}
// โโ Benutzerverwaltung โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function UserManagement({ toast }) {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [form, setForm] = useState({ username:'', password:'', role:'user' });
const [resetId, setResetId] = useState(null);
const [resetPw, setResetPw] = useState('');
const [delId, setDelId] = useState(null);
const { confirm, ConfirmDialog } = useConfirm();
const load = () => {
setLoading(true);
api('/admin/users').then(setUsers).catch(e=>toast(e.message,'error')).finally(()=>setLoading(false));
};
useEffect(()=>{ load(); },[]);
const create = async () => {
if (!form.username.trim()||!form.password) { toast('Alle Felder ausfรผllen','error'); return; }
try {
await api('/admin/users',{body:form});
toast(`Benutzer "${form.username}" angelegt`);
setForm({username:'',password:'',role:'user'}); load();
} catch(e) { toast(e.message,'error'); }
};
const del = async id => {
if (!await confirm('Benutzer und alle seine Daten wirklich lรถschen?')) return;
try { await api(`/admin/users/${id}`,{method:'DELETE'}); toast('Gelรถscht'); load(); }
catch(e) { toast(e.message,'error'); }
};
const resetPassword = async () => {
if (!resetPw||resetPw.length<8) { toast('Mind. 8 Zeichen','error'); return; }
try {
await api(`/admin/users/${resetId}/reset-password`,{method:'PUT',body:{newPassword:resetPw}});
toast('Passwort zurรผckgesetzt'); setResetId(null); setResetPw('');
} catch(e) { toast(e.message,'error'); }
};
return (
{/* Neuen Benutzer anlegen */}
setForm(f=>({...f,username:e.target.value}))} autoCapitalize="none"/>
setForm(f=>({...f,password:e.target.value}))}/>
+ Benutzer anlegen
{/* Passwort-Reset Modal */}
{resetId && (
Passwort zurรผcksetzen
setResetPw(e.target.value)}/>
โ Setzen
{setResetId(null);setResetPw('');}} style={{...S.btn('#ff6b9d'),flex:1,textAlign:'center',padding:'10px 0'}}>Abbrechen
)}
{/* Benutzerliste */}
{loading && Lรคdtโฆ
}
{users.map(u => (
{u.username}
{u.role}
{setResetId(u.id);setResetPw('');}}
style={{...S.btn('#ffe66d',true),fontSize:10}}>๐ PW
del(u.id)} style={{...S.btn('#ff6b9d',true)}}>
{/* Statistik */}
{[
['โ Archiv', u.archiv],
['๐ฆ Bestellungen', u.bestellung],
['โ Todos', u.todos],
['๐ Links', u.links],
['๐ Dateien', u.files],
['๐
iCals', u.icals],
[`โ ${u.noteLen} Zeichen`, null],
].map(([label, val]) => (
{label}{val !== null ? `: ${val}` : ''}
))}
))}
);
}
// Field auรerhalb definiert โ Tastatur bleibt beim Tippen erhalten
const Field = ({ label, type='text', value, onChange, placeholder='', autoCapitalize='off' }) => (
);
function AdminPanel({ toast, mobile, user }) {
const [section, setSection] = useState('profil');
const [pw, setPw] = useState({ current:'', newPw:'', confirm:'' });
const [avatar, setAvatar] = useState(localStorage.getItem('dd_avatar') || null);
const [un, setUn] = useState({ newUsername:'', unPw:'' });
const [file, setFile] = useState(null);
const [busy, setBusy] = useState({ pw:false, un:false, restore:false });
const set = (key, val) => setBusy(b => ({...b, [key]:val}));
const changePw = async () => {
if (pw.newPw !== pw.confirm) { toast('Passwรถrter stimmen nicht รผberein', 'error'); return; }
if (pw.newPw.length < 8) { toast('Mindestens 8 Zeichen', 'error'); return; }
set('pw', true);
try { await api('/auth/change-password', { body:{ currentPassword:pw.current, newPassword:pw.newPw } }); toast('Passwort geรคndert'); setPw({ current:'', newPw:'', confirm:'' }); }
catch(e) { toast(e.message, 'error'); } finally { set('pw', false); }
};
const changeUn = async () => {
if (!un.newUsername.trim()) { toast('Benutzername eingeben', 'error'); return; }
set('un', true);
try {
await api('/auth/change-username', { body:{ newUsername:un.newUsername.trim(), password:un.unPw } });
toast('Benutzername geรคndert โ bitte neu einloggen');
setUn({ newUsername:'', unPw:'' });
setTimeout(() => { localStorage.removeItem('sk_token'); window.location.reload(); }, 1800);
} catch(e) { toast(e.message, 'error'); } finally { set('un', false); }
};
const backup = async () => {
try {
const blob = await api('/admin/backup');
const a = Object.assign(document.createElement('a'), {
href: URL.createObjectURL(blob),
download: `dickendock-backup-${new Date().toISOString().split('T')[0]}.db`
});
a.click(); URL.revokeObjectURL(a.href); toast('Backup heruntergeladen');
} catch(e) { toast(e.message, 'error'); }
};
const restore = async () => {
if (!file) { toast('Datei auswรคhlen', 'error'); return; }
if (!window.confirm('โ ๏ธ Aktuelle Datenbank wird ersetzt. Fortfahren?')) return;
set('restore', true);
try {
const f = new FormData(); f.append('database', file);
await api('/admin/restore', { method:'POST', body:f, isFile:true });
toast('Wiederhergestellt'); setFile(null);
} catch(e) { toast(e.message, 'error'); } finally { set('restore', false); }
};
return (
Einstellungen
{[['profil','Profil'],['dashboard','Dashboard'], ...(user?.role==='admin'?[['benutzer','Benutzer'],['backup','Backup']]:[])]
.map(([k,l]) => (
setSection(k)} style={{
padding:'6px 16px', borderRadius:20, fontFamily:'monospace', fontSize:12, cursor:'pointer',
border:`1px solid ${section===k?'rgba(78,205,196,0.5)':'rgba(255,255,255,0.15)'}`,
background: section===k?'rgba(78,205,196,0.12)':'transparent',
color: section===k?'#4ecdc4':'rgba(255,255,255,0.55)',
}}>{l}
))}
{section === 'profil' && (
{avatar
?

:
}
{
const file = e.target.files?.[0]; if(!file) return;
const reader = new FileReader();
reader.onload = ev => {
const canvas = document.createElement('canvas');
const img = new Image(); img.onload = () => {
const size = 120;
canvas.width = size; canvas.height = size;
const ctx = canvas.getContext('2d');
ctx.beginPath(); ctx.arc(size/2,size/2,size/2,0,Math.PI*2);
ctx.clip();
const min = Math.min(img.width,img.height);
ctx.drawImage(img,(img.width-min)/2,(img.height-min)/2,min,min,0,0,size,size);
const data = canvas.toDataURL('image/jpeg',0.85);
setAvatar(data); localStorage.setItem('dd_avatar',data); toast('Avatar gespeichert');
}; img.src = ev.target.result;
}; reader.readAsDataURL(file);
}} style={{...S.inp,fontSize:13,padding:'7px 10px',cursor:'pointer'}}/>
{avatar && {setAvatar(null);localStorage.removeItem('dd_avatar');toast('Avatar entfernt');}}
style={{...S.btn('#ff6b9d',true)}}>โ Entfernen}
{user?.role==='admin' &&
setUn(p=>({...p,newUsername:e.target.value}))} />
setUn(p=>({...p,unPw:e.target.value}))} />
{busy.un ? 'โฆ' : 'โ Benutzername รคndern'}
}
setPw(p=>({...p,current:e.target.value}))} />
setPw(p=>({...p,newPw:e.target.value}))} />
setPw(p=>({...p,confirm:e.target.value}))} />
{busy.pw ? 'โฆ' : 'โ Passwort รคndern'}
Lรถscht deinen Account und alle deine Daten unwiderruflich.
)}
{section === 'benutzer' && user?.role==='admin' && (
)}
{section === 'dashboard' && (
{user?.role==='admin' &&
}
)}
{section === 'backup' && (
Vollstรคndige SQLite-Datenbank sichern oder wiederherstellen.
โ Backup herunterladen
setFile(e.target.files[0])}
style={{ ...S.inp, flex:1, padding:'9px 10px', cursor:'pointer', fontSize:13 }} />
{busy.restore ? 'โฆ' : 'โ'}
{file && {file.name}
}
)}
);
}
// โโ App Shell โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
export default function App() {
const mobile = useIsMobile();
const [splash, setSplash] = useState(() => window.matchMedia('(display-mode: standalone)').matches);
const [user,setUser]=useState(null);
const [active,setActive]=useState('dashboard');
const [toastMsg,setToastMsg]=useState({msg:'',type:'success'});
const [updateInfo,setUpdateInfo]=useState(null);
const [showUpd,setShowUpd]=useState(false);
useEffect(()=>{
const token=localStorage.getItem('sk_token');
if(!token)return;
try{const p=JSON.parse(atob(token.split('.')[1]));
if(p.exp*1000>Date.now())setUser({id:p.id,username:p.username,role:p.role});
else localStorage.removeItem('sk_token');
}catch{localStorage.removeItem('sk_token');}
},[]);
useEffect(()=>{
if(!user)return;
const check=()=>api('/system/update-check').then(setUpdateInfo).catch(()=>{});
check();
const t=setInterval(check,5*60*1000);
return()=>clearInterval(t);
},[user]);
const toast=useCallback((msg,type='success')=>{
setToastMsg({msg,type});
setTimeout(()=>setToastMsg({msg:'',type:'success'}),3500);
},[]);
if(splash) return setSplash(false)}/>;
if(!user) return setUser(u)}/>;
const activeTool = TOOLS.find(t=>t.id===active);
// Mobile padding bottom fรผr fixed nav
const mainStyle = { flex:1, overflowY:'auto', ...(mobile ? { paddingBottom:'calc(56px + env(safe-area-inset-bottom, 0px))' } : {}) };
return(
<>
{!mobile && (
{localStorage.removeItem('sk_token');setUser(null);}}
updateInfo={updateInfo} onUpdateClick={()=>setShowUpd(true)}/>
)}
{active==='dashboard' && }
{active==='admin' && }
{activeTool && }
{mobile && (
{localStorage.removeItem('sk_token');setUser(null);}}
updateInfo={updateInfo} onUpdateClick={()=>setShowUpd(true)}/>
)}
{showUpd&&updateInfo&&setShowUpd(false)}/>}
>
);
}