feat: Group completed tasks in collapsible section

- Added 'Completed Tasks' collapsible block with count badge
- Completed section collapsed by default (prevents scrolling)
- In-progress and planned tasks always visible at top
- Click completed section header to expand/collapse
- Improves navigation to active work
This commit is contained in:
mindesbunister
2025-11-25 09:54:32 +01:00
parent b2c7551d5b
commit 72f974a52a

View File

@@ -16,6 +16,7 @@ export default function RoadmapPage() {
const [roadmap, setRoadmap] = useState<RoadmapItem[]>([])
const [loading, setLoading] = useState(true)
const [expandedItems, setExpandedItems] = useState<Set<number>>(new Set())
const [showCompleted, setShowCompleted] = useState(false)
useEffect(() => {
fetchRoadmap()
@@ -155,80 +156,193 @@ export default function RoadmapPage() {
</div>
{/* Roadmap Items */}
<div className="space-y-4">
{roadmap.map((item, index) => {
const isExpanded = expandedItems.has(index)
return (
<div
key={index}
className={`bg-gray-800/50 backdrop-blur-sm border rounded-xl overflow-hidden transition-all ${getStatusColor(item.status)}`}
<div className="space-y-8">
{/* Completed Tasks Section */}
{stats.complete > 0 && (
<div className="bg-gray-800/50 backdrop-blur-sm border border-green-500/30 rounded-xl overflow-hidden">
<button
onClick={() => setShowCompleted(!showCompleted)}
className="w-full p-6 flex items-center justify-between hover:bg-gray-700/20 transition-colors"
>
<div
className="p-6 cursor-pointer hover:bg-gray-700/20 transition-colors"
onClick={() => toggleItem(index)}
>
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<span className="text-2xl">{getStatusIcon(item.status)}</span>
<h3 className="text-xl font-bold text-white">{item.title}</h3>
<span className="px-3 py-1 rounded-full text-xs font-semibold bg-gray-700/50 text-gray-300">
{item.phase}
</span>
</div>
<p className="text-gray-300">{item.description}</p>
{item.completed && (
<div className="text-xs text-green-400 mt-2">
Completed: {item.completed}
</div>
)}
</div>
<button
className="ml-4 text-gray-400 hover:text-white transition-colors"
onClick={(e) => {
e.stopPropagation()
toggleItem(index)
}}
>
<svg
className={`w-6 h-6 transition-transform ${isExpanded ? 'rotate-180' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
</div>
<div className="flex items-center gap-3">
<span className="text-2xl"></span>
<h2 className="text-xl font-bold text-white">Completed Tasks</h2>
<span className="px-3 py-1 rounded-full text-sm font-semibold bg-green-500/20 text-green-400">
{stats.complete} items
</span>
</div>
<svg
className={`w-6 h-6 text-gray-400 transition-transform ${showCompleted ? 'rotate-180' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
{isExpanded && (
<div className="px-6 pb-6 border-t border-gray-700">
<div className="pt-4">
<div className="bg-gray-900/50 rounded-lg p-3 mb-3">
<div className="text-xs text-gray-400 mb-1">Expected Impact</div>
<div className="text-sm text-white font-semibold">{item.impact}</div>
</div>
{item.items && item.items.length > 0 && (
<div>
<div className="text-sm text-gray-400 mb-2">Implementation Details:</div>
<ul className="space-y-1">
{item.items.map((subItem, subIndex) => (
<li key={subIndex} className="text-sm text-gray-300 flex items-start gap-2">
<span className="text-gray-500 mt-1"></span>
<span>{subItem}</span>
</li>
))}
</ul>
{showCompleted && (
<div className="p-6 pt-0 space-y-4 border-t border-gray-700">
{roadmap
.map((item, index) => ({ item, index }))
.filter(({ item }) => item.status === 'complete')
.map(({ item, index }) => {
const isExpanded = expandedItems.has(index)
return (
<div
key={index}
className={`bg-gray-800/50 backdrop-blur-sm border rounded-xl overflow-hidden transition-all ${getStatusColor(item.status)}`}
>
<div
className="p-6 cursor-pointer hover:bg-gray-700/20 transition-colors"
onClick={() => toggleItem(index)}
>
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<span className="text-2xl">{getStatusIcon(item.status)}</span>
<h3 className="text-xl font-bold text-white">{item.title}</h3>
<span className="px-3 py-1 rounded-full text-xs font-semibold bg-gray-700/50 text-gray-300">
{item.phase}
</span>
</div>
<p className="text-gray-300">{item.description}</p>
{item.completed && (
<div className="text-xs text-green-400 mt-2">
Completed: {item.completed}
</div>
)}
</div>
<button
className="ml-4 text-gray-400 hover:text-white transition-colors"
onClick={(e) => {
e.stopPropagation()
toggleItem(index)
}}
>
<svg
className={`w-6 h-6 transition-transform ${isExpanded ? 'rotate-180' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
</div>
</div>
{isExpanded && (
<div className="px-6 pb-6 border-t border-gray-700">
<div className="pt-4">
<div className="bg-gray-900/50 rounded-lg p-3 mb-3">
<div className="text-xs text-gray-400 mb-1">Expected Impact</div>
<div className="text-sm text-white font-semibold">{item.impact}</div>
</div>
{item.items && item.items.length > 0 && (
<div>
<div className="text-sm text-gray-400 mb-2">Implementation Details:</div>
<ul className="space-y-1">
{item.items.map((subItem, subIndex) => (
<li key={subIndex} className="text-sm text-gray-300 flex items-start gap-2">
<span className="text-gray-500 mt-1"></span>
<span>{subItem}</span>
</li>
))}
</ul>
</div>
)}
</div>
</div>
)}
</div>
)}
)
})}
</div>
)}
</div>
)}
{/* In-Progress and Planned Tasks */}
<div className="space-y-4">
{roadmap
.map((item, index) => ({ item, index }))
.filter(({ item }) => item.status !== 'complete')
.map(({ item, index }) => {
const isExpanded = expandedItems.has(index)
return (
<div
key={index}
className={`bg-gray-800/50 backdrop-blur-sm border rounded-xl overflow-hidden transition-all ${getStatusColor(item.status)}`}
>
<div
className="p-6 cursor-pointer hover:bg-gray-700/20 transition-colors"
onClick={() => toggleItem(index)}
>
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<span className="text-2xl">{getStatusIcon(item.status)}</span>
<h3 className="text-xl font-bold text-white">{item.title}</h3>
<span className="px-3 py-1 rounded-full text-xs font-semibold bg-gray-700/50 text-gray-300">
{item.phase}
</span>
</div>
<p className="text-gray-300">{item.description}</p>
{item.completed && (
<div className="text-xs text-green-400 mt-2">
Completed: {item.completed}
</div>
)}
</div>
<button
className="ml-4 text-gray-400 hover:text-white transition-colors"
onClick={(e) => {
e.stopPropagation()
toggleItem(index)
}}
>
<svg
className={`w-6 h-6 transition-transform ${isExpanded ? 'rotate-180' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
</div>
</div>
{isExpanded && (
<div className="px-6 pb-6 border-t border-gray-700">
<div className="pt-4">
<div className="bg-gray-900/50 rounded-lg p-3 mb-3">
<div className="text-xs text-gray-400 mb-1">Expected Impact</div>
<div className="text-sm text-white font-semibold">{item.impact}</div>
</div>
{item.items && item.items.length > 0 && (
<div>
<div className="text-sm text-gray-400 mb-2">Implementation Details:</div>
<ul className="space-y-1">
{item.items.map((subItem, subIndex) => (
<li key={subIndex} className="text-sm text-gray-300 flex items-start gap-2">
<span className="text-gray-500 mt-1"></span>
<span>{subItem}</span>
</li>
))}
</ul>
</div>
)}
</div>
</div>
)}
</div>
)}
</div>
)
})}
)
})}
</div>
</div>
{/* Footer */}