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 ─────────────────────────────────────────────────────────── // ── Desktop Sidebar ───────────────────────────────────────────────────────────
function Sidebar({ active, setActive, user, onLogout, updateInfo, onUpdateClick }) { function Sidebar({ active, setActive, user, onLogout, updateInfo, onUpdateClick }) {
const [appsOpen, setAppsOpen] = useState(true); const [appsOpen, setAppsOpen] = useState(true);
const [collapsed, setCollapsed] = useState(false);
const NavBtn = ({ item, sub=false }) => { const NavBtn = ({ item, sub=false }) => {
const ic = active===item.id ? '#4ecdc4' : 'rgba(255,255,255,0.55)'; 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 }}> <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>} {item.Icon ? (() => { const I = item.Icon; return <I size={15} color={ic}/>; })() : <span style={{color:ic,fontSize:13}}>{item.icon}</span>}
</span> </span>
<span style={{ flex:1, color:ic }}>{item.label}</span> {!collapsed && <span style={{ flex:1, color:ic }}>{item.label}</span>}
</button> </button>
); );
}; };
return ( return (
<aside style={{ width:210, minHeight:'100vh', background:'#0d0d0f', <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 }}> 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 }}> transition:'width 0.2s ease', overflow:'hidden' }}>
<div style={{ display:'flex', alignItems:'center', gap:10 }}> <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)', <div style={{ width:30, height:30, background:'linear-gradient(135deg,#4ecdc4,#ff6b9d)',
borderRadius:7, display:'flex', alignItems:'center', justifyContent:'center', fontSize:14 }}></div> borderRadius:7, display:'flex', alignItems:'center', justifyContent:'center', fontSize:14, flexShrink:0 }}></div>
<div> {!collapsed && <div style={{flex:1,minWidth:0}}>
<div style={{ color:'#fff', fontFamily:"'Space Mono',monospace", fontSize:13, fontWeight:700 }}> <div style={{ color:'#fff', fontFamily:"'Space Mono',monospace", fontSize:13, fontWeight:700, whiteSpace:'nowrap' }}>
DICKEN<span style={{ color:'#4ecdc4' }}>DOCK</span> DICKEN<span style={{ color:'#4ecdc4' }}>DOCK</span>
</div> </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>
</div> </div>
<nav style={{ flex:1, padding:'0 8px' }}> <nav style={{ flex:1, padding:'0 8px' }}>
@@ -190,8 +196,8 @@ function Sidebar({ active, setActive, user, onLogout, updateInfo, onUpdateClick
</button> </button>
{appsOpen && getGroupedTools().map(([group, tools]) => ( {appsOpen && getGroupedTools().map(([group, tools]) => (
<div key={group}> <div key={group}>
<div style={{ color:'rgba(255,255,255,0.22)', fontSize:9, fontFamily:'monospace', {!collapsed && <div style={{ color:'rgba(255,255,255,0.55)', fontSize:9, fontFamily:'monospace',
letterSpacing:2, padding:'8px 12px 3px 12px' }}>{group.toUpperCase()}</div> letterSpacing:2, padding:'8px 12px 3px 12px' }}>{group.toUpperCase()}</div>}
{tools.map(t => <NavBtn key={t.id} item={t} sub />)} {tools.map(t => <NavBtn key={t.id} item={t} sub />)}
</div> </div>
))} ))}
@@ -210,12 +216,17 @@ function Sidebar({ active, setActive, user, onLogout, updateInfo, onUpdateClick
</button> </button>
)} )}
<div style={{ padding:'10px 12px 16px', borderTop:'1px solid rgba(255,255,255,0.06)' }}> <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)', <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 style={{ color:'#fff', fontSize:11, fontFamily:'monospace' }}>{user?.username}</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></>}
{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> </div>
</aside> </aside>
); );
@@ -269,7 +280,7 @@ function BottomNav({ active, setActive, user, onLogout, updateInfo, onUpdateClic
<div style={{ paddingBottom:4 }}> <div style={{ paddingBottom:4 }}>
{getGroupedTools().map(([group, tools]) => ( {getGroupedTools().map(([group, tools]) => (
<div key={group}> <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> letterSpacing:2, padding:'12px 20px 5px' }}>{group.toUpperCase()}</div>
{tools.map(t => ( {tools.map(t => (
<MI key={t.id} Icon={t.Icon} label={t.label} <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(() => {}); }, []); 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 () => { 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; }
let url = form.url.trim(); let url = form.url.trim();
if (!/^https?:\/\//i.test(url)) url = 'https://' + url; if (!/^https?:\/\//i.test(url)) url = 'https://' + url;
// Auto favicon if user left default
const icon = form.icon === '🔗' ? (getFavicon(url) || '🔗') : form.icon;
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, icon, 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, icon, url} });
setLinks(p => [...p, n]); setLinks(p => [...p, n]);
} }
toast(editId ? 'Aktualisiert' : 'Gespeichert'); toast(editId ? 'Aktualisiert' : 'Gespeichert');
@@ -370,7 +388,7 @@ function QuickLinks({ toast, mobile }) {
<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)}> <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' : '✎'} {editMode ? '✓ Fertig' : '✎'}
</button> </button>
</div> </div>
@@ -396,7 +414,9 @@ function QuickLinks({ toast, mobile }) {
padding:'10px 14px', background:'rgba(255,255,255,0.04)', padding:'10px 14px', background:'rgba(255,255,255,0.04)',
border:'1px solid rgba(255,255,255,0.08)', borderRadius:10, border:'1px solid rgba(255,255,255,0.08)', borderRadius:10,
textDecoration:'none', minWidth:60, cursor:editMode ? 'default' : 'pointer' }}> 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', <span style={{ color:'rgba(255,255,255,0.65)', fontSize:9, fontFamily:'monospace',
maxWidth:64, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>{l.title}</span> maxWidth:64, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>{l.title}</span>
</a> </a>
@@ -568,7 +588,7 @@ function BestellStats() {
<div style={{...S.card,marginBottom:10}}> <div style={{...S.card,marginBottom:10}}>
<button onClick={toggle} style={{width:'100%',background:'transparent',border:'none', <button onClick={toggle} style={{width:'100%',background:'transparent',border:'none',
display:'flex',justifyContent:'space-between',alignItems:'center',cursor:'pointer',padding:0}}> 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', <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> transform:open?'rotate(90deg)':'rotate(0)',transition:'transform 0.2s'}}></span>
</button> </button>
@@ -640,6 +660,7 @@ const Field = ({ label, type='text', value, onChange, placeholder='', autoCapita
function AdminPanel({ toast, mobile }) { function AdminPanel({ toast, mobile }) {
const [pw, setPw] = useState({ current:'', newPw:'', confirm:'' }); const [pw, setPw] = useState({ current:'', newPw:'', confirm:'' });
const [avatar, setAvatar] = useState(localStorage.getItem('dd_avatar') || null);
const [un, setUn] = useState({ newUsername:'', unPw:'' }); const [un, setUn] = useState({ newUsername:'', unPw:'' });
const [file, setFile] = useState(null); const [file, setFile] = useState(null);
const [busy, setBusy] = useState({ pw:false, un:false, restore:false }); 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 }}> <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> <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"> <Sec title="BENUTZERNAME ÄNDERN">
<Field label="NEUER BENUTZERNAME" value={un.newUsername} onChange={e=>setUn(p=>({...p,newUsername:e.target.value}))} /> <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}))} /> <Field label="PASSWORT ZUR BESTÄTIGUNG" type="password" value={un.unPw} onChange={e=>setUn(p=>({...p,unPw:e.target.value}))} />