Dateien nach "frontend/src" hochladen
This commit is contained in:
@@ -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}))} />
|
||||
|
||||
Reference in New Issue
Block a user