Changelog: ToDo-Auswahl zum Miterledigen; Admin-Stats: Code-Schnipsel
This commit is contained in:
@@ -1260,15 +1260,26 @@ function ChangelogModal({ user, toast, onClose, onRead }) {
|
|||||||
const [entries, setEntries] = useState([]);
|
const [entries, setEntries] = useState([]);
|
||||||
const [form, setForm] = useState({ version:'', title:'', body:'' });
|
const [form, setForm] = useState({ version:'', title:'', body:'' });
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
|
const [boardItems, setBoardItems] = useState([]);
|
||||||
|
const [toDelete, setToDelete] = useState(new Set()); // ids to delete on submit
|
||||||
const isMobile = window.innerWidth < 768;
|
const isMobile = window.innerWidth < 768;
|
||||||
|
|
||||||
useEffect(()=>{
|
useEffect(()=>{
|
||||||
api('/dashboard/changelog').then(setEntries).catch(()=>{});
|
api('/dashboard/changelog').then(setEntries).catch(()=>{});
|
||||||
api('/dashboard/changelog/read',{method:'POST'}).then(()=>{ if(onRead) onRead(); }).catch(()=>{});
|
api('/dashboard/changelog/read',{method:'POST'}).then(()=>{ if(onRead) onRead(); }).catch(()=>{});
|
||||||
|
if (user?.role==='admin') {
|
||||||
|
api('/dashboard/board').then(d=>setBoardItems(d.items||[])).catch(()=>{});
|
||||||
|
}
|
||||||
},[]);
|
},[]);
|
||||||
|
|
||||||
|
const toggleDelete = id => setToDelete(p => {
|
||||||
|
const n = new Set(p);
|
||||||
|
n.has(id) ? n.delete(id) : n.add(id);
|
||||||
|
return n;
|
||||||
|
});
|
||||||
|
|
||||||
const add = async () => {
|
const add = async () => {
|
||||||
if (!form.version.trim() || !form.title.trim()) { toast('Version und Titel erforderlich','error'); return; }
|
if (!form.version.trim() || !form.title.trim()) { toast('Stichwort und Titel erforderlich','error'); return; }
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
try {
|
try {
|
||||||
const r = await api('/dashboard/changelog',{body:{
|
const r = await api('/dashboard/changelog',{body:{
|
||||||
@@ -1276,6 +1287,12 @@ function ChangelogModal({ user, toast, onClose, onRead }) {
|
|||||||
build_time: typeof __BUILD_TIME__ !== 'undefined' ? __BUILD_TIME__ : null,
|
build_time: typeof __BUILD_TIME__ !== 'undefined' ? __BUILD_TIME__ : null,
|
||||||
}});
|
}});
|
||||||
setEntries(p=>[r,...p]);
|
setEntries(p=>[r,...p]);
|
||||||
|
// Markierte Board-Items löschen
|
||||||
|
if (toDelete.size > 0) {
|
||||||
|
await Promise.all([...toDelete].map(id => api(`/dashboard/board/${id}`,{method:'DELETE'}).catch(()=>{})));
|
||||||
|
setBoardItems(p => p.filter(i => !toDelete.has(i.id)));
|
||||||
|
setToDelete(new Set());
|
||||||
|
}
|
||||||
setForm({version:'',title:'',body:''});
|
setForm({version:'',title:'',body:''});
|
||||||
toast('Eintrag hinzugefügt ✓');
|
toast('Eintrag hinzugefügt ✓');
|
||||||
} catch(e){ toast(e.message,'error'); }
|
} catch(e){ toast(e.message,'error'); }
|
||||||
@@ -1289,13 +1306,17 @@ function ChangelogModal({ user, toast, onClose, onRead }) {
|
|||||||
|
|
||||||
const fmtDate = s => { if (!s) return ''; const d = new Date(s.replace(' ','T')); return isNaN(d)?'':d.toLocaleString('de-DE',{day:'2-digit',month:'2-digit',year:'numeric',hour:'2-digit',minute:'2-digit'}); };
|
const fmtDate = s => { if (!s) return ''; const d = new Date(s.replace(' ','T')); return isNaN(d)?'':d.toLocaleString('de-DE',{day:'2-digit',month:'2-digit',year:'numeric',hour:'2-digit',minute:'2-digit'}); };
|
||||||
|
|
||||||
|
const roadmap = boardItems.filter(i=>i.type==='roadmap');
|
||||||
|
const wishes = boardItems.filter(i=>i.type==='wish');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{position:'fixed',inset:0,background:'rgba(0,0,0,0.85)',zIndex:7000,
|
<div style={{position:'fixed',inset:0,background:'rgba(0,0,0,0.85)',zIndex:7000,
|
||||||
display:'flex',alignItems:isMobile?'flex-end':'center',justifyContent:'center',padding:isMobile?0:24}}
|
display:'flex',alignItems:isMobile?'flex-end':'center',justifyContent:'center',padding:isMobile?0:24}}
|
||||||
onClick={e=>e.target===e.currentTarget&&onClose()}>
|
onClick={e=>e.target===e.currentTarget&&onClose()}>
|
||||||
<div style={{background:'#1a1a1e',borderRadius:isMobile?'16px 16px 0 0':16,width:'100%',maxWidth:540,
|
<div style={{background:'#1a1a1e',borderRadius:isMobile?'16px 16px 0 0':16,
|
||||||
|
width:'100%',maxWidth: isAdmin ? 860 : 540,
|
||||||
border:'1px solid rgba(255,255,255,0.12)',display:'flex',flexDirection:'column',
|
border:'1px solid rgba(255,255,255,0.12)',display:'flex',flexDirection:'column',
|
||||||
maxHeight:isMobile?'88vh':'82vh',overflow:'hidden'}}>
|
maxHeight:isMobile?'92vh':'84vh',overflow:'hidden'}}>
|
||||||
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div style={{padding:'16px 20px',borderBottom:'1px solid rgba(255,255,255,0.08)',flexShrink:0,
|
<div style={{padding:'16px 20px',borderBottom:'1px solid rgba(255,255,255,0.08)',flexShrink:0,
|
||||||
@@ -1306,20 +1327,30 @@ function ChangelogModal({ user, toast, onClose, onRead }) {
|
|||||||
<button onClick={onClose} style={{background:'transparent',border:'none',color:'rgba(255,255,255,0.4)',cursor:'pointer',fontSize:18}}>✕</button>
|
<button onClick={onClose} style={{background:'transparent',border:'none',color:'rgba(255,255,255,0.4)',cursor:'pointer',fontSize:18}}>✕</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{overflowY:'auto',flex:1,padding:'0 20px'}}>
|
<div style={{display:'flex',flex:1,overflow:'hidden',flexDirection:isMobile?'column':'row'}}>
|
||||||
|
|
||||||
|
{/* Linke Spalte: Formular + Einträge */}
|
||||||
|
<div style={{flex:1,overflowY:'auto',padding:'0 20px',minWidth:0}}>
|
||||||
{/* Admin-Formular */}
|
{/* Admin-Formular */}
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
<div style={{padding:'14px 0',borderBottom:'1px solid rgba(255,255,255,0.08)'}}>
|
<div style={{padding:'14px 0',borderBottom:'1px solid rgba(255,255,255,0.08)'}}>
|
||||||
<div style={{...S.head,marginBottom:8}}>NEUER EINTRAG</div>
|
<div style={{...S.head,marginBottom:8}}>NEUER EINTRAG</div>
|
||||||
<div style={{display:'grid',gridTemplateColumns:'1fr 2fr',gap:8,marginBottom:8}}>
|
<div style={{display:'grid',gridTemplateColumns:'1fr 2fr',gap:8,marginBottom:8}}>
|
||||||
<input value={form.version} onChange={e=>setForm(p=>({...p,version:e.target.value}))}
|
<input value={form.version} onChange={e=>setForm(p=>({...p,version:e.target.value}))}
|
||||||
placeholder="Stichwort (z.B. Nachrichten)" style={{...S.inp,fontSize:12}}/>
|
placeholder="Stichwort" style={{...S.inp,fontSize:12}}/>
|
||||||
<input value={form.title} onChange={e=>setForm(p=>({...p,title:e.target.value}))}
|
<input value={form.title} onChange={e=>setForm(p=>({...p,title:e.target.value}))}
|
||||||
placeholder="Kurztitel" style={{...S.inp,fontSize:12}}/>
|
placeholder="Kurztitel" style={{...S.inp,fontSize:12}}/>
|
||||||
</div>
|
</div>
|
||||||
<textarea value={form.body} onChange={e=>setForm(p=>({...p,body:e.target.value}))}
|
<textarea value={form.body} onChange={e=>setForm(p=>({...p,body:e.target.value}))}
|
||||||
placeholder="Beschreibung der Änderungen (Markdown-artig, z.B. - Neue Funktion X - Fix: Bug Y)"
|
placeholder="Beschreibung…"
|
||||||
rows={3} style={{...S.inp,resize:'vertical',fontSize:12,marginBottom:8,width:'100%',boxSizing:'border-box'}}/>
|
rows={3} style={{...S.inp,resize:'vertical',fontSize:12,marginBottom:8,width:'100%',boxSizing:'border-box'}}/>
|
||||||
|
{toDelete.size>0 && (
|
||||||
|
<div style={{background:'rgba(255,107,157,0.07)',border:'1px solid rgba(255,107,157,0.2)',
|
||||||
|
borderRadius:6,padding:'6px 10px',marginBottom:8,color:'rgba(255,107,157,0.7)',
|
||||||
|
fontFamily:'monospace',fontSize:10}}>
|
||||||
|
🗑 {toDelete.size} ToDo{toDelete.size!==1?'s':''} wird beim Hinzufügen gelöscht
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<button onClick={add} disabled={busy}
|
<button onClick={add} disabled={busy}
|
||||||
style={{...S.btn('#4ecdc4'),width:'100%',textAlign:'center',padding:'9px 0',opacity:busy?0.5:1}}>
|
style={{...S.btn('#4ecdc4'),width:'100%',textAlign:'center',padding:'9px 0',opacity:busy?0.5:1}}>
|
||||||
{busy?'…':'+ Eintrag hinzufügen'}
|
{busy?'…':'+ Eintrag hinzufügen'}
|
||||||
@@ -1365,11 +1396,65 @@ function ChangelogModal({ user, toast, onClose, onRead }) {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Rechte Spalte: Board-Items zum Abhaken (nur Admin, nur wenn vorhanden) */}
|
||||||
|
{isAdmin && boardItems.length>0 && (
|
||||||
|
<div style={{width: isMobile?'100%':240,flexShrink:0,borderLeft:isMobile?'none':'1px solid rgba(255,255,255,0.07)',
|
||||||
|
borderTop:isMobile?'1px solid rgba(255,255,255,0.07)':'none',
|
||||||
|
overflowY:'auto',padding:'14px 16px',background:'rgba(0,0,0,0.15)'}}>
|
||||||
|
<div style={{...S.head,marginBottom:8}}>TODOS MITERLEDIGEN</div>
|
||||||
|
<p style={{color:'rgba(255,255,255,0.25)',fontFamily:'monospace',fontSize:9,marginBottom:12,lineHeight:1.6}}>
|
||||||
|
Markierte Einträge werden beim Hinzufügen des Changelogs gelöscht.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{roadmap.length>0 && (
|
||||||
|
<>
|
||||||
|
<div style={{color:'rgba(255,255,255,0.3)',fontFamily:'monospace',fontSize:9,letterSpacing:1,marginBottom:6}}>🗺 ROADMAP</div>
|
||||||
|
{roadmap.map(item=>(
|
||||||
|
<label key={item.id} style={{display:'flex',alignItems:'flex-start',gap:8,
|
||||||
|
padding:'6px 0',borderBottom:'1px solid rgba(255,255,255,0.04)',cursor:'pointer'}}>
|
||||||
|
<input type="checkbox" checked={toDelete.has(item.id)} onChange={()=>toggleDelete(item.id)}
|
||||||
|
style={{marginTop:2,accentColor:'#4ecdc4',flexShrink:0}}/>
|
||||||
|
<div>
|
||||||
|
<div style={{color: toDelete.has(item.id)?'rgba(255,255,255,0.35)':'rgba(255,255,255,0.7)',
|
||||||
|
fontFamily:'monospace',fontSize:11,
|
||||||
|
textDecoration: toDelete.has(item.id)?'line-through':'none'}}>
|
||||||
|
{item.title}
|
||||||
|
</div>
|
||||||
|
{item.description&&<div style={{color:'rgba(255,255,255,0.25)',fontFamily:'monospace',fontSize:9}}>{item.description}</div>}
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{wishes.length>0 && (
|
||||||
|
<div style={{marginTop: roadmap.length>0?12:0}}>
|
||||||
|
<div style={{color:'rgba(255,255,255,0.3)',fontFamily:'monospace',fontSize:9,letterSpacing:1,marginBottom:6}}>💡 WÜNSCHE</div>
|
||||||
|
{wishes.map(item=>(
|
||||||
|
<label key={item.id} style={{display:'flex',alignItems:'flex-start',gap:8,
|
||||||
|
padding:'6px 0',borderBottom:'1px solid rgba(255,255,255,0.04)',cursor:'pointer'}}>
|
||||||
|
<input type="checkbox" checked={toDelete.has(item.id)} onChange={()=>toggleDelete(item.id)}
|
||||||
|
style={{marginTop:2,accentColor:'#ffe66d',flexShrink:0}}/>
|
||||||
|
<div>
|
||||||
|
<div style={{color: toDelete.has(item.id)?'rgba(255,255,255,0.35)':'rgba(255,255,255,0.7)',
|
||||||
|
fontFamily:'monospace',fontSize:11,
|
||||||
|
textDecoration: toDelete.has(item.id)?'line-through':'none'}}>
|
||||||
|
{item.title}
|
||||||
|
</div>
|
||||||
|
{item.description&&<div style={{color:'rgba(255,255,255,0.25)',fontFamily:'monospace',fontSize:9}}>{item.description}</div>}
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Dashboard({ toast, mobile, setActive, unreadMsgs=0, unreadBoard=0, unreadPromoted=0, onBoardRead, unreadChangelog=0, onChangelogRead, user }) {
|
function Dashboard({ toast, mobile, setActive, unreadMsgs=0, unreadBoard=0, unreadPromoted=0, onBoardRead, unreadChangelog=0, onChangelogRead, user }) {
|
||||||
const [now, setNow] = useState(new Date());
|
const [now, setNow] = useState(new Date());
|
||||||
const [showBoard, setShowBoard] = useState(false);
|
const [showBoard, setShowBoard] = useState(false);
|
||||||
|
|||||||
Reference in New Issue
Block a user