diff --git a/backend/app/routes/kanban/boards.py b/backend/app/routes/kanban/boards.py
index b69ed4e..05c6455 100644
--- a/backend/app/routes/kanban/boards.py
+++ b/backend/app/routes/kanban/boards.py
@@ -27,12 +27,15 @@ def get_boards():
@load_board_owned
def get_board(board_id, board):
"""Get a single board with all its details"""
+ from app.models import User
+
# Get all lists for this board
lists_data = []
for lst in board.lists.filter_by(closed=False).order_by(List.pos).all():
cards_data = []
for card in lst.cards.filter_by(closed=False).order_by(Card.pos).all():
card_dict = card.to_dict()
+
# Add labels for this card
card_dict["labels"] = [
label.to_dict()
@@ -43,6 +46,24 @@ def get_board(board_id, board):
.all()
)
]
+
+ # Add comments for this card
+ card_dict["comments"] = []
+ for comment in card.comments.all():
+ comment_dict = comment.to_dict()
+ user = db.session.get(User, comment.user_id)
+ comment_dict["user"] = user.to_dict() if user else None
+ card_dict["comments"].append(comment_dict)
+
+ # Add checklists with items for this card
+ card_dict["checklists"] = [
+ {
+ **checklist.to_dict(),
+ "items": [item.to_dict() for item in checklist.check_items.all()],
+ }
+ for checklist in card.checklists.all()
+ ]
+
cards_data.append(card_dict)
lists_data.append({**lst.to_dict(), "cards": cards_data})
diff --git a/backend/app/schemas/board.py b/backend/app/schemas/board.py
index ef98a32..89fd41e 100644
--- a/backend/app/schemas/board.py
+++ b/backend/app/schemas/board.py
@@ -105,3 +105,5 @@ class BoardWithDetailsResponse(BaseModel):
date_last_activity: Optional[datetime] = None
lists: List[Dict[str, Any]] = Field(default_factory=list)
labels: List[Dict[str, Any]] = Field(default_factory=list)
+ checklists: List[Dict[str, Any]] = Field(default_factory=list)
+ comments: List[Dict[str, Any]] = Field(default_factory=list)
diff --git a/frontend/src/components/CardPreviewModal.tsx b/frontend/src/components/CardPreviewModal.tsx
new file mode 100644
index 0000000..5e3826f
--- /dev/null
+++ b/frontend/src/components/CardPreviewModal.tsx
@@ -0,0 +1,155 @@
+import { CardWithDetails } from '../types/kanban';
+import { Link } from 'react-router-dom';
+
+interface CardPreviewModalProps {
+ card: CardWithDetails;
+ onClose: () => void;
+}
+
+export function CardPreviewModal({ card, onClose }: CardPreviewModalProps) {
+ return (
+
+
+
+ {card.name}
+
+
+ ×
+
+
+
+ {/* Labels */}
+ {card.labels && card.labels.length > 0 && (
+
+
+ {card.labels.map((label) => (
+
+ {label.name}
+
+ ))}
+
+
+ )}
+
+ {/* Description */}
+ {card.description && (
+
+
Description
+
{card.description}
+
+ )}
+
+ {/* Checklists */}
+ {card.checklists && card.checklists.length > 0 && (
+
+
Checklists
+
+ {card.checklists.map((checklist) => {
+ const completedItems = checklist.items.filter(
+ (item) => item.state === 'complete'
+ ).length;
+ const totalItems = checklist.items.length;
+ const progress = totalItems > 0 ? (completedItems / totalItems) * 100 : 0;
+
+ return (
+
+
+
{checklist.name}
+
+ {completedItems}/{totalItems}
+
+
+
+ {/* Checklist Items */}
+
+ {checklist.items.map((item) => (
+
+
+ {item.state === 'complete' && (
+
+
+
+ )}
+
+
+ {item.name}
+
+
+ ))}
+
+
+ );
+ })}
+
+
+ )}
+
+ {/* Comments Preview */}
+ {card.comments && card.comments.length > 0 && (
+
+
Comments
+
+ {card.comments.slice(0, 3).map((comment) => (
+
+
+
+ {comment.user?.username.charAt(0).toUpperCase() || '?'}
+
+
+
+ {comment.user?.username || 'Unknown'}
+
+
+ {new Date(comment.created_at).toLocaleDateString()}
+
+
+
+
{comment.text}
+
+ ))}
+ {card.comments.length > 3 && (
+
+ +{card.comments.length - 3} more comments
+
+ )}
+
+
+ )}
+
+ );
+}
diff --git a/frontend/src/components/kanban/KanbanCard.tsx b/frontend/src/components/kanban/KanbanCard.tsx
index 3ad5740..30fbd1a 100644
--- a/frontend/src/components/kanban/KanbanCard.tsx
+++ b/frontend/src/components/kanban/KanbanCard.tsx
@@ -1,13 +1,16 @@
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { Card as CardType } from '../../types/kanban';
+import MessageSquareIcon from '../icons/MessageSquareIcon';
+import CheckSquareIcon from '../icons/CheckSquareIcon';
+import { Link } from 'react-router-dom';
interface KanbanCardProps {
card: CardType;
- onClick: () => void;
+ onOpenModal: () => void;
}
-export function KanbanCard({ card, onClick }: KanbanCardProps) {
+export function KanbanCard({ card, onOpenModal }: KanbanCardProps) {
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: `CARD_${card.id}`,
});
@@ -18,28 +21,109 @@ export function KanbanCard({ card, onClick }: KanbanCardProps) {
opacity: isDragging ? 0.5 : 1,
};
+ // Calculate checklist progress
+ const checklists = (card as any).checklists || [];
+ const totalItems = checklists.reduce(
+ (sum: number, checklist: any) => sum + checklist.items.length,
+ 0
+ );
+ const completedItems = checklists.reduce(
+ (sum: number, checklist: any) =>
+ sum + checklist.items.filter((item: any) => item.state === 'complete').length,
+ 0
+ );
+ const hasChecklists = totalItems > 0;
+
+ // Calculate comment count
+ const comments = (card as any).comments || [];
+ const commentCount = comments.length;
+ const hasComments = commentCount > 0;
+
+ // Get labels
+ const labels = (card as any).labels || [];
+ const hasLabels = labels.length > 0;
+
return (
{
+ // Only open modal if not clicking title link
+ if (!(e.target as HTMLElement).closest('a')) {
+ onOpenModal();
+ }
+ }}
className="bg-gray-700 rounded-lg p-4 mb-3 cursor-pointer hover:bg-gray-600 transition-colors border border-gray-600 shadow-sm"
>
-
{card.name}
-
- {card.description && (
-
{card.description}
+ {/* Labels */}
+ {hasLabels && (
+
+ {labels.slice(0, 3).map((label: any) => (
+
+ {label.name}
+
+ ))}
+ {labels.length > 3 && (
+
+ +{labels.length - 3}
+
+ )}
+
)}
-
-
#{card.id_short}
+
+ e.stopPropagation()}
+ className="hover:underline"
+ >
+ {card.name}
+
+
+
+ {card.description && (
+
{card.description}
+ )}
+
+ {/* Badges */}
+
+ {/* Comments Badge */}
+ {hasComments && (
+
+
+
+
+ {commentCount}
+
+ )}
+
+ {/* Checklist Badge */}
+ {hasChecklists && (
+
+
+
+
+
+ {completedItems}/{totalItems}
+
+
+ )}
+
+ {/* Due Date Badge */}
{card.due && (
Due: {new Date(card.due).toLocaleDateString()}
)}
+
+ {/* Card ID */}
+
#{card.id_short}
);
diff --git a/frontend/src/components/kanban/KanbanColumn.tsx b/frontend/src/components/kanban/KanbanColumn.tsx
index f2acb25..617dd40 100644
--- a/frontend/src/components/kanban/KanbanColumn.tsx
+++ b/frontend/src/components/kanban/KanbanColumn.tsx
@@ -8,11 +8,11 @@ import { useModal } from '../../context/modals/useModal';
interface KanbanColumnProps {
list: ListWithCards;
cards: CardType[];
- onCardClick: (card: CardType) => void;
+ onOpenCardModal: (card: CardType) => void;
onCardCreate: (data: { name: string; description?: string }) => Promise
;
}
-export function KanbanColumn({ list, cards, onCardClick, onCardCreate }: KanbanColumnProps) {
+export function KanbanColumn({ list, cards, onOpenCardModal, onCardCreate }: KanbanColumnProps) {
const { setNodeRef, isOver } = useDroppable({
id: `LIST_${list.id}`,
});
@@ -42,7 +42,7 @@ export function KanbanColumn({ list, cards, onCardClick, onCardCreate }: KanbanC
className={`min-h-[200px] flex-1 transition-colors ${isOver ? 'bg-gray-750' : ''}`}
>
{cards.map((card) => (
- onCardClick(card)} />
+ onOpenCardModal(card)} />
))}
diff --git a/frontend/src/pages/BoardDetail.tsx b/frontend/src/pages/BoardDetail.tsx
index a6f7fc5..ae173e8 100644
--- a/frontend/src/pages/BoardDetail.tsx
+++ b/frontend/src/pages/BoardDetail.tsx
@@ -1,9 +1,10 @@
-import { useParams, Link, useNavigate } from 'react-router-dom';
+import { useParams, Link } from 'react-router-dom';
import { useBoard } from '../hooks/useBoard';
import { useCardMutations } from '../hooks/useCardMutations';
import { useListMutations } from '../hooks/useListMutations';
import { KanbanColumn } from '../components/kanban/KanbanColumn';
import { CreateListModal } from '../components/kanban/CreateListModal';
+import { CardPreviewModal } from '../components/CardPreviewModal';
import { useModal } from '../context/modals/useModal';
import {
DndContext,
@@ -21,7 +22,6 @@ import { useState } from 'react';
export function BoardDetail() {
const { id } = useParams<{ id: string }>();
- const navigate = useNavigate();
const { board, fetchBoard } = useBoard(parseInt(id || '0'));
const { createCard, moveCard } = useCardMutations(parseInt(id || '0'), fetchBoard);
const { createList } = useListMutations(parseInt(id || '0'), fetchBoard);
@@ -160,8 +160,8 @@ export function BoardDetail() {
await moveCard(activeCard, activeList.id, overListContainingCard.id, overCardIndex);
};
- const handleCardClick = (card: CardType) => {
- navigate(`/boards/${id}/cards/${card.id}`);
+ const handleOpenCardModal = (card: CardType) => {
+ openModal((props) => );
};
const handleAddList = () => {
@@ -229,7 +229,7 @@ export function BoardDetail() {
key={list.id}
list={list}
cards={list.cards}
- onCardClick={handleCardClick}
+ onOpenCardModal={handleOpenCardModal}
onCardCreate={handleAddCard(list.id)}
/>
))}