Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 11 additions & 11 deletions packages/web/src/components/ActivityFeed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -594,7 +594,7 @@ const ActivityFeed: React.FC<ActivityFeedProps> = ({ filteredNodes }) => {
</div>

{/* Activity List */}
<div className="flex-1 overflow-auto p-6">
<div className="flex-1 overflow-auto p-3 sm:p-6">
{paginatedActivities.length === 0 ? (
<div className="text-center py-12">
<AlertCircle className="h-12 w-12 text-gray-500 mx-auto mb-4" />
Expand Down Expand Up @@ -630,8 +630,8 @@ const ActivityFeed: React.FC<ActivityFeedProps> = ({ filteredNodes }) => {
</div>

<div className="flex-1 min-w-0">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center space-x-3">
<div className="flex flex-wrap items-center justify-between gap-2 mb-3 sm:flex-nowrap">
<div className="flex flex-wrap items-center gap-2 min-w-0 sm:flex-nowrap sm:space-x-3 sm:gap-0">
<h3 className="text-base font-semibold text-white">{activity.title}</h3>
{(() => {
const priorityConfig = PRIORITY_OPTIONS.find(opt => opt.value === activity.priority);
Expand Down Expand Up @@ -662,18 +662,18 @@ const ActivityFeed: React.FC<ActivityFeedProps> = ({ filteredNodes }) => {

<p className="text-sm text-gray-300 mb-4 leading-relaxed">{activity.description}</p>

<div className="flex items-center justify-between">
<div className="flex items-center space-x-4 text-xs">
<div className="flex items-center space-x-2 px-2 py-1 bg-gray-700/50 rounded-lg">
<User className="h-3 w-3 text-gray-400" />
<span className="text-gray-300 font-medium">{activity.user}</span>
<div className="flex flex-wrap items-center justify-between gap-2 sm:flex-nowrap">
<div className="flex flex-wrap items-center gap-2 min-w-0 text-xs sm:flex-nowrap sm:space-x-4 sm:gap-0">
<div className="flex items-center space-x-2 px-2 py-1 bg-gray-700/50 rounded-lg min-w-0">
<User className="h-3 w-3 text-gray-400 flex-shrink-0" />
<span className="truncate text-gray-300 font-medium">{activity.user}</span>
</div>
<div className="flex items-center space-x-2 px-2 py-1 bg-gray-700/50 rounded-lg">
<Target className="h-3 w-3 text-gray-400" />
<div className="flex items-center space-x-2 px-2 py-1 bg-gray-700/50 rounded-lg min-w-0">
<Target className="h-3 w-3 text-gray-400 flex-shrink-0" />
<span className="truncate max-w-32 text-gray-300 font-medium">{activity.nodeTitle}</span>
</div>
</div>
<div className="text-xs text-gray-500 bg-gray-700/30 px-2 py-1 rounded">
<div className="text-xs text-gray-500 bg-gray-700/30 px-2 py-1 rounded flex-shrink-0">
{activity.timestamp.toLocaleString()}
</div>
</div>
Expand Down
76 changes: 74 additions & 2 deletions packages/web/src/components/CalendarView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ const getStatusColor = (status: string) => {
const CalendarViewComponent: React.FC<CalendarViewProps> = ({ filteredNodes }) => {
const [currentDate, setCurrentDate] = useState(new Date());
const [selectedDate, setSelectedDate] = useState<Date | null>(null);
const [viewMode, setViewMode] = useState<'month' | 'week' | 'agenda'>('month');
const [viewMode, setViewMode] = useState<'month' | 'week' | 'agenda'>(() =>
typeof window !== 'undefined' && window.matchMedia('(max-width: 639px)').matches ? 'agenda' : 'month'
);
const [filterPriority, setFilterPriority] = useState<string>('all');
const [filterType, setFilterType] = useState<string>('all');
const [filterStatus, setFilterStatus] = useState<string>('all');
Expand Down Expand Up @@ -110,6 +112,14 @@ const CalendarViewComponent: React.FC<CalendarViewProps> = ({ filteredNodes }) =
return grouped;
}, [filteredNodes, showCompleted, filterPriority, filterType, filterStatus, searchQuery]);

// Flatten grouped nodes into a date-sorted list for the agenda view
const agendaDays = useMemo(() => {
return Object.keys(nodesByDate)
.filter(dateKey => nodesByDate[dateKey].length > 0)
.sort((a, b) => new Date(a).getTime() - new Date(b).getTime())
.map(dateKey => ({ date: new Date(dateKey), nodes: nodesByDate[dateKey] }));
}, [nodesByDate]);

// Generate calendar days
const calendarDays = useMemo(() => {
const year = currentDate.getFullYear();
Expand Down Expand Up @@ -420,7 +430,66 @@ const CalendarViewComponent: React.FC<CalendarViewProps> = ({ filteredNodes }) =
</button>
</div>

{/* Agenda List */}
{viewMode === 'agenda' && (
<div className="flex-1 p-2 sm:p-4 overflow-y-auto space-y-4">
{agendaDays.length === 0 ? (
<div className="text-center py-12 text-gray-400">
<CalendarDays className="h-8 w-8 mx-auto mb-2 opacity-50" />
<p>No scheduled tasks</p>
</div>
) : (
agendaDays.map(({ date, nodes }) => (
<div key={date.toDateString()}>
<div className={`flex items-center space-x-2 mb-2 text-sm font-semibold ${
isToday(date) ? 'text-green-400' : 'text-gray-300'
}`}>
<Clock className="h-4 w-4 flex-shrink-0" />
<span className="truncate">
{date.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric' })}
</span>
</div>
<div className="space-y-2">
{nodes.map((node, index) => {
const statusConfig = getStatusConfig(node.status as WorkItemStatus);
const typeConfig = getTypeConfig(node.type as WorkItemType);
const priority = node.priority || 0;
const priorityConfig = getPriorityConfig(priority);

return (
<div
key={index}
onClick={() => setSelectedTask(selectedTask === node.id ? null : node.id)}
className="flex items-start justify-between gap-2 p-3 bg-gray-700 rounded-lg hover:bg-gray-600 transition-colors cursor-pointer"
>
<div className="flex items-start space-x-3 min-w-0">
<div className={`mt-1 w-3 h-3 rounded-full flex-shrink-0 ${statusConfig.bgColor}`} />
<div className="min-w-0">
<div className="text-sm font-medium text-white truncate">{node.title}</div>
<div className="flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-gray-400">
<span className="flex items-center">{React.createElement(typeConfig.icon as any, { className: 'h-3 w-3 inline mr-1' })} {node.type}</span>
<span className={priorityConfig.color}>{priorityConfig.label}</span>
{node.assignedTo && (
<span className="truncate">{node.assignedTo.name}</span>
)}
</div>
</div>
</div>
<span className={`text-xs px-2 py-1 rounded flex-shrink-0 ${statusConfig.bgColor} ${statusConfig.color}`}>
{node.status.replace('_', ' ')}
</span>
</div>
);
})}
</div>
</div>
))
)}
</div>
)}

{/* Days of Week Header */}
{viewMode !== 'agenda' && (
<div className="grid grid-cols-7 gap-1 p-2 border-b border-gray-700">
{['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'].map((day, index) => (
<div key={day} className={`text-center py-3 text-sm font-medium ${
Expand All @@ -431,8 +500,10 @@ const CalendarViewComponent: React.FC<CalendarViewProps> = ({ filteredNodes }) =
</div>
))}
</div>
)}

{/* Calendar Grid */}
{viewMode !== 'agenda' && (
<div className="flex-1 p-2 overflow-auto">
<div className="grid grid-cols-7 gap-1 h-full">
{calendarDays.map((day, index) => {
Expand Down Expand Up @@ -528,8 +599,9 @@ const CalendarViewComponent: React.FC<CalendarViewProps> = ({ filteredNodes }) =
})}
</div>
</div>
)}
</div>

{/* Selected Date Details Panel */}
{selectedDate && (
<div className="mt-4 bg-gray-800/50 backdrop-blur-sm rounded-lg border border-gray-700/50 p-4">
Expand Down
45 changes: 44 additions & 1 deletion packages/web/src/components/GanttChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -452,7 +452,50 @@ const GanttChart: React.FC<GanttChartProps> = ({ filteredNodes }) => {

{/* Main Content */}
<div className="flex-1 overflow-hidden">
<div className="h-full overflow-auto" ref={scrollRef}>
{/* Mobile Schedule List */}
<div className="sm:hidden h-full overflow-y-auto p-4 space-y-3">
{timelineData.length === 0 ? (
<div className="text-center text-sm text-gray-400 py-8">No tasks to display</div>
) : (
timelineData.map(item => {
const statusConfig = getStatusConfig(item.status as WorkItemStatus);
const priorityConfig = getPriorityConfig(item.priority);
return (
<div
key={item.id}
className={`rounded-lg border p-3 ${
selectedTask === item.id
? `${WORK_ITEM_STATUSES.COMPLETED.bgColor} ${WORK_ITEM_STATUSES.COMPLETED.borderColor}`
: 'bg-gray-800/50 border-gray-700/50'
}`}
onClick={() => setSelectedTask(selectedTask === item.id ? null : item.id)}
>
<div className="flex items-center space-x-2 mb-2">
<div
className="w-1 h-6 rounded-full shrink-0"
style={{ backgroundColor: priorityConfig.color.replace('text-', '') }}
title={`Priority: ${priorityConfig.label}`}
/>
<div className="text-sm font-medium text-white truncate">{item.title}</div>
</div>
<div className="flex items-center space-x-2 text-xs text-gray-400 mb-1">
<span className={`w-2.5 h-2.5 rounded-full shrink-0 ${statusConfig.bgColor}`} />
<span>{item.status.replace('_', ' ')}</span>
<span>•</span>
<span>{item.progress}%</span>
</div>
<div className="flex items-center justify-between text-xs text-gray-400">
<span>{item.startDate.toLocaleDateString()} – {item.endDate.toLocaleDateString()}</span>
<span>{item.duration}d</span>
</div>
</div>
);
})
)}
</div>

{/* Interactive Gantt Timeline */}
<div className="hidden sm:block h-full overflow-auto" ref={scrollRef}>
<div className="bg-gray-800/50 backdrop-blur-sm border border-gray-700/50">
<div className="overflow-x-auto">
<div className="relative" style={{ minWidth: `${800 * zoomLevel}px`, transform: `scale(${zoomLevel})`, transformOrigin: 'top left' }}>
Expand Down
6 changes: 3 additions & 3 deletions packages/web/src/components/KanbanView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -122,13 +122,13 @@ const KanbanView: React.FC<KanbanViewProps> = ({ filteredNodes, handleEditNode,
}, {} as Record<string, WorkItem[]>);

return (
<div className="flex space-x-4 p-6 overflow-x-auto h-full">
<div className="flex flex-col gap-4 p-3 overflow-y-auto sm:flex-row sm:space-x-4 sm:gap-0 sm:p-6 sm:overflow-x-auto h-full">
{statuses.map((status) => {
const nodes = nodesByStatus[status] || [];
const config = getStatusConfig(status);

return (
<div key={status} className="flex-shrink-0 w-80">
<div key={status} className="w-full sm:w-80 flex-shrink-0">
<div className="bg-gray-800/40 backdrop-blur-sm rounded-lg h-full">
<div className={`p-4 border-b border-gray-600/30`}>
<div className="flex items-center justify-between">
Expand Down
117 changes: 109 additions & 8 deletions packages/web/src/components/TableView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,115 @@ const getContributorAvatar = (contributor?: string) => {
};

const TableView: React.FC<TableViewProps> = ({ filteredNodes, handleEditNode, edges }) => {
const sortedNodes = [...filteredNodes].sort((a, b) => {
const dateA = new Date(a.updatedAt || a.createdAt).getTime();
const dateB = new Date(b.updatedAt || b.createdAt).getTime();
return dateB - dateA; // Most recent first
});

return (
<div className="p-6">
<div className="bg-gray-800/80 backdrop-blur-sm rounded-xl border border-gray-700/50 shadow-2xl overflow-hidden max-w-full">
<div className="p-4 sm:p-6">
<div className="sm:hidden space-y-4">
{sortedNodes.map((node) => {
const { incomingCount, outgoingCount, totalCount } = getConnectionDetails(node, edges);
return (
<div
key={node.id}
onClick={() => handleEditNode(node)}
className={`${getNodeTypeRowBackground(node.type)} rounded-xl shadow-lg cursor-pointer active:brightness-125 transition-all duration-200`}
style={{
borderLeft: `4px solid ${getNodeTypeBorderColor(node.type)}`,
borderRight: `2px solid ${getNodeTypeBorderColor(node.type)}`
}}
>
<div className="p-4 space-y-3">
<div>
<div className="text-white font-medium text-base break-words">{node.title}</div>
{node.description && (
<div className="text-gray-400 text-sm mt-1 break-words">{node.description}</div>
)}
</div>
<div className="flex flex-wrap items-center gap-2">
<span className={`inline-flex items-center px-3 py-1 rounded-full text-xs font-medium ${getNodeTypeColor(node.type)} shadow-sm`}>
{getTypeIconElement(node.type as WorkItemType, "w-3 h-3")}
<span className="ml-1">{formatLabel(node.type)}</span>
</span>
<span className="inline-flex items-center">
<span className={`mr-1 ${getStatusConfig(node.status as WorkItemStatus).color}`}>
{getStatusIconElement(node.status as WorkItemStatus, "h-4 w-4")}
</span>
<span className={`text-sm font-medium ${getStatusColor(node.status)}`}>
{formatLabel(node.status)}
</span>
</span>
</div>
<div className="grid grid-cols-2 gap-3 text-sm">
<div>
<div className="text-gray-500 text-xs uppercase tracking-wider mb-1">Priority</div>
<AnimatedPriority
value={getNodePriority(node)}
className="text-sm font-bold text-white"
/>
</div>
<div>
<div className="text-gray-500 text-xs uppercase tracking-wider mb-1">Contributor</div>
{node.assignedTo ? (
<span className="text-gray-300 text-sm break-words">{node.assignedTo.name}</span>
) : (
<span className="text-gray-500 text-sm">Available</span>
)}
</div>
</div>
<div>
<div className="text-gray-500 text-xs uppercase tracking-wider mb-1">Connections</div>
{totalCount === 0 ? (
<div className="flex items-center space-x-2 text-gray-500">
<GitBranch className="h-4 w-4" />
<span className="text-sm">None</span>
</div>
) : (
<div className="flex items-center space-x-3">
<div className="flex items-center space-x-1">
<GitBranch className="h-4 w-4 text-gray-400" />
<span className="text-sm font-medium text-white">{totalCount}</span>
</div>
{incomingCount > 0 && (
<div className="flex items-center space-x-1 px-2 py-1 bg-red-900/20 border border-red-500/30 rounded-md">
<ArrowLeft className="h-3 w-3 text-red-400" />
<span className="text-xs font-medium text-red-300">{incomingCount}</span>
</div>
)}
{outgoingCount > 0 && (
<div className="flex items-center space-x-1 px-2 py-1 bg-purple-900/20 border border-purple-500/30 rounded-md">
<ArrowRight className="h-3 w-3 text-purple-400" />
<span className="text-xs font-medium text-purple-300">{outgoingCount}</span>
</div>
)}
</div>
)}
</div>
<div>
<div className="text-gray-500 text-xs uppercase tracking-wider mb-1">Due Date</div>
{node.dueDate ? (
<div className={`inline-flex items-center px-3 py-2 text-sm font-medium rounded-lg border ${getDueDateColorScheme(node.dueDate).bg} ${getDueDateColorScheme(node.dueDate).border} ${getDueDateColorScheme(node.dueDate).text}`}>
{new Date(node.dueDate).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric'
})}
</div>
) : (
<span className="text-gray-500 text-sm">No due date</span>
)}
</div>
<TagDisplay tags={node.tags} compact />
</div>
</div>
);
})}
</div>

<div className="hidden sm:block bg-gray-800/80 backdrop-blur-sm rounded-xl border border-gray-700/50 shadow-2xl overflow-hidden max-w-full">
<div className="overflow-x-auto max-w-full">
<table className="w-max min-w-full">
<thead className="bg-gradient-to-r from-gray-700 to-gray-800 border-b border-gray-600/50">
Expand All @@ -127,12 +233,7 @@ const TableView: React.FC<TableViewProps> = ({ filteredNodes, handleEditNode, ed
</tr>
</thead>
<tbody className="divide-y divide-gray-600/30 backdrop-blur-sm">
{[...filteredNodes]
.sort((a, b) => {
const dateA = new Date(a.updatedAt || a.createdAt).getTime();
const dateB = new Date(b.updatedAt || b.createdAt).getTime();
return dateB - dateA; // Most recent first
})
{sortedNodes
.map((node) => {
return (
<tr
Expand Down
Loading
Loading