Dateien nach "frontend/src" hochladen

This commit is contained in:
2026-05-26 09:28:16 +02:00
parent 7c2de6fa7f
commit b8b1432f20

View File

@@ -137,6 +137,7 @@ function UpdateModal({ data, onClose }) {
// ── 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)';
@@ -153,24 +154,29 @@ function Sidebar({ active, setActive, user, onLogout, updateInfo, onUpdateClick
<span style={{ display:'flex', alignItems:'center', width:18, justifyContent:'center', flexShrink:0 }}>
{item.Icon ? (() => { const I = item.Icon; return <I size={15} color={ic}/>; })() : <span style={{color:ic,fontSize:13}}>{item.icon}</span>}
</span>
<span style={{ flex:1, color:ic }}>{item.label}</span>
{!collapsed && <span style={{ flex:1, color:ic }}>{item.label}</span>}
</button>
);
};
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 }}>
<aside style={{ width:collapsed?60:210, minHeight:'100vh', background:'#0d0d0f',
borderRight:'1px solid rgba(255,255,255,0.06)', display:'flex', flexDirection:'column', flexShrink:0,
transition:'width 0.2s ease', overflow:'hidden' }}>
<div style={{ padding:'14px 12px', borderBottom:'1px solid rgba(255,255,255,0.06)', marginBottom:8 }}>
<div style={{ display:'flex', alignItems:'center', gap:8 }}>
<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 }}>
borderRadius:7, display:'flex', alignItems:'center', justifyContent:'center', fontSize:14, flexShrink:0 }}></div>
{!collapsed && <div style={{flex:1,minWidth:0}}>
<div style={{ color:'#fff', fontFamily:"'Space Mono',monospace", fontSize:13, fontWeight:700, whiteSpace:'nowrap' }}>
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>}
<button onClick={()=>setCollapsed(v=>!v)} style={{ background:'transparent', border:'none',
color:'rgba(255,255,255,0.3)', cursor:'pointer', padding:4, flexShrink:0,
fontSize:12, lineHeight:1 }} title={collapsed?'Ausklappen':'Einklappen'}>
{collapsed ? '' : ''}
</button>
</div>
</div>
<nav style={{ flex:1, padding:'0 8px' }}>
@@ -190,8 +196,8 @@ function Sidebar({ active, setActive, user, onLogout, updateInfo, onUpdateClick
</button>
{appsOpen && getGroupedTools().map(([group, tools]) => (
<div key={group}>
<div style={{ color:'rgba(255,255,255,0.22)', fontSize:9, fontFamily:'monospace',
letterSpacing:2, padding:'8px 12px 3px 12px' }}>{group.toUpperCase()}</div>
{!collapsed && <div style={{ color:'rgba(255,255,255,0.55)', fontSize:9, fontFamily:'monospace',
letterSpacing:2, padding:'8px 12px 3px 12px' }}>{group.toUpperCase()}</div>}
{tools.map(t => <NavBtn key={t.id} item={t} sub />)}
</div>
))}
@@ -210,12 +216,17 @@ function Sidebar({ active, setActive, user, onLogout, updateInfo, onUpdateClick
</button>
)}
<div style={{ padding:'10px 12px 16px', borderTop:'1px solid rgba(255,255,255,0.06)' }}>
<div style={{ display:'flex', alignItems:'center', gap:8, marginBottom:8 }}>
{!collapsed && <><div style={{ display:'flex', alignItems:'center', gap:8, marginBottom:8 }}>
<div style={{ width:24, height:24, borderRadius:'50%', background:'linear-gradient(135deg,#ff6b9d,#4ecdc4)',
display:'flex', alignItems:'center', justifyContent:'center' }}><UserIcon size={14} color="#0d0d0f"/></div>
display:'flex', alignItems:'center', justifyContent:'center', overflow:'hidden', flexShrink:0 }}>
{localStorage.getItem('dd_avatar')
? <img src={localStorage.getItem('dd_avatar')} style={{width:'100%',height:'100%',objectFit:'cover'}} alt=""/>
: <UserIcon size={14} color="#0d0d0f"/>}
</div>
<div style={{ color:'#fff', fontSize:11, fontFamily:'monospace' }}>{user?.username}</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></>}
{collapsed && <button onClick={onLogout} title="Ausloggen" style={{ background:'transparent', border:'none', cursor:'pointer', padding:'4px', width:'100%', display:'flex', justifyContent:'center' }}><LogOutIcon size={18} color="rgba(255,107,157,0.7)"/></button>}
</div>
</aside>
);
@@ -269,7 +280,7 @@ function BottomNav({ active, setActive, user, onLogout, updateInfo, onUpdateClic
<div style={{ paddingBottom:4 }}>
{getGroupedTools().map(([group, tools]) => (
<div key={group}>
<div style={{ color:'rgba(255,255,255,0.28)', fontSize:9, fontFamily:'monospace',
<div style={{ color:'rgba(255,255,255,0.6)', fontSize:9, fontFamily:'monospace',
letterSpacing:2, padding:'12px 20px 5px' }}>{group.toUpperCase()}</div>
{tools.map(t => (
<MI key={t.id} Icon={t.Icon} label={t.label}
@@ -340,16 +351,23 @@ function QuickLinks({ toast, mobile }) {
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, url} });
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, url} });
const n = await api('/dashboard/links', { body:{...form, icon, url} });
setLinks(p => [...p, n]);
}
toast(editId ? 'Aktualisiert' : 'Gespeichert');
@@ -370,7 +388,7 @@ function QuickLinks({ toast, mobile }) {
<div style={S.head}>SCHNELLZUGRIFF</div>
<div style={{ display:'flex', gap:6 }}>
{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)}>
<button onClick={() => { setEdit(v => !v); setForm(false); }} style={{ background:'transparent', border:`1px solid ${editMode?'rgba(255,107,157,0.4)':'rgba(255,255,255,0.15)'}`, borderRadius:7, padding:'5px 10px', color:editMode?'#ff6b9d':'rgba(255,255,255,0.5)', cursor:'pointer', fontFamily:'monospace', fontSize:11 }}>
{editMode ? '✓ Fertig' : '✎'}
</button>
</div>
@@ -396,7 +414,9 @@ function QuickLinks({ toast, mobile }) {
padding:'10px 14px', background:'rgba(255,255,255,0.04)',
border:'1px solid rgba(255,255,255,0.08)', borderRadius:10,
textDecoration:'none', minWidth:60, cursor:editMode ? 'default' : 'pointer' }}>
<span style={{ fontSize:26 }}>{l.icon}</span>
{l.icon && l.icon.startsWith('http')
? <img src={l.icon} alt="" style={{width:26,height:26,objectFit:'contain'}} onError={e=>e.target.style.display='none'}/>
: <span style={{ fontSize:26 }}>{l.icon}</span>}
<span style={{ color:'rgba(255,255,255,0.65)', fontSize:9, fontFamily:'monospace',
maxWidth:64, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>{l.title}</span>
</a>
@@ -568,7 +588,7 @@ function BestellStats() {
<div style={{...S.card,marginBottom:10}}>
<button onClick={toggle} style={{width:'100%',background:'transparent',border:'none',
display:'flex',justifyContent:'space-between',alignItems:'center',cursor:'pointer',padding:0}}>
<div style={S.head}>STATISTIK</div>
<div style={S.head}>3D-DRUCK STATISTIK</div>
<span style={{color:'rgba(255,255,255,0.3)',fontSize:11,display:'inline-block',
transform:open?'rotate(90deg)':'rotate(0)',transition:'transform 0.2s'}}></span>
</button>
@@ -640,6 +660,7 @@ const Field = ({ label, type='text', value, onChange, placeholder='', autoCapita
function AdminPanel({ toast, mobile }) {
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 });
@@ -690,6 +711,41 @@ function AdminPanel({ toast, mobile }) {
<div style={{ padding: mobile ? '14px 12px 90px' : '36px 44px', maxWidth:560 }}>
<h1 style={{ color:'#fff', fontFamily:"'Space Mono',monospace", fontSize:mobile ? 17 : 22, marginBottom:18 }}>Admin</h1>
<Sec title="AVATAR">
<div style={{ display:'flex', alignItems:'center', gap:14, marginBottom:12 }}>
<div style={{ width:60, height:60, borderRadius:'50%', overflow:'hidden', flexShrink:0,
background:'linear-gradient(135deg,#ff6b9d,#4ecdc4)',
display:'flex', alignItems:'center', justifyContent:'center' }}>
{avatar
? <img src={avatar} alt="Avatar" style={{width:'100%',height:'100%',objectFit:'cover'}}/>
: <UserIcon size={28} color="#0d0d0f"/>}
</div>
<div style={{flex:1}}>
<label style={{...S.head,display:'block',marginBottom:6}}>PROFILBILD</label>
<input type="file" accept="image/*" onChange={async e => {
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'}}/>
</div>
</div>
{avatar && <button onClick={()=>{setAvatar(null);localStorage.removeItem('dd_avatar');toast('Avatar entfernt');}}
style={{...S.btn('#ff6b9d',true)}}> Entfernen</button>}
</Sec>
<Sec title="BENUTZERNAME ÄNDERN">
<Field label="NEUER BENUTZERNAME" value={un.newUsername} onChange={e=>setUn(p=>({...p,newUsername:e.target.value}))} />
<Field label="PASSWORT ZUR BESTÄTIGUNG" type="password" value={un.unPw} onChange={e=>setUn(p=>({...p,unPw:e.target.value}))} />