From d0adfc07ac1aa3a5f40b8e26daeb022859cb481a Mon Sep 17 00:00:00 2001 From: Dicken Date: Mon, 25 May 2026 19:40:05 +0200 Subject: [PATCH] Dateien nach "frontend/src" hochladen --- frontend/src/App.jsx | 597 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 597 insertions(+) create mode 100644 frontend/src/App.jsx diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx new file mode 100644 index 0000000..7df10b9 --- /dev/null +++ b/frontend/src/App.jsx @@ -0,0 +1,597 @@ +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 mobile = useIsMobile(); + 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, padding:'12px 14px' }} 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 [appsOpen, setAppsOpen] = useState(true); + + const NavBtn = ({ item, indent=false }) => ( + + ); + + 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'})}`:'โœ“'):'โ€ฆ'} +
+
+