import { useState, useEffect, useRef, useCallback } from 'react'; import { api, S } from '../lib.js'; // ── Spracherkennung ─────────────────────────────────────────────────────────── const LANG_PATTERNS = [ { lang:'javascript', label:'JavaScript', patterns:[/\b(const|let|var|function|=>|require|import|export|console\.log)\b/] }, { lang:'typescript', label:'TypeScript', patterns:[/\b(interface|type\s+\w+\s*=|:\s*(string|number|boolean|void)|)\b/] }, { lang:'python', label:'Python', patterns:[/\b(def |import |from .* import|elif |print\(|self\.|__init__)\b/] }, { lang:'jsx', label:'JSX/React', patterns:[/<[A-Z][a-zA-Z]+|useState|useEffect|return\s*\([\s\S]*<|className=/] }, { lang:'html', label:'HTML', patterns:[//] }, { lang:'csharp', label:'C#', patterns:[/\b(using System|namespace |class \w+|public void|Console\.Write)\b/] }, { lang:'cpp', label:'C/C++', patterns:[/#include|std::|cout <<|int main\(|nullptr/] }, { lang:'yaml', label:'YAML', patterns:[/^[\w-]+:\s*$/m, /^\s+-\s+\w/m] }, { lang:'dockerfile', label:'Dockerfile', patterns:[/^FROM |^RUN |^CMD |^EXPOSE |^COPY /m] }, ]; const LANG_COLORS = { javascript:'#f7df1e', typescript:'#3178c6', python:'#3572a5', jsx:'#61dafb', html:'#e34c26', css:'#563d7c', sql:'#e38c00', bash:'#4EAA25', json:'#4ecdc4', rust:'#dea584', go:'#00add8', java:'#b07219', php:'#4f5d95', csharp:'#178600', cpp:'#f34b7d', yaml:'#cb171e', dockerfile:'#2496ed', text:'rgba(255,255,255,0.3)', }; function detectLanguage(code) { if (!code?.trim()) return 'text'; for (const { lang, patterns } of LANG_PATTERNS) { if (patterns.some(p => p.test(code))) return lang; } return 'text'; } function getLangLabel(lang) { return LANG_PATTERNS.find(l=>l.lang===lang)?.label || lang.toUpperCase(); } function fmtDt(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'}); } // ── Share Modal ─────────────────────────────────────────────────────────────── function SnippetShareModal({ snippet, onClose, toast }) { const [username, setUsername] = useState(''); const [shares, setShares] = useState([]); const load = () => api(`/tools/snippets/${snippet.id}/shares`).then(setShares).catch(()=>{}); useEffect(()=>{ load(); },[]); const share = async () => { if (!username.trim()) return; try { await api(`/tools/snippets/${snippet.id}/share`,{body:{username:username.trim()}}); toast('Geteilt ✓'); setUsername(''); load(); } catch(e){ toast(e.message,'error'); } }; const unshare = async uid => { try { await api(`/tools/snippets/${snippet.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 (
e.target===e.currentTarget&&onClose()}>
{isMob&&
}
🤝 {snippet.title}

Empfänger können lesen, aber nicht bearbeiten.

