Kanban Board: neues Tool in Werkzeuge (Spalten konfigurierbar, Drag&Drop + Pfeile, Prioritäten)
This commit is contained in:
@@ -176,3 +176,8 @@ export const ChartIcon = ({size=20,color='currentColor',sw}) => base(<>
|
||||
<rect x="9" y="7" width="4" height="13" rx="1" fill={color}/>
|
||||
<rect x="15" y="3" width="4" height="17" rx="1" fill={color}/>
|
||||
</>, size, color, sw);
|
||||
export const KanbanIcon = ({size=20,color='currentColor',sw}) => base(<>
|
||||
<rect x="3" y="3" width="5" height="13" rx="1.5" stroke={color} strokeWidth={sw||1.5} fill="none"/>
|
||||
<rect x="10" y="3" width="5" height="8" rx="1.5" stroke={color} strokeWidth={sw||1.5} fill="none"/>
|
||||
<rect x="17" y="3" width="4" height="5" rx="1.5" stroke={color} strokeWidth={sw||1.5} fill="none"/>
|
||||
</>, size, color, sw);
|
||||
|
||||
@@ -9,7 +9,8 @@ import Skizze from './tools/skizze.jsx';
|
||||
import DevTools from './tools/devtools.jsx';
|
||||
import Media from './tools/media.jsx';
|
||||
import PaywallKiller from './tools/paywallkiller.jsx';
|
||||
import { CalculatorIcon, ArchiveIcon, DatabaseIcon, AppsIcon, MessageIcon, LinkIcon, CodeIcon, SkizzeIcon, WrenchIcon, FilmIcon, PaywallIcon, ChartIcon } from './icons.jsx';
|
||||
import Kanban from './tools/kanban.jsx';
|
||||
import { CalculatorIcon, ArchiveIcon, DatabaseIcon, AppsIcon, MessageIcon, LinkIcon, CodeIcon, SkizzeIcon, WrenchIcon, FilmIcon, PaywallIcon, ChartIcon, KanbanIcon } from './icons.jsx';
|
||||
|
||||
export const TOOLS = [
|
||||
{
|
||||
@@ -84,6 +85,14 @@ export const TOOLS = [
|
||||
group: 'Werkzeuge',
|
||||
component: Skizze,
|
||||
},
|
||||
{
|
||||
id: 'kanban',
|
||||
Icon: KanbanIcon,
|
||||
label: 'Kanban',
|
||||
navLabel: 'Kanban',
|
||||
group: 'Werkzeuge',
|
||||
component: Kanban,
|
||||
},
|
||||
{
|
||||
id: 'devtools',
|
||||
Icon: WrenchIcon,
|
||||
|
||||
451
frontend/src/tools/kanban.jsx
Normal file
451
frontend/src/tools/kanban.jsx
Normal file
@@ -0,0 +1,451 @@
|
||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { api, S } from '../lib.js';
|
||||
|
||||
// ── Prioritäten ───────────────────────────────────────────────────────────────
|
||||
const PRIORITIES = [
|
||||
{ id: 'none', label: 'Keine', color: '#888888' },
|
||||
{ id: 'low', label: 'Niedrig', color: '#4ecdc4' },
|
||||
{ id: 'medium', label: 'Mittel', color: '#ffe66d' },
|
||||
{ id: 'high', label: 'Hoch', color: '#ff6b9d' },
|
||||
];
|
||||
|
||||
const prioOf = id => PRIORITIES.find(p => p.id === id) || PRIORITIES[0];
|
||||
|
||||
// ── Karten-Modal (Erstellen + Bearbeiten) ─────────────────────────────────────
|
||||
// Absichtlich AUSSERHALB der Hauptkomponente definiert (Lesson Learned: kein Remount)
|
||||
function CardModal({ card, columnId, onSave, onClose }) {
|
||||
const [title, setTitle] = useState(card?.title || '');
|
||||
const [desc, setDesc] = useState(card?.description || '');
|
||||
const [prio, setPrio] = useState(card?.priority || 'none');
|
||||
const [saving, setSaving] = useState(false);
|
||||
const titleRef = useRef(null);
|
||||
|
||||
useEffect(() => { titleRef.current?.focus(); }, []);
|
||||
|
||||
const save = async () => {
|
||||
if (!title.trim()) return;
|
||||
setSaving(true);
|
||||
try {
|
||||
if (card) {
|
||||
await api(`/tools/kanban/cards/${card.id}`, { method: 'PATCH', body: { title, description: desc, priority: prio } });
|
||||
} else {
|
||||
await api('/tools/kanban/cards', { body: { column_id: columnId, title, description: desc, priority: prio } });
|
||||
}
|
||||
onSave();
|
||||
} catch (e) { alert(e.message); }
|
||||
finally { setSaving(false); }
|
||||
};
|
||||
|
||||
const onKey = e => { if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) save(); if (e.key === 'Escape') onClose(); };
|
||||
|
||||
return (
|
||||
<div onClick={onClose} style={{ position:'fixed',inset:0,background:'rgba(0,0,0,0.6)',zIndex:1000,display:'flex',alignItems:'center',justifyContent:'center',padding:16 }}>
|
||||
<div onClick={e=>e.stopPropagation()} style={{ background:'#1a1a1e',border:'1px solid rgba(255,255,255,0.1)',borderRadius:14,padding:24,width:'100%',maxWidth:440 }}>
|
||||
<div style={{ color:'rgba(255,255,255,0.6)',fontSize:10,fontFamily:'monospace',letterSpacing:2,marginBottom:14 }}>
|
||||
{card ? 'KARTE BEARBEITEN' : 'NEUE KARTE'}
|
||||
</div>
|
||||
|
||||
<input
|
||||
ref={titleRef}
|
||||
value={title}
|
||||
onChange={e=>setTitle(e.target.value)}
|
||||
onKeyDown={onKey}
|
||||
placeholder="Titel"
|
||||
style={{ ...S.inp, marginBottom: 10 }}
|
||||
/>
|
||||
|
||||
<textarea
|
||||
value={desc}
|
||||
onChange={e=>setDesc(e.target.value)}
|
||||
onKeyDown={onKey}
|
||||
placeholder="Beschreibung (optional)"
|
||||
rows={4}
|
||||
style={{ ...S.inp, resize:'vertical', marginBottom:12 }}
|
||||
/>
|
||||
|
||||
{/* Priorität */}
|
||||
<div style={{ marginBottom:16 }}>
|
||||
<div style={{ ...S.head, marginBottom:8 }}>PRIORITÄT</div>
|
||||
<div style={{ display:'flex', gap:6, flexWrap:'wrap' }}>
|
||||
{PRIORITIES.map(p => (
|
||||
<button key={p.id} onClick={()=>setPrio(p.id)}
|
||||
style={{ ...S.btn(p.color, true),
|
||||
background: prio===p.id ? `${p.color}30` : `${p.color}10`,
|
||||
borderColor: prio===p.id ? p.color : `${p.color}40`,
|
||||
fontWeight: prio===p.id ? 700 : 400 }}>
|
||||
{p.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display:'flex', gap:8, justifyContent:'flex-end' }}>
|
||||
<button onClick={onClose} style={S.btn('#888888', true)}>Abbrechen</button>
|
||||
<button onClick={save} disabled={!title.trim()||saving} style={S.btn('#4ecdc4', true)}>
|
||||
{saving ? '…' : card ? 'Speichern' : 'Erstellen'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Spalten-Titel Inline-Edit ──────────────────────────────────────────────────
|
||||
// Ebenfalls außerhalb definiert
|
||||
function ColumnTitle({ col, onRename, onDelete }) {
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [val, setVal] = useState(col.title);
|
||||
const inputRef = useRef(null);
|
||||
|
||||
useEffect(() => { if (editing) inputRef.current?.focus(); }, [editing]);
|
||||
|
||||
const commit = async () => {
|
||||
if (!val.trim()) { setVal(col.title); setEditing(false); return; }
|
||||
if (val.trim() !== col.title) await onRename(col.id, val.trim());
|
||||
setEditing(false);
|
||||
};
|
||||
|
||||
if (editing) return (
|
||||
<input
|
||||
ref={inputRef}
|
||||
value={val}
|
||||
onChange={e=>setVal(e.target.value)}
|
||||
onBlur={commit}
|
||||
onKeyDown={e=>{ if(e.key==='Enter') commit(); if(e.key==='Escape'){setVal(col.title);setEditing(false);} }}
|
||||
style={{ ...S.inp, fontSize:13, fontWeight:700, padding:'4px 8px', flex:1 }}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div style={{ display:'flex', alignItems:'center', gap:6, flex:1, minWidth:0 }}>
|
||||
<span
|
||||
onClick={()=>setEditing(true)}
|
||||
title="Doppelklick zum Umbenennen"
|
||||
onDoubleClick={()=>setEditing(true)}
|
||||
style={{ fontWeight:700, fontSize:13, fontFamily:'monospace', color:'#fff',
|
||||
cursor:'pointer', flex:1, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>
|
||||
{col.title}
|
||||
</span>
|
||||
<span style={{ color:'rgba(255,255,255,0.3)', fontSize:11, fontFamily:'monospace', flexShrink:0 }}>
|
||||
{col.cards.length}
|
||||
</span>
|
||||
<button onClick={()=>onDelete(col.id)} title="Spalte löschen"
|
||||
style={{ background:'transparent', border:'none', color:'rgba(255,255,255,0.25)',
|
||||
cursor:'pointer', fontSize:14, padding:'0 2px', flexShrink:0, lineHeight:1 }}>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Einzelne Karte ─────────────────────────────────────────────────────────────
|
||||
function KanbanCard({ card, col, allCols, onEdit, onDelete, onMove, onMoveCol, isDragging, dragHandlers }) {
|
||||
const prio = prioOf(card.priority);
|
||||
const colIdx = allCols.findIndex(c => c.id === col.id);
|
||||
const cardIdx = col.cards.findIndex(c => c.id === card.id);
|
||||
|
||||
return (
|
||||
<div
|
||||
{...dragHandlers}
|
||||
style={{
|
||||
background: isDragging ? 'rgba(78,205,196,0.08)' : 'rgba(255,255,255,0.04)',
|
||||
border: `1px solid ${isDragging ? '#4ecdc4' : 'rgba(255,255,255,0.08)'}`,
|
||||
borderLeft: `3px solid ${prio.color}`,
|
||||
borderRadius: 8, padding: '10px 10px 8px', marginBottom: 6,
|
||||
cursor: 'grab', opacity: isDragging ? 0.5 : 1,
|
||||
transition: 'border-color 0.15s, opacity 0.15s',
|
||||
userSelect: 'none',
|
||||
}}
|
||||
>
|
||||
{/* Titel-Zeile */}
|
||||
<div style={{ display:'flex', alignItems:'flex-start', gap:6, marginBottom: card.description ? 6 : 0 }}>
|
||||
<span style={{ flex:1, fontSize:13, fontFamily:'monospace', color:'#fff', wordBreak:'break-word', lineHeight:1.4 }}>
|
||||
{card.title}
|
||||
</span>
|
||||
<div style={{ display:'flex', gap:4, flexShrink:0 }}>
|
||||
<button onClick={()=>onEdit(card)} title="Bearbeiten"
|
||||
style={{ background:'transparent',border:'none',color:'rgba(255,255,255,0.3)',
|
||||
cursor:'pointer',fontSize:12,padding:'1px 3px',lineHeight:1 }}>✎</button>
|
||||
<button onClick={()=>onDelete(card.id)} title="Löschen"
|
||||
style={{ background:'transparent',border:'none',color:'rgba(255,255,255,0.2)',
|
||||
cursor:'pointer',fontSize:12,padding:'1px 3px',lineHeight:1 }}>✕</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Beschreibung */}
|
||||
{card.description && (
|
||||
<div style={{ fontSize:11, color:'rgba(255,255,255,0.4)', fontFamily:'monospace',
|
||||
lineHeight:1.4, whiteSpace:'pre-wrap', wordBreak:'break-word', marginBottom:8 }}>
|
||||
{card.description}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Steuerzeile: Priorität-Badge + Pfeile */}
|
||||
<div style={{ display:'flex', alignItems:'center', justifyContent:'space-between', marginTop:4 }}>
|
||||
<span style={{ fontSize:9, fontFamily:'monospace', letterSpacing:1,
|
||||
color: prio.color, background:`${prio.color}15`,
|
||||
border:`1px solid ${prio.color}30`, borderRadius:4, padding:'1px 5px' }}>
|
||||
{prio.label.toUpperCase()}
|
||||
</span>
|
||||
|
||||
{/* Positions- & Spalten-Navigation */}
|
||||
<div style={{ display:'flex', gap:3 }}>
|
||||
{/* Position hoch (innerhalb Spalte) */}
|
||||
<button onClick={()=>onMove(card.id, col.id, cardIdx-1)} disabled={cardIdx===0}
|
||||
title="Nach oben" style={{ background:'transparent',border:'none',
|
||||
color: cardIdx===0 ? 'rgba(255,255,255,0.1)' : 'rgba(255,255,255,0.35)',
|
||||
cursor: cardIdx===0 ? 'default':'pointer', fontSize:13, padding:'1px 3px', lineHeight:1 }}>
|
||||
↑
|
||||
</button>
|
||||
{/* Position runter */}
|
||||
<button onClick={()=>onMove(card.id, col.id, cardIdx+1)} disabled={cardIdx===col.cards.length-1}
|
||||
title="Nach unten" style={{ background:'transparent',border:'none',
|
||||
color: cardIdx===col.cards.length-1 ? 'rgba(255,255,255,0.1)' : 'rgba(255,255,255,0.35)',
|
||||
cursor: cardIdx===col.cards.length-1 ? 'default':'pointer', fontSize:13, padding:'1px 3px', lineHeight:1 }}>
|
||||
↓
|
||||
</button>
|
||||
{/* Spalte links */}
|
||||
<button onClick={()=>onMoveCol(card.id, allCols[colIdx-1]?.id)} disabled={colIdx===0}
|
||||
title="Spalte zurück" style={{ background:'transparent',border:'none',
|
||||
color: colIdx===0 ? 'rgba(255,255,255,0.1)' : 'rgba(255,255,255,0.35)',
|
||||
cursor: colIdx===0 ? 'default':'pointer', fontSize:13, padding:'1px 3px', lineHeight:1 }}>
|
||||
←
|
||||
</button>
|
||||
{/* Spalte rechts */}
|
||||
<button onClick={()=>onMoveCol(card.id, allCols[colIdx+1]?.id)} disabled={colIdx===allCols.length-1}
|
||||
title="Nächste Spalte" style={{ background:'transparent',border:'none',
|
||||
color: colIdx===allCols.length-1 ? 'rgba(255,255,255,0.1)' : 'rgba(255,255,255,0.35)',
|
||||
cursor: colIdx===allCols.length-1 ? 'default':'pointer', fontSize:13, padding:'1px 3px', lineHeight:1 }}>
|
||||
→
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Hauptkomponente ───────────────────────────────────────────────────────────
|
||||
export default function Kanban() {
|
||||
const [columns, setColumns] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [modal, setModal] = useState(null); // null | { card?, columnId }
|
||||
const [newColInput, setNewColInput] = useState('');
|
||||
const [addingCol, setAddingCol] = useState(false);
|
||||
const [dragCardId, setDragCardId] = useState(null);
|
||||
const [dragOverCol, setDragOverCol] = useState(null);
|
||||
const [dragOverPos, setDragOverPos] = useState(null);
|
||||
const newColRef = useRef(null);
|
||||
|
||||
// Hooks immer vor Early Returns (Lesson Learned)
|
||||
const load = useCallback(() => {
|
||||
setLoading(true);
|
||||
api('/tools/kanban/board')
|
||||
.then(d => setColumns(d.columns || []))
|
||||
.catch(e => console.error('Kanban load error', e))
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
useEffect(() => { load(); }, [load]);
|
||||
useEffect(() => { if (addingCol) newColRef.current?.focus(); }, [addingCol]);
|
||||
|
||||
// ── Spalten-Aktionen ────────────────────────────────────────────────────────
|
||||
const addColumn = async () => {
|
||||
if (!newColInput.trim()) return;
|
||||
try {
|
||||
await api('/tools/kanban/columns', { body: { title: newColInput.trim() } });
|
||||
setNewColInput(''); setAddingCol(false); load();
|
||||
} catch (e) { alert(e.message); }
|
||||
};
|
||||
|
||||
const renameColumn = async (id, title) => {
|
||||
try { await api(`/tools/kanban/columns/${id}`, { method:'PATCH', body:{ title } }); load(); }
|
||||
catch (e) { alert(e.message); }
|
||||
};
|
||||
|
||||
const deleteColumn = async (id) => {
|
||||
const col = columns.find(c => c.id === id);
|
||||
const msg = col?.cards?.length
|
||||
? `Spalte "${col.title}" und alle ${col.cards.length} Karte(n) löschen?`
|
||||
: `Spalte "${col?.title}" löschen?`;
|
||||
if (!confirm(msg)) return;
|
||||
try { await api(`/tools/kanban/columns/${id}`, { method:'DELETE' }); load(); }
|
||||
catch (e) { alert(e.message); }
|
||||
};
|
||||
|
||||
// ── Karten-Aktionen ─────────────────────────────────────────────────────────
|
||||
const deleteCard = async (id) => {
|
||||
if (!confirm('Karte löschen?')) return;
|
||||
try { await api(`/tools/kanban/cards/${id}`, { method:'DELETE' }); load(); }
|
||||
catch (e) { alert(e.message); }
|
||||
};
|
||||
|
||||
// Position innerhalb einer Spalte
|
||||
const moveCard = async (cardId, colId, newPos) => {
|
||||
try { await api(`/tools/kanban/cards/${cardId}/move`, { body:{ column_id:colId, position:newPos } }); load(); }
|
||||
catch (e) { alert(e.message); }
|
||||
};
|
||||
|
||||
// In andere Spalte verschieben (Pfeil-Buttons) — ans Ende der Zielspalte
|
||||
const moveCardToCol = async (cardId, targetColId) => {
|
||||
if (!targetColId) return;
|
||||
try { await api(`/tools/kanban/cards/${cardId}/move`, { body:{ column_id:targetColId } }); load(); }
|
||||
catch (e) { alert(e.message); }
|
||||
};
|
||||
|
||||
// ── Drag & Drop ─────────────────────────────────────────────────────────────
|
||||
const onDragStart = (e, cardId) => {
|
||||
setDragCardId(cardId);
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
};
|
||||
|
||||
const onDragEnd = () => {
|
||||
setDragCardId(null); setDragOverCol(null); setDragOverPos(null);
|
||||
};
|
||||
|
||||
const onDragOverCard = (e, colId, pos) => {
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = 'move';
|
||||
setDragOverCol(colId); setDragOverPos(pos);
|
||||
};
|
||||
|
||||
const onDragOverCol = (e, colId) => {
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = 'move';
|
||||
setDragOverCol(colId);
|
||||
// ans Ende, wenn über Spalten-Hintergrund (nicht über Karte)
|
||||
const col = columns.find(c => c.id === colId);
|
||||
setDragOverPos(col ? col.cards.length : 0);
|
||||
};
|
||||
|
||||
const onDrop = async (e, colId, pos) => {
|
||||
e.preventDefault();
|
||||
if (!dragCardId) return;
|
||||
const targetPos = pos !== undefined ? pos : dragOverPos;
|
||||
setDragCardId(null); setDragOverCol(null); setDragOverPos(null);
|
||||
try {
|
||||
await api(`/tools/kanban/cards/${dragCardId}/move`, { body:{ column_id:colId, position:targetPos } });
|
||||
load();
|
||||
} catch (err) { alert(err.message); }
|
||||
};
|
||||
|
||||
// ── Render ───────────────────────────────────────────────────────────────────
|
||||
if (loading) return (
|
||||
<div style={{ color:'rgba(255,255,255,0.35)', fontFamily:'monospace', fontSize:12, padding:20 }}>
|
||||
Lädt…
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div style={{ height:'100%', display:'flex', flexDirection:'column' }}>
|
||||
{/* ── Header ─────────────────────────────────────────────────────────── */}
|
||||
<div style={{ display:'flex', alignItems:'center', gap:10, marginBottom:16, flexWrap:'wrap' }}>
|
||||
<div style={{ ...S.head, marginBottom:0, flex:1 }}>KANBAN BOARD</div>
|
||||
{addingCol ? (
|
||||
<div style={{ display:'flex', gap:6 }}>
|
||||
<input
|
||||
ref={newColRef}
|
||||
value={newColInput}
|
||||
onChange={e=>setNewColInput(e.target.value)}
|
||||
onKeyDown={e=>{ if(e.key==='Enter') addColumn(); if(e.key==='Escape'){setAddingCol(false);setNewColInput('');} }}
|
||||
placeholder="Spaltenname"
|
||||
style={{ ...S.inp, width:160, fontSize:12 }}
|
||||
/>
|
||||
<button onClick={addColumn} style={S.btn('#4ecdc4', true)}>+ Hinzufügen</button>
|
||||
<button onClick={()=>{setAddingCol(false);setNewColInput('');}} style={S.btn('#888888', true)}>✕</button>
|
||||
</div>
|
||||
) : (
|
||||
<button onClick={()=>setAddingCol(true)} style={S.btn('#4ecdc4', true)}>+ Spalte</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ── Board ──────────────────────────────────────────────────────────── */}
|
||||
{columns.length === 0 ? (
|
||||
<div style={{ color:'rgba(255,255,255,0.25)', fontFamily:'monospace', fontSize:12, padding:'30px 0', textAlign:'center' }}>
|
||||
Noch keine Spalten. Erstelle eine Spalte um loszulegen.
|
||||
</div>
|
||||
) : (
|
||||
<div style={{
|
||||
display:'flex', gap:12, overflowX:'auto', flex:1,
|
||||
paddingBottom:8,
|
||||
// Scrollbar-Styling
|
||||
scrollbarWidth:'thin', scrollbarColor:'rgba(255,255,255,0.1) transparent',
|
||||
}}>
|
||||
{columns.map((col, colIdx) => (
|
||||
<div
|
||||
key={col.id}
|
||||
onDragOver={e=>onDragOverCol(e, col.id)}
|
||||
onDrop={e=>onDrop(e, col.id, col.cards.length)}
|
||||
style={{
|
||||
background: dragOverCol===col.id && dragCardId ? 'rgba(78,205,196,0.04)' : 'rgba(255,255,255,0.03)',
|
||||
border: `1px solid ${dragOverCol===col.id && dragCardId ? 'rgba(78,205,196,0.3)' : 'rgba(255,255,255,0.07)'}`,
|
||||
borderRadius:12, padding:12,
|
||||
minWidth:260, maxWidth:300, width:280,
|
||||
flexShrink:0, display:'flex', flexDirection:'column',
|
||||
transition:'border-color 0.15s, background 0.15s',
|
||||
}}
|
||||
>
|
||||
{/* Spalten-Header */}
|
||||
<div style={{ display:'flex', alignItems:'center', gap:6, marginBottom:12 }}>
|
||||
<ColumnTitle col={col} onRename={renameColumn} onDelete={deleteColumn}/>
|
||||
</div>
|
||||
|
||||
{/* Karten */}
|
||||
<div style={{ flex:1, overflowY:'auto', scrollbarWidth:'thin', scrollbarColor:'rgba(255,255,255,0.08) transparent' }}>
|
||||
{col.cards.map((card, cardIdx) => (
|
||||
<div
|
||||
key={card.id}
|
||||
onDragOver={e=>{ e.stopPropagation(); onDragOverCard(e, col.id, cardIdx); }}
|
||||
onDrop={e=>{ e.stopPropagation(); onDrop(e, col.id, cardIdx); }}
|
||||
style={{ position:'relative' }}
|
||||
>
|
||||
{/* Drop-Indikator oberhalb der Karte */}
|
||||
{dragCardId && dragOverCol===col.id && dragOverPos===cardIdx && (
|
||||
<div style={{ height:3, background:'#4ecdc4', borderRadius:2, marginBottom:4 }}/>
|
||||
)}
|
||||
<KanbanCard
|
||||
card={card}
|
||||
col={col}
|
||||
allCols={columns}
|
||||
onEdit={card=>setModal({ card, columnId:col.id })}
|
||||
onDelete={deleteCard}
|
||||
onMove={moveCard}
|
||||
onMoveCol={moveCardToCol}
|
||||
isDragging={dragCardId===card.id}
|
||||
dragHandlers={{
|
||||
draggable: true,
|
||||
onDragStart: e => onDragStart(e, card.id),
|
||||
onDragEnd,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Drop-Zone ans Ende der Spalte */}
|
||||
{dragCardId && dragOverCol===col.id && dragOverPos===col.cards.length && (
|
||||
<div style={{ height:3, background:'#4ecdc4', borderRadius:2, marginTop:2 }}/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Karte hinzufügen */}
|
||||
<button
|
||||
onClick={()=>setModal({ card:null, columnId:col.id })}
|
||||
style={{ ...S.btn('#4ecdc4', true), width:'100%', marginTop:10, textAlign:'center' }}>
|
||||
+ Karte
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Modal ──────────────────────────────────────────────────────────── */}
|
||||
{modal && (
|
||||
<CardModal
|
||||
card={modal.card}
|
||||
columnId={modal.columnId}
|
||||
onSave={()=>{ setModal(null); load(); }}
|
||||
onClose={()=>setModal(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user