diff --git a/components/ChangelogTabs.tsx b/components/ChangelogTabs.tsx
index d73ae00..a92d8f5 100644
--- a/components/ChangelogTabs.tsx
+++ b/components/ChangelogTabs.tsx
@@ -4,6 +4,8 @@ import { useEffect, useState } from 'react'
import { PortableTextRenderer } from '@/components/PortableTextRenderer'
import { RoadmapItem } from '@/components/RoadmapItem'
import type { ChangelogEntry, RoadmapItemData, Tab, VoteState } from '@/types/changelog'
+import { useAuth } from '@/context/AuthContext'
+import Link from 'next/link'
const TABS: { id: Tab; label: string }[] = [
{ id: 'whats-new', label: "What's New" },
@@ -44,8 +46,71 @@ export function ChangelogTabs({
)
)
- // Filled in Task 5 — no-op for now
- function handleVote(_id: string) {}
+ const { user } = useAuth()
+ const [showSignInPrompt, setShowSignInPrompt] = useState(false)
+ const [votesFetched, setVotesFetched] = useState(false)
+
+ // Fetch vote state when the What's Next tab is first viewed
+ useEffect(() => {
+ if (activeTab !== 'whats-next' || votesFetched || roadmapItems.length === 0) return
+ setVotesFetched(true)
+
+ const ids = roadmapItems.map((i) => i._id).join(',')
+ fetch(`/api/roadmap/votes?ids=${encodeURIComponent(ids)}`, {
+ credentials: 'include',
+ })
+ .then((r) => (r.ok ? r.json() : {}))
+ .then((data: Record) => {
+ setVoteState((prev) => {
+ const next = { ...prev }
+ for (const [id, v] of Object.entries(data)) {
+ next[id] = { count: v.count, voted: v.voted, loading: false }
+ }
+ return next
+ })
+ })
+ .catch(() => {
+ // Spring Boot not yet available — silently keep count=0
+ })
+ }, [activeTab, votesFetched, roadmapItems])
+
+ async function handleVote(id: string) {
+ if (!user) {
+ setShowSignInPrompt(true)
+ setTimeout(() => setShowSignInPrompt(false), 4000)
+ return
+ }
+
+ const current = voteState[id] ?? { count: 0, voted: false, loading: false }
+
+ // Optimistic update
+ setVoteState((prev) => ({
+ ...prev,
+ [id]: {
+ count: current.voted ? current.count - 1 : current.count + 1,
+ voted: !current.voted,
+ loading: true,
+ },
+ }))
+
+ try {
+ const res = await fetch(`/api/roadmap/${encodeURIComponent(id)}/vote`, {
+ method: 'POST',
+ credentials: 'include',
+ })
+ if (res.ok) {
+ const data: { count: number; voted: boolean } = await res.json()
+ setVoteState((prev) => ({
+ ...prev,
+ [id]: { count: data.count, voted: data.voted, loading: false },
+ }))
+ } else {
+ setVoteState((prev) => ({ ...prev, [id]: { ...current, loading: false } }))
+ }
+ } catch {
+ setVoteState((prev) => ({ ...prev, [id]: { ...current, loading: false } }))
+ }
+ }
useEffect(() => {
const hash = window.location.hash.replace('#', '') as Tab
@@ -140,63 +205,67 @@ export function ChangelogTabs({
) : (
<>
- {roadmapItems.filter((i) => i.status === 'in-progress').length > 0 && (
-
-
- In Progress
-
-
- {roadmapItems
- .filter((i) => i.status === 'in-progress')
- .map((item) => (
-
- ))}
-
-
- )}
+ {(() => {
+ const inProgress = roadmapItems.filter((i) => i.status === 'in-progress')
+ const planned = roadmapItems.filter((i) => i.status === 'planned')
+ return (
+ <>
+ {inProgress.length > 0 && (
+
+
+ In Progress
+
+
+ {inProgress.map((item) => (
+
+ ))}
+
+
+ )}
- {roadmapItems.filter((i) => i.status === 'planned').length > 0 && (
-
-
- Planned
-
-
- {roadmapItems
- .filter((i) => i.status === 'planned')
- .map((item) => (
-
- ))}
-
-
- )}
+ {planned.length > 0 && (
+
+
+ Planned
+
+
+ {planned.map((item) => (
+
+ ))}
+
+
+ )}
+ >
+ )
+ })()}
Don't see what you need?{' '}
@@ -207,6 +276,15 @@ export function ChangelogTabs({
Give us feedback →
+
+ {showSignInPrompt && (
+
+
+ Sign in
+ {' '}
+ to upvote roadmap items.
+
+ )}
>
)}