DGSoft cfd7068af5 feat: Add blinking envelope icons for unread messages
- Implement unread message indicators with Material-UI icons
- Add BlinkingEnvelope component with theme-compatible colors
- Create UnreadMessagesContext for managing unread states
- Integrate WebSocket message handling for real-time notifications
- Icons only appear for inactive channels/DMs, disappear when opened
- Add test functionality (double-click to mark as unread)
- Fix WebSocket URL handling for production deployment
- Unify WebSocket architecture using presence connection for all messages
2025-12-12 11:26:36 +01:00

187 lines
6.4 KiB
TypeScript

import React, { useState } from 'react';
import type { KanbanColumnWithCards, KanbanCard } from '../../types';
import KanbanCardComponent from './KanbanCard';
interface KanbanColumnProps {
column: KanbanColumnWithCards;
onUpdateColumn: (columnId: number, updates: Partial<KanbanColumnWithCards>) => void;
onDeleteColumn: (columnId: number) => void;
onCreateCard: (columnId: number) => void;
onDeleteCard: (cardId: number) => void;
onMoveCard: (cardId: number, targetColumnId: number, newPosition: number) => void;
onCardClick: (card: KanbanCard) => void;
}
const KanbanColumn: React.FC<KanbanColumnProps> = ({
column,
onUpdateColumn,
onDeleteColumn,
onCreateCard,
onDeleteCard,
onMoveCard,
onCardClick
}) => {
const [isEditing, setIsEditing] = useState(false);
const [editName, setEditName] = useState(column.name);
const [draggedOver, setDraggedOver] = useState(false);
const handleSaveName = () => {
const defaultColumns = ['ToDo', 'In Progress', 'Waiting', 'Done'];
if (defaultColumns.includes(column.name)) {
// Prevent renaming default columns
setEditName(column.name);
setIsEditing(false);
return;
}
if (editName.trim() && editName !== column.name) {
onUpdateColumn(column.id, { name: editName.trim() });
}
setIsEditing(false);
};
const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
handleSaveName();
} else if (e.key === 'Escape') {
setEditName(column.name);
setIsEditing(false);
}
};
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault();
setDraggedOver(true);
};
const handleDragLeave = () => {
setDraggedOver(false);
};
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
setDraggedOver(false);
const cardId = parseInt(e.dataTransfer.getData('cardId'));
const sourceColumnId = parseInt(e.dataTransfer.getData('sourceColumnId'));
if (sourceColumnId === column.id) return; // Same column, ignore
// Find the position to drop at
const rect = e.currentTarget.getBoundingClientRect();
const y = e.clientY - rect.top;
const cardHeight = 120; // Approximate card height
const position = Math.floor(y / cardHeight);
onMoveCard(cardId, column.id, Math.min(position, column.cards.length));
};
const getColumnColor = () => {
if (column.color) {
return column.color;
}
// Default colors based on position
const colors = ['#3B82F6', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6', '#06B6D4'];
return colors[column.position % colors.length];
};
return (
<div
className={`flex-1 min-w-72 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-md p-3 ${
draggedOver ? 'ring-1 ring-blue-500 bg-blue-50 dark:bg-blue-900/10' : ''
}`}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
>
{/* Column Header */}
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: getColumnColor() }}
/>
{isEditing ? (
<input
type="text"
value={editName}
onChange={(e) => setEditName(e.target.value)}
onBlur={handleSaveName}
onKeyDown={handleKeyPress}
className="px-2 py-1 text-sm font-medium bg-white dark:bg-gray-700 border rounded"
autoFocus
/>
) : (
<h3
className={`text-sm font-medium text-gray-900 dark:text-white ${
(() => {
const defaultColumns = ['ToDo', 'In Progress', 'Waiting', 'Done'];
return !defaultColumns.includes(column.name) ? 'cursor-pointer hover:text-blue-600' : '';
})()
}`}
onClick={() => {
const defaultColumns = ['ToDo', 'In Progress', 'Waiting', 'Done'];
if (!defaultColumns.includes(column.name)) {
setIsEditing(true);
}
}}
>
{column.name}
</h3>
)}
<span className="text-xs text-gray-500 bg-gray-100 dark:bg-gray-700 px-1.5 py-0.5 rounded">
{column.cards.length}
</span>
</div>
<div className="flex items-center gap-1">
<button
onClick={() => onCreateCard(column.id)}
className="text-gray-400 hover:text-green-500 p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-700"
title="Neue Karte hinzufügen"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
</button>
{(() => {
const defaultColumns = ['ToDo', 'In Progress', 'Waiting', 'Done'];
return !defaultColumns.includes(column.name) && (
<button
onClick={() => onDeleteColumn(column.id)}
className="text-gray-400 hover:text-red-500 p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-700"
title="Spalte löschen"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
);
})()}
</div>
</div>
{/* Cards */}
<div className="space-y-2 min-h-[150px]">
{column.cards.map((card) => (
<KanbanCardComponent
key={card.id}
card={card}
onClick={() => onCardClick(card)}
onDelete={onDeleteCard}
sourceColumnId={column.id}
/>
))}
</div>
{/* Add Card Button */}
<button
onClick={() => onCreateCard(column.id)}
className="w-full mt-2 p-2 border border-dashed border-gray-300 dark:border-gray-600 rounded text-gray-500 hover:border-gray-400 dark:hover:border-gray-500 hover:text-gray-600 dark:hover:text-gray-400 transition-colors text-sm"
>
+ Karte hinzufügen
</button>
</div>
);
};
export default KanbanColumn;