"use client"; import type { RoadmapItem, RoadmapStatus } from "convex-feedback"; import { useState } from "react"; import { FeedbackProvider, useFeedbackUi, } from "../shared/context/FeedbackProvider.js"; import { allowAuthenticatedAction } from "../shared/helpers.js"; import type { RoadmapScreenProps } from "../shared/types/index.js"; import { FeedbackActionError, useFeedbackAction } from "./action.js"; import { FeedbackBoard, FeedbackEntry } from "./primitives.js"; const stages: readonly { value: RoadmapStatus }[] = [ { value: "planned" }, { value: "in_progress" }, { value: "shipped" }, ]; export function RoadmapScreen({ hooks, messages, theme, unstyled, onEntryOpen, onUnauthenticated, pageSize, entryPageSize, ...colors }: RoadmapScreenProps) { const resolvedPageSize = pageSize ?? hooks.pageSizes.roadmap; const resolvedEntryPageSize = entryPageSize ?? hooks.pageSizes.entries; return ( ); } function RoadmapScreenInner({ hooks, onEntryOpen, onUnauthenticated, pageSize, entryPageSize, ...colors }: Omit & { pageSize: number; entryPageSize: number; }) { const [query, setQuery] = useState(""); const [selectedId, setSelectedId] = useState(null); const roadmap = hooks.useRoadmap(); const search = hooks.useSearchRoadmap(query); const selectedQuery = hooks.useRoadmapItem(selectedId); const searching = query.trim().length > 0; const items = searching ? (search ?? []) : roadmap.results; const optimisticSelected = items.find((item) => item.id === selectedId) ?? null; const selected = selectedQuery === undefined ? optimisticSelected : selectedQuery; return ( {selected === null ? ( setSelectedId(item.id)} canLoadMore={ !searching && (roadmap.status === "CanLoadMore" || roadmap.status === "LoadingMore") } loadingMore={roadmap.status === "LoadingMore"} onLoadMore={() => roadmap.loadMore(pageSize)} /> ) : ( setSelectedId(null)} /> )} ); } function RoadmapBoard({ items, loading, searching, query, onQueryChange, onItemOpen, canLoadMore, loadingMore, onLoadMore, }: { items: readonly RoadmapItem[]; loading: boolean; searching: boolean; query: string; onQueryChange: (value: string) => void; onItemOpen: (item: RoadmapItem) => void; canLoadMore: boolean; loadingMore: boolean; onLoadMore: () => void; }) { const { messages } = useFeedbackUi(); return ( <>
{messages.roadmap.title}

{messages.roadmap.subtitle}

{loading ? ( {messages.board.loading} ) : items.length === 0 ? ( {searching ? messages.roadmap.noSearchResults : messages.roadmap.noItems} ) : (
{stages.map((stage) => { const stageItems = items .filter((item) => item.status === stage.value) .sort((a, b) => a.position - b.position); return (
{stageItems.length}
{stageItems.length === 0 ? (

{messages.roadmap.noItems}

) : ( stageItems.map((item) => ( onItemOpen(item)} /> )) )}
); })}
)} {canLoadMore && ( )} ); } function RoadmapCard({ item, onOpen, }: { item: RoadmapItem; onOpen: () => void; }) { const { messages } = useFeedbackUi(); return ( ); } function RoadmapDetail({ item, hooks, entryPageSize, onEntryOpen, onUnauthenticated, onBack, }: { item: RoadmapItem; hooks: RoadmapScreenProps["hooks"]; entryPageSize: number; onEntryOpen: RoadmapScreenProps["onEntryOpen"]; onUnauthenticated: RoadmapScreenProps["onUnauthenticated"]; onBack: () => void; }) { const { messages } = useFeedbackUi(); const feedback = hooks.useRoadmapFeedback(item.id); return (
{messages.roadmap.statuses[item.status]}

{item.title}

{item.description && (

{item.description}

)}

{messages.roadmap.attachedFeedback}

{feedback.status === "LoadingFirstPage" ? (

{messages.board.loading}

) : feedback.results.length === 0 ? (

{messages.roadmap.noAttachedFeedback}

) : (
{feedback.results.map((entry) => ( ))}
)} {(feedback.status === "CanLoadMore" || feedback.status === "LoadingMore") && ( )}
); } function RoadmapEntry({ entry, hooks, onOpen, onUnauthenticated, }: { entry: Parameters[0]["entry"]; hooks: RoadmapScreenProps["hooks"]; onOpen: RoadmapScreenProps["onEntryOpen"]; onUnauthenticated: RoadmapScreenProps["onUnauthenticated"]; }) { const isAuthenticated = hooks.useIsAuthenticated(); const setUpvote = hooks.useSetEntryUpvote(); const action = useFeedbackAction(); const toggle = (desiredState: boolean) => { if ( !allowAuthenticatedAction(isAuthenticated, onUnauthenticated) || action.pending ) { return; } void action.run(() => setUpvote({ entryId: entry.id, desiredState })); }; return ( <> onOpen?.(entry.id)} onKeyDown={(event) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); onOpen?.(entry.id); } }} role={onOpen === undefined ? undefined : "button"} tabIndex={onOpen === undefined ? undefined : 0} >
); }