Dateien nach "frontend/src" hochladen

This commit is contained in:
2026-05-26 15:24:20 +02:00
parent e12c1ea01e
commit 0c2074d7aa
2 changed files with 122 additions and 21 deletions

View File

@@ -445,14 +445,8 @@ function QuickLinks({ toast, mobile }) {
return ( return (
<div style={{ ...S.card, marginBottom:10 }}> <div style={{ ...S.card, marginBottom:10 }}>
<ConfirmDialog/> <ConfirmDialog/>
<div style={{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:10 }}> <div style={{ marginBottom:10 }}>
<div style={S.head}>SCHNELLZUGRIFF</div> <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={{ 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>
</div> </div>
{/* Link-Grid */} {/* Link-Grid */}
@@ -461,20 +455,13 @@ function QuickLinks({ toast, mobile }) {
: <div style={{ display:'flex', flexWrap:'wrap', gap:8 }}> : <div style={{ display:'flex', flexWrap:'wrap', gap:8 }}>
{links.map(l => ( {links.map(l => (
<div key={l.id} style={{ position:'relative' }}> <div key={l.id} style={{ position:'relative' }}>
{editMode && (
<div style={{ position:'absolute', top:-6, right:-6, display:'flex', gap:2, zIndex:1 }}>
<button onClick={() => { setF({ title:l.title, url:l.url, icon:l.icon }); setEditId(l.id); setForm(true); }}
style={{ width:20, height:20, borderRadius:'50%', background:'#4ecdc4', border:'none', color:'#0d0d0f', fontSize:10, cursor:'pointer' }}></button>
<button onClick={() => del(l.id)}
style={{ width:20, height:20, borderRadius:'50%', background:'#ff6b9d', border:'none', color:'#0d0d0f', fontSize:11, cursor:'pointer' }}></button>
</div>
)}
<a href={l.url} target="_blank" rel="noopener noreferrer" <a href={l.url} target="_blank" rel="noopener noreferrer"
onClick={editMode ? e => e.preventDefault() : undefined}
style={{ display:'flex', flexDirection:'column', alignItems:'center', gap:4, style={{ display:'flex', flexDirection:'column', alignItems:'center', gap:4,
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:'pointer' }}>
{l.icon && l.icon.startsWith('http') {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'}/> ? <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={{ fontSize:26 }}>{l.icon}</span>}
@@ -754,6 +741,107 @@ function DeleteAccountSection({ toast, user }) {
); );
} }
// ── QuickLinks Einstellungen ─────────────────────────────────────────────────
function QuickLinksSettings({ toast }) {
const [links, setLinks] = useState([]);
const [showForm, setForm] = useState(false);
const [form, setF] = useState({ title:'', url:'', icon:'🔗' });
const [editId, setEditId] = useState(null);
const { confirm, ConfirmDialog } = useConfirm();
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;
const icon = form.icon === '🔗' ? (getFavicon(url) || '🔗') : form.icon;
try {
if (editId) {
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, icon, url} });
setLinks(p => [...p, n]);
}
toast(editId ? 'Aktualisiert' : 'Gespeichert');
setF({ title:'', url:'', icon:'🔗' }); setForm(false); setEditId(null);
} catch(e) { toast(e.message, 'error'); }
};
const del = async id => {
if (!await confirm('Link wirklich löschen?')) return;
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'); }
};
return (
<div>
<ConfirmDialog/>
{/* Link-Liste */}
{links.length > 0 && (
<div style={{ marginBottom:12 }}>
{links.map(l => (
<div key={l.id} style={{ display:'flex', alignItems:'center', gap:10, padding:'8px 0',
borderBottom:'1px solid rgba(255,255,255,0.05)' }}>
<span style={{ fontSize:18, width:24, textAlign:'center', flexShrink:0 }}>
{l.icon && l.icon.startsWith('http')
? <img src={l.icon} alt="" style={{width:18,height:18,objectFit:'contain'}} onError={e=>e.target.style.display='none'}/>
: l.icon}
</span>
<div style={{ flex:1, minWidth:0 }}>
<div style={{ color:'#fff', fontFamily:'monospace', fontSize:13 }}>{l.title}</div>
<div style={{ color:'rgba(255,255,255,0.4)', fontFamily:'monospace', fontSize:10,
overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>{l.url}</div>
</div>
<div style={{ display:'flex', gap:5, flexShrink:0 }}>
<button onClick={() => { setF({ title:l.title, url:l.url, icon:l.icon }); setEditId(l.id); setForm(true); }}
style={S.btn('#4ecdc4', true)}></button>
<button onClick={() => del(l.id)} style={S.btn('#ff6b9d', true)}></button>
</div>
</div>
))}
</div>
)}
{/* Formular */}
{showForm ? (
<div style={{ background:'rgba(0,0,0,0.2)', border:'1px solid rgba(255,255,255,0.07)', borderRadius:10, padding:12 }}>
<div style={{ marginBottom:10 }}>
<div style={{ ...S.head, marginBottom:6 }}>ICON</div>
<div style={{ display:'flex', flexWrap:'wrap', gap:5 }}>
{['🔗','🌐','📊','📁','🛠','📧','🗓','💾','🖥','📡','🔒','📖','🎯','⚡','🏠','🔧','📱','🎮','🚀','💡'].map(ic => (
<button key={ic} onClick={() => setF(f => ({...f, icon:ic}))} style={{
width:34, height:34, borderRadius:7, fontSize:17, cursor:'pointer',
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)',
}}>{ic}</button>
))}
</div>
</div>
<Field label="TITEL" value={form.title} onChange={e => setF(f => ({...f, title:e.target.value}))} placeholder="Mein Link"/>
<Field label="URL" value={form.url} onChange={e => setF(f => ({...f, url:e.target.value}))} placeholder="https://…" autoCapitalize="none"/>
<div style={{ display:'flex', gap:8, marginTop:4 }}>
<button onClick={save} style={{ ...S.btn('#4ecdc4'), flex:1, textAlign:'center', padding:'10px 0' }}>{editId ? '✓ Aktualisieren' : '✓ Speichern'}</button>
<button onClick={() => { setForm(false); setEditId(null); setF({ title:'', url:'', icon:'🔗' }); }}
style={{ ...S.btn('#ff6b9d'), flex:1, textAlign:'center', padding:'10px 0' }}>Abbrechen</button>
</div>
</div>
) : (
<button onClick={() => { setForm(true); setEditId(null); setF({ title:'', url:'', icon:'🔗' }); }}
style={{ ...S.btn('#4ecdc4'), width:'100%', textAlign:'center', padding:'10px 0' }}>
+ Link hinzufügen
</button>
)}
</div>
);
}
// ── Benutzerverwaltung ──────────────────────────────────────────────────────── // ── Benutzerverwaltung ────────────────────────────────────────────────────────
function UserManagement({ toast }) { function UserManagement({ toast }) {
const [users, setUsers] = useState([]); const [users, setUsers] = useState([]);
@@ -1025,6 +1113,9 @@ function AdminPanel({ toast, mobile, user }) {
{section === 'dashboard' && ( {section === 'dashboard' && (
<div> <div>
<Sec title="SCHNELLZUGRIFF">
<QuickLinksSettings toast={toast}/>
</Sec>
<Sec title="KALENDER (iCal)"> <Sec title="KALENDER (iCal)">
<CalendarSettings toast={toast}/> <CalendarSettings toast={toast}/>
</Sec> </Sec>

View File

@@ -9,8 +9,10 @@ function parseIcal(raw) {
// 2. Alle Properties als Map extrahieren // 2. Alle Properties als Map extrahieren
function getProps(block) { function getProps(block) {
// VALARM-Blöcke entfernen (haben eigene SUMMARY/DESCRIPTION die nicht zum Termin gehören)
const stripped = block.replace(/BEGIN:VALARM[\s\S]*?END:VALARM/g, '');
const props = {}; const props = {};
for (const line of block.split('\n')) { for (const line of stripped.split('\n')) {
const col = line.indexOf(':'); const col = line.indexOf(':');
if (col < 0) continue; if (col < 0) continue;
const namepart = line.slice(0, col).toUpperCase(); const namepart = line.slice(0, col).toUpperCase();
@@ -206,9 +208,17 @@ export default function CalendarWidget() {
<div style={{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:10 }}> <div style={{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:10 }}>
<button onClick={prevMonth} style={{ background:'transparent', border:'none', color:'rgba(255,255,255,0.5)', <button onClick={prevMonth} style={{ background:'transparent', border:'none', color:'rgba(255,255,255,0.5)',
cursor:'pointer', fontSize:18, padding:'0 6px' }}></button> cursor:'pointer', fontSize:18, padding:'0 6px' }}></button>
<span style={{ color:'#fff', fontFamily:"'Space Mono',monospace", fontSize:13, fontWeight:700 }}> <div style={{ display:'flex', alignItems:'center', gap:8 }}>
{MONTHS[cur.m]} {cur.y} <span style={{ color:'#fff', fontFamily:"'Space Mono',monospace", fontSize:13, fontWeight:700 }}>
</span> {MONTHS[cur.m]} {cur.y}
</span>
{(cur.m !== today.getMonth() || cur.y !== today.getFullYear()) && (
<button onClick={() => { setCur({ y:today.getFullYear(), m:today.getMonth() }); setSelected(new Date()); }}
style={{ background:'rgba(78,205,196,0.15)', border:'1px solid rgba(78,205,196,0.3)',
borderRadius:6, color:'#4ecdc4', fontFamily:'monospace', fontSize:9,
cursor:'pointer', padding:'2px 8px' }}>Heute</button>
)}
</div>
<button onClick={nextMonth} style={{ background:'transparent', border:'none', color:'rgba(255,255,255,0.5)', <button onClick={nextMonth} style={{ background:'transparent', border:'none', color:'rgba(255,255,255,0.5)',
cursor:'pointer', fontSize:18, padding:'0 6px' }}></button> cursor:'pointer', fontSize:18, padding:'0 6px' }}></button>
</div> </div>