Dateien nach "frontend/src" hochladen
This commit is contained in:
@@ -55,6 +55,32 @@ function Modal({ title, onClose, children }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── PWA Splash Screen ─────────────────────────────────────────────────────────
|
||||||
|
function SplashScreen({ onDone }) {
|
||||||
|
const [fade, setFade] = useState(false);
|
||||||
|
useEffect(() => {
|
||||||
|
const t1 = setTimeout(() => setFade(true), 1200);
|
||||||
|
const t2 = setTimeout(() => onDone(), 1700);
|
||||||
|
return () => { clearTimeout(t1); clearTimeout(t2); };
|
||||||
|
}, []);
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
position:'fixed', inset:0, background:'#0d0d0f',
|
||||||
|
display:'flex', flexDirection:'column', alignItems:'center', justifyContent:'center',
|
||||||
|
zIndex:99999, transition:'opacity 0.5s ease',
|
||||||
|
opacity: fade ? 0 : 1, pointerEvents: fade ? 'none' : 'auto',
|
||||||
|
}}>
|
||||||
|
<div style={{ color:'#fff', fontFamily:"'Space Mono',monospace", fontSize:22, fontWeight:700, marginBottom:24, letterSpacing:2 }}>
|
||||||
|
DICKEN<span style={{color:'#4ecdc4'}}>DOCK</span>
|
||||||
|
</div>
|
||||||
|
<div style={{
|
||||||
|
width:72, height:72, background:'linear-gradient(135deg,#4ecdc4,#ff6b9d)',
|
||||||
|
borderRadius:18, display:'flex', alignItems:'center', justifyContent:'center', fontSize:36,
|
||||||
|
}}>⚒</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Login ─────────────────────────────────────────────────────────────────────
|
// ── Login ─────────────────────────────────────────────────────────────────────
|
||||||
function Login({ onLogin }) {
|
function Login({ onLogin }) {
|
||||||
const [u,setU]=useState(''); const [p,setP]=useState('');
|
const [u,setU]=useState(''); const [p,setP]=useState('');
|
||||||
@@ -142,17 +168,19 @@ function Sidebar({ active, setActive, user, onLogout, updateInfo, onUpdateClick
|
|||||||
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)';
|
||||||
return (
|
return (
|
||||||
<button onClick={()=>setActive(item.id)} style={{
|
<button onClick={()=>setActive(item.id)} title={collapsed ? item.label : undefined} style={{
|
||||||
width:'100%', display:'flex', alignItems:'center', gap:9,
|
width:'100%', display:'flex', alignItems:'center', gap:9,
|
||||||
padding:'8px 12px', borderRadius:7, border:'none',
|
padding: collapsed ? '8px 0' : '8px 12px',
|
||||||
|
justifyContent: collapsed ? 'center' : 'flex-start',
|
||||||
|
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',
|
||||||
cursor:'pointer', fontSize:12,
|
cursor:'pointer', fontSize:12,
|
||||||
fontFamily:"'Space Mono',monospace", textAlign:'left', marginBottom:1,
|
fontFamily:"'Space Mono',monospace", textAlign:'left', marginBottom:1,
|
||||||
borderLeft: active===item.id ? '2px solid #4ecdc4' : '2px solid transparent',
|
borderLeft: active===item.id ? '2px solid #4ecdc4' : '2px solid transparent',
|
||||||
}}>
|
}}>
|
||||||
{sub && <span style={{ width:1, alignSelf:'stretch', background:'rgba(255,255,255,0.08)', marginRight:3, flexShrink:0 }}/>}
|
{!collapsed && sub && <span style={{ width:1, alignSelf:'stretch', background:'rgba(255,255,255,0.08)', marginRight:3, flexShrink:0 }}/>}
|
||||||
<span style={{ display:'flex', alignItems:'center', width:18, justifyContent:'center', flexShrink:0 }}>
|
<span style={{ display:'flex', alignItems:'center', 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={collapsed?18:15} color={ic}/>; })() : <span style={{color:ic,fontSize:collapsed?16:13}}>{item.icon}</span>}
|
||||||
</span>
|
</span>
|
||||||
{!collapsed && <span style={{ flex:1, color:ic }}>{item.label}</span>}
|
{!collapsed && <span style={{ flex:1, color:ic }}>{item.label}</span>}
|
||||||
</button>
|
</button>
|
||||||
@@ -507,7 +535,7 @@ function TodoList({ toast }) {
|
|||||||
</div>
|
</div>
|
||||||
<div style={{ maxHeight:220, overflowY:'auto' }}>
|
<div style={{ maxHeight:220, overflowY:'auto' }}>
|
||||||
{open.length === 0 && done.length === 0 && (
|
{open.length === 0 && done.length === 0 && (
|
||||||
<div style={{ color:'rgba(255,255,255,0.15)', fontSize:11, fontFamily:'monospace', padding:'6px 0' }}>Noch keine Aufgaben.</div>
|
<div style={{ color:'rgba(255,255,255,0.45)', fontSize:11, fontFamily:'monospace', padding:'6px 0' }}>Noch keine Aufgaben.</div>
|
||||||
)}
|
)}
|
||||||
{open.map(item => (
|
{open.map(item => (
|
||||||
<div key={item.id} style={{ display:'flex', alignItems:'center', gap:10, padding:'11px 0', borderBottom:'1px solid rgba(255,255,255,0.04)' }}>
|
<div key={item.id} style={{ display:'flex', alignItems:'center', gap:10, padding:'11px 0', borderBottom:'1px solid rgba(255,255,255,0.04)' }}>
|
||||||
@@ -636,7 +664,7 @@ function Dashboard({ toast, mobile }) {
|
|||||||
<div style={{ padding: mobile ? '14px 12px 90px' : '36px 44px', maxWidth:1100 }}>
|
<div style={{ padding: mobile ? '14px 12px 90px' : '36px 44px', maxWidth:1100 }}>
|
||||||
<div style={{ marginBottom:16 }}>
|
<div style={{ marginBottom:16 }}>
|
||||||
<h1 style={{ color:'#fff', fontFamily:"'Space Mono',monospace", fontSize:mobile ? 17 : 22, margin:0 }}>Dashboard</h1>
|
<h1 style={{ color:'#fff', fontFamily:"'Space Mono',monospace", fontSize:mobile ? 17 : 22, margin:0 }}>Dashboard</h1>
|
||||||
<p style={{ color:'rgba(255,255,255,0.25)', fontFamily:'monospace', fontSize:10, marginTop:3 }}>
|
<p style={{ color:'rgba(255,255,255,0.55)', fontFamily:'monospace', fontSize:10, marginTop:3 }}>
|
||||||
{new Date().toLocaleDateString('de-DE', { weekday:'long', day:'numeric', month:'long' })}
|
{new Date().toLocaleDateString('de-DE', { weekday:'long', day:'numeric', month:'long' })}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -721,93 +749,93 @@ 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:16 }}>Einstellungen</h1>
|
<h1 style={{ color:'#fff', fontFamily:"'Space Mono',monospace", fontSize:mobile?17:22, marginBottom:16 }}>Einstellungen</h1>
|
||||||
|
|
||||||
{/* Subnavigation */}
|
|
||||||
<div style={{ display:'flex', gap:6, marginBottom:18 }}>
|
<div style={{ display:'flex', gap:6, marginBottom:18 }}>
|
||||||
{[['profil','Profil'],['backup','Backup']].map(([k,l]) => (
|
{[['profil','Profil'],['backup','Backup']].map(([k,l]) => (
|
||||||
<button key={k} onClick={()=>setSection(k)} style={{
|
<button key={k} onClick={()=>setSection(k)} style={{
|
||||||
padding:'6px 16px', borderRadius:20, fontFamily:'monospace', fontSize:12, cursor:'pointer',
|
padding:'6px 16px', borderRadius:20, fontFamily:'monospace', fontSize:12, cursor:'pointer',
|
||||||
border:`1px solid ${section===k?'rgba(78,205,196,0.5)':'rgba(255,255,255,0.1)'}`,
|
border:`1px solid ${section===k?'rgba(78,205,196,0.5)':'rgba(255,255,255,0.15)'}`,
|
||||||
background: section===k?'rgba(78,205,196,0.12)':'transparent',
|
background: section===k?'rgba(78,205,196,0.12)':'transparent',
|
||||||
color: section===k?'#4ecdc4':'rgba(255,255,255,0.45)',
|
color: section===k?'#4ecdc4':'rgba(255,255,255,0.55)',
|
||||||
}}>{l}</button>
|
}}>{l}</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{section === 'profil' && <div>
|
{section === 'profil' && (
|
||||||
<Sec title="AVATAR">
|
<div>
|
||||||
<div style={{ display:'flex', alignItems:'center', gap:14, marginBottom:12 }}>
|
<Sec title="AVATAR">
|
||||||
<div style={{ width:60, height:60, borderRadius:'50%', overflow:'hidden', flexShrink:0,
|
<div style={{ display:'flex', alignItems:'center', gap:14, marginBottom:12 }}>
|
||||||
background:'linear-gradient(135deg,#ff6b9d,#4ecdc4)',
|
<div style={{ width:60, height:60, borderRadius:'50%', overflow:'hidden', flexShrink:0,
|
||||||
display:'flex', alignItems:'center', justifyContent:'center' }}>
|
background:'linear-gradient(135deg,#ff6b9d,#4ecdc4)',
|
||||||
{avatar
|
display:'flex', alignItems:'center', justifyContent:'center' }}>
|
||||||
? <img src={avatar} alt="Avatar" style={{width:'100%',height:'100%',objectFit:'cover'}}/>
|
{avatar
|
||||||
: <UserIcon size={28} color="#0d0d0f"/>}
|
? <img src={avatar} alt="Avatar" style={{width:'100%',height:'100%',objectFit:'cover'}}/>
|
||||||
</div>
|
: <UserIcon size={28} color="#0d0d0f"/>}
|
||||||
<div style={{flex:1}}>
|
</div>
|
||||||
<label style={{...S.head,display:'block',marginBottom:6}}>PROFILBILD</label>
|
<div style={{flex:1}}>
|
||||||
<input type="file" accept="image/*" onChange={async e => {
|
<label style={{...S.head,display:'block',marginBottom:6}}>PROFILBILD</label>
|
||||||
const file = e.target.files?.[0]; if(!file) return;
|
<input type="file" accept="image/*" onChange={async e => {
|
||||||
const reader = new FileReader();
|
const file = e.target.files?.[0]; if(!file) return;
|
||||||
reader.onload = ev => {
|
const reader = new FileReader();
|
||||||
const canvas = document.createElement('canvas');
|
reader.onload = ev => {
|
||||||
const img = new Image(); img.onload = () => {
|
const canvas = document.createElement('canvas');
|
||||||
const size = 120;
|
const img = new Image(); img.onload = () => {
|
||||||
canvas.width = size; canvas.height = size;
|
const size = 120;
|
||||||
const ctx = canvas.getContext('2d');
|
canvas.width = size; canvas.height = size;
|
||||||
ctx.beginPath(); ctx.arc(size/2,size/2,size/2,0,Math.PI*2);
|
const ctx = canvas.getContext('2d');
|
||||||
ctx.clip();
|
ctx.beginPath(); ctx.arc(size/2,size/2,size/2,0,Math.PI*2);
|
||||||
const min = Math.min(img.width,img.height);
|
ctx.clip();
|
||||||
ctx.drawImage(img,(img.width-min)/2,(img.height-min)/2,min,min,0,0,size,size);
|
const min = Math.min(img.width,img.height);
|
||||||
const data = canvas.toDataURL('image/jpeg',0.85);
|
ctx.drawImage(img,(img.width-min)/2,(img.height-min)/2,min,min,0,0,size,size);
|
||||||
setAvatar(data); localStorage.setItem('dd_avatar',data); toast('Avatar gespeichert');
|
const data = canvas.toDataURL('image/jpeg',0.85);
|
||||||
}; img.src = ev.target.result;
|
setAvatar(data); localStorage.setItem('dd_avatar',data); toast('Avatar gespeichert');
|
||||||
}; reader.readAsDataURL(file);
|
}; img.src = ev.target.result;
|
||||||
}} style={{...S.inp,fontSize:13,padding:'7px 10px',cursor:'pointer'}}/>
|
}; reader.readAsDataURL(file);
|
||||||
</div>
|
}} 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}))} />
|
||||||
|
<button onClick={changeUn} disabled={busy.un} style={{ ...S.btn('#4ecdc4'), width:'100%', textAlign:'center', padding:'11px 0' }}>
|
||||||
|
{busy.un ? '…' : '✓ Benutzername ändern'}
|
||||||
|
</button>
|
||||||
|
</Sec>
|
||||||
|
<Sec title="PASSWORT ÄNDERN">
|
||||||
|
<Field label="AKTUELLES PASSWORT" type="password" value={pw.current} onChange={e=>setPw(p=>({...p,current:e.target.value}))} />
|
||||||
|
<Field label="NEUES PASSWORT" type="password" value={pw.newPw} onChange={e=>setPw(p=>({...p,newPw:e.target.value}))} />
|
||||||
|
<Field label="BESTÄTIGEN" type="password" value={pw.confirm} onChange={e=>setPw(p=>({...p,confirm:e.target.value}))} />
|
||||||
|
<button onClick={changePw} disabled={busy.pw} style={{ ...S.btn('#4ecdc4'), width:'100%', textAlign:'center', padding:'11px 0' }}>
|
||||||
|
{busy.pw ? '…' : '✓ Passwort ändern'}
|
||||||
|
</button>
|
||||||
|
</Sec>
|
||||||
</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">
|
{section === 'backup' && (
|
||||||
<Field label="NEUER BENUTZERNAME" value={un.newUsername} onChange={e=>setUn(p=>({...p,newUsername:e.target.value}))} />
|
<div>
|
||||||
<Field label="PASSWORT ZUR BESTÄTIGUNG" type="password" value={un.unPw} onChange={e=>setUn(p=>({...p,unPw:e.target.value}))} />
|
<Sec title="DATENBANK">
|
||||||
<button onClick={changeUn} disabled={busy.un} style={{ ...S.btn('#4ecdc4'), width:'100%', textAlign:'center', padding:'11px 0' }}>
|
<p style={{ color:'rgba(255,255,255,0.55)', fontSize:12, fontFamily:'monospace', marginBottom:12, lineHeight:1.6 }}>
|
||||||
{busy.un ? '…' : '✓ Benutzername ändern'}
|
Vollständige SQLite-Datenbank sichern oder wiederherstellen.
|
||||||
</button>
|
</p>
|
||||||
</Sec>
|
<button onClick={backup} style={{ ...S.btn('#ffe66d'), width:'100%', textAlign:'center', padding:'11px 0', marginBottom:12 }}>
|
||||||
|
↓ Backup herunterladen
|
||||||
<Sec title="PASSWORT ÄNDERN">
|
</button>
|
||||||
<Field label="AKTUELLES PASSWORT" type="password" value={pw.current} onChange={e=>setPw(p=>({...p,current:e.target.value}))} />
|
<label style={{ ...S.head, display:'block', marginBottom:4 }}>SICHERUNG EINSPIELEN (.db)</label>
|
||||||
<Field label="NEUES PASSWORT" type="password" value={pw.newPw} onChange={e=>setPw(p=>({...p,newPw:e.target.value}))} />
|
<div style={{ display:'flex', gap:8 }}>
|
||||||
<Field label="BESTÄTIGEN" type="password" value={pw.confirm} onChange={e=>setPw(p=>({...p,confirm:e.target.value}))} />
|
<input type="file" accept=".db" onChange={e => setFile(e.target.files[0])}
|
||||||
<button onClick={changePw} disabled={busy.pw} style={{ ...S.btn('#4ecdc4'), width:'100%', textAlign:'center', padding:'11px 0' }}>
|
style={{ ...S.inp, flex:1, padding:'9px 10px', cursor:'pointer', fontSize:13 }} />
|
||||||
{busy.pw ? '…' : '✓ Passwort ändern'}
|
<button onClick={restore} disabled={busy.restore || !file}
|
||||||
</button>
|
style={{ ...S.btn('#ff6b9d'), opacity: busy.restore || !file ? 0.4 : 1, padding:'0 14px', flexShrink:0 }}>
|
||||||
</Sec>
|
{busy.restore ? '…' : '↑'}
|
||||||
|
</button>
|
||||||
</div>}
|
</div>
|
||||||
|
{file && <div style={{ color:'rgba(255,255,255,0.55)', fontSize:10, fontFamily:'monospace', marginTop:5 }}>{file.name}</div>}
|
||||||
{section === 'backup' && <div>
|
</Sec>
|
||||||
<Sec title="DATENBANK">
|
|
||||||
<p style={{ color:'rgba(255,255,255,0.3)', fontSize:12, fontFamily:'monospace', marginBottom:12, lineHeight:1.6 }}>
|
|
||||||
Vollständige SQLite-Datenbank sichern oder wiederherstellen.
|
|
||||||
</p>
|
|
||||||
<button onClick={backup} style={{ ...S.btn('#ffe66d'), width:'100%', textAlign:'center', padding:'11px 0', marginBottom:12 }}>
|
|
||||||
↓ Backup herunterladen
|
|
||||||
</button>
|
|
||||||
<label style={{ ...S.head, display:'block', marginBottom:4 }}>SICHERUNG EINSPIELEN (.db)</label>
|
|
||||||
<div style={{ display:'flex', gap:8 }}>
|
|
||||||
<input type="file" accept=".db" onChange={e => setFile(e.target.files[0])}
|
|
||||||
style={{ ...S.inp, flex:1, padding:'9px 10px', cursor:'pointer', fontSize:13 }} />
|
|
||||||
<button onClick={restore} disabled={busy.restore || !file}
|
|
||||||
style={{ ...S.btn('#ff6b9d'), opacity: busy.restore || !file ? 0.4 : 1, padding:'0 14px', flexShrink:0 }}>
|
|
||||||
{busy.restore ? '…' : '↑'}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
{file && <div style={{ color:'rgba(255,255,255,0.25)', fontSize:10, fontFamily:'monospace', marginTop:5 }}>{file.name}</div>}
|
)}
|
||||||
</Sec>
|
|
||||||
</div>}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -816,6 +844,7 @@ function AdminPanel({ toast, mobile }) {
|
|||||||
// ── App Shell ─────────────────────────────────────────────────────────────────
|
// ── App Shell ─────────────────────────────────────────────────────────────────
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const mobile = useIsMobile();
|
const mobile = useIsMobile();
|
||||||
|
const [splash, setSplash] = useState(() => window.matchMedia('(display-mode: standalone)').matches);
|
||||||
const [user,setUser]=useState(null);
|
const [user,setUser]=useState(null);
|
||||||
const [active,setActive]=useState('dashboard');
|
const [active,setActive]=useState('dashboard');
|
||||||
const [toastMsg,setToastMsg]=useState({msg:'',type:'success'});
|
const [toastMsg,setToastMsg]=useState({msg:'',type:'success'});
|
||||||
@@ -844,6 +873,7 @@ export default function App() {
|
|||||||
setTimeout(()=>setToastMsg({msg:'',type:'success'}),3500);
|
setTimeout(()=>setToastMsg({msg:'',type:'success'}),3500);
|
||||||
},[]);
|
},[]);
|
||||||
|
|
||||||
|
if(splash) return <SplashScreen onDone={()=>setSplash(false)}/>;
|
||||||
if(!user) return <Login onLogin={u=>setUser(u)}/>;
|
if(!user) return <Login onLogin={u=>setUser(u)}/>;
|
||||||
|
|
||||||
const activeTool = TOOLS.find(t=>t.id===active);
|
const activeTool = TOOLS.find(t=>t.id===active);
|
||||||
|
|||||||
Reference in New Issue
Block a user