feat: add roadmap tab with static RoadmapItem component
This commit is contained in:
@@ -2,7 +2,8 @@
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { PortableTextRenderer } from '@/components/PortableTextRenderer'
|
||||
import type { ChangelogEntry, RoadmapItemData, Tab } from '@/types/changelog'
|
||||
import { RoadmapItem } from '@/components/RoadmapItem'
|
||||
import type { ChangelogEntry, RoadmapItemData, Tab, VoteState } from '@/types/changelog'
|
||||
|
||||
const TABS: { id: Tab; label: string }[] = [
|
||||
{ id: 'whats-new', label: "What's New" },
|
||||
@@ -33,6 +34,19 @@ export function ChangelogTabs({
|
||||
}) {
|
||||
const [activeTab, setActiveTab] = useState<Tab>('whats-new')
|
||||
|
||||
const [voteState, setVoteState] = useState<Record<string, VoteState>>(
|
||||
() =>
|
||||
Object.fromEntries(
|
||||
roadmapItems.map((item) => [
|
||||
item._id,
|
||||
{ count: 0, voted: false, loading: false },
|
||||
])
|
||||
)
|
||||
)
|
||||
|
||||
// Filled in Task 5 — no-op for now
|
||||
function handleVote(_id: string) {}
|
||||
|
||||
useEffect(() => {
|
||||
const hash = window.location.hash.replace('#', '') as Tab
|
||||
if (TABS.some((t) => t.id === hash)) setActiveTab(hash)
|
||||
@@ -117,9 +131,85 @@ export function ChangelogTabs({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── What's Next — stub, filled in Task 3 ── */}
|
||||
{/* ── What's Next ── */}
|
||||
{activeTab === 'whats-next' && (
|
||||
<p className="text-sm text-zinc-500">Roadmap coming soon.</p>
|
||||
<div>
|
||||
{roadmapItems.length === 0 ? (
|
||||
<p className="text-sm text-zinc-500">
|
||||
No roadmap items yet — check back soon.
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
{roadmapItems.filter((i) => i.status === 'in-progress').length > 0 && (
|
||||
<div className="mb-8">
|
||||
<p className="mb-3 text-[10px] font-bold uppercase tracking-[0.18em] text-zinc-600">
|
||||
In Progress
|
||||
</p>
|
||||
<div className="flex flex-col gap-2.5">
|
||||
{roadmapItems
|
||||
.filter((i) => i.status === 'in-progress')
|
||||
.map((item) => (
|
||||
<RoadmapItem
|
||||
key={item._id}
|
||||
id={item._id}
|
||||
title={item.title}
|
||||
description={item.description}
|
||||
status={item.status}
|
||||
vote={
|
||||
voteState[item._id] ?? {
|
||||
count: 0,
|
||||
voted: false,
|
||||
loading: false,
|
||||
}
|
||||
}
|
||||
onVote={handleVote}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{roadmapItems.filter((i) => i.status === 'planned').length > 0 && (
|
||||
<div>
|
||||
<p className="mb-3 text-[10px] font-bold uppercase tracking-[0.18em] text-zinc-600">
|
||||
Planned
|
||||
</p>
|
||||
<div className="flex flex-col gap-2.5">
|
||||
{roadmapItems
|
||||
.filter((i) => i.status === 'planned')
|
||||
.map((item) => (
|
||||
<RoadmapItem
|
||||
key={item._id}
|
||||
id={item._id}
|
||||
title={item.title}
|
||||
description={item.description}
|
||||
status={item.status}
|
||||
vote={
|
||||
voteState[item._id] ?? {
|
||||
count: 0,
|
||||
voted: false,
|
||||
loading: false,
|
||||
}
|
||||
}
|
||||
onVote={handleVote}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="mt-8 text-xs text-zinc-600">
|
||||
Don't see what you need?{' '}
|
||||
<button
|
||||
onClick={() => handleTabChange('give-feedback')}
|
||||
className="underline underline-offset-2 hover:text-zinc-300"
|
||||
>
|
||||
Give us feedback →
|
||||
</button>
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Give Feedback — stub, filled in Task 6 ── */}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
'use client'
|
||||
|
||||
import { ChevronUpIcon } from '@heroicons/react/20/solid'
|
||||
import type { VoteState } from '@/types/changelog'
|
||||
|
||||
export function RoadmapItem({
|
||||
id,
|
||||
title,
|
||||
description,
|
||||
status,
|
||||
vote,
|
||||
onVote,
|
||||
}: {
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
status: 'planned' | 'in-progress'
|
||||
vote: VoteState
|
||||
onVote: (id: string) => void
|
||||
}) {
|
||||
const statusBadgeClass =
|
||||
status === 'in-progress'
|
||||
? 'border-amber-500/25 bg-amber-500/10 text-amber-400'
|
||||
: 'border-zinc-700 bg-zinc-800/40 text-zinc-500'
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-[52px_1fr_auto] items-center gap-4 rounded-xl border border-zinc-800/60 bg-zinc-950/60 p-5">
|
||||
{/* Upvote button */}
|
||||
<button
|
||||
onClick={() => onVote(id)}
|
||||
disabled={vote.loading}
|
||||
title={vote.voted ? 'Remove vote' : 'Upvote'}
|
||||
className={[
|
||||
'flex h-[52px] w-11 flex-col items-center justify-center gap-0.5 rounded-lg border text-xs font-bold transition-colors disabled:opacity-50',
|
||||
vote.voted
|
||||
? 'border-amber-500/40 bg-amber-500/10 text-amber-400'
|
||||
: 'border-zinc-700 text-zinc-500 hover:border-amber-500/40 hover:text-amber-400',
|
||||
].join(' ')}
|
||||
>
|
||||
<ChevronUpIcon className="h-4 w-4" />
|
||||
<span>{vote.count}</span>
|
||||
</button>
|
||||
|
||||
{/* Content */}
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-zinc-200">{title}</p>
|
||||
<p className="mt-1 text-xs leading-relaxed text-zinc-500">{description}</p>
|
||||
</div>
|
||||
|
||||
{/* Status badge */}
|
||||
<span
|
||||
className={`rounded-full border px-2.5 py-0.5 text-[10px] font-bold uppercase tracking-wider ${statusBadgeClass}`}
|
||||
>
|
||||
{status === 'in-progress' ? 'In Progress' : 'Planned'}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user