setUsername(e.target.value)} onKeyDown={e=>e.key==='Enter'&&share()} placeholder="Benutzername" autoCapitalize="none" style={{...S.inp,flex:1}}/>
{shares.length>0 ? shares.map(s=>(
{s.username}
)) :
Noch nicht geteilt.
}
); } // ── 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(h=>{ setHistory(h); if(h.length) setSelected(h[0]); }).catch(()=>{}); },[]); const isMob = window.innerWidth < 768; const getOlderCode = (h) => { const idx = history.indexOf(h); // Newest history entry (idx=0) compares against CURRENT snippet code if (idx === 0) return snippet.code; // Older entries compare against the next newer history entry return history[idx - 1].code; }; return (
e.target===e.currentTarget&&onClose()}>
📜 Versionshistorie
{history.length===0 ? (
Keine älteren Versionen vorhanden.
) : (
{history.map((h,i)=>( ))}
{selected && ( <>
{getLangLabel(selected.language)} · {fmtDt(selected.saved_at)} {history.indexOf(selected) === history.length-1 && (älteste Version)} {!snippet.is_shared && selected.code !== snippet.code && ( )}
{showDiff ? :
                        {selected.code}
                      
} )}
)}
); } // ── Snippet Card ────────────────────────────────────────────────────────────── function SnippetCard({ snippet, onEdit, onDelete, onShare, onHistory, toast }) { const [expanded, setExpanded] = useState(false); const [copied, setCopied] = useState(false); const langColor = LANG_COLORS[snippet.language] || LANG_COLORS.text; const copy = async () => { try { await navigator.clipboard.writeText(snippet.code); setCopied(true); setTimeout(()=>setCopied(false),1500); } catch { toast('Kopieren fehlgeschlagen','error'); } }; return (
{/* Header */}
setExpanded(v=>!v)} style={{padding:'12px 14px',cursor:'pointer', display:'flex',alignItems:'center',gap:10}}>
{snippet.title} {snippet.is_shared && ( von {snippet.owner} )}
{snippet.description && (
{snippet.description}
)}
{getLangLabel(snippet.language)} {snippet.tags?.map(t=>( {t} ))}
{/* Expanded */} {expanded && (
{snippet.description && (
{snippet.description}
)} {/* Code */}
              {snippet.code}
            
{/* Meta + Aktionen */}
erstellt: {fmtDt(snippet.created_at)}
{snippet.updated_at !== snippet.created_at && (
bearbeitet: {fmtDt(snippet.updated_at)}
)}
{!snippet.is_shared && ( <> )}
)}
); } // ── Snippet Form ────────────────────────────────────────────────────────────── function SnippetForm({ initial, onSave, onCancel, toast, existingTags=[] }) { const ALL_LANGS = ['text',...LANG_PATTERNS.map(l=>l.lang)]; const [title, setTitle] = useState(initial?.title || ''); const [code, setCode] = useState(initial?.code || ''); const [lang, setLang] = useState(initial?.language || 'text'); const [desc, setDesc] = useState(initial?.description || ''); const [tags, setTags] = useState(initial?.tags?.filter(t=>t!==initial?.language).join(', ') || ''); const [busy, setBusy] = useState(false); const [autoLang,setAutoLang]= useState(!initial?.id); useEffect(()=>{ if (!autoLang) return; const detected = detectLanguage(code); setLang(detected); },[code, autoLang]); // Nur Komma als Trennzeichen const parseTags = () => { const langTag = lang !== 'text' ? [lang] : []; const manual = tags.split(',').map(t=>t.trim().toLowerCase()).filter(Boolean); return [...new Set([...langTag, ...manual])]; }; const addSuggestedTag = tag => { const cur = tags.split(',').map(t=>t.trim()).filter(Boolean); if (cur.includes(tag)) return; setTags(cur.length ? cur.join(', ') + ', ' + tag : tag); }; // Tags die noch nicht eingetragen sind als Vorschläge const currentParsed = parseTags(); const suggestions = existingTags.filter(t => t !== lang && !currentParsed.includes(t)); const save = async () => { if (!title.trim()) { toast('Titel erforderlich','error'); return; } setBusy(true); try { await onSave({ title:title.trim(), code, language:lang, description:desc.trim(), tags:parseTags() }); } catch(e) { toast(e.message,'error'); } setBusy(false); }; return (
{initial?.id ? 'BEARBEITEN' : 'NEUER SCHNIPSEL'}
setTitle(e.target.value)} placeholder="Titel *" style={{...S.inp,marginBottom:8}}/> setDesc(e.target.value)} placeholder="Beschreibung (optional)" style={{...S.inp,marginBottom:8,fontSize:12}}/> {/* Code */}