Dateien nach "frontend/src" hochladen
This commit is contained in:
@@ -445,14 +445,8 @@ function QuickLinks({ toast, mobile }) {
|
||||
return (
|
||||
<div style={{ ...S.card, marginBottom:10 }}>
|
||||
<ConfirmDialog/>
|
||||
<div style={{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:10 }}>
|
||||
<div style={{ marginBottom:10 }}>
|
||||
<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>
|
||||
|
||||
{/* Link-Grid */}
|
||||
@@ -461,20 +455,13 @@ function QuickLinks({ toast, mobile }) {
|
||||
: <div style={{ display:'flex', flexWrap:'wrap', gap:8 }}>
|
||||
{links.map(l => (
|
||||
<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"
|
||||
onClick={editMode ? e => e.preventDefault() : undefined}
|
||||
|
||||
style={{ display:'flex', flexDirection:'column', alignItems:'center', gap:4,
|
||||
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' }}>
|
||||
textDecoration:'none', minWidth:60, cursor:'pointer' }}>
|
||||
{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>}
|
||||
@@ -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 ────────────────────────────────────────────────────────
|
||||
function UserManagement({ toast }) {
|
||||
const [users, setUsers] = useState([]);
|
||||
@@ -1025,6 +1113,9 @@ function AdminPanel({ toast, mobile, user }) {
|
||||
|
||||
{section === 'dashboard' && (
|
||||
<div>
|
||||
<Sec title="SCHNELLZUGRIFF">
|
||||
<QuickLinksSettings toast={toast}/>
|
||||
</Sec>
|
||||
<Sec title="KALENDER (iCal)">
|
||||
<CalendarSettings toast={toast}/>
|
||||
</Sec>
|
||||
|
||||
@@ -9,8 +9,10 @@ function parseIcal(raw) {
|
||||
|
||||
// 2. Alle Properties als Map extrahieren
|
||||
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 = {};
|
||||
for (const line of block.split('\n')) {
|
||||
for (const line of stripped.split('\n')) {
|
||||
const col = line.indexOf(':');
|
||||
if (col < 0) continue;
|
||||
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 }}>
|
||||
<button onClick={prevMonth} style={{ background:'transparent', border:'none', color:'rgba(255,255,255,0.5)',
|
||||
cursor:'pointer', fontSize:18, padding:'0 6px' }}>‹</button>
|
||||
<div style={{ display:'flex', alignItems:'center', gap:8 }}>
|
||||
<span style={{ color:'#fff', fontFamily:"'Space Mono',monospace", fontSize:13, fontWeight:700 }}>
|
||||
{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)',
|
||||
cursor:'pointer', fontSize:18, padding:'0 6px' }}>›</button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user