feat: Ideas UI verbeteringen — hernoeming, tab-states, timeline refresh

- Nav-label 'Ideeën' hernoemd naar 'Ideas'; breadcrumb idem
- Grill/Plan tabs disabled (grijs, cursor-not-allowed) zolang er geen
  content is; groene stip zodra grill_md resp. plan_md beschikbaar is
- SSE hook roept router.refresh() aan bij job done/failed zodat de
  Timeline automatisch de nieuwe GRILL_RESULT/PLAN_RESULT logs toont

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Janpeter Visser 2026-05-05 21:13:56 +02:00
parent 474a8da053
commit 649c87b658
3 changed files with 27 additions and 15 deletions

View file

@ -40,13 +40,6 @@ const API_TO_DB: Record<IdeaStatusApi, Parameters<typeof getIdeaStatusBadge>[0]>
type TabKey = 'idee' | 'grill' | 'plan' | 'timeline'
const TABS: { key: TabKey; label: string }[] = [
{ key: 'idee', label: 'Idee' },
{ key: 'grill', label: 'Grill' },
{ key: 'plan', label: 'Plan' },
{ key: 'timeline', label: 'Timeline' },
]
interface IdeaLog {
id: string
type: string
@ -106,7 +99,8 @@ export function IdeaDetailLayout({
const searchParams = useSearchParams()
const [pending, startTransition] = useTransition()
const tab = (TABS.some((t) => t.key === initialTab) ? initialTab : 'idee') as TabKey
const TAB_KEYS: TabKey[] = ['idee', 'grill', 'plan', 'timeline']
const tab = (TAB_KEYS.includes(initialTab as TabKey) ? initialTab : 'idee') as TabKey
function setTab(key: TabKey) {
const params = new URLSearchParams(searchParams.toString())
@ -138,7 +132,7 @@ export function IdeaDetailLayout({
className="inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground"
>
<ArrowLeft className="size-4" />
Alle ideeën
Alle ideas
</Link>
{/* Header */}
@ -171,18 +165,29 @@ export function IdeaDetailLayout({
{/* Tab-switcher */}
<nav className="border-b border-input flex gap-1">
{TABS.map((t) => (
{([
{ key: 'idee' as TabKey, label: 'Idee', disabled: false, hasContent: true },
{ key: 'grill' as TabKey, label: 'Grill', disabled: !grill_md, hasContent: !!grill_md },
{ key: 'plan' as TabKey, label: 'Plan', disabled: !plan_md, hasContent: !!plan_md },
{ key: 'timeline' as TabKey, label: 'Timeline', disabled: false, hasContent: true },
] as const).map((t) => (
<button
key={t.key}
type="button"
onClick={() => setTab(t.key)}
onClick={() => !t.disabled && setTab(t.key)}
disabled={t.disabled}
className={`px-4 py-2 text-sm border-b-2 transition-colors ${
tab === t.key
? 'border-primary text-foreground'
: 'border-transparent text-muted-foreground hover:text-foreground'
t.disabled
? 'border-transparent text-muted-foreground/40 cursor-not-allowed'
: tab === t.key
? 'border-primary text-foreground'
: 'border-transparent text-muted-foreground hover:text-foreground'
}`}
>
{t.label}
{t.hasContent && !t.disabled && t.key !== 'idee' && t.key !== 'timeline' && (
<span className="ml-1 text-[10px] text-status-done"></span>
)}
{t.key === 'timeline' && (logs.length > 0 || questions.length > 0) ? (
<span className="ml-1.5 text-xs text-muted-foreground">
({logs.length + questions.length})

View file

@ -140,7 +140,7 @@ export function NavBar({
)
: disabledSpan('Solo')}
{navLink('/insights', 'Insights', pathname.startsWith('/insights'))}
{navLink('/ideas', 'Ideeën', pathname.startsWith('/ideas'))}
{navLink('/ideas', 'Ideas', pathname.startsWith('/ideas'))}
{navLink('/todos', "Todo's", pathname.startsWith('/todos'))}
{roles.includes('ADMIN') && navLink('/admin', 'Admin', pathname.startsWith('/admin'))}
</nav>

View file

@ -122,6 +122,13 @@ export function useNotificationsRealtime() {
status: payload.status as 'queued',
error: payload.error,
})
// Refresh zodra job klaar is — server heeft nu grill_md/plan_md
// geschreven en de idea-status bijgewerkt. router.refresh() triggert
// een server-component re-fetch zodat de Timeline de nieuwe
// GRILL_RESULT/PLAN_RESULT logs en de bijgewerkte status oppikt.
if (payload.status === 'done' || payload.status === 'failed') {
router.refresh()
}
return
}