import { useState, useEffect, useRef, useCallback } from 'react'; import { api, S } from './lib.js'; import { TOOLS } from './toolRegistry.js'; const ICONS = ['๐Ÿ”—','๐ŸŒ','๐Ÿ“Š','๐Ÿ“','๐Ÿ› ','๐Ÿ“ง','๐Ÿ—“','๐Ÿ’พ','๐Ÿ–ฅ','๐Ÿ“ก','๐Ÿ”’','๐Ÿ“–','๐ŸŽฏ','โšก','๐Ÿ ','๐Ÿ”ง','๐Ÿ“ฑ','๐ŸŽฎ','๐Ÿš€','๐Ÿ’ก']; // โ”€โ”€ Responsive Hook โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ function useIsMobile() { const [mobile, setMobile] = useState(window.innerWidth < 768); useEffect(() => { const fn = () => setMobile(window.innerWidth < 768); 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 (
e.target===e.currentTarget&&onClose()}>
{title}
{children}
); } // โ”€โ”€ 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 (
โš’
DICKENDOCK
{[['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 }} autoCapitalize="none" />
))} {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 allNav = [ { id:'dashboard', icon:'โฌก', label:'Dashboard' }, ...TOOLS.map(t => ({ id:t.id, icon:t.icon, label:t.label })), ...(user?.role==='admin' ? [{ id:'admin', icon:'โš™', label:'Admin' }] : []), ]; return ( ); } // โ”€โ”€ Mobile Bottom Navigation โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ function BottomNav({ active, setActive, user, onLogout, updateInfo, onUpdateClick }) { const [menuOpen, setMenuOpen] = useState(false); const NAV_H = 56; const mainNav = [ { id:'dashboard', icon:'โฌก', label:'Home' }, ...TOOLS.slice(0,3).map(t => ({ id:t.id, icon:t.icon, label:t.navLabel || t.label })), { id:'_menu', icon:'โ‹ฏ', label:'Mehr' }, ]; const MI = ({icon,label,onClick,color='#fff'}) => ( ); return ( <> {menuOpen && (
setMenuOpen(false)}>
e.stopPropagation()}>
{updateInfo?.hasUpdate && {setMenuOpen(false);onUpdateClick();}} color="#ffe66d"/>} {TOOLS.slice(3).map(t=>{setActive(t.id);setMenuOpen(false);}}/>)} {user?.role==='admin' && {setActive('admin');setMenuOpen(false);}}/>}
{}} color="rgba(255,255,255,0.35)"/> {onLogout();setMenuOpen(false);}} color="#ff6b9d"/>
)} ); } // โ”€โ”€ Dashboard Widgets โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ function QuickLinks({ toast, mobile }) { 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 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; try { if (editId) { const u=await api(`/dashboard/links/${editId}`,{method:'PUT',body:{...form,url}}); setLinks(p=>p.map(l=>l.id===editId?u:l)); } else { const n=await api('/dashboard/links',{body:{...form,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 => { 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 (
SCHNELLZUGRIFF
{editMode && }
{links.length===0&&!editMode&&
Tippe auf "โœŽ" um Links hinzuzufรผgen.
} {links.map(l=>(
{editMode&&(
)} e.preventDefault():undefined} style={{ display:'flex',flexDirection:'column',alignItems:'center',gap:4,padding:'10px 12px', background:'rgba(255,255,255,0.04)',border:'1px solid rgba(255,255,255,0.08)', borderRadius:10,textDecoration:'none',minWidth:64,cursor:editMode?'default':'pointer' }}> {l.icon} {l.title}
))}
{showForm&&(
ICON
{ICONS.map(ic=>( ))}
setF(f=>({...f,title:e.target.value}))} style={{ ...S.inp,fontSize:16 }} placeholder="Mein Link" />
setF(f=>({...f,url:e.target.value}))} style={{ ...S.inp,fontSize:16 }} placeholder="https://example.com" inputMode="url" autoCapitalize="none" />
)}
); } function TodoList({ toast }) { const [items,setItems]=useState([]); const [text,setText]=useState(''); 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), done=items.filter(i=>i.done); return(
TO-DO LISTE
setText(e.target.value)} onKeyDown={e=>e.key==='Enter'&&add()} style={{...S.inp,flex:1,fontSize:16}} placeholder="Neue Aufgabeโ€ฆ"/>
{open.map(item=>(
))} {done.length>0&&<>
ERLEDIGT
{done.map(item=>(
{item.text}
))} } {items.length===0&&
Noch keine Aufgaben.
}
); } function Notepad({ toast }) { const [content,setContent]=useState(''); const [saved,setSaved]=useState(true); const [saveTime,setSaveTime]=useState(null); 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(
NOTIZBLOCK
{saved?(saveTime?`โœ“ ${new Date(saveTime).toLocaleTimeString('de-DE',{hour:'2-digit',minute:'2-digit'})}`:'โœ“'):'โ€ฆ'}