feat(ST-355): add /solo route, cookie helper, product picker, and solo product page

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Janpeter Visser 2026-04-26 16:29:31 +02:00
parent 802aa58157
commit ca631e71ec
7 changed files with 230 additions and 1 deletions

View file

@ -0,0 +1,81 @@
import { notFound, redirect } from 'next/navigation'
import { getSession } from '@/lib/auth'
import { getAccessibleProduct } from '@/lib/product-access'
import { setLastProductCookie } from '@/lib/cookies'
import { prisma } from '@/lib/prisma'
import { SoloBoard } from '@/components/solo/solo-board'
import { NoActiveSprint } from '@/components/solo/no-active-sprint'
import type { SoloTask } from '@/components/solo/solo-board'
interface Props {
params: Promise<{ id: string }>
}
export default async function SoloProductPage({ params }: Props) {
const { id } = await params
const session = await getSession()
if (!session.userId) redirect('/login')
const product = await getAccessibleProduct(id, session.userId)
if (!product) notFound()
await setLastProductCookie(id)
const sprint = await prisma.sprint.findFirst({
where: { product_id: id, status: 'ACTIVE' },
})
if (!sprint) {
return (
<div className="flex flex-col h-full">
<NoActiveSprint productId={id} productName={product.name} />
</div>
)
}
const [rawTasks, unassignedCount] = await Promise.all([
prisma.task.findMany({
where: {
story: {
sprint_id: sprint.id,
assignee_id: session.userId,
},
},
include: {
story: { select: { id: true, title: true } },
},
orderBy: [
{ story: { sort_order: 'asc' } },
{ priority: 'asc' },
{ sort_order: 'asc' },
],
}),
prisma.story.count({
where: { sprint_id: sprint.id, assignee_id: null },
}),
])
const tasks: SoloTask[] = rawTasks.map(t => ({
id: t.id,
title: t.title,
description: t.description,
implementation_plan: t.implementation_plan,
priority: t.priority,
sort_order: t.sort_order,
status: t.status as SoloTask['status'],
story_id: t.story.id,
story_title: t.story.title,
}))
return (
<SoloBoard
productId={id}
productName={product.name}
sprintGoal={sprint.sprint_goal}
tasks={tasks}
unassignedCount={unassignedCount}
isDemo={session.isDemo ?? false}
currentUserId={session.userId}
/>
)
}

26
app/(app)/solo/page.tsx Normal file
View file

@ -0,0 +1,26 @@
import { redirect } from 'next/navigation'
import { getSession } from '@/lib/auth'
import { getLastProductCookie } from '@/lib/cookies'
import { getAccessibleProduct, productAccessFilter } from '@/lib/product-access'
import { prisma } from '@/lib/prisma'
import { ProductPicker } from '@/components/solo/product-picker'
export default async function SoloPage() {
const session = await getSession()
if (!session.userId) redirect('/login')
const lastProductId = await getLastProductCookie()
if (lastProductId) {
const product = await getAccessibleProduct(lastProductId, session.userId)
if (product && !product.archived) redirect(`/products/${lastProductId}/solo`)
}
const products = await prisma.product.findMany({
where: { archived: false, ...productAccessFilter(session.userId) },
orderBy: { created_at: 'desc' },
select: { id: true, name: true, description: true },
})
return <ProductPicker products={products} />
}