frontend/src/App.jsx aktualisiert

This commit is contained in:
2026-05-25 14:43:04 +02:00
parent a2f63832cd
commit e8b75b9c5f

View File

@@ -4,15 +4,26 @@ import { TOOLS } from './toolRegistry.js';
const ICONS = ['🔗','🌐','📊','📁','🛠','📧','🗓','💾','🖥','📡','🔒','📖','🎯','⚡','🏠','🔧','📱','🎮','🚀','💡']; 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 ───────────────────────────────────────────────────────────────────── // ── Toast ─────────────────────────────────────────────────────────────────────
function Toast({ msg, type }) { function Toast({ msg, type }) {
if (!msg) return null; if (!msg) return null;
const c = type === 'error' ? '#ff6b9d' : '#4ecdc4'; const c = type === 'error' ? '#ff6b9d' : '#4ecdc4';
return ( return (
<div style={{ position:'fixed', bottom:24, right:24, zIndex:9999, background:'#1a1a1e', <div style={{ position:'fixed', bottom:80, right:16, zIndex:9999, background:'#1a1a1e',
border:`1px solid ${c}55`, borderRadius:10, padding:'11px 18px', border:`1px solid ${c}55`, borderRadius:10, padding:'10px 16px',
color:c, fontFamily:'monospace', fontSize:12, animation:'fadeIn 0.2s ease', color:c, fontFamily:'monospace', fontSize:12, animation:'fadeIn 0.2s ease',
boxShadow:`0 4px 24px ${c}22` }}> boxShadow:`0 4px 24px ${c}22`, maxWidth:'calc(100vw - 32px)' }}>
{type==='error'?'✕ ':'✓ '}{msg} {type==='error'?'✕ ':'✓ '}{msg}
</div> </div>
); );
@@ -21,14 +32,16 @@ function Toast({ msg, type }) {
// ── Modal ───────────────────────────────────────────────────────────────────── // ── Modal ─────────────────────────────────────────────────────────────────────
function Modal({ title, onClose, children }) { function Modal({ title, onClose, children }) {
return ( return (
<div style={{ position:'fixed', inset:0, background:'rgba(0,0,0,0.75)', zIndex:5000, <div style={{ position:'fixed', inset:0, background:'rgba(0,0,0,0.8)', zIndex:5000,
display:'flex', alignItems:'center', justifyContent:'center', padding:24 }} display:'flex', alignItems:'flex-end', justifyContent:'center' }}
onClick={e=>e.target===e.currentTarget&&onClose()}> onClick={e=>e.target===e.currentTarget&&onClose()}>
<div style={{ background:'#1a1a1e', border:'1px solid rgba(255,255,255,0.1)', <div style={{ background:'#1a1a1e', border:'1px solid rgba(255,255,255,0.1)',
borderRadius:14, padding:28, maxWidth:560, width:'100%', maxHeight:'80vh', overflowY:'auto' }}> borderRadius:'16px 16px 0 0', padding:'20px 20px 40px', width:'100%',
<div style={{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:20 }}> maxWidth:600, maxHeight:'85vh', overflowY:'auto' }}>
<span style={{ color:'#fff', fontFamily:"'Space Mono',monospace", fontSize:15, fontWeight:700 }}>{title}</span> <div style={{ width:36, height:4, background:'rgba(255,255,255,0.2)', borderRadius:2, margin:'0 auto 20px' }}/>
<button onClick={onClose} style={{ background:'transparent', border:'none', color:'rgba(255,255,255,0.4)', fontSize:18, cursor:'pointer' }}></button> <div style={{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:18 }}>
<span style={{ color:'#fff', fontFamily:"'Space Mono',monospace", fontSize:14, fontWeight:700 }}>{title}</span>
<button onClick={onClose} style={{ background:'transparent', border:'none', color:'rgba(255,255,255,0.4)', fontSize:20, cursor:'pointer', padding:'0 4px' }}></button>
</div> </div>
{children} {children}
</div> </div>
@@ -46,10 +59,10 @@ function Login({ onLogin }) {
catch(e) { setErr(e.message); } finally { setBusy(false); } catch(e) { setErr(e.message); } finally { setBusy(false); }
}; };
return ( return (
<div style={{ minHeight:'100vh', background:'#0d0d0f', display:'flex', alignItems:'center', justifyContent:'center' }}> <div style={{ minHeight:'100vh', background:'#0d0d0f', display:'flex', alignItems:'center', justifyContent:'center', padding:20 }}>
<div style={{ width:360, ...S.card, padding:40 }}> <div style={{ width:'100%', maxWidth:360, ...S.card, padding:32 }}>
<div style={{ textAlign:'center', marginBottom:28 }}> <div style={{ textAlign:'center', marginBottom:28 }}>
<div style={{ fontSize:32 }}></div> <div style={{ fontSize:36 }}></div>
<div style={{ color:'#fff', fontFamily:"'Space Mono',monospace", fontSize:18, fontWeight:700, marginTop:8 }}> <div style={{ color:'#fff', fontFamily:"'Space Mono',monospace", fontSize:18, fontWeight:700, marginTop:8 }}>
DICKEN<span style={{ color:'#4ecdc4' }}>DOCK</span> DICKEN<span style={{ color:'#4ecdc4' }}>DOCK</span>
</div> </div>
@@ -57,27 +70,78 @@ function Login({ onLogin }) {
{[['BENUTZERNAME',u,setU,'text'],['PASSWORT',p,setP,'password']].map(([l,v,s,t])=>( {[['BENUTZERNAME',u,setU,'text'],['PASSWORT',p,setP,'password']].map(([l,v,s,t])=>(
<div key={l} style={{ marginBottom:14 }}> <div key={l} style={{ marginBottom:14 }}>
<label style={{ ...S.head, display:'block', marginBottom:3 }}>{l}</label> <label style={{ ...S.head, display:'block', marginBottom:3 }}>{l}</label>
<input type={t} value={v} onChange={e=>s(e.target.value)} onKeyDown={e=>e.key==='Enter'&&go()} style={S.inp} /> <input type={t} value={v} onChange={e=>s(e.target.value)}
onKeyDown={e=>e.key==='Enter'&&go()}
style={{ ...S.inp, fontSize:16 }} autoCapitalize="none" />
</div> </div>
))} ))}
{err && <div style={{ color:'#ff6b9d', fontSize:12, fontFamily:'monospace', marginBottom:12, textAlign:'center' }}> {err}</div>} {err && <div style={{ color:'#ff6b9d', fontSize:12, fontFamily:'monospace', marginBottom:12, textAlign:'center' }}> {err}</div>}
<button onClick={go} disabled={busy} style={{ <button onClick={go} disabled={busy} style={{
width:'100%', background:'linear-gradient(135deg,#4ecdc4,#45b7d1)', border:'none', width:'100%', background:'linear-gradient(135deg,#4ecdc4,#45b7d1)', border:'none',
borderRadius:8, padding:'11px 0', color:'#0d0d0f', fontFamily:"'Space Mono',monospace", borderRadius:10, padding:'13px 0', color:'#0d0d0f', fontFamily:"'Space Mono',monospace",
fontWeight:700, fontSize:13, cursor:busy?'default':'pointer', opacity:busy?0.7:1, fontWeight:700, fontSize:14, cursor:busy?'default':'pointer', opacity:busy?0.7:1,
}}>{busy?'…':'Einloggen →'}</button> }}>{busy?'…':'Einloggen →'}</button>
</div> </div>
</div> </div>
); );
} }
// ── Sidebar ─────────────────────────────────────────────────────────────────── // ── Update Modal ──────────────────────────────────────────────────────────────
function Sidebar({ active, setActive, user, onLogout, updateInfo, onUpdateClick }) { function UpdateModal({ data, onClose }) {
const coreNav = [{ id:'dashboard', icon:'⬡', label:'Dashboard' }]; return (
const adminNav = user?.role==='admin' ? [{ id:'admin', icon:'⚙', label:'Admin' }] : []; <Modal title="📦 Update verfügbar" onClose={onClose}>
<div style={{ marginBottom:14, padding:'8px 12px', background:'rgba(255,230,109,0.06)',
border:'1px solid rgba(255,230,109,0.2)', borderRadius:8 }}>
<span style={{ color:'rgba(255,255,255,0.5)', fontSize:12, fontFamily:'monospace' }}>
Installiert: <span style={{ color:'#4ecdc4' }}>{data.currentVersion}</span>
{' → '}<span style={{ color:'#ffe66d' }}>{data.latestVersion}</span>
</span>
</div>
{(data.releases||[]).map((r,i)=>(
<div key={i} style={{ marginBottom:12, padding:'12px 14px',
background:i===0?'rgba(78,205,196,0.05)':'rgba(255,255,255,0.02)',
border:`1px solid ${i===0?'rgba(78,205,196,0.2)':'rgba(255,255,255,0.05)'}`, borderRadius:8 }}>
<div style={{ display:'flex', justifyContent:'space-between', marginBottom:6 }}>
<span style={{ color:i===0?'#4ecdc4':'#fff', fontFamily:"'Space Mono',monospace", fontSize:13, fontWeight:700 }}>{r.name||r.version}</span>
<span style={{ color:'rgba(255,255,255,0.3)', fontSize:10, fontFamily:'monospace' }}>
{r.publishedAt?new Date(r.publishedAt).toLocaleDateString('de-DE'):''}
</span>
</div>
<pre style={{ color:'rgba(255,255,255,0.5)', fontSize:12, fontFamily:'monospace', whiteSpace:'pre-wrap', lineHeight:1.6, margin:0 }}>{r.body}</pre>
</div>
))}
<div style={{ color:'rgba(255,255,255,0.25)', fontSize:11, fontFamily:'monospace', marginTop:8, lineHeight:1.8 }}>
Update: <code style={{ color:'#4ecdc4' }}>docker compose up -d --build</code>
</div>
</Modal>
);
}
const NavBtn = ({ item }) => ( // ── Desktop Sidebar ───────────────────────────────────────────────────────────
<button onClick={()=>setActive(item.id)} style={{ 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 (
<aside style={{ width:210, minHeight:'100vh', background:'#0d0d0f',
borderRight:'1px solid rgba(255,255,255,0.06)', display:'flex', flexDirection:'column', flexShrink:0 }}>
<div style={{ padding:'22px 18px 16px', borderBottom:'1px solid rgba(255,255,255,0.06)', marginBottom:8 }}>
<div style={{ display:'flex', alignItems:'center', gap:10 }}>
<div style={{ width:30, height:30, background:'linear-gradient(135deg,#4ecdc4,#ff6b9d)',
borderRadius:7, display:'flex', alignItems:'center', justifyContent:'center', fontSize:14 }}></div>
<div>
<div style={{ color:'#fff', fontFamily:"'Space Mono',monospace", fontSize:13, fontWeight:700 }}>
DICKEN<span style={{ color:'#4ecdc4' }}>DOCK</span>
</div>
<div style={{ color:'rgba(255,255,255,0.25)', fontSize:9, fontFamily:'monospace' }}>v1.0.0</div>
</div>
</div>
</div>
<nav style={{ flex:1, padding:'0 8px' }}>
{allNav.map(item => (
<button key={item.id} onClick={()=>setActive(item.id)} style={{
width:'100%', display:'flex', alignItems:'center', gap:9, padding:'9px 12px', width:'100%', display:'flex', alignItems:'center', gap:9, padding:'9px 12px',
borderRadius:7, border:'none', borderRadius:7, border:'none',
background: active===item.id ? 'linear-gradient(90deg,rgba(78,205,196,0.14),rgba(78,205,196,0.02))' : 'transparent', background: active===item.id ? 'linear-gradient(90deg,rgba(78,205,196,0.14),rgba(78,205,196,0.02))' : 'transparent',
@@ -88,62 +152,24 @@ function Sidebar({ active, setActive, user, onLogout, updateInfo, onUpdateClick
<span style={{ fontSize:14 }}>{item.icon}</span> <span style={{ fontSize:14 }}>{item.icon}</span>
<span style={{ flex:1 }}>{item.label}</span> <span style={{ flex:1 }}>{item.label}</span>
</button> </button>
); ))}
return (
<aside style={{ width:220, minHeight:'100vh', background:'#0d0d0f',
borderRight:'1px solid rgba(255,255,255,0.06)', display:'flex', flexDirection:'column', flexShrink:0 }}>
<div style={{ padding:'24px 20px 18px', borderBottom:'1px solid rgba(255,255,255,0.06)', marginBottom:10 }}>
<div style={{ display:'flex', alignItems:'center', gap:10 }}>
<div style={{ width:30, height:30, background:'linear-gradient(135deg,#4ecdc4,#ff6b9d)',
borderRadius:7, display:'flex', alignItems:'center', justifyContent:'center', fontSize:15 }}></div>
<div>
<div style={{ color:'#fff', fontFamily:"'Space Mono',monospace", fontSize:13, fontWeight:700, letterSpacing:1 }}>
DICKEN<span style={{ color:'#4ecdc4' }}>DOCK</span>
</div>
<div style={{ color:'rgba(255,255,255,0.25)', fontSize:9, fontFamily:'monospace' }}>v1.0.0</div>
</div>
</div>
</div>
<nav style={{ flex:1, padding:'0 10px' }}>
{/* Core */}
{coreNav.map(i => <NavBtn key={i.id} item={i} />)}
{/* Tools aus Registry automatisch generiert */}
<div style={{ ...S.head, padding:'10px 12px 4px', marginBottom:0 }}>TOOLS</div>
{TOOLS.map(t => <NavBtn key={t.id} item={t} />)}
{/* Admin */}
{adminNav.length>0 && <>
<div style={{ ...S.head, padding:'10px 12px 4px', marginBottom:0 }}>SYSTEM</div>
{adminNav.map(i => <NavBtn key={i.id} item={i} />)}
</>}
</nav> </nav>
{/* Update Badge */}
{updateInfo?.hasUpdate && ( {updateInfo?.hasUpdate && (
<button onClick={onUpdateClick} style={{ <button onClick={onUpdateClick} style={{
margin:'0 10px 8px', background:'rgba(255,230,109,0.08)', margin:'0 8px 8px', background:'rgba(255,230,109,0.08)',
border:'1px solid rgba(255,230,109,0.3)', borderRadius:8, border:'1px solid rgba(255,230,109,0.3)', borderRadius:8,
padding:'8px 12px', cursor:'pointer', display:'flex', alignItems:'center', gap:8, padding:'8px 12px', cursor:'pointer', display:'flex', alignItems:'center', gap:8,
}}> }}>
<span style={{ width:7, height:7, borderRadius:'50%', background:'#ffe66d', flexShrink:0, <span style={{ width:7, height:7, borderRadius:'50%', background:'#ffe66d', flexShrink:0, boxShadow:'0 0 6px #ffe66d', display:'inline-block' }} />
boxShadow:'0 0 6px #ffe66d', display:'inline-block' }} /> <span style={{ color:'#ffe66d', fontSize:11, fontFamily:'monospace' }}>Update</span>
<span style={{ color:'#ffe66d', fontSize:11, fontFamily:'monospace' }}>Update verfügbar</span>
<span style={{ color:'rgba(255,230,109,0.5)', fontSize:10, fontFamily:'monospace', marginLeft:'auto' }}>{updateInfo.latestVersion}</span> <span style={{ color:'rgba(255,230,109,0.5)', fontSize:10, fontFamily:'monospace', marginLeft:'auto' }}>{updateInfo.latestVersion}</span>
</button> </button>
)} )}
<div style={{ padding:'10px 12px 16px', borderTop:'1px solid rgba(255,255,255,0.06)' }}>
{/* User */}
<div style={{ padding:'12px 14px 18px', borderTop:'1px solid rgba(255,255,255,0.06)' }}>
<div style={{ display:'flex', alignItems:'center', gap:8, marginBottom:8 }}> <div style={{ display:'flex', alignItems:'center', gap:8, marginBottom:8 }}>
<div style={{ width:26, height:26, borderRadius:'50%', background:'linear-gradient(135deg,#ff6b9d,#4ecdc4)', <div style={{ width:24, height:24, borderRadius:'50%', background:'linear-gradient(135deg,#ff6b9d,#4ecdc4)',
display:'flex', alignItems:'center', justifyContent:'center', fontSize:11 }}>👤</div> display:'flex', alignItems:'center', justifyContent:'center', fontSize:10 }}>👤</div>
<div>
<div style={{ color:'#fff', fontSize:11, fontFamily:'monospace' }}>{user?.username}</div> <div style={{ color:'#fff', fontSize:11, fontFamily:'monospace' }}>{user?.username}</div>
<div style={{ color:'rgba(255,255,255,0.25)', fontSize:9, fontFamily:'monospace' }}>{user?.role}</div>
</div>
</div> </div>
<button onClick={onLogout} style={{ ...S.btn('#ff6b9d',true), width:'100%', textAlign:'center' }}>Ausloggen</button> <button onClick={onLogout} style={{ ...S.btn('#ff6b9d',true), width:'100%', textAlign:'center' }}>Ausloggen</button>
</div> </div>
@@ -151,44 +177,101 @@ function Sidebar({ active, setActive, user, onLogout, updateInfo, onUpdateClick
); );
} }
// ── Update Modal ────────────────────────────────────────────────────────────── // ── Mobile Bottom Navigation ──────────────────────────────────────────────────
function UpdateModal({ data, onClose }) { function BottomNav({ active, setActive, user, updateInfo, onUpdateClick }) {
const [menuOpen, setMenuOpen] = useState(false);
const mainNav = [
{ id:'dashboard', icon:'⬡', label:'Home' },
...TOOLS.slice(0,3).map(t => ({ id:t.id, icon:t.icon, label:t.label.length>8?t.label.slice(0,7)+'…':t.label })),
{ id:'_menu', icon:'⋯', label:'Mehr' },
];
return ( return (
<Modal title="📦 Verfügbare Updates" onClose={onClose}> <>
<div style={{ marginBottom:14, padding:'8px 12px', background:'rgba(255,230,109,0.06)', {/* Menu Overlay */}
border:'1px solid rgba(255,230,109,0.2)', borderRadius:8 }}> {menuOpen && (
<span style={{ color:'rgba(255,255,255,0.5)', fontSize:12, fontFamily:'monospace' }}> <div style={{ position:'fixed', inset:0, zIndex:4000 }} onClick={()=>setMenuOpen(false)}>
Installiert: <span style={{ color:'#4ecdc4' }}>{data.currentVersion}</span> <div style={{ position:'absolute', bottom:70, left:0, right:0,
{' → '}Aktuell: <span style={{ color:'#ffe66d' }}>{data.latestVersion}</span> background:'#1a1a1e', border:'1px solid rgba(255,255,255,0.08)',
</span> borderRadius:'16px 16px 0 0', padding:'12px 0 8px' }}
</div> onClick={e=>e.stopPropagation()}>
{(data.releases||[]).map((r,i)=>( {/* Update badge */}
<div key={i} style={{ marginBottom:12, padding:'12px 14px', {updateInfo?.hasUpdate && (
background:i===0?'rgba(78,205,196,0.05)':'rgba(255,255,255,0.02)', <button onClick={()=>{setMenuOpen(false);onUpdateClick();}} style={{
border:`1px solid ${i===0?'rgba(78,205,196,0.2)':'rgba(255,255,255,0.05)'}`, borderRadius:8 }}> width:'100%', padding:'12px 20px', background:'rgba(255,230,109,0.08)',
<div style={{ display:'flex', justifyContent:'space-between', marginBottom:6 }}> border:'none', borderBottom:'1px solid rgba(255,255,255,0.06)',
<span style={{ color:i===0?'#4ecdc4':'#fff', fontFamily:"'Space Mono',monospace", fontSize:13, fontWeight:700 }}>{r.name||r.version}</span> display:'flex', alignItems:'center', gap:10, cursor:'pointer',
<span style={{ color:'rgba(255,255,255,0.3)', fontSize:10, fontFamily:'monospace' }}>{r.publishedAt?new Date(r.publishedAt).toLocaleDateString('de-DE'):''}</span> }}>
</div> <span style={{ width:8, height:8, borderRadius:'50%', background:'#ffe66d', boxShadow:'0 0 6px #ffe66d', display:'inline-block', flexShrink:0 }} />
<pre style={{ color:'rgba(255,255,255,0.5)', fontSize:12, fontFamily:'monospace', whiteSpace:'pre-wrap', lineHeight:1.6, margin:0 }}>{r.body}</pre> <span style={{ color:'#ffe66d', fontSize:13, fontFamily:'monospace' }}>Update verfügbar {updateInfo.latestVersion}</span>
</div> </button>
)}
{/* Extra tools */}
{TOOLS.slice(3).map(t => (
<button key={t.id} onClick={()=>{setActive(t.id);setMenuOpen(false);}} style={{
width:'100%', padding:'13px 20px', background:'transparent',
border:'none', borderBottom:'1px solid rgba(255,255,255,0.04)',
display:'flex', alignItems:'center', gap:12, cursor:'pointer',
}}>
<span style={{ fontSize:18 }}>{t.icon}</span>
<span style={{ color:'#fff', fontFamily:'monospace', fontSize:13 }}>{t.label}</span>
</button>
))} ))}
<div style={{ color:'rgba(255,255,255,0.25)', fontSize:11, fontFamily:'monospace', marginTop:8, lineHeight:1.8 }}> {user?.role==='admin' && (
Update: <code style={{ color:'#4ecdc4' }}>sh build.sh && docker compose up -d</code> <button onClick={()=>{setActive('admin');setMenuOpen(false);}} style={{
width:'100%', padding:'13px 20px', background:'transparent',
border:'none', borderBottom:'1px solid rgba(255,255,255,0.04)',
display:'flex', alignItems:'center', gap:12, cursor:'pointer',
}}>
<span style={{ fontSize:18 }}></span>
<span style={{ color:'#fff', fontFamily:'monospace', fontSize:13 }}>Admin</span>
</button>
)}
</div> </div>
</Modal> </div>
)}
{/* Bottom Bar */}
<nav style={{ position:'fixed', bottom:0, left:0, right:0, zIndex:3000,
background:'#0d0d0f', borderTop:'1px solid rgba(255,255,255,0.08)',
display:'flex', height:64, paddingBottom:'env(safe-area-inset-bottom)' }}>
{mainNav.map(item => {
const isMenu = item.id === '_menu';
const isActive = isMenu ? menuOpen : active === item.id;
return (
<button key={item.id}
onClick={() => isMenu ? setMenuOpen(v=>!v) : (setActive(item.id), setMenuOpen(false))}
style={{ flex:1, display:'flex', flexDirection:'column', alignItems:'center',
justifyContent:'center', gap:3, background:'transparent', border:'none',
cursor:'pointer', position:'relative' }}>
{isMenu && updateInfo?.hasUpdate && (
<span style={{ position:'absolute', top:8, right:'calc(50% - 14px)',
width:7, height:7, borderRadius:'50%', background:'#ffe66d',
boxShadow:'0 0 6px #ffe66d', display:'inline-block' }} />
)}
<span style={{ fontSize:18, lineHeight:1 }}>{item.icon}</span>
<span style={{ fontSize:9, fontFamily:'monospace',
color: isActive ? '#4ecdc4' : 'rgba(255,255,255,0.4)',
letterSpacing:0.5 }}>{item.label}</span>
{isActive && !isMenu && (
<span style={{ position:'absolute', top:0, left:'50%', transform:'translateX(-50%)',
width:24, height:2, background:'#4ecdc4', borderRadius:'0 0 2px 2px' }} />
)}
</button>
);
})}
</nav>
</>
); );
} }
// ── Dashboard Widgets ───────────────────────────────────────────────────────── // ── Dashboard Widgets ─────────────────────────────────────────────────────────
function QuickLinks({ toast }) { function QuickLinks({ toast, mobile }) {
const [links, setLinks] = useState([]); const [links,setLinks]=useState([]); const [editMode,setEdit]=useState(false);
const [editMode, setEdit] = useState(false); const [showForm,setForm]=useState(false); const [form,setF]=useState({title:'',url:'',icon:'🔗'});
const [showForm, setForm] = useState(false); const [editId,setEditId]=useState(null);
const [form, setF] = useState({ title:'', url:'', icon:'🔗' });
const [editId, setEditId] = useState(null);
useEffect(() => { api('/dashboard/links').then(setLinks).catch(()=>{}); }, []); useEffect(()=>{ api('/dashboard/links').then(setLinks).catch(()=>{}); },[]);
const save = async () => { const save = async () => {
if (!form.title.trim()||!form.url.trim()) { toast('Titel und URL erforderlich','error'); return; } if (!form.title.trim()||!form.url.trim()) { toast('Titel und URL erforderlich','error'); return; }
@@ -196,33 +279,35 @@ function QuickLinks({ toast }) {
if (!/^https?:\/\//i.test(url)) url = 'https://'+url; if (!/^https?:\/\//i.test(url)) url = 'https://'+url;
try { try {
if (editId) { if (editId) {
const u = await api(`/dashboard/links/${editId}`,{method:'PUT',body:{...form,url}}); const u=await api(`/dashboard/links/${editId}`,{method:'PUT',body:{...form,url}});
setLinks(p=>p.map(l=>l.id===editId?u:l)); setLinks(p=>p.map(l=>l.id===editId?u:l));
} else { } else {
const n = await api('/dashboard/links',{body:{...form,url}}); const n=await api('/dashboard/links',{body:{...form,url}});
setLinks(p=>[...p,n]); setLinks(p=>[...p,n]);
} }
toast(editId?'Link aktualisiert':'Link gespeichert'); toast(editId?'Aktualisiert':'Gespeichert');
setF({title:'',url:'',icon:'🔗'}); setForm(false); setEditId(null); setF({title:'',url:'',icon:'🔗'}); setForm(false); setEditId(null);
} catch(e) { toast(e.message,'error'); } } catch(e) { toast(e.message,'error'); }
}; };
const del = async id => { const del = async id => {
try { await api(`/dashboard/links/${id}`,{method:'DELETE'}); setLinks(p=>p.filter(l=>l.id!==id)); toast('Link gelöscht'); } 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'); } catch(e) { toast(e.message,'error'); }
}; };
return ( return (
<div style={{ ...S.card, marginBottom:14 }}> <div style={{ ...S.card, marginBottom:12 }}>
<div style={{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:14 }}> <div style={{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:12 }}>
<div style={S.head}>SCHNELLZUGRIFF</div> <div style={S.head}>SCHNELLZUGRIFF</div>
<div style={{ display:'flex', gap:6 }}> <div style={{ display:'flex', gap:6 }}>
{editMode && <button onClick={()=>{setForm(true);setEditId(null);setF({title:'',url:'',icon:'🔗'});}} style={S.btn('#4ecdc4',true)}>+ Link</button>} {editMode && <button onClick={()=>{setForm(true);setEditId(null);setF({title:'',url:'',icon:'🔗'});}} style={S.btn('#4ecdc4',true)}>+ Link</button>}
<button onClick={()=>{setEdit(v=>!v);setForm(false);}} style={S.btn(editMode?'#ff6b9d':'rgba(255,255,255,0.4)',true)}>{editMode?'✓ Fertig':'✎ Bearbeiten'}</button> <button onClick={()=>{setEdit(v=>!v);setForm(false);}} style={S.btn(editMode?'#ff6b9d':'rgba(255,255,255,0.4)',true)}>
{editMode?'✓ Fertig':'✎'}
</button>
</div> </div>
</div> </div>
<div style={{ display:'flex', flexWrap:'wrap', gap:10, minHeight:50 }}> <div style={{ display:'flex', flexWrap:'wrap', gap:8, minHeight:44 }}>
{links.length===0&&!editMode&&<div style={{ color:'rgba(255,255,255,0.2)', fontSize:12, fontFamily:'monospace', padding:'8px 0' }}>Klicke "Bearbeiten" um Links hinzuzufügen.</div>} {links.length===0&&!editMode&&<div style={{ color:'rgba(255,255,255,0.2)', fontSize:11, fontFamily:'monospace' }}>Tippe auf "✎" um Links hinzuzufügen.</div>}
{links.map(l=>( {links.map(l=>(
<div key={l.id} style={{ position:'relative' }}> <div key={l.id} style={{ position:'relative' }}>
{editMode&&( {editMode&&(
@@ -231,35 +316,34 @@ function QuickLinks({ toast }) {
<button onClick={()=>del(l.id)} style={{ width:18,height:18,borderRadius:'50%',background:'#ff6b9d',border:'none',color:'#0d0d0f',fontSize:10,cursor:'pointer' }}></button> <button onClick={()=>del(l.id)} style={{ width:18,height:18,borderRadius:'50%',background:'#ff6b9d',border:'none',color:'#0d0d0f',fontSize:10,cursor:'pointer' }}></button>
</div> </div>
)} )}
<a href={l.url} target="_blank" rel="noopener noreferrer" onClick={editMode?e=>e.preventDefault():undefined} <a href={l.url} target="_blank" rel="noopener noreferrer"
style={{ display:'flex',flexDirection:'column',alignItems:'center',gap:5,padding:'10px 14px', onClick={editMode?e=>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)', background:'rgba(255,255,255,0.04)',border:'1px solid rgba(255,255,255,0.08)',
borderRadius:10,textDecoration:'none',minWidth:70,cursor:editMode?'default':'pointer' }}> borderRadius:10,textDecoration:'none',minWidth:64,cursor:editMode?'default':'pointer' }}>
<span style={{ fontSize:22 }}>{l.icon}</span> <span style={{ fontSize:24 }}>{l.icon}</span>
<span style={{ color:'rgba(255,255,255,0.7)',fontSize:10,fontFamily:'monospace',maxWidth:70,overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap' }}>{l.title}</span> <span style={{ color:'rgba(255,255,255,0.7)',fontSize:9,fontFamily:'monospace',maxWidth:64,overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap' }}>{l.title}</span>
</a> </a>
</div> </div>
))} ))}
</div> </div>
{showForm&&( {showForm&&(
<div style={{ marginTop:14,padding:14,background:'rgba(0,0,0,0.2)',border:'1px solid rgba(255,255,255,0.06)',borderRadius:10 }}> <div style={{ marginTop:12,padding:12,background:'rgba(0,0,0,0.2)',border:'1px solid rgba(255,255,255,0.06)',borderRadius:10 }}>
<div style={{ marginBottom:10 }}> <div style={{ marginBottom:10 }}>
<div style={S.head}>ICON WÄHLEN</div> <div style={S.head}>ICON</div>
<div style={{ display:'flex',flexWrap:'wrap',gap:4 }}> <div style={{ display:'flex',flexWrap:'wrap',gap:4 }}>
{ICONS.map(ic=>( {ICONS.map(ic=>(
<button key={ic} onClick={()=>setF(f=>({...f,icon:ic}))} style={{ width:28,height:28,borderRadius:5,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)',cursor:'pointer',fontSize:14 }}>{ic}</button> <button key={ic} onClick={()=>setF(f=>({...f,icon:ic}))} style={{ width:32,height:32,borderRadius:6,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)',cursor:'pointer',fontSize:16 }}>{ic}</button>
))} ))}
</div> </div>
</div> </div>
<div style={{ display:'grid',gridTemplateColumns:'1fr 1fr',gap:10,marginBottom:10 }}> <div style={{ marginBottom:8 }}>
<div>
<label style={{ ...S.head, display:'block', marginBottom:3 }}>TITEL</label> <label style={{ ...S.head, display:'block', marginBottom:3 }}>TITEL</label>
<input value={form.title} onChange={e=>setF(f=>({...f,title:e.target.value}))} style={S.inp} placeholder="Mein Link" /> <input value={form.title} onChange={e=>setF(f=>({...f,title:e.target.value}))} style={{ ...S.inp,fontSize:16 }} placeholder="Mein Link" />
</div> </div>
<div> <div style={{ marginBottom:10 }}>
<label style={{ ...S.head, display:'block', marginBottom:3 }}>URL</label> <label style={{ ...S.head, display:'block', marginBottom:3 }}>URL</label>
<input value={form.url} onChange={e=>setF(f=>({...f,url:e.target.value}))} onKeyDown={e=>e.key==='Enter'&&save()} style={S.inp} placeholder="https://example.com" /> <input value={form.url} onChange={e=>setF(f=>({...f,url:e.target.value}))} style={{ ...S.inp,fontSize:16 }} placeholder="https://example.com" inputMode="url" autoCapitalize="none" />
</div>
</div> </div>
<div style={{ display:'flex',gap:8 }}> <div style={{ display:'flex',gap:8 }}>
<button onClick={save} style={S.btn('#4ecdc4')}>{editId?'✓ Aktualisieren':'✓ Speichern'}</button> <button onClick={save} style={S.btn('#4ecdc4')}>{editId?'✓ Aktualisieren':'✓ Speichern'}</button>
@@ -274,7 +358,7 @@ function QuickLinks({ toast }) {
function TodoList({ toast }) { function TodoList({ toast }) {
const [items,setItems]=useState([]); const [text,setText]=useState(''); const [items,setItems]=useState([]); const [text,setText]=useState('');
useEffect(()=>{api('/dashboard/todos').then(setItems).catch(()=>{});},[]); useEffect(()=>{api('/dashboard/todos').then(setItems).catch(()=>{});},[]);
const add = async()=>{ const add=async()=>{
if(!text.trim())return; if(!text.trim())return;
try{const n=await api('/dashboard/todos',{body:{text:text.trim()}});setItems(p=>[...p,n]);setText('');} try{const n=await api('/dashboard/todos',{body:{text:text.trim()}});setItems(p=>[...p,n]);setText('');}
catch(e){toast(e.message,'error');} catch(e){toast(e.message,'error');}
@@ -289,31 +373,32 @@ function TodoList({ toast }) {
}; };
const open=items.filter(i=>!i.done), done=items.filter(i=>i.done); const open=items.filter(i=>!i.done), done=items.filter(i=>i.done);
return( return(
<div style={{...S.card,flex:1}}> <div style={{...S.card,marginBottom:12}}>
<div style={S.head}>TO-DO LISTE</div> <div style={S.head}>TO-DO LISTE</div>
<div style={{display:'flex',gap:6,marginBottom:12}}> <div style={{display:'flex',gap:6,marginBottom:10}}>
<input value={text} onChange={e=>setText(e.target.value)} onKeyDown={e=>e.key==='Enter'&&add()} style={{...S.inp,flex:1}} placeholder="Neue Aufgabe…"/> <input value={text} onChange={e=>setText(e.target.value)} onKeyDown={e=>e.key==='Enter'&&add()}
<button onClick={add} style={S.btn('#4ecdc4')}>+</button> style={{...S.inp,flex:1,fontSize:16}} placeholder="Neue Aufgabe…"/>
<button onClick={add} style={{...S.btn('#4ecdc4'),padding:'8px 14px',fontSize:18}}>+</button>
</div> </div>
<div style={{maxHeight:220,overflowY:'auto'}}> <div style={{maxHeight:240,overflowY:'auto'}}>
{open.map(item=>( {open.map(item=>(
<div key={item.id} style={{display:'flex',alignItems:'center',gap:8,padding:'6px 0',borderBottom:'1px solid rgba(255,255,255,0.04)'}}> <div key={item.id} style={{display:'flex',alignItems:'center',gap:10,padding:'10px 0',borderBottom:'1px solid rgba(255,255,255,0.04)'}}>
<button onClick={()=>toggle(item)} style={{width:16,height:16,borderRadius:3,border:'1px solid rgba(78,205,196,0.4)',background:'transparent',cursor:'pointer',flexShrink:0}}/> <button onClick={()=>toggle(item)} style={{width:20,height:20,borderRadius:4,border:'1px solid rgba(78,205,196,0.4)',background:'transparent',cursor:'pointer',flexShrink:0}}/>
<span style={{color:'rgba(255,255,255,0.75)',fontSize:12,fontFamily:'monospace',flex:1,lineHeight:1.4}}>{item.text}</span> <span style={{color:'rgba(255,255,255,0.8)',fontSize:13,fontFamily:'monospace',flex:1,lineHeight:1.4}}>{item.text}</span>
<button onClick={()=>del(item.id)} style={{background:'transparent',border:'none',color:'rgba(255,255,255,0.2)',cursor:'pointer',fontSize:13}}></button> <button onClick={()=>del(item.id)} style={{background:'transparent',border:'none',color:'rgba(255,255,255,0.2)',cursor:'pointer',fontSize:16,padding:'0 4px'}}></button>
</div> </div>
))} ))}
{done.length>0&&<> {done.length>0&&<>
<div style={{...S.head,padding:'8px 0 4px',marginBottom:0}}>ERLEDIGT</div> <div style={{...S.head,padding:'8px 0 4px',marginBottom:0}}>ERLEDIGT</div>
{done.map(item=>( {done.map(item=>(
<div key={item.id} style={{display:'flex',alignItems:'center',gap:8,padding:'5px 0',opacity:0.4}}> <div key={item.id} style={{display:'flex',alignItems:'center',gap:10,padding:'8px 0',opacity:0.4}}>
<button onClick={()=>toggle(item)} style={{width:16,height:16,borderRadius:3,border:'1px solid rgba(78,205,196,0.3)',background:'rgba(78,205,196,0.15)',cursor:'pointer',flexShrink:0,fontSize:10,color:'#4ecdc4'}}></button> <button onClick={()=>toggle(item)} style={{width:20,height:20,borderRadius:4,border:'1px solid rgba(78,205,196,0.3)',background:'rgba(78,205,196,0.15)',cursor:'pointer',flexShrink:0,fontSize:11,color:'#4ecdc4'}}></button>
<span style={{color:'rgba(255,255,255,0.4)',fontSize:12,fontFamily:'monospace',flex:1,textDecoration:'line-through'}}>{item.text}</span> <span style={{color:'rgba(255,255,255,0.4)',fontSize:13,fontFamily:'monospace',flex:1,textDecoration:'line-through'}}>{item.text}</span>
<button onClick={()=>del(item.id)} style={{background:'transparent',border:'none',color:'rgba(255,255,255,0.2)',cursor:'pointer',fontSize:13}}></button> <button onClick={()=>del(item.id)} style={{background:'transparent',border:'none',color:'rgba(255,255,255,0.2)',cursor:'pointer',fontSize:16}}></button>
</div> </div>
))} ))}
</>} </>}
{items.length===0&&<div style={{color:'rgba(255,255,255,0.15)',fontSize:11,fontFamily:'monospace',padding:'8px 0'}}>Noch keine Aufgaben.</div>} {items.length===0&&<div style={{color:'rgba(255,255,255,0.15)',fontSize:12,fontFamily:'monospace',padding:'8px 0'}}>Noch keine Aufgaben.</div>}
</div> </div>
</div> </div>
); );
@@ -331,40 +416,40 @@ function Notepad({ toast }) {
},900); },900);
}; };
return( return(
<div style={{...S.card,flex:1,display:'flex',flexDirection:'column'}}> <div style={{...S.card}}>
<div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:12}}> <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:10}}>
<div style={S.head}>NOTIZBLOCK</div> <div style={S.head}>NOTIZBLOCK</div>
<div style={{color:saved?'rgba(78,205,196,0.5)':'rgba(255,230,109,0.5)',fontSize:10,fontFamily:'monospace'}}> <div style={{color:saved?'rgba(78,205,196,0.5)':'rgba(255,230,109,0.5)',fontSize:10,fontFamily:'monospace'}}>
{saved?(saveTime?`gespeichert ${new Date(saveTime).toLocaleTimeString('de-DE',{hour:'2-digit',minute:'2-digit'})}`:'gespeichert'):'…'} {saved?(saveTime?` ${new Date(saveTime).toLocaleTimeString('de-DE',{hour:'2-digit',minute:'2-digit'})}`:''):'…'}
</div> </div>
</div> </div>
<textarea value={content} onChange={e=>change(e.target.value)} <textarea value={content} onChange={e=>change(e.target.value)}
placeholder="Notizen, IPs, Zugangsdaten…" placeholder="Notizen, IPs, Zugangsdaten…"
style={{...S.inp,flex:1,minHeight:200,resize:'none',lineHeight:1.7,padding:'10px 12px'}}/> style={{...S.inp,width:'100%',minHeight:160,resize:'none',lineHeight:1.7,padding:'10px 12px',fontSize:13}}/>
</div> </div>
); );
} }
function Dashboard({ toast }) { function Dashboard({ toast, mobile }) {
return( return(
<div style={{padding:'36px 44px',maxWidth:1100}}> <div style={{padding: mobile ? '16px 14px 80px' : '36px 44px', maxWidth:1100}}>
<div style={{marginBottom:22}}> <div style={{marginBottom:18}}>
<h1 style={{color:'#fff',fontFamily:"'Space Mono',monospace",fontSize:22,margin:0}}>Dashboard</h1> <h1 style={{color:'#fff',fontFamily:"'Space Mono',monospace",fontSize:mobile?18:22,margin:0}}>Dashboard</h1>
<p style={{color:'rgba(255,255,255,0.25)',fontFamily:'monospace',fontSize:11,marginTop:5}}> <p style={{color:'rgba(255,255,255,0.25)',fontFamily:'monospace',fontSize:10,marginTop:4}}>
{new Date().toLocaleDateString('de-DE',{weekday:'long',year:'numeric',month:'long',day:'numeric'})} {new Date().toLocaleDateString('de-DE',{weekday:'long',year:'numeric',month:'long',day:'numeric'})}
</p> </p>
</div> </div>
<QuickLinks toast={toast}/> <QuickLinks toast={toast} mobile={mobile}/>
<div style={{display:'flex',gap:14}}><TodoList toast={toast}/><Notepad toast={toast}/></div> <TodoList toast={toast}/>
<Notepad toast={toast}/>
</div> </div>
); );
} }
// ── Admin Panel ─────────────────────────────────────────────────────────────── // ── Admin Panel ───────────────────────────────────────────────────────────────
function AdminPanel({ toast }) { function AdminPanel({ toast, mobile }) {
const [pw,setPw]=useState({current:'',newPw:'',confirm:''}); const [busy,setBusy]=useState(false); const [pw,setPw]=useState({current:'',newPw:'',confirm:''}); const [busy,setBusy]=useState(false);
const [file,setFile]=useState(null); const [restoring,setRestoring]=useState(false); const [file,setFile]=useState(null); const [restoring,setRestoring]=useState(false);
const changePw=async()=>{ const changePw=async()=>{
if(pw.newPw!==pw.confirm){toast('Passwörter stimmen nicht überein','error');return;} if(pw.newPw!==pw.confirm){toast('Passwörter stimmen nicht überein','error');return;}
if(pw.newPw.length<8){toast('Mindestens 8 Zeichen','error');return;} if(pw.newPw.length<8){toast('Mindestens 8 Zeichen','error');return;}
@@ -383,32 +468,30 @@ function AdminPanel({ toast }) {
try{const f=new FormData();f.append('database',file);await api('/admin/restore',{method:'POST',body:f,isFile:true});toast('Wiederhergestellt');setFile(null);} 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{setRestoring(false);} catch(e){toast(e.message,'error');}finally{setRestoring(false);}
}; };
const Sec=({title,children})=><div style={{...S.card,marginBottom:16}}><div style={S.head}>{title}</div>{children}</div>; const Sec=({title,children})=><div style={{...S.card,marginBottom:12}}><div style={S.head}>{title}</div>{children}</div>;
return( return(
<div style={{padding:'36px 44px',maxWidth:640}}> <div style={{padding: mobile ? '16px 14px 80px' : '36px 44px', maxWidth:640}}>
<h1 style={{color:'#fff',fontFamily:"'Space Mono',monospace",fontSize:22,marginBottom:24}}>Admin-Bereich</h1> <h1 style={{color:'#fff',fontFamily:"'Space Mono',monospace",fontSize:mobile?18:22,marginBottom:20}}>Admin-Bereich</h1>
<Sec title="PASSWORT ÄNDERN"> <Sec title="PASSWORT ÄNDERN">
{[['Aktuelles Passwort','current'],['Neues Passwort','newPw'],['Bestätigen','confirm']].map(([l,k])=>( {[['Aktuelles Passwort','current'],['Neues Passwort','newPw'],['Bestätigen','confirm']].map(([l,k])=>(
<div key={k} style={{marginBottom:12}}> <div key={k} style={{marginBottom:12}}>
<label style={{...S.head,display:'block',marginBottom:3}}>{l.toUpperCase()}</label> <label style={{...S.head,display:'block',marginBottom:3}}>{l.toUpperCase()}</label>
<input type="password" value={pw[k]} onChange={e=>setPw(p=>({...p,[k]:e.target.value}))} style={S.inp}/> <input type="password" value={pw[k]} onChange={e=>setPw(p=>({...p,[k]:e.target.value}))} style={{...S.inp,fontSize:16}}/>
</div> </div>
))} ))}
<button onClick={changePw} disabled={busy} style={S.btn('#4ecdc4')}>{busy?'…':'✓ Passwort ändern'}</button> <button onClick={changePw} disabled={busy} style={S.btn('#4ecdc4')}>{busy?'…':'✓ Passwort ändern'}</button>
</Sec> </Sec>
<Sec title="DATENBANK-BACKUP"> <Sec title="DATENBANK-BACKUP">
<p style={{color:'rgba(255,255,255,0.3)',fontSize:12,fontFamily:'monospace',marginBottom:14,lineHeight:1.6}}>Lädt die vollständige SQLite-Datenbank herunter.</p> <p style={{color:'rgba(255,255,255,0.3)',fontSize:12,fontFamily:'monospace',marginBottom:12,lineHeight:1.6}}>Vollständige SQLite-Datenbank herunterladen.</p>
<button onClick={backup} style={S.btn('#ffe66d')}> Backup herunterladen</button> <button onClick={backup} style={S.btn('#ffe66d')}> Backup herunterladen</button>
</Sec> </Sec>
<Sec title="SICHERUNG EINSPIELEN"> <Sec title="SICHERUNG EINSPIELEN">
<p style={{color:'rgba(255,255,255,0.3)',fontSize:12,fontFamily:'monospace',marginBottom:14,lineHeight:1.6}}> Aktuelle Datenbank wird vollständig ersetzt.</p> <p style={{color:'rgba(255,255,255,0.3)',fontSize:12,fontFamily:'monospace',marginBottom:12,lineHeight:1.6}}> Aktuelle Datenbank wird vollständig ersetzt.</p>
<div style={{display:'flex',gap:10,alignItems:'flex-end'}}> <div style={{marginBottom:10}}>
<div style={{flex:1}}>
<label style={{...S.head,display:'block',marginBottom:3}}>BACKUP-DATEI (.db)</label> <label style={{...S.head,display:'block',marginBottom:3}}>BACKUP-DATEI (.db)</label>
<input type="file" accept=".db" onChange={e=>setFile(e.target.files[0])} style={{...S.inp,padding:'6px 10px',cursor:'pointer'}}/> <input type="file" accept=".db" onChange={e=>setFile(e.target.files[0])} style={{...S.inp,padding:'6px 10px',cursor:'pointer'}}/>
</div> </div>
<button onClick={restore} disabled={restoring||!file} style={{...S.btn('#ff6b9d'),opacity:restoring||!file?0.45:1}}>{restoring?'…':'↑ Einspielen'}</button> <button onClick={restore} disabled={restoring||!file} style={{...S.btn('#ff6b9d'),opacity:restoring||!file?0.45:1}}>{restoring?'…':'↑ Einspielen'}</button>
</div>
{file&&<div style={{color:'rgba(255,255,255,0.25)',fontSize:10,fontFamily:'monospace',marginTop:6}}>{file.name}</div>} {file&&<div style={{color:'rgba(255,255,255,0.25)',fontSize:10,fontFamily:'monospace',marginTop:6}}>{file.name}</div>}
</Sec> </Sec>
</div> </div>
@@ -417,11 +500,12 @@ function AdminPanel({ toast }) {
// ── App Shell ───────────────────────────────────────────────────────────────── // ── App Shell ─────────────────────────────────────────────────────────────────
export default function App() { export default function App() {
const [user, setUser] = useState(null); const mobile = useIsMobile();
const [active, setActive] = useState('dashboard'); const [user,setUser]=useState(null);
const [toastMsg, setToastMsg] = useState({ msg:'', type:'success' }); const [active,setActive]=useState('dashboard');
const [updateInfo,setUpdateInfo]= useState(null); const [toastMsg,setToastMsg]=useState({msg:'',type:'success'});
const [showUpd, setShowUpd] = useState(false); const [updateInfo,setUpdateInfo]=useState(null);
const [showUpd,setShowUpd]=useState(false);
useEffect(()=>{ useEffect(()=>{
const token=localStorage.getItem('sk_token'); const token=localStorage.getItem('sk_token');
@@ -447,30 +531,39 @@ export default function App() {
if(!user) return <Login onLogin={u=>setUser(u)}/>; if(!user) return <Login onLogin={u=>setUser(u)}/>;
// Aktive Tool-Komponente ermitteln
const activeTool = TOOLS.find(t=>t.id===active); const activeTool = TOOLS.find(t=>t.id===active);
// Mobile padding bottom für fixed nav
const mainStyle = { flex:1, overflowY:'auto', ...(mobile ? { paddingBottom:64 } : {}) };
return( return(
<> <>
<style>{` <style>{`
@import url('https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&display=swap');
*{margin:0;padding:0;box-sizing:border-box;} *{margin:0;padding:0;box-sizing:border-box;}
body{background:#111114;} body{background:#111114;-webkit-tap-highlight-color:transparent;}
input,textarea,select{-webkit-appearance:none;appearance:none;}
input[type=number]::-webkit-inner-spin-button{opacity:0.25;} input[type=number]::-webkit-inner-spin-button{opacity:0.25;}
@keyframes fadeIn{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}} @keyframes fadeIn{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}
::-webkit-scrollbar{width:3px;}::-webkit-scrollbar-thumb{background:rgba(255,255,255,0.1);border-radius:2px;} ::-webkit-scrollbar{width:3px;}::-webkit-scrollbar-thumb{background:rgba(255,255,255,0.1);border-radius:2px;}
select option,textarea{background:#111114;} select option,textarea{background:#111114;}
`}</style> `}</style>
<div style={{display:'flex',minHeight:'100vh',background:'#111114'}}> <div style={{display:'flex',minHeight:'100vh',background:'#111114'}}>
{!mobile && (
<Sidebar active={active} setActive={setActive} user={user} <Sidebar active={active} setActive={setActive} user={user}
onLogout={()=>{localStorage.removeItem('sk_token');setUser(null);}} onLogout={()=>{localStorage.removeItem('sk_token');setUser(null);}}
updateInfo={updateInfo} onUpdateClick={()=>setShowUpd(true)}/> updateInfo={updateInfo} onUpdateClick={()=>setShowUpd(true)}/>
<main style={{flex:1,overflowY:'auto'}}> )}
{active==='dashboard' && <Dashboard toast={toast}/>} <main style={mainStyle}>
{active==='admin' && user?.role==='admin' && <AdminPanel toast={toast}/>} {active==='dashboard' && <Dashboard toast={toast} mobile={mobile}/>}
{activeTool && <activeTool.component toast={toast}/>} {active==='admin' && user?.role==='admin' && <AdminPanel toast={toast} mobile={mobile}/>}
{activeTool && <activeTool.component toast={toast} mobile={mobile}/>}
</main> </main>
</div> </div>
{mobile && (
<BottomNav active={active} setActive={setActive} user={user}
updateInfo={updateInfo} onUpdateClick={()=>setShowUpd(true)}/>
)}
<Toast msg={toastMsg.msg} type={toastMsg.type}/> <Toast msg={toastMsg.msg} type={toastMsg.type}/>
{showUpd&&updateInfo&&<UpdateModal data={updateInfo} onClose={()=>setShowUpd(false)}/>} {showUpd&&updateInfo&&<UpdateModal data={updateInfo} onClose={()=>setShowUpd(false)}/>}
</> </>