import { useState, useEffect } from 'react' import { useParams, useSearchParams } from 'react-router-dom' import { useBranding } from '../hooks/useBranding' interface EmbedPost { id: string title: string type: 'FEATURE_REQUEST' | 'BUG_REPORT' status: string voteCount: number isPinned: boolean commentCount: number createdAt: string } interface EmbedData { board: { name: string; slug: string } posts: EmbedPost[] } const STATUS_DARK: Record = { OPEN: { bg: 'rgba(245,158,11,0.12)', color: '#F59E0B', label: 'Open' }, UNDER_REVIEW: { bg: 'rgba(8,196,228,0.12)', color: '#08C4E4', label: 'Under Review' }, PLANNED: { bg: 'rgba(109,181,252,0.12)', color: '#6DB5FC', label: 'Planned' }, IN_PROGRESS: { bg: 'rgba(234,179,8,0.12)', color: '#EAB308', label: 'In Progress' }, DONE: { bg: 'rgba(34,197,94,0.12)', color: '#22C55E', label: 'Done' }, DECLINED: { bg: 'rgba(249,138,138,0.12)', color: '#F98A8A', label: 'Declined' }, } const STATUS_LIGHT: Record = { OPEN: { bg: 'rgba(112,73,9,0.12)', color: '#704909', label: 'Open' }, UNDER_REVIEW: { bg: 'rgba(10,92,115,0.12)', color: '#0A5C73', label: 'Under Review' }, PLANNED: { bg: 'rgba(26,64,176,0.12)', color: '#1A40B0', label: 'Planned' }, IN_PROGRESS: { bg: 'rgba(116,67,12,0.12)', color: '#74430C', label: 'In Progress' }, DONE: { bg: 'rgba(22,101,52,0.12)', color: '#166534', label: 'Done' }, DECLINED: { bg: 'rgba(153,25,25,0.12)', color: '#991919', label: 'Declined' }, } function timeAgo(date: string): string { const s = Math.floor((Date.now() - new Date(date).getTime()) / 1000) if (s < 60) return 'just now' const m = Math.floor(s / 60) if (m < 60) return `${m}m ago` const h = Math.floor(m / 60) if (h < 24) return `${h}h ago` const d = Math.floor(h / 24) if (d < 30) return `${d}d ago` return `${Math.floor(d / 30)}mo ago` } export default function EmbedBoard() { const { boardSlug } = useParams<{ boardSlug: string }>() const [searchParams] = useSearchParams() const [data, setData] = useState(null) const [error, setError] = useState(false) const { appName, poweredByVisible } = useBranding() const theme = searchParams.get('theme') || 'dark' const limit = searchParams.get('limit') || '10' const sort = searchParams.get('sort') || 'top' const isDark = theme === 'dark' useEffect(() => { if (!boardSlug) return const params = new URLSearchParams({ limit, sort }) fetch(`/api/v1/embed/${boardSlug}/posts?${params}`) .then((r) => { if (!r.ok) throw new Error(); return r.json() }) .then(setData) .catch(() => setError(true)) }, [boardSlug, limit, sort]) const colors = isDark ? { bg: '#161616', surface: '#1e1e1e', border: 'rgba(255,255,255,0.08)', text: '#f0f0f0', textSec: 'rgba(240,240,240,0.72)', textTer: 'rgba(240,240,240,0.71)', accent: '#F59E0B' } : { bg: '#f7f8fa', surface: '#ffffff', border: 'rgba(0,0,0,0.08)', text: '#1a1a1a', textSec: '#4a4a4a', textTer: '#545454', accent: '#704909' } if (error) { return (
Board not found
) } if (!data) { return (
Loading...
) } // build the base URL for linking back to the main app const origin = window.location.origin return (
{/* Header */}
{data.board.name} View all
{/* Posts */} {data.posts.length === 0 && (
No posts yet
)} {data.posts.map((post) => { const statusMap = isDark ? STATUS_DARK : STATUS_LIGHT const sc = statusMap[post.status] || { bg: colors.surface, color: colors.textSec, label: post.status } return ( { e.currentTarget.style.borderColor = colors.accent }} onMouseLeave={(e) => { e.currentTarget.style.borderColor = colors.border }} onFocus={(e) => { e.currentTarget.style.borderColor = colors.accent }} onBlur={(e) => { e.currentTarget.style.borderColor = colors.border }} > {/* Vote count */}
{post.voteCount}
{/* Content */}
{post.isPinned && Pinned} {post.title}
{sc.label} {post.commentCount} comment{post.commentCount !== 1 ? 's' : ''}
) })} {/* Footer */} {poweredByVisible && (
Powered by {appName}
)}
) }