From 82fcfd07c54ea95e8c7901da5f196d56f436b2d6 Mon Sep 17 00:00:00 2001 From: Dicken Date: Fri, 29 May 2026 13:45:33 +0200 Subject: [PATCH] Code-Schnipsel: Diff-History, Beschreibung sichtbar, Multi-Tag-Filter, Geteilt-von-mir Tab, nur genutzte Tags --- backend/src/tools/snippets/routes.js | 11 +- frontend/src/tools/codeschnipsel.jsx | 186 ++++++++++++++++++--------- 2 files changed, 137 insertions(+), 60 deletions(-) diff --git a/backend/src/tools/snippets/routes.js b/backend/src/tools/snippets/routes.js index f36b9ac..7e5b5f5 100644 --- a/backend/src/tools/snippets/routes.js +++ b/backend/src/tools/snippets/routes.js @@ -27,7 +27,16 @@ router.get('/', authenticate, (req, res) => { ORDER BY s.updated_at DESC `).all(me).map(enrich); - res.json({ own, shared }); + const sharedByMe = db.prepare(` + SELECT s.*, u.username as shared_with_name + FROM snippets s + JOIN snippet_shares sh ON sh.snippet_id=s.id + JOIN users u ON u.id=sh.shared_with + WHERE s.user_id=? + ORDER BY s.title ASC + `).all(me).map(enrich); + + res.json({ own, shared, sharedByMe }); }); // ── Einzelnes Snippet ──────────────────────────────────────────────────────── diff --git a/frontend/src/tools/codeschnipsel.jsx b/frontend/src/tools/codeschnipsel.jsx index 75b16da..c17a01c 100644 --- a/frontend/src/tools/codeschnipsel.jsx +++ b/frontend/src/tools/codeschnipsel.jsx @@ -96,76 +96,123 @@ function SnippetShareModal({ snippet, onClose, toast }) { ); } -// ── History Modal ───────────────────────────────────────────────────────────── +// ── Simple Diff ─────────────────────────────────────────────────────────────── +function DiffView({ oldCode, newCode }) { + if (!oldCode) return ( +
+      {newCode}
+    
+ ); + const oldLines = oldCode.split('\n'); + const newLines = newCode.split('\n'); + const maxLen = Math.max(oldLines.length, newLines.length); + const rows = []; + for (let i = 0; i < maxLen; i++) { + const o = oldLines[i]; + const n = newLines[i]; + if (o === n) { + rows.push({ type:'same', text: n ?? '' }); + } else { + if (o !== undefined) rows.push({ type:'del', text: o }); + if (n !== undefined) rows.push({ type:'add', text: n }); + } + } + return ( +
+ {rows.map((r,i) => ( +
+ + {r.type==='add'?'+':r.type==='del'?'−':' '} + + {r.text} +
+ ))} +
+ ); +} function HistoryModal({ snippet, onClose, onRestore, toast }) { const [history, setHistory] = useState([]); const [selected, setSelected] = useState(null); + const [showDiff, setShowDiff] = useState(true); useEffect(()=>{ - api(`/tools/snippets/${snippet.id}/history`).then(setHistory).catch(()=>{}); + api(`/tools/snippets/${snippet.id}/history`).then(h=>{ setHistory(h); if(h.length) setSelected(h[0]); }).catch(()=>{}); },[]); const isMob = window.innerWidth < 768; + const getOlderCode = (h) => { + const idx = history.indexOf(h); + return idx < history.length-1 ? history[idx+1].code : null; + }; return (
e.target===e.currentTarget&&onClose()}>
-
-
- 📜 Versionshistorie + width:'100%',maxWidth:680,border:'1px solid rgba(255,255,255,0.12)', + display:'flex',flexDirection:'column',maxHeight:isMob?'88vh':'82vh',overflow:'hidden'}}> +
+
📜 Versionshistorie
+
+ +
-
{history.length===0 ? ( -
- Keine älteren Versionen vorhanden. -
+
Keine älteren Versionen vorhanden.
) : (
- {/* Liste */} -
+
{history.map((h,i)=>( ))}
- {/* Preview */}
- {selected ? ( + {selected && ( <> -
- +
+ {getLangLabel(selected.language)} · {fmtDt(selected.saved_at)} + {showDiff && getOlderCode(selected)===null && (älteste Version)} {!snippet.is_shared && ( - )}
-
-                    {selected.code}
-                  
+ {showDiff + ? + :
+                        {selected.code}
+                      
+ } - ) : ( -
- Version auswählen -
)}
@@ -203,6 +250,12 @@ function SnippetCard({ snippet, onEdit, onDelete, onShare, onHistory, toast }) { borderRadius:4,padding:'1px 5px',flexShrink:0}}>von {snippet.owner} )}
+ {snippet.description && ( +
+ {snippet.description} +
+ )}
{getLangLabel(snippet.language)} @@ -404,25 +457,30 @@ function SnippetForm({ initial, onSave, onCancel, toast, existingTags=[] }) { export default function CodeSchnipsel({ toast, mobile }) { const [own, setOwn] = useState([]); const [shared, setShared] = useState([]); + const [sharedByMe,setSharedByMe]= useState([]); const [tab, setTab] = useState('own'); const [search, setSearch] = useState(''); - const [tagFilter, setTagFilter] = useState(''); + const [tagFilters,setTagFilters]= useState([]); // multi-select array const [showForm, setShowForm] = useState(false); const [editItem, setEditItem] = useState(null); const [shareItem, setShareItem] = useState(null); const [histItem, setHistItem] = useState(null); const load = useCallback(()=>{ - api('/tools/snippets').then(d=>{ setOwn(d.own||[]); setShared(d.shared||[]); }).catch(()=>{}); + api('/tools/snippets').then(d=>{ setOwn(d.own||[]); setShared(d.shared||[]); setSharedByMe(d.sharedByMe||[]); }).catch(()=>{}); },[]); useEffect(()=>{ load(); },[load]); - const allTags = [...new Set([...own,...shared].flatMap(s=>s.tags||[]))].sort(); + // Only tags that are currently in use + const allItems = [...own, ...shared]; + const allTags = [...new Set(allItems.flatMap(s=>s.tags||[]))].sort(); + + const toggleTag = t => setTagFilters(p => p.includes(t) ? p.filter(x=>x!==t) : [...p, t]); const filterItems = items => { let res = items; - if (tagFilter) res = res.filter(s=>s.tags?.includes(tagFilter)); + if (tagFilters.length) res = res.filter(s => tagFilters.every(t=>s.tags?.includes(t))); if (search.trim()) { const q = search.toLowerCase(); res = res.filter(s => @@ -436,7 +494,8 @@ export default function CodeSchnipsel({ toast, mobile }) { return res; }; - const items = filterItems(tab==='own' ? own : shared); + const curList = tab==='own' ? own : tab==='shared' ? shared : sharedByMe; + const items = filterItems(curList); const handleSave = async form => { if (editItem?.id) { @@ -496,8 +555,8 @@ export default function CodeSchnipsel({ toast, mobile }) { {/* Tabs */}
- {[['own','Meine',own.length],['shared','Geteilt mit mir',shared.length]].map(([k,l,cnt])=>( - }
- {/* Tag-Filter */} + {/* Tag-Filter (Mehrfachauswahl) */} {allTags.length>0 && ( -
- - {allTags.map(t=>( - - ))} + {allTags.map(t=>{ + const active = tagFilters.includes(t); + const c = LANG_COLORS[t] || '#4ecdc4'; + return ( + + ); + })} + {tagFilters.length>0 && ( + + ({tagFilters.length} aktiv) + + )}
)} @@ -545,7 +613,7 @@ export default function CodeSchnipsel({ toast, mobile }) {
{''}
- {search||tagFilter ? 'Keine Treffer.' : tab==='own' ? 'Noch keine Schnipsel – füge deinen ersten hinzu.' : 'Noch nichts geteilt.'} + {search||tagFilters.length ? 'Keine Treffer.' : tab==='own' ? 'Noch keine Schnipsel – füge deinen ersten hinzu.' : 'Noch nichts geteilt.'}
) : items.map(s=>(