Neue Linkliste unter Werkzeuge mit Teilen und Schnellzugriff-Integration
This commit is contained in:
@@ -279,6 +279,33 @@ for (const [k, v] of loginDefaults) {
|
||||
db.prepare('INSERT INTO admin_settings (key, value) VALUES (?, ?)').run(k, v);
|
||||
}
|
||||
|
||||
// ── Link-Liste ────────────────────────────────────────────────────────────────
|
||||
if (!db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='link_list'").get()) {
|
||||
db.exec(`
|
||||
CREATE TABLE link_list (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
title TEXT NOT NULL,
|
||||
url TEXT NOT NULL,
|
||||
icon TEXT NOT NULL DEFAULT '🔗',
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
created_at DATETIME DEFAULT NULL
|
||||
)
|
||||
`);
|
||||
}
|
||||
if (!db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='link_list_shares'").get()) {
|
||||
db.exec(`
|
||||
CREATE TABLE link_list_shares (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
link_id INTEGER NOT NULL REFERENCES link_list(id) ON DELETE CASCADE,
|
||||
shared_by INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
shared_with INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
created_at DATETIME DEFAULT NULL,
|
||||
UNIQUE(link_id, shared_with)
|
||||
)
|
||||
`);
|
||||
}
|
||||
|
||||
// ── Kalkulator-Shares ─────────────────────────────────────────────────────────
|
||||
if (!db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='calculation_shares'").get()) {
|
||||
db.exec(`
|
||||
|
||||
91
backend/src/tools/linkliste/routes.js
Normal file
91
backend/src/tools/linkliste/routes.js
Normal file
@@ -0,0 +1,91 @@
|
||||
const express = require('express');
|
||||
const db = require('../../db');
|
||||
const { authenticate } = require('../../middleware/auth');
|
||||
const router = express.Router();
|
||||
|
||||
// ── Eigene Links ──────────────────────────────────────────────────────────────
|
||||
router.get('/', authenticate, (req, res) => {
|
||||
const uid = req.user.id;
|
||||
const own = db.prepare(`
|
||||
SELECT l.*, 0 as is_shared, NULL as owner_name,
|
||||
(SELECT COUNT(*) FROM link_list_shares WHERE link_id=l.id) as share_count
|
||||
FROM link_list l WHERE l.user_id=? ORDER BY l.created_at DESC
|
||||
`).all(uid);
|
||||
|
||||
const shared = db.prepare(`
|
||||
SELECT l.*, 1 as is_shared, u.username as owner_name, 0 as share_count
|
||||
FROM link_list l
|
||||
JOIN link_list_shares s ON s.link_id=l.id
|
||||
JOIN users u ON u.id=l.user_id
|
||||
WHERE s.shared_with=?
|
||||
ORDER BY l.created_at DESC
|
||||
`).all(uid);
|
||||
|
||||
const sharedByMe = db.prepare(`
|
||||
SELECT l.*, 0 as is_shared, u.username as shared_with_name
|
||||
FROM link_list l
|
||||
JOIN link_list_shares s ON s.link_id=l.id
|
||||
JOIN users u ON u.id=s.shared_with
|
||||
WHERE l.user_id=?
|
||||
ORDER BY l.title ASC
|
||||
`).all(uid);
|
||||
|
||||
res.json({ own, shared, sharedByMe });
|
||||
});
|
||||
|
||||
router.post('/', authenticate, (req, res) => {
|
||||
const uid = req.user.id;
|
||||
const { title, url, icon = '🔗', description = '' } = req.body;
|
||||
if (!title?.trim() || !url?.trim()) return res.status(400).json({ error: 'Titel und URL erforderlich' });
|
||||
const r = db.prepare(`
|
||||
INSERT INTO link_list (user_id, title, url, icon, description, created_at)
|
||||
VALUES (?,?,?,?,?,datetime('now','localtime'))
|
||||
`).run(uid, title.trim(), url.trim(), icon, description.trim());
|
||||
res.json(db.prepare('SELECT * FROM link_list WHERE id=?').get(r.lastInsertRowid));
|
||||
});
|
||||
|
||||
router.put('/:id', authenticate, (req, res) => {
|
||||
const link = db.prepare('SELECT * FROM link_list WHERE id=? AND user_id=?').get(req.params.id, req.user.id);
|
||||
if (!link) return res.status(404).json({ error: 'Nicht gefunden' });
|
||||
const { title, url, icon, description } = req.body;
|
||||
db.prepare('UPDATE link_list SET title=?, url=?, icon=?, description=? WHERE id=?')
|
||||
.run(title??link.title, url??link.url, icon??link.icon, description??link.description, link.id);
|
||||
res.json(db.prepare('SELECT * FROM link_list WHERE id=?').get(link.id));
|
||||
});
|
||||
|
||||
router.delete('/:id', authenticate, (req, res) => {
|
||||
const r = db.prepare('DELETE FROM link_list WHERE id=? AND user_id=?').run(req.params.id, req.user.id);
|
||||
if (!r.changes) return res.status(404).json({ error: 'Nicht gefunden' });
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
// ── Shares ────────────────────────────────────────────────────────────────────
|
||||
router.get('/:id/shares', authenticate, (req, res) => {
|
||||
const link = db.prepare('SELECT * FROM link_list WHERE id=? AND user_id=?').get(req.params.id, req.user.id);
|
||||
if (!link) return res.status(404).json({ error: 'Nicht gefunden' });
|
||||
res.json(db.prepare(`
|
||||
SELECT u.id, u.username, s.created_at as shared_at
|
||||
FROM link_list_shares s JOIN users u ON u.id=s.shared_with
|
||||
WHERE s.link_id=?
|
||||
`).all(link.id));
|
||||
});
|
||||
|
||||
router.post('/:id/share', authenticate, (req, res) => {
|
||||
const link = db.prepare('SELECT * FROM link_list WHERE id=? AND user_id=?').get(req.params.id, req.user.id);
|
||||
if (!link) return res.status(404).json({ error: 'Nicht gefunden' });
|
||||
const target = db.prepare('SELECT * FROM users WHERE username=?').get(req.body.username);
|
||||
if (!target) return res.status(404).json({ error: 'Benutzer nicht gefunden' });
|
||||
if (target.id === req.user.id) return res.status(400).json({ error: 'Kann nicht mit dir selbst teilen' });
|
||||
db.prepare(`INSERT OR IGNORE INTO link_list_shares (link_id, shared_by, shared_with, created_at) VALUES (?,?,?,datetime('now','localtime'))`)
|
||||
.run(link.id, req.user.id, target.id);
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
router.delete('/:id/share/:userId', authenticate, (req, res) => {
|
||||
const link = db.prepare('SELECT * FROM link_list WHERE id=? AND user_id=?').get(req.params.id, req.user.id);
|
||||
if (!link) return res.status(404).json({ error: 'Nicht gefunden' });
|
||||
db.prepare('DELETE FROM link_list_shares WHERE link_id=? AND shared_with=?').run(link.id, req.params.userId);
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -131,14 +131,13 @@ export const DatabaseIcon = ({size=20,color='currentColor',sw}) => base(<>
|
||||
<path d="M3 13v4c0 1.66 4.03 3 9 3s9-1.34 9-3v-4"/>
|
||||
</>, size, color, sw);
|
||||
|
||||
export const LinkIcon = ({size=20,color='currentColor',sw}) => base(<>
|
||||
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/>
|
||||
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>
|
||||
</>, size, color, sw);
|
||||
|
||||
export const ZapIcon = ({size=20,color='currentColor',sw}) => base(<>
|
||||
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>
|
||||
</>, size, color, sw);
|
||||
export const MessageIcon = ({size=20,color='currentColor',sw}) => base(<>
|
||||
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" stroke={color} strokeWidth={sw||1.5} fill="none" strokeLinejoin="round" strokeLinecap="round"/>
|
||||
</>, size, color, sw);
|
||||
export const LinkIcon = ({size=20,color='currentColor',sw}) => base(<>
|
||||
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" stroke={color} strokeWidth={sw||1.5} fill="none" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" stroke={color} strokeWidth={sw||1.5} fill="none" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</>, size, color, sw);
|
||||
|
||||
@@ -2,7 +2,8 @@ import Kalkulator3D, { SavedList } from './tools/kalkulator3d.jsx';
|
||||
import Bestellungen from './tools/bestellungen.jsx';
|
||||
import Dateien from './tools/dateien.jsx';
|
||||
import Nachrichten from './tools/nachrichten.jsx';
|
||||
import { CalculatorIcon, ArchiveIcon, DatabaseIcon, AppsIcon, MessageIcon } from './icons.jsx';
|
||||
import Linkliste from './tools/linkliste.jsx';
|
||||
import { CalculatorIcon, ArchiveIcon, DatabaseIcon, AppsIcon, MessageIcon, LinkIcon } from './icons.jsx';
|
||||
|
||||
export const TOOLS = [
|
||||
{
|
||||
@@ -45,6 +46,14 @@ export const TOOLS = [
|
||||
group: 'Werkzeuge',
|
||||
component: Nachrichten,
|
||||
},
|
||||
{
|
||||
id: 'linkliste',
|
||||
Icon: LinkIcon,
|
||||
label: 'Linkliste',
|
||||
navLabel: 'Linkliste',
|
||||
group: 'Werkzeuge',
|
||||
component: Linkliste,
|
||||
},
|
||||
// Neues Tool: { id:'...', Icon:..., label:'...', navLabel:'...', group:'...', component:... },
|
||||
];
|
||||
|
||||
|
||||
262
frontend/src/tools/linkliste.jsx
Normal file
262
frontend/src/tools/linkliste.jsx
Normal file
@@ -0,0 +1,262 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { api, S } from '../lib.js';
|
||||
|
||||
const ICONS = ['🔗','📄','🌐','📊','🎯','⚙️','📁','🔧','📱','💡','🏠','🛒','📺','🎮','🎵','📧','💬','📰','🔬','🏥','🏦','✈️','🚀'];
|
||||
|
||||
function extIcon(url) {
|
||||
try {
|
||||
return `https://www.google.com/s2/favicons?sz=32&domain=${new URL(url).hostname}`;
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
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 () => {
|
||||
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'); }
|
||||
};
|
||||
const unshare = async uid => {
|
||||
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;
|
||||
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}}
|
||||
onClick={e=>e.target===e.currentTarget&&onClose()}>
|
||||
<div style={{background:'#1a1a1e',borderRadius:isMob?'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'}}/>}
|
||||
<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>
|
||||
<div style={{display:'flex',gap:8,marginBottom:14}}>
|
||||
<input value={username} onChange={e=>setUsername(e.target.value)} onKeyDown={e=>e.key==='Enter'&&share()}
|
||||
placeholder="Benutzername" autoCapitalize="none" style={{...S.inp,flex:1}}/>
|
||||
<button onClick={share} style={S.btn('#4ecdc4')}>Teilen</button>
|
||||
</div>
|
||||
{shares.length>0 ? shares.map(s=>(
|
||||
<div key={s.id} style={{display:'flex',justifyContent:'space-between',alignItems:'center',
|
||||
padding:'8px 0',borderBottom:'1px solid rgba(255,255,255,0.05)'}}>
|
||||
<span style={{color:'rgba(255,255,255,0.7)',fontFamily:'monospace',fontSize:13}}>{s.username}</span>
|
||||
<button onClick={()=>unshare(s.id)} style={S.btn('#ff6b9d',true)}>✕</button>
|
||||
</div>
|
||||
)) : <div style={{color:'rgba(255,255,255,0.3)',fontFamily:'monospace',fontSize:11}}>Noch mit niemandem geteilt.</div>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function LinkForm({ initial, onSave, onCancel, toast }) {
|
||||
const [form, setForm] = useState({ title:'', url:'', icon:'🔗', description:'', ...initial });
|
||||
const [busy, setBusy] = useState(false);
|
||||
const set = (k,v) => setForm(p=>({...p,[k]:v}));
|
||||
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;
|
||||
setBusy(true);
|
||||
try { await onSave({...form, url}); }
|
||||
catch(e) { toast(e.message,'error'); }
|
||||
setBusy(false);
|
||||
};
|
||||
return (
|
||||
<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={{display:'flex',flexWrap:'wrap',gap:4,marginBottom:6}}>
|
||||
{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>
|
||||
))}
|
||||
</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}}/>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Linkliste({ toast, mobile }) {
|
||||
const [own, setOwn] = useState([]);
|
||||
const [shared, setShared] = useState([]);
|
||||
const [byMe, setByMe] = useState([]);
|
||||
const [tab, setTab] = useState('own');
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const [editItem, setEditItem] = useState(null);
|
||||
const [shareItem, setShareItem] = useState(null);
|
||||
const [search, setSearch] = useState('');
|
||||
|
||||
const load = () => api('/tools/linkliste')
|
||||
.then(d=>{ setOwn(d.own||[]); setShared(d.shared||[]); setByMe(d.sharedByMe||[]); })
|
||||
.catch(()=>{});
|
||||
|
||||
useEffect(()=>{ load(); },[]);
|
||||
|
||||
const addLink = async form => {
|
||||
const r = await api('/tools/linkliste',{body:form});
|
||||
setOwn(p=>[r,...p]); 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 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;
|
||||
|
||||
const LinkCard = ({ link, isShared=false, isByMe=false }) => {
|
||||
const favicon = extIcon(link.url);
|
||||
return (
|
||||
<div style={{...S.card,marginBottom:8,padding:'12px 14px'}}>
|
||||
<div style={{display:'flex',alignItems:'center',gap:10}}>
|
||||
{/* Icon */}
|
||||
<div style={{width:36,height:36,borderRadius:9,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>
|
||||
</div>
|
||||
{/* Info */}
|
||||
<div style={{flex:1,minWidth:0}}>
|
||||
<a href={link.url} target="_blank" rel="noopener noreferrer"
|
||||
style={{color:'#fff',fontFamily:'monospace',fontSize:13,fontWeight:700,
|
||||
textDecoration:'none',display:'block',overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>
|
||||
{link.title}
|
||||
</a>
|
||||
<div style={{color:'rgba(78,205,196,0.55)',fontFamily:'monospace',fontSize:10,
|
||||
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>}
|
||||
</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>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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}}>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
{/* Formular */}
|
||||
{showForm && <LinkForm onSave={addLink} onCancel={()=>setShowForm(false)} toast={toast}/>}
|
||||
{editItem && <LinkForm initial={editItem} 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={{
|
||||
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>
|
||||
))}
|
||||
</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…"
|
||||
style={{...S.inp,paddingLeft:34}}/>
|
||||
{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>
|
||||
</div>
|
||||
) : filtered.map(link=>(
|
||||
<LinkCard key={link.id} link={link} isShared={tab==='shared'} isByMe={tab==='byMe'}/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user