feat: Add collapsible roadmap items with completed collapsed by default
- Added expandedItems state to track which items are expanded - Auto-expands only non-complete items on load (in-progress, planned) - Complete items collapsed by default for better overview - Click anywhere on item header or chevron to toggle - Smooth transitions and hover effects - Improves readability when many items are complete
This commit is contained in:
@@ -15,6 +15,7 @@ interface RoadmapItem {
|
|||||||
export default function RoadmapPage() {
|
export default function RoadmapPage() {
|
||||||
const [roadmap, setRoadmap] = useState<RoadmapItem[]>([])
|
const [roadmap, setRoadmap] = useState<RoadmapItem[]>([])
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [expandedItems, setExpandedItems] = useState<Set<number>>(new Set())
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchRoadmap()
|
fetchRoadmap()
|
||||||
@@ -25,6 +26,15 @@ export default function RoadmapPage() {
|
|||||||
const response = await fetch('/api/roadmap')
|
const response = await fetch('/api/roadmap')
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
setRoadmap(data.roadmap || [])
|
setRoadmap(data.roadmap || [])
|
||||||
|
|
||||||
|
// Auto-expand only non-complete items by default
|
||||||
|
const autoExpand = new Set<number>()
|
||||||
|
data.roadmap?.forEach((item: RoadmapItem, index: number) => {
|
||||||
|
if (item.status !== 'complete') {
|
||||||
|
autoExpand.add(index)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
setExpandedItems(autoExpand)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch roadmap:', error)
|
console.error('Failed to fetch roadmap:', error)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -32,6 +42,16 @@ export default function RoadmapPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const toggleItem = (index: number) => {
|
||||||
|
const newExpanded = new Set(expandedItems)
|
||||||
|
if (newExpanded.has(index)) {
|
||||||
|
newExpanded.delete(index)
|
||||||
|
} else {
|
||||||
|
newExpanded.add(index)
|
||||||
|
}
|
||||||
|
setExpandedItems(newExpanded)
|
||||||
|
}
|
||||||
|
|
||||||
const getStatusColor = (status: string) => {
|
const getStatusColor = (status: string) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'complete':
|
case 'complete':
|
||||||
@@ -136,12 +156,18 @@ export default function RoadmapPage() {
|
|||||||
|
|
||||||
{/* Roadmap Items */}
|
{/* Roadmap Items */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{roadmap.map((item, index) => (
|
{roadmap.map((item, index) => {
|
||||||
|
const isExpanded = expandedItems.has(index)
|
||||||
|
return (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className={`bg-gray-800/50 backdrop-blur-sm border rounded-xl p-6 ${getStatusColor(item.status)}`}
|
className={`bg-gray-800/50 backdrop-blur-sm border rounded-xl overflow-hidden transition-all ${getStatusColor(item.status)}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-start justify-between mb-4">
|
<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-1">
|
||||||
<div className="flex items-center gap-3 mb-2">
|
<div className="flex items-center gap-3 mb-2">
|
||||||
<span className="text-2xl">{getStatusIcon(item.status)}</span>
|
<span className="text-2xl">{getStatusIcon(item.status)}</span>
|
||||||
@@ -150,21 +176,42 @@ export default function RoadmapPage() {
|
|||||||
{item.phase}
|
{item.phase}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-gray-300 mb-3">{item.description}</p>
|
<p className="text-gray-300">{item.description}</p>
|
||||||
<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.completed && (
|
{item.completed && (
|
||||||
<div className="text-xs text-green-400">
|
<div className="text-xs text-green-400 mt-2">
|
||||||
✓ Completed: {item.completed}
|
✓ Completed: {item.completed}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
{item.items && item.items.length > 0 && (
|
{item.items && item.items.length > 0 && (
|
||||||
<div className="border-t border-gray-700 pt-4 mt-4">
|
<div>
|
||||||
<div className="text-sm text-gray-400 mb-2">Implementation Details:</div>
|
<div className="text-sm text-gray-400 mb-2">Implementation Details:</div>
|
||||||
<ul className="space-y-1">
|
<ul className="space-y-1">
|
||||||
{item.items.map((subItem, subIndex) => (
|
{item.items.map((subItem, subIndex) => (
|
||||||
@@ -177,7 +224,11 @@ export default function RoadmapPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
|
|||||||
Reference in New Issue
Block a user