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, TrashIcon, MessageIcon } from './icons.jsx'; import { getOrCreateKeyPair, getPublicKeyJwk, getFingerprint, exportEncrypted, importEncrypted, generateNewKeyPair } from './crypto.js'; const CONST_ICONS = ['๐Ÿ”—','๐ŸŒ','๐Ÿ“Š','๐Ÿ“','๐Ÿ› ','๐Ÿ“ง','๐Ÿ—“','๐Ÿ’พ','๐Ÿ–ฅ','๐Ÿ“ก','๐Ÿ”’','๐Ÿ“–','๐ŸŽฏ','โšก','๐Ÿ ','๐Ÿ”ง','๐Ÿ“ฑ','๐ŸŽฎ','๐Ÿš€','๐Ÿ’ก']; // Schlรผsselpaar beim Login generieren und public key registrieren (fire & forget) async function initNachrichtenKeys() { const LS_KEY = 'dd_msg_keypair'; let pubJwk; const stored = localStorage.getItem(LS_KEY); if (stored) { try { pubJwk = JSON.parse(stored).pub; } catch { localStorage.removeItem(LS_KEY); } } if (!pubJwk) { const kp = await crypto.subtle.generateKey({ name:'ECDH', namedCurve:'P-256' }, true, ['deriveKey']); pubJwk = await crypto.subtle.exportKey('jwk', kp.publicKey); const priv = await crypto.subtle.exportKey('jwk', kp.privateKey); localStorage.setItem(LS_KEY, JSON.stringify({ pub: pubJwk, priv })); } try { await api('/tools/nachrichten/keys', { body: { public_key: JSON.stringify(pubJwk) } }); } catch {} } // โ”€โ”€ 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 (
DICKENDOCK
โš’
); } // โ”€โ”€ 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); initNachrichtenKeys(); 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, padding:'13px 14px', borderRadius:10, border:'1px solid rgba(255,255,255,0.12)' }} autoCapitalize="none" autoCorrect="off" />
))} {err &&
โœ• {err}
}
); } // โ”€โ”€ Update Modal โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ // โ”€โ”€ Desktop Sidebar โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ function Sidebar({ active, setActive, user, onLogout, unreadMsgs=0, hexWarsTurns=0 }) { const [appsOpen, setAppsOpen] = useState(true); const [collapsed, setCollapsed] = useState(false); const [collapsedGroups, setCollapsedGroups] = useState({}); const toggleGroup = (group) => setCollapsedGroups(prev => ({ ...prev, [group]: !prev[group] })); 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, unreadMsgs=0, hexWarsTurns=0 }) { const [menuOpen, setMenuOpen] = useState(false); const [appsOpen, setAppsOpen] = useState(false); const [mobileCollapsedGroups, setMobileCollapsedGroups] = useState({}); const NAV_H = 56; const MI = ({Icon, icon, label, onClick, color='rgba(255,255,255,0.85)', dot=false}) => { 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]) => { const isGrpCollapsed = !!mobileCollapsedGroups[group]; return (
{!isGrpCollapsed && tools.map(t => ( 0} onClick={() => { setActive(t.id); setAppsOpen(false); }} /> ))}
); })}
{/* Mehr Sheet */} setMenuOpen(false)}>
{}} color="rgba(255,255,255,0.35)"/> {onLogout();setMenuOpen(false);}} color="#ff6b9d"/> {/* Bottom Bar */} ); } // โ”€โ”€ Dashboard Widgets โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ // โ”€โ”€ QuickLinks Carousel (Mobile) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ function QuickLinksCarousel({ links, onFolderClick, navigateTool }) { const perRow = 4; const gap = 6; const pageSize = perRow * 2; const pageCount = Math.ceil(links.length / pageSize); const [page, setPage] = useState(0); const scrollRef = useRef(null); const onScroll = () => { if (!scrollRef.current) return; const p = Math.round(scrollRef.current.scrollLeft / scrollRef.current.offsetWidth); setPage(p); }; const canLeft = page > 0; const canRight = page < pageCount - 1; const scrollTo = (p) => { scrollRef.current?.scrollTo({ left: p * scrollRef.current.offsetWidth, behavior:'smooth' }); }; if (pageCount <= 1) return (
{links.map(l => )}
); return (
{Array.from({length: pageCount}, (_,pi) => (
{links.slice(pi*pageSize, (pi+1)*pageSize).map(l => )}
))}
{/* Page dots */} {pageCount > 1 && (
{Array.from({length:pageCount}, (_,i) => (
scrollTo(i)} style={{ width: i===page ? 14 : 5, height:5, borderRadius:3, cursor:'pointer', background: i===page ? '#4ecdc4' : 'rgba(255,255,255,0.2)', transition:'all 0.2s', }}/> ))}
)}
); } function QuickLinkIcon({ l, onFolderClick, navigateTool }) { const [imgOk, setImgOk] = useState(true); const isTool = l.url?.startsWith('tool://'); const hasCustomIcon = l.icon && l.icon !== '๐Ÿ”—'; const faviconUrl = (!hasCustomIcon && !l.isFolder && !isTool) ? (() => { try { return `https://www.google.com/s2/favicons?sz=64&domain=${new URL(l.url).hostname}`; } catch { return null; } })() : null; const showFavicon = !hasCustomIcon && !!faviconUrl && imgOk && !l.isFolder && !isTool; const showCustomImg = hasCustomIcon && !l.isFolder && l.icon.startsWith('http'); const showEmoji = !showFavicon && !showCustomImg; const emoji = l.isFolder ? (l.icon||'๐Ÿ“') : (hasCustomIcon ? l.icon : (isTool ? 'โšก' : '๐Ÿ”—')); const content = ( <> {showFavicon && setImgOk(false)}/>} {showCustomImg && setImgOk(false)}/>} {showEmoji && {emoji}} {l.title||l.name} ); const baseStyle = {display:'flex',flexDirection:'column',alignItems:'center',justifyContent:'center',gap:3, padding:'4px 2px',background: isTool ? 'rgba(160,132,250,0.06)' : 'rgba(255,255,255,0.04)', border: l.isFolder ? '1px solid rgba(78,205,196,0.25)' : isTool ? '1px solid rgba(160,132,250,0.2)' : '1px solid rgba(255,255,255,0.08)', borderRadius:10,textDecoration:'none',height:48,width:'100%',boxSizing:'border-box',cursor:'pointer'}; if (isTool) { return ; } if (l.isFolder && onFolderClick) { const realId = typeof l.id==='string'&&l.id.startsWith('folder-') ? parseInt(l.id.replace('folder-','')) : l.id; return ; } return {content}; } function FolderModal({ folder, onClose, navigateTool }) { const [links, setLinks] = useState([]); const [loading, setLoading] = useState(true); useEffect(()=>{ api(`/tools/linkliste/folders/${folder.id}/links`) .then(d=>{ setLinks(d.links||[]); setLoading(false); }) .catch(()=>setLoading(false)); },[folder.id]); const mob = window.innerWidth < 768; return (
e.target===e.currentTarget&&onClose()}>
{mob&&
}
{folder.icon} {folder.name}
{loading ?
โ€ฆ
: links.length===0 ?
Ordner ist leer
:
{links.map(l=>)}
}
); } function QuickLinks({ toast, mobile, setActive, setDevToolsNav }) { const [links, setLinks] = useState([]); const [folderModal, setFolderModal] = useState(null); const [editMode, setEditMode] = useState(false); useEffect(() => { api('/dashboard/links').then(setLinks).catch(() => {}); }, []); // Tool-Navigation fรผr tool://-Links const navigateTool = (url) => { if (!url?.startsWith('tool://')) return; const withoutScheme = url.replace('tool://', ''); const [toolId, query] = withoutScheme.split('?'); const sub = query?.replace('sub=', ''); if (toolId === 'devtools' && sub && setDevToolsNav) { setActive('devtools'); setDevToolsNav({ tool: sub, ts: Date.now() }); } else if (setActive) { setActive(toolId); } }; const legacyItems = links.filter(l => l.item_type === 'quicklink' || !l.item_type); const hasLegacy = legacyItems.length > 0; const deleteLegacy = async id => { try { await api(`/dashboard/links/${id}`, { method:'DELETE' }); setLinks(p => p.filter(l => l.id !== id)); toast('Entfernt'); } catch(e) { toast(e.message,'error'); } }; const allItems = links.map(l => l.item_type === 'folder' ? { ...l, id:`folder-${l.id}`, isFolder:true, icon:l.icon||'๐Ÿ“', title:l.name } : l ); return (
{folderModal && setFolderModal(null)} navigateTool={navigateTool}/>}
SCHNELLZUGRIFF
{hasLegacy && ( )}
{/* Edit-Modus: alte Quick-Links verwalten */} {editMode && hasLegacy && (
ร„LTERE LINKS (aus Einstellungen) โ€” kรถnnen hier gelรถscht werden
{legacyItems.map(l=>(
{l.icon?.startsWith('http') ? : l.icon||'๐Ÿ”—'}
{l.title}
))}
Neue Links รผber Linkliste โ†’ โ— Schnellzugriff verwalten
)} {allItems.length === 0 ?
Links oder Ordner in der Linkliste mit โ— Schnellzugriff aktivieren.
: mobile ? :
{allItems.map(l => )}
}
); } 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 => (
{item.text}
))} )}
}
); } 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 (
{isOpen &&
{saved ? (saveTime ? `โœ“ ${new Date(saveTime).toLocaleTimeString('de-DE', { hour:'2-digit', minute:'2-digit' })}` : 'โœ“') : 'โ€ฆ'}
} {isOpen &&