mirror of
https://github.com/OHV-IT/collabrix.git
synced 2025-12-15 16:48:36 +01:00
- 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
187 lines
6.4 KiB
TypeScript
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; |