Linkliste: Ordner, Schnellzugriff-Toggle, Sortierung per Drag/Pfeile, Ordner-Modal im Dashboard

This commit is contained in:
2026-06-04 21:27:34 +02:00
parent c5ccfccf3e
commit e7cd93c766
5 changed files with 551 additions and 242 deletions

View File

@@ -1,20 +1,23 @@
import { useState, useEffect } from 'react';
import { useState, useEffect, useRef } from 'react';
import { api, S } from '../lib.js';
const ICONS = ['🔗','📄','🌐','📊','🎯','⚙️','📁','🔧','📱','💡','🏠','🛒','📺','🎮','🎵','📧','💬','📰','🔬','🏥','🏦','✈️','🚀'];
const LINK_ICONS = ['🔗','📄','🌐','📊','🎯','⚙️','📁','🔧','📱','💡','🏠','🛒','📺','🎮','🎵','📧','💬','📰','🔬','🏥','🏦','✈️','🚀','🔐','📂','⭐','🔑','💻','🖥','📡'];
const FOLDER_ICONS = ['📁','📂','🗂','💼','🗃','📦','🏷','🔖','📋','📌','🗓','🖇'];
function extIcon(url) {
try {
return `https://www.google.com/s2/favicons?sz=32&domain=${new URL(url).hostname}`;
} catch { return null; }
try { return `https://www.google.com/s2/favicons?sz=32&domain=${new URL(url).hostname}`; }
catch { return null; }
}
function isMob() { return window.innerWidth < 768; }
// ── Share Modal ───────────────────────────────────────────────────────────────
function ShareModal({ link, onClose, toast }) {
const [username, setUsername] = useState('');
const [shares, setShares] = useState([]);
const load = () => api(`/tools/linkliste/${link.id}/shares`).then(setShares).catch(()=>{});
useEffect(()=>{ load(); },[]);
const share = async () => {
const share = async () => {
if (!username.trim()) return;
try { await api(`/tools/linkliste/${link.id}/share`,{body:{username:username.trim()}}); toast('Geteilt ✓'); setUsername(''); load(); }
catch(e) { toast(e.message,'error'); }
@@ -23,19 +26,17 @@ function ShareModal({ link, onClose, toast }) {
try { await api(`/tools/linkliste/${link.id}/share/${uid}`,{method:'DELETE'}); setShares(p=>p.filter(s=>s.id!==uid)); }
catch(e) { toast(e.message,'error'); }
};
const isMob = window.innerWidth < 768;
const mob = isMob();
return (
<div style={{position:'fixed',inset:0,background:'rgba(0,0,0,0.8)',zIndex:6000,
display:'flex',alignItems:isMob?'flex-end':'center',justifyContent:'center',padding:isMob?0:24}}
display:'flex',alignItems:mob?'flex-end':'center',justifyContent:'center',padding:mob?0:24}}
onClick={e=>e.target===e.currentTarget&&onClose()}>
<div style={{background:'#1a1a1e',borderRadius:isMob?'16px 16px 0 0':14,width:'100%',maxWidth:400,
<div style={{background:'#1a1a1e',borderRadius:mob?'16px 16px 0 0':14,width:'100%',maxWidth:400,
padding:'20px 20px 28px',border:'1px solid rgba(255,255,255,0.12)'}}>
{isMob&&<div style={{width:36,height:4,background:'rgba(255,255,255,0.15)',borderRadius:2,margin:'0 auto 14px'}}/>}
{mob&&<div style={{width:36,height:4,background:'rgba(255,255,255,0.15)',borderRadius:2,margin:'0 auto 14px'}}/>}
<div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:10}}>
<div style={{color:'#fff',fontFamily:"'Space Mono',monospace",fontSize:13,fontWeight:700,overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>
🤝 {link.title}
</div>
<button onClick={onClose} style={{background:'transparent',border:'none',color:'rgba(255,255,255,0.4)',cursor:'pointer',fontSize:18,padding:'0 4px',flexShrink:0}}></button>
<div style={{color:'#fff',fontFamily:"'Space Mono',monospace",fontSize:13,fontWeight:700}}>🤝 {link.title}</div>
<button onClick={onClose} style={{background:'transparent',border:'none',color:'rgba(255,255,255,0.4)',cursor:'pointer',fontSize:18}}></button>
</div>
<div style={{display:'flex',gap:8,marginBottom:14}}>
<input value={username} onChange={e=>setUsername(e.target.value)} onKeyDown={e=>e.key==='Enter'&&share()}
@@ -54,8 +55,9 @@ function ShareModal({ link, onClose, toast }) {
);
}
function LinkForm({ initial, onSave, onCancel, toast }) {
const [form, setForm] = useState({ title:'', url:'', icon:'🔗', description:'', ...initial });
// ── Link Form ─────────────────────────────────────────────────────────────────
function LinkForm({ initial, folders, onSave, onCancel, toast }) {
const [form, setForm] = useState({ title:'', url:'', icon:'🔗', description:'', folder_id:null, ...initial });
const [busy, setBusy] = useState(false);
const set = (k,v) => setForm(p=>({...p,[k]:v}));
const save = async () => {
@@ -68,29 +70,34 @@ function LinkForm({ initial, onSave, onCancel, toast }) {
setBusy(false);
};
return (
<div style={{...S.card, marginBottom:12}}>
<div style={{...S.head,marginBottom:10}}>{initial?.id ? 'LINK BEARBEITEN' : 'NEUER LINK'}</div>
<div style={{...S.card,marginBottom:12}}>
<div style={{...S.head,marginBottom:10}}>{initial?.id?'LINK BEARBEITEN':'NEUER LINK'}</div>
<div style={{marginBottom:8}}>
<label style={{...S.head,display:'block',marginBottom:4,fontSize:9}}>ICON</label>
<div style={{...S.head,fontSize:9,marginBottom:4}}>ICON</div>
<div style={{display:'flex',flexWrap:'wrap',gap:4,marginBottom:6}}>
{ICONS.map(ic=>(
{LINK_ICONS.map(ic=>(
<button key={ic} onClick={()=>set('icon',ic)} style={{
width:32,height:32,borderRadius:7,fontSize:16,cursor:'pointer',border:'none',
background:form.icon===ic?'rgba(78,205,196,0.2)':'rgba(255,255,255,0.05)',
outline:form.icon===ic?'1px solid #4ecdc4':'none',
}}>{ic}</button>
outline:form.icon===ic?'1px solid #4ecdc4':'none'}}>{ic}</button>
))}
</div>
</div>
<input value={form.title} onChange={e=>set('title',e.target.value)} placeholder="Titel *"
style={{...S.inp,marginBottom:8}}/>
<input value={form.url} onChange={e=>set('url',e.target.value)} placeholder="URL * (z.B. https://example.com)"
autoCapitalize="none" style={{...S.inp,marginBottom:8}}/>
<input value={form.description} onChange={e=>set('description',e.target.value)} placeholder="Beschreibung (optional)"
style={{...S.inp,marginBottom:12,fontSize:13}}/>
<input value={form.title} onChange={e=>set('title',e.target.value)} placeholder="Titel *" style={{...S.inp,marginBottom:8}}/>
<input value={form.url} onChange={e=>set('url',e.target.value)} placeholder="URL *" autoCapitalize="none" style={{...S.inp,marginBottom:8}}/>
<input value={form.description} onChange={e=>set('description',e.target.value)} placeholder="Beschreibung (optional)" style={{...S.inp,marginBottom:8,fontSize:13}}/>
{folders.length>0 && (
<div style={{marginBottom:12}}>
<div style={{...S.head,fontSize:9,marginBottom:4}}>IN ORDNER</div>
<select value={form.folder_id||''} onChange={e=>set('folder_id',e.target.value?parseInt(e.target.value):null)}
style={{...S.inp,cursor:'pointer'}}>
<option value=""> Kein Ordner </option>
{folders.map(f=><option key={f.id} value={f.id}>{f.icon} {f.name}</option>)}
</select>
</div>
)}
<div style={{display:'flex',gap:8}}>
<button onClick={save} disabled={busy}
style={{...S.btn('#4ecdc4'),flex:1,textAlign:'center',padding:'10px 0',opacity:busy?0.5:1}}>
<button onClick={save} disabled={busy} style={{...S.btn('#4ecdc4'),flex:1,textAlign:'center',padding:'10px 0',opacity:busy?0.5:1}}>
{busy?'…':'✓ Speichern'}
</button>
<button onClick={onCancel} style={{...S.btn('#ff6b9d',true),padding:'0 16px'}}>Abbrechen</button>
@@ -99,62 +106,202 @@ function LinkForm({ initial, onSave, onCancel, toast }) {
);
}
// ── Folder Form ───────────────────────────────────────────────────────────────
function FolderForm({ initial, onSave, onCancel, toast }) {
const [form, setForm] = useState({ name:'', icon:'📁', ...initial });
const [busy, setBusy] = useState(false);
const save = async () => {
if (!form.name.trim()) { toast('Name erforderlich','error'); return; }
setBusy(true);
try { await onSave(form); }
catch(e) { toast(e.message,'error'); }
setBusy(false);
};
return (
<div style={{...S.card,marginBottom:12}}>
<div style={{...S.head,marginBottom:10}}>{initial?.id?'ORDNER BEARBEITEN':'NEUER ORDNER'}</div>
<div style={{marginBottom:8}}>
<div style={{...S.head,fontSize:9,marginBottom:4}}>ICON</div>
<div style={{display:'flex',flexWrap:'wrap',gap:4,marginBottom:6}}>
{FOLDER_ICONS.map(ic=>(
<button key={ic} onClick={()=>setForm(p=>({...p,icon:ic}))} style={{
width:32,height:32,borderRadius:7,fontSize:16,cursor:'pointer',border:'none',
background:form.icon===ic?'rgba(78,205,196,0.2)':'rgba(255,255,255,0.05)',
outline:form.icon===ic?'1px solid #4ecdc4':'none'}}>{ic}</button>
))}
</div>
</div>
<input value={form.name} onChange={e=>setForm(p=>({...p,name:e.target.value}))} placeholder="Ordner-Name *"
style={{...S.inp,marginBottom:12}}/>
<div style={{display:'flex',gap:8}}>
<button onClick={save} disabled={busy} style={{...S.btn('#4ecdc4'),flex:1,textAlign:'center',padding:'10px 0',opacity:busy?0.5:1}}>
{busy?'…':'✓ Speichern'}
</button>
<button onClick={onCancel} style={{...S.btn('#ff6b9d',true),padding:'0 16px'}}>Abbrechen</button>
</div>
</div>
);
}
// ── Main Component ────────────────────────────────────────────────────────────
export default function Linkliste({ toast, mobile }) {
const [own, setOwn] = useState([]);
const [folders, setFolders] = useState([]);
const [shared, setShared] = useState([]);
const [byMe, setByMe] = useState([]);
const [tab, setTab] = useState('own');
const [showForm, setShowForm] = useState(false);
const [showFolderForm, setShowFolderForm] = useState(false);
const [editItem, setEditItem] = useState(null);
const [editFolder,setEditFolder]= useState(null);
const [shareItem, setShareItem] = useState(null);
const [search, setSearch] = useState('');
const [expanded, setExpanded] = useState({}); // folder_id → bool
const [sortMode, setSortMode] = useState(false);
const dragRef = useRef(null);
const load = () => api('/tools/linkliste')
.then(d=>{ setOwn(d.own||[]); setShared(d.shared||[]); setByMe(d.sharedByMe||[]); })
.then(d=>{ setOwn(d.own||[]); setFolders(d.folders||[]); setShared(d.shared||[]); setByMe(d.sharedByMe||[]); })
.catch(()=>{});
useEffect(()=>{ load(); },[]);
// ── CRUD Links ──────────────────────────────────────────────────────────────
const addLink = async form => {
const r = await api('/tools/linkliste',{body:form});
setOwn(p=>[r,...p]); setShowForm(false);
setOwn(p=>[...p,r]); setShowForm(false);
};
const updateLink = async form => {
const r = await api(`/tools/linkliste/${editItem.id}`,{method:'PUT',body:form});
setOwn(p=>p.map(l=>l.id===r.id?r:l)); setEditItem(null);
};
const deleteLink = async id => {
await api(`/tools/linkliste/${id}`,{method:'DELETE'});
setOwn(p=>p.filter(l=>l.id!==id)); toast('Gelöscht');
};
const addToQuickLinks = async link => {
try {
await api('/dashboard/links',{body:{title:link.title,url:link.url,icon:link.icon}});
toast(`${link.title}" zu Schnellzugriffen hinzugefügt ✓`);
} catch(e) { toast(e.message,'error'); }
const toggleQA = async link => {
const val = link.in_quickaccess ? 0 : 1;
const r = await api(`/tools/linkliste/${link.id}`,{method:'PUT',body:{in_quickaccess:val}});
setOwn(p=>p.map(l=>l.id===r.id?r:l));
toast(val ? `${link.title}" zum Schnellzugriff hinzugefügt ✓` : `${link.title}" aus Schnellzugriff entfernt`);
};
const items = tab==='own' ? own : tab==='shared' ? shared : byMe;
const filtered = search.trim()
? items.filter(l=>l.title.toLowerCase().includes(search.toLowerCase())||l.url.toLowerCase().includes(search.toLowerCase())||(l.description&&l.description.toLowerCase().includes(search.toLowerCase())))
: items;
// ── CRUD Folders ────────────────────────────────────────────────────────────
const addFolder = async form => {
const r = await api('/tools/linkliste/folders',{body:form});
setFolders(p=>[...p,r]); setShowFolderForm(false);
};
const updateFolder = async form => {
const r = await api(`/tools/linkliste/folders/${editFolder.id}`,{method:'PUT',body:form});
setFolders(p=>p.map(f=>f.id===r.id?r:f)); setEditFolder(null);
};
const deleteFolder = async id => {
await api(`/tools/linkliste/folders/${id}`,{method:'DELETE'});
setFolders(p=>p.filter(f=>f.id!==id));
// Links aus Ordner herauslösen (Backend macht das, Frontend nachführen)
setOwn(p=>p.map(l=>l.folder_id===id?{...l,folder_id:null}:l));
toast('Ordner gelöscht');
};
const toggleFolderQA = async folder => {
const val = folder.in_quickaccess ? 0 : 1;
const r = await api(`/tools/linkliste/folders/${folder.id}`,{method:'PUT',body:{in_quickaccess:val}});
setFolders(p=>p.map(f=>f.id===r.id?r:f));
toast(val ? `Ordner „${folder.name}" zum Schnellzugriff hinzugefügt ✓` : `Ordner „${folder.name}" aus Schnellzugriff entfernt`);
};
const LinkCard = ({ link, isShared=false, isByMe=false }) => {
const favicon = extIcon(link.url);
// ── Sortierung ──────────────────────────────────────────────────────────────
const moveLink = (id, dir) => {
setOwn(prev => {
const arr = [...prev];
const idx = arr.findIndex(l=>l.id===id);
const nxt = idx+dir;
if (nxt<0||nxt>=arr.length) return prev;
[arr[idx],arr[nxt]] = [arr[nxt],arr[idx]];
// Persist
api('/tools/linkliste/sort',{method:'PUT',body:{links:arr.map(l=>l.id)}}).catch(()=>{});
return arr;
});
};
const moveFolder = (id, dir) => {
setFolders(prev => {
const arr = [...prev];
const idx = arr.findIndex(f=>f.id===id);
const nxt = idx+dir;
if (nxt<0||nxt>=arr.length) return prev;
[arr[idx],arr[nxt]] = [arr[nxt],arr[idx]];
api('/tools/linkliste/sort',{method:'PUT',body:{folders:arr.map(f=>f.id)}}).catch(()=>{});
return arr;
});
};
// ── Suche ───────────────────────────────────────────────────────────────────
const q = search.trim().toLowerCase();
const matchLink = l =>
l.title.toLowerCase().includes(q) ||
l.url.toLowerCase().includes(q) ||
(l.description||'').toLowerCase().includes(q);
const matchFolder = f => f.name.toLowerCase().includes(q);
// Für "Meine" Tab: flache Liste aller Links die passen + Ordner die passen
const filteredOwn = q ? own.filter(matchLink) : own;
const filteredFolders = q ? folders.filter(f => matchFolder(f) || own.some(l=>l.folder_id===f.id&&matchLink(l))) : folders;
const filteredShared = shared.filter(l=>!q||matchLink(l));
const filteredByMe = byMe.filter(l=>!q||matchLink(l));
// ── Drag für Sortierung ─────────────────────────────────────────────────────
const onDragStart = (e,id) => { dragRef.current = id; e.dataTransfer.effectAllowed='move'; };
const onDragOver = (e,id,type) => {
e.preventDefault();
if (dragRef.current===id) return;
if (type==='link') {
const arr=[...own]; const from=arr.findIndex(l=>l.id===dragRef.current); const to=arr.findIndex(l=>l.id===id);
if (from<0||to<0) return;
arr.splice(to,0,arr.splice(from,1)[0]); setOwn(arr);
}
if (type==='folder') {
const arr=[...folders]; const from=arr.findIndex(f=>f.id===dragRef.current); const to=arr.findIndex(f=>f.id===id);
if (from<0||to<0) return;
arr.splice(to,0,arr.splice(from,1)[0]); setFolders(arr);
}
};
const onDrop = (type) => {
if (type==='link') api('/tools/linkliste/sort',{method:'PUT',body:{links:own.map(l=>l.id)}}).catch(()=>{});
if (type==='folder') api('/tools/linkliste/sort',{method:'PUT',body:{folders:folders.map(f=>f.id)}}).catch(()=>{});
dragRef.current=null;
};
// ── Render ──────────────────────────────────────────────────────────────────
const QABadge = ({active}) => (
<span style={{
display:'inline-block', width:7, height:7, borderRadius:'50%',
background:active?'#4ecdc4':'rgba(255,255,255,0.15)',
boxShadow:active?'0 0 5px #4ecdc4':'none',
marginRight:3, flexShrink:0,
}}/>
);
function LinkCard({ link, isShared=false, isByMe=false, inSort=false }) {
const [imgOk, setImgOk] = useState(true);
const favicon = (link.icon==='🔗'||!link.icon) ? extIcon(link.url) : null;
const showFavicon = !!favicon && imgOk;
return (
<div style={{...S.card,marginBottom:8,padding:'12px 14px'}}>
<div style={{...S.card,marginBottom:6,padding:'10px 12px'}}
draggable={sortMode&&!isShared&&!isByMe}
onDragStart={sortMode?e=>onDragStart(e,link.id):undefined}
onDragOver={sortMode?e=>onDragOver(e,link.id,'link'):undefined}
onDrop={sortMode?()=>onDrop('link'):undefined}>
<div style={{display:'flex',alignItems:'center',gap:10}}>
{/* Sort handle */}
{sortMode && !isShared && !isByMe && (
<span style={{color:'rgba(255,255,255,0.2)',fontSize:14,cursor:'grab',flexShrink:0}}></span>
)}
{/* Icon */}
<div style={{width:36,height:36,borderRadius:9,flexShrink:0,overflow:'hidden',
<div style={{width:32,height:32,borderRadius:8,flexShrink:0,overflow:'hidden',
background:'rgba(255,255,255,0.05)',border:'1px solid rgba(255,255,255,0.08)',
display:'flex',alignItems:'center',justifyContent:'center',fontSize:18}}>
{favicon
? <img src={favicon} onError={e=>{e.target.style.display='none';e.target.nextSibling.style.display='flex';}} style={{width:20,height:20,objectFit:'contain'}} alt=""/>
: null}
<span style={{display:favicon?'none':'flex'}}>{link.icon}</span>
display:'flex',alignItems:'center',justifyContent:'center',fontSize:16}}>
{showFavicon
? <img src={favicon} onError={()=>setImgOk(false)} style={{width:18,height:18,objectFit:'contain'}} alt=""/>
: <span>{link.icon||'🔗'}</span>}
</div>
{/* Info */}
<div style={{flex:1,minWidth:0}}>
@@ -167,96 +314,203 @@ export default function Linkliste({ toast, mobile }) {
overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap',marginTop:1}}>
{link.url}
</div>
{link.description && (
<div style={{color:'rgba(255,255,255,0.4)',fontFamily:'monospace',fontSize:10,marginTop:2}}>
{link.description}
</div>
)}
{isShared && <div style={{color:'rgba(255,255,255,0.25)',fontFamily:'monospace',fontSize:9,marginTop:2}}>von {link.owner_name}</div>}
{isByMe && <div style={{color:'rgba(255,255,255,0.25)',fontFamily:'monospace',fontSize:9,marginTop:2}}> {link.shared_with_name}</div>}
{link.description&&<div style={{color:'rgba(255,255,255,0.4)',fontFamily:'monospace',fontSize:10,marginTop:1}}>{link.description}</div>}
{isShared&&<div style={{color:'rgba(255,255,255,0.25)',fontFamily:'monospace',fontSize:9,marginTop:1}}>von {link.owner_name}</div>}
{isByMe&&<div style={{color:'rgba(255,255,255,0.25)',fontFamily:'monospace',fontSize:9,marginTop:1}}> {link.shared_with_name}</div>}
</div>
</div>
{/* Aktionen */}
<div style={{display:'flex',gap:5,marginTop:10,flexWrap:'wrap'}}>
<button onClick={()=>addToQuickLinks(link)}
title="Zum Dashboard-Schnellzugriff hinzufügen"
style={{...S.btn('#4ecdc4',true),fontSize:10,padding:'5px 10px'}}>
Schnellzugriff
</button>
{!isShared && !isByMe && (
<>
<button onClick={()=>setShareItem(link)}
style={{...S.btn('#ffe66d',true),fontSize:10,padding:'5px 10px'}}>
🤝 Teilen{link.share_count>0?` (${link.share_count})`:''}
</button>
<button onClick={()=>setEditItem(link)}
style={{...S.btn('#4ecdc4',true),fontSize:10,padding:'5px 10px'}}></button>
<button onClick={()=>deleteLink(link.id)}
style={{...S.btn('#ff6b9d',true),fontSize:10,padding:'5px 10px'}}></button>
</>
{/* Sort buttons */}
{sortMode && !isShared && !isByMe && (
<div style={{display:'flex',flexDirection:'column',gap:2,flexShrink:0}}>
<button onClick={()=>moveLink(link.id,-1)} style={{background:'rgba(255,255,255,0.05)',border:'none',
borderRadius:4,color:'rgba(255,255,255,0.5)',cursor:'pointer',padding:'1px 7px',fontSize:11}}></button>
<button onClick={()=>moveLink(link.id,+1)} style={{background:'rgba(255,255,255,0.05)',border:'none',
borderRadius:4,color:'rgba(255,255,255,0.5)',cursor:'pointer',padding:'1px 7px',fontSize:11}}></button>
</div>
)}
</div>
{/* Aktionen */}
{!sortMode && (
<div style={{display:'flex',gap:5,marginTop:8,flexWrap:'wrap'}}>
{!isShared && !isByMe && (
<button onClick={()=>toggleQA(link)}
style={{...S.btn(link.in_quickaccess?'#4ecdc4':'rgba(255,255,255,0.15)',!!link.in_quickaccess),
fontSize:10,padding:'4px 10px',display:'flex',alignItems:'center',gap:4}}>
<QABadge active={!!link.in_quickaccess}/> Schnellzugriff
</button>
)}
{!isShared && !isByMe && (
<>
<button onClick={()=>setShareItem(link)} style={{...S.btn('#ffe66d',true),fontSize:10,padding:'4px 10px'}}>
🤝{link.share_count>0?` (${link.share_count})`:''}
</button>
<button onClick={()=>setEditItem(link)} style={{...S.btn('#4ecdc4',true),fontSize:10,padding:'4px 10px'}}></button>
<button onClick={()=>deleteLink(link.id)} style={{...S.btn('#ff6b9d',true),fontSize:10,padding:'4px 10px'}}></button>
</>
)}
</div>
)}
</div>
);
};
}
function FolderSection({ folder }) {
const isOpen = !!expanded[folder.id];
const folderLinks = filteredOwn.filter(l=>l.folder_id===folder.id);
return (
<div style={{marginBottom:8}}>
<div style={{...S.card,padding:'10px 12px',marginBottom:isOpen?0:0,
borderRadius:isOpen?'10px 10px 0 0':'10px',borderBottom:isOpen?'1px solid rgba(255,255,255,0.06)':undefined}}
draggable={sortMode}
onDragStart={sortMode?e=>onDragStart(e,folder.id):undefined}
onDragOver={sortMode?e=>onDragOver(e,folder.id,'folder'):undefined}
onDrop={sortMode?()=>onDrop('folder'):undefined}>
<div style={{display:'flex',alignItems:'center',gap:10}}>
{sortMode && <span style={{color:'rgba(255,255,255,0.2)',fontSize:14,cursor:'grab',flexShrink:0}}></span>}
<button onClick={()=>setExpanded(p=>({...p,[folder.id]:!isOpen}))}
style={{background:'none',border:'none',cursor:'pointer',color:'rgba(255,255,255,0.4)',
fontSize:12,padding:0,flexShrink:0,width:16}}>
{isOpen?'▾':'▸'}
</button>
<span style={{fontSize:18,flexShrink:0}}>{folder.icon}</span>
<div style={{flex:1,minWidth:0}}>
<span style={{color:'#fff',fontFamily:'monospace',fontSize:13,fontWeight:700}}>{folder.name}</span>
<span style={{color:'rgba(255,255,255,0.3)',fontFamily:'monospace',fontSize:10,marginLeft:8}}>
{folderLinks.length} Link{folderLinks.length!==1?'s':''}
</span>
</div>
{sortMode && (
<div style={{display:'flex',flexDirection:'column',gap:2,flexShrink:0}}>
<button onClick={()=>moveFolder(folder.id,-1)} style={{background:'rgba(255,255,255,0.05)',border:'none',
borderRadius:4,color:'rgba(255,255,255,0.5)',cursor:'pointer',padding:'1px 7px',fontSize:11}}></button>
<button onClick={()=>moveFolder(folder.id,+1)} style={{background:'rgba(255,255,255,0.05)',border:'none',
borderRadius:4,color:'rgba(255,255,255,0.5)',cursor:'pointer',padding:'1px 7px',fontSize:11}}></button>
</div>
)}
{!sortMode && (
<div style={{display:'flex',gap:5,flexShrink:0}}>
<button onClick={()=>toggleFolderQA(folder)}
style={{...S.btn(folder.in_quickaccess?'#4ecdc4':'rgba(255,255,255,0.15)',!!folder.in_quickaccess),
fontSize:10,padding:'4px 10px',display:'flex',alignItems:'center',gap:4}}>
<QABadge active={!!folder.in_quickaccess}/> Schnellzugriff
</button>
<button onClick={()=>setEditFolder(folder)} style={{...S.btn('#4ecdc4',true),fontSize:10,padding:'4px 10px'}}></button>
<button onClick={()=>deleteFolder(folder.id)} style={{...S.btn('#ff6b9d',true),fontSize:10,padding:'4px 10px'}}></button>
</div>
)}
</div>
</div>
{isOpen && (
<div style={{background:'rgba(255,255,255,0.02)',border:'1px solid rgba(255,255,255,0.07)',
borderTop:'none',borderRadius:'0 0 10px 10px',padding:'8px 8px 4px'}}>
{folderLinks.length===0
? <div style={{color:'rgba(255,255,255,0.3)',fontFamily:'monospace',fontSize:11,padding:'8px 4px',textAlign:'center'}}>
Ordner ist leer bearbeite einen Link und wähle diesen Ordner
</div>
: folderLinks.map(link=>(
<LinkCard key={link.id} link={link}/>
))
}
{!sortMode && (
<button onClick={()=>{ setShowForm(true); setShowFolderForm(false); setEditItem({folder_id:folder.id}); }}
style={{...S.btn('#4ecdc4',true),fontSize:10,padding:'5px 12px',width:'100%',textAlign:'center',marginBottom:4}}>
+ Link in diesem Ordner hinzufügen
</button>
)}
</div>
)}
</div>
);
}
const unfoldered = filteredOwn.filter(l=>!l.folder_id);
return (
<div style={{padding:mobile?'14px 14px 90px':'36px 44px',maxWidth:760}}>
{shareItem && <ShareModal link={shareItem} onClose={()=>{setShareItem(null);load();}} toast={toast}/>}
{/* Header */}
<div style={{display:'flex',alignItems:'center',justifyContent:'space-between',marginBottom:14}}>
<div style={{display:'flex',alignItems:'center',justifyContent:'space-between',marginBottom:14,gap:8,flexWrap:'wrap'}}>
<h1 style={{color:'#fff',fontFamily:"'Space Mono',monospace",fontSize:mobile?17:22,margin:0}}>Linkliste</h1>
<div style={{display:'flex',gap:8}}>
<button onClick={load} title="Neu laden"
style={{background:'transparent',border:'1px solid rgba(255,255,255,0.1)',borderRadius:8,
color:'rgba(255,255,255,0.4)',cursor:'pointer',padding:'6px 10px',fontSize:14,fontFamily:'monospace'}}></button>
{!showForm && !editItem && tab==='own' && (
<button onClick={()=>setShowForm(true)}
style={{...S.btn('#4ecdc4'),padding:'7px 16px',fontSize:12}}>+ Neuer Link</button>
<div style={{display:'flex',gap:6,flexWrap:'wrap'}}>
<button onClick={load} style={{background:'transparent',border:'1px solid rgba(255,255,255,0.1)',
borderRadius:8,color:'rgba(255,255,255,0.4)',cursor:'pointer',padding:'6px 10px',fontSize:14,fontFamily:'monospace'}}></button>
{tab==='own' && (
<>
<button onClick={()=>setSortMode(p=>!p)}
style={{...S.btn(sortMode?'#ffe66d':'rgba(255,255,255,0.1)',!sortMode),fontSize:11,padding:'6px 12px'}}>
{sortMode?'✓ Fertig':'⇅ Sortieren'}
</button>
{!showFolderForm && !editFolder && !sortMode && (
<button onClick={()=>{setShowFolderForm(true);setShowForm(false);}}
style={{...S.btn('#ffe66d',true),fontSize:11,padding:'6px 12px'}}>+ Ordner</button>
)}
{!showForm && !editItem && !sortMode && (
<button onClick={()=>{setShowForm(true);setShowFolderForm(false);setEditItem(null);}}
style={{...S.btn('#4ecdc4'),padding:'7px 16px',fontSize:12}}>+ Link</button>
)}
</>
)}
</div>
</div>
{/* Formular */}
{showForm && <LinkForm onSave={addLink} onCancel={()=>setShowForm(false)} toast={toast}/>}
{editItem && <LinkForm initial={editItem} onSave={updateLink} onCancel={()=>setEditItem(null)} toast={toast}/>}
{/* Formulare */}
{showFolderForm && !editFolder && <FolderForm onSave={addFolder} onCancel={()=>setShowFolderForm(false)} toast={toast}/>}
{editFolder && <FolderForm initial={editFolder} onSave={updateFolder} onCancel={()=>setEditFolder(null)} toast={toast}/>}
{showForm && !editItem && <LinkForm folders={folders} onSave={addLink} onCancel={()=>setShowForm(false)} toast={toast}/>}
{editItem && <LinkForm initial={editItem} folders={folders} onSave={updateLink} onCancel={()=>setEditItem(null)} toast={toast}/>}
{/* Tabs */}
<div style={{display:'flex',gap:6,marginBottom:12,flexWrap:'wrap'}}>
{[['own','Meine',own.length],['shared','Geteilt mit mir',shared.length],['byMe','Geteilt von mir',byMe.length]].map(([k,l,cnt])=>(
<button key={k} onClick={()=>{setTab(k);setSearch('');}} style={{
{[['own','Meine',own.length+folders.length],['shared','Geteilt mit mir',shared.length],['byMe','Geteilt von mir',byMe.length]].map(([k,l,cnt])=>(
<button key={k} onClick={()=>{setTab(k);setSearch('');setSortMode(false);}} style={{
padding:'6px 14px',borderRadius:20,fontFamily:'monospace',fontSize:11,cursor:'pointer',
background:tab===k?'#4ecdc4':'rgba(255,255,255,0.05)',
color:tab===k?'#0d0d0f':'rgba(255,255,255,0.55)',
border:tab===k?'none':'1px solid rgba(255,255,255,0.1)',
fontWeight:tab===k?700:400,
}}>
{l}{cnt>0&&<span style={{marginLeft:4,opacity:0.7}}>({cnt})</span>}
</button>
}}>{l}{cnt>0&&<span style={{marginLeft:4,opacity:0.7}}>({cnt})</span>}</button>
))}
</div>
{/* Suche */}
<div style={{position:'relative',marginBottom:12}}>
<span style={{position:'absolute',left:12,top:'50%',transform:'translateY(-50%)',color:'rgba(255,255,255,0.5)',pointerEvents:'none'}}></span>
<input value={search} onChange={e=>setSearch(e.target.value)} placeholder="Suchen…"
<input value={search} onChange={e=>setSearch(e.target.value)} placeholder="Suchen in Links, Ordnern, URLs…"
style={{...S.inp,paddingLeft:34}}/>
{search && <button onClick={()=>setSearch('')} style={{position:'absolute',right:10,top:'50%',transform:'translateY(-50%)',
{search&&<button onClick={()=>setSearch('')} style={{position:'absolute',right:10,top:'50%',transform:'translateY(-50%)',
background:'transparent',border:'none',color:'rgba(255,255,255,0.5)',cursor:'pointer',fontSize:16}}></button>}
</div>
{/* Liste */}
{filtered.length===0 ? (
<div style={{...S.card,textAlign:'center',padding:40}}>
<div style={{fontSize:28,marginBottom:10}}>🔗</div>
<div style={{color:'rgba(255,255,255,0.4)',fontFamily:'monospace',fontSize:12}}>
{search?'Keine Treffer.':tab==='own'?'Noch keine Links füge deinen ersten Link hinzu.':'Nichts geteilt.'}
</div>
{/* Inhalt */}
{tab==='own' && (
<div>
{/* Ordner */}
{filteredFolders.map(folder=><FolderSection key={folder.id} folder={folder}/>)}
{/* Links ohne Ordner */}
{unfoldered.length>0 && filteredFolders.length>0 && (
<div style={{...S.head,fontSize:9,margin:'12px 0 6px',color:'rgba(255,255,255,0.3)'}}>OHNE ORDNER</div>
)}
{unfoldered.map(link=><LinkCard key={link.id} link={link}/>)}
{filteredFolders.length===0 && unfoldered.length===0 && (
<div style={{...S.card,textAlign:'center',padding:40}}>
<div style={{fontSize:28,marginBottom:10}}>🔗</div>
<div style={{color:'rgba(255,255,255,0.4)',fontFamily:'monospace',fontSize:12}}>
{search?'Keine Treffer.':'Noch keine Links füge deinen ersten Link oder Ordner hinzu.'}
</div>
</div>
)}
</div>
) : filtered.map(link=>(
<LinkCard key={link.id} link={link} isShared={tab==='shared'} isByMe={tab==='byMe'}/>
))}
)}
{tab==='shared' && (
filteredShared.length===0
? <div style={{...S.card,textAlign:'center',padding:40}}><div style={{color:'rgba(255,255,255,0.4)',fontFamily:'monospace',fontSize:12}}>Nichts geteilt.</div></div>
: filteredShared.map(link=><LinkCard key={link.id} link={link} isShared/>)
)}
{tab==='byMe' && (
filteredByMe.length===0
? <div style={{...S.card,textAlign:'center',padding:40}}><div style={{color:'rgba(255,255,255,0.4)',fontFamily:'monospace',fontSize:12}}>Noch nichts geteilt.</div></div>
: filteredByMe.map(link=><LinkCard key={link.id} link={link} isByMe/>)
)}
</div>
);
}