Skip to content
Open
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
149 changes: 134 additions & 15 deletions apps/roadmap/app/(dashboard)/roadmap/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import Link from "next/link"
import {
getAllFeatures,
getStatusCounts,
Expand All @@ -6,13 +7,88 @@ import {
getLaneLabel,
getOwnerProfile,
type Lane,
type FeatureStatus,
} from "@/lib/features"
import RoadmapTimeline from "@/components/RoadmapTimeline"

export default function DashboardPage() {
const features = getAllFeatures()
const totals = getStatusCounts(features)
const VALID_STATUSES: FeatureStatus[] = [
"not-started",
"in-progress",
"complete",
"blocked",
]

const STATUS_LABELS: Record<FeatureStatus, string> = {
"not-started": "Not Started",
"in-progress": "In Progress",
complete: "Complete",
blocked: "Blocked",
}

function formatRoadmapRange(
features: ReturnType<typeof getAllFeatures>,
): string {
const datedFeatures = features.filter((feature) => feature.start_date)
if (datedFeatures.length === 0) return "No scheduled range"

let minDate = new Date(datedFeatures[0].start_date + "T00:00:00")
let maxDate = new Date(
minDate.getTime() + (datedFeatures[0].duration - 1) * 86400000,
)

for (const feature of datedFeatures) {
const start = new Date(feature.start_date + "T00:00:00")
const end = new Date(start.getTime() + (feature.duration - 1) * 86400000)
if (start < minDate) minDate = start
if (end > maxDate) maxDate = end
}

const sameYear = minDate.getFullYear() === maxDate.getFullYear()
const sameMonth = sameYear && minDate.getMonth() === maxDate.getMonth()

if (sameMonth) {
return minDate.toLocaleDateString("en-US", {
month: "long",
year: "numeric",
})
}

if (sameYear) {
return `${minDate.toLocaleDateString("en-US", {
month: "long",
})} – ${maxDate.toLocaleDateString("en-US", {
month: "long",
year: "numeric",
})}`
}

return `${minDate.toLocaleDateString("en-US", {
month: "long",
year: "numeric",
})} – ${maxDate.toLocaleDateString("en-US", {
month: "long",
year: "numeric",
})}`
}

export default async function DashboardPage({
searchParams,
}: {
searchParams: Promise<{ status?: string }>
}) {
const { status: statusParam } = await searchParams
const activeStatus =
statusParam && VALID_STATUSES.includes(statusParam as FeatureStatus)
? (statusParam as FeatureStatus)
: null

const allFeatures = getAllFeatures()
const totals = getStatusCounts(allFeatures)
const features = activeStatus
? allFeatures.filter((f) => f.status === activeStatus)
: allFeatures
const owners = getAllOwners()
const roadmapRange = formatRoadmapRange(allFeatures)
const laneLabels = Object.fromEntries(
ALL_LANES.map((l) => [l, getLaneLabel(l)]),
) as Record<Lane, string>
Expand All @@ -24,8 +100,9 @@ export default function DashboardPage() {
<div className="space-y-6">
<div>
<h1 className="text-2xl font-bold">Roadmap</h1>
<p className="mt-1 text-sm text-gray-400">
April – May 2026 · {features.length} features
<p className="mt-1 text-sm text-stone-400">
{roadmapRange} · {features.length} features
{activeStatus && ` · Filtered: ${STATUS_LABELS[activeStatus]}`}
</p>
</div>

Expand All @@ -34,26 +111,57 @@ export default function DashboardPage() {
<StatCard
label="Not Started"
count={totals["not-started"]}
color="text-gray-400"
color="text-stone-400"
href="/roadmap?status=not-started"
active={activeStatus === "not-started"}
/>
<StatCard
label="In Progress"
count={totals["in-progress"]}
color="text-blue-400"
href="/roadmap?status=in-progress"
active={activeStatus === "in-progress"}
/>
<StatCard
label="Complete"
count={totals.complete}
color="text-green-400"
href="/roadmap?status=complete"
active={activeStatus === "complete"}
/>
<StatCard
label="Blocked"
count={totals.blocked}
color="text-red-400"
href="/roadmap?status=blocked"
active={activeStatus === "blocked"}
/>
<StatCard label="Blocked" count={totals.blocked} color="text-red-400" />
</div>

{/* Active filter indicator */}
{activeStatus && (
<div className="flex items-center gap-2 text-sm text-stone-400">
<span>
Showing{" "}
<strong className="text-stone-200">
{STATUS_LABELS[activeStatus]}
</strong>{" "}
tickets
</span>
<Link
href="/roadmap"
className="rounded bg-stone-800 px-2 py-0.5 text-xs text-stone-400 hover:bg-stone-700 hover:text-white"
>
Clear filter
</Link>
</div>
)}

{/* Legend */}
<div className="flex flex-wrap gap-x-3 gap-y-1 text-xs text-gray-400">
<div className="flex flex-wrap gap-x-3 gap-y-1 text-xs text-stone-400">
<span className="flex items-center gap-1.5">
<span className="inline-block h-2 w-2 rounded-full bg-gray-400" /> Not
Started
<span className="inline-block h-2 w-2 rounded-full bg-stone-400" />{" "}
Not Started
</span>
<span className="flex items-center gap-1.5">
<span className="inline-block h-2 w-2 rounded-full bg-blue-400" /> In
Expand All @@ -67,10 +175,10 @@ export default function DashboardPage() {
<span className="inline-block h-2 w-2 rounded-full bg-red-400" />{" "}
Blocked
</span>
<span className="flex items-center gap-2 border-l border-gray-700 pl-3">
<span className="flex items-center gap-2 border-l border-stone-700 pl-3">
<span className="border-l-2 border-l-red-500 pl-1">P0</span>
<span className="border-l-2 border-l-yellow-500 pl-1">P1</span>
<span className="border-l-2 border-l-gray-500 pl-1">P2</span>
<span className="border-l-2 border-l-stone-500 pl-1">P2</span>
</span>
</div>

Expand All @@ -90,15 +198,26 @@ function StatCard({
label,
count,
color,
href,
active,
}: {
label: string
count: number
color: string
href: string
active: boolean
}) {
return (
<div className="rounded-lg border border-[var(--color-border)] bg-[var(--color-card)] p-3 sm:p-4">
<Link
href={active ? "/roadmap" : href}
className={`rounded-lg border p-3 transition-colors sm:p-4 ${
active
? "border-stone-500 bg-stone-800"
: "border-[var(--color-border)] bg-[var(--color-card)] hover:border-stone-500"
}`}
>
<div className={`text-2xl font-bold sm:text-3xl ${color}`}>{count}</div>
<div className="mt-1 text-xs text-gray-400 sm:text-sm">{label}</div>
</div>
<div className="mt-1 text-xs text-stone-400 sm:text-sm">{label}</div>
</Link>
)
}
38 changes: 23 additions & 15 deletions apps/roadmap/app/(public)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export default function HomePage() {
<WorldMapBackground />

{/* Warm signpost */}
<div className="rounded-lg border border-[var(--color-border)] bg-[var(--color-card)] px-4 py-3 text-center text-sm text-gray-400">
<div className="rounded-lg border border-[var(--color-border)] bg-[var(--color-card)] px-4 py-3 text-center text-sm text-stone-400">
Looking for Jesus Film?{" "}
<a
href="https://www.jesusfilm.org"
Expand All @@ -63,37 +63,40 @@ export default function HomePage() {
<br />
through the power of AI
</h1>
<p className="mx-auto max-w-2xl text-lg leading-relaxed text-gray-400">
<p className="mx-auto max-w-2xl text-lg leading-relaxed text-stone-400">
The Digital Strategies Department is building trusted AI capabilities
that help people discover gospel content, engage meaningfully with
Scripture, and take faithful next steps.
</p>
<p className="text-sm text-gray-500">
<p className="text-sm text-stone-500">
This is our public roadmap. See what we&apos;re building, what
we&apos;ve shipped, and where we&apos;re headed.
</p>
</section>

{/* Momentum stats */}
<section className="space-y-4">
<h2 className="text-center text-xs font-semibold uppercase tracking-wider text-gray-500">
<h2 className="text-center text-xs font-semibold uppercase tracking-wider text-stone-500">
Progress at a glance
</h2>
<div className="grid grid-cols-3 gap-3">
<StatCard
label="Features Shipped"
count={totals.complete}
color="text-green-400"
href="/roadmap?status=complete"
/>
<StatCard
label="In Progress"
count={inProgressCount}
color="text-blue-400"
href="/roadmap?status=in-progress"
/>
<StatCard
label="Total Planned"
count={features.length}
color="text-gray-300"
color="text-stone-300"
href="/roadmap"
/>
</div>
</section>
Expand All @@ -109,13 +112,13 @@ export default function HomePage() {
className="rounded-lg border border-[var(--color-border)] bg-[var(--color-card)]"
>
<Link href={`/ticket/${feature.id}`} className="block p-4">
<div className="text-xs text-gray-500">
<div className="text-xs text-stone-500">
{getLaneLabel(feature.lane)}
</div>
<h3 className="mt-1 text-sm font-semibold">
{feature.title}
</h3>
<div className="mt-2 text-xs text-gray-500">
<div className="mt-2 text-xs text-stone-500">
Completed {formatDate(getCompletedEndDate(feature))}
</div>
</Link>
Expand All @@ -142,12 +145,12 @@ export default function HomePage() {
</span>
<h3 className="text-sm font-semibold">{exp.title}</h3>
</div>
<p className="mt-2 line-clamp-2 text-xs leading-relaxed text-gray-400">
<p className="mt-2 line-clamp-2 text-xs leading-relaxed text-stone-400">
{exp.description}
</p>
<div className="mt-3">
{exp.comingSoon ? (
<span className="text-xs text-gray-500">Coming soon</span>
<span className="text-xs text-stone-500">Coming soon</span>
) : exp.links[0] ? (
<a
href={exp.links[0].href}
Expand All @@ -157,7 +160,7 @@ export default function HomePage() {
>
{exp.links[0].label} &#8599;
{exp.loginRequired && (
<span className="ml-1 text-gray-500">
<span className="ml-1 text-stone-500">
(login required)
</span>
)}
Expand All @@ -173,14 +176,14 @@ export default function HomePage() {
<section className="space-y-4 text-center">
<Link
href="/roadmap"
className="inline-flex items-center gap-2 rounded-lg bg-blue-600 px-6 py-3 text-sm font-semibold text-white transition-colors hover:bg-blue-500"
className="inline-flex items-center gap-2 rounded-lg bg-[#EF3340] px-6 py-3 text-sm font-semibold text-white transition-colors hover:bg-[#d92d39]"
>
Explore the Roadmap &rarr;
</Link>
<div>
<Link
href="/about"
className="text-sm text-gray-400 hover:text-gray-300"
className="text-sm text-stone-400 hover:text-stone-300"
>
Learn more about our mission
</Link>
Expand All @@ -194,15 +197,20 @@ function StatCard({
label,
count,
color,
href,
}: {
label: string
count: number
color: string
href: string
}) {
return (
<div className="rounded-lg border border-[var(--color-border)] bg-[var(--color-card)] p-4 text-center">
<Link
href={href}
className="rounded-lg border border-[var(--color-border)] bg-[var(--color-card)] p-4 text-center transition-colors hover:border-stone-500"
>
<div className={`text-2xl font-bold sm:text-3xl ${color}`}>{count}</div>
<div className="mt-1 text-xs text-gray-400">{label}</div>
</div>
<div className="mt-1 text-xs text-stone-400">{label}</div>
</Link>
)
}
Loading