Files
zeekayandHanzo Dev db1312976e Build the board's head and give a card its words
The board began at its columns, so the screen had no trail, no title, no
view row and no filter — the whole band the reference draws above the
work, and the band every measurement below it is taken from.

A card drew a count where its tags belong, one face where a team was,
and a ring reading zero on every card because nothing carried an
estimate. Tags, components and collaborators are documents the model
already names; the board reads them in one pass rather than one per
card.

An issue is opened IN a project, so a board across all of them has no
`add` and draws no control that would refuse. The same reasoning gives
the three answers a region has before a space is open — still opening,
refused, nowhere — one mapping, so the board and the inbox beside it
cannot report different fictions.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-08 15:51:18 -07:00

95 lines
3.9 KiB
TypeScript

/**
* EVERY ADDRESS THIS CLIENT ANSWERS, and the only module that knows both a
* navigator and a view.
*
* The shell is composed HERE rather than inside each surface, so a surface
* cannot forget it and no two surfaces can draw it differently — one rail, one
* navigator column, one aside, whatever screen is showing. A surface component
* renders only its own region.
*
* The rail's list lives in `~/shell/surfaces`, and the paths below are that
* table's. A surface reachable from the rail and missing here is a glyph that
* goes nowhere, which is exactly what one declaration prevents.
*
* A board's default view is the board's own address, so Kanban has one address
* and not two. The splat ranks last and catches every address this product ever
* answered and no longer does, so a stale bookmark is a redirect rather than
* the router's own error page.
*/
import { EmptyState } from '@hanzo/ui/product'
import { SquareCheck } from '@hanzogui/lucide-icons-2'
import type { ReactNode } from 'react'
import type { RouteObject } from 'react-router'
import { Navigate, useParams } from 'react-router'
import { useProjects } from '~/serve'
import { Chat, Channels } from '~/chat'
import { Inbox } from '~/inbox'
import { Nav } from '~/nav'
import { Shell } from '~/shell'
import { Tracker } from '~/tracker'
/** A screen this build has not written. It says which one and what belongs on
* it, because a blank screen and an unfinished one look identical and only one
* of them is honest. */
const soon = (title: string, what: string) => (
<EmptyState icon={SquareCheck} title={title} description={`${what} Not built yet.`} />
)
/** A project's page, named from the address so the screen is about the thing you
* clicked rather than about the router. */
const Page = () => {
const { page } = useParams()
const name = page ? page[0].toUpperCase() + page.slice(1) : 'This page'
return soon(name, `${name} for one project.`)
}
/**
* Where a visit starts: your first project's board.
*
* An issue is opened IN a project, so the board that can open one is a
* project's — and that is the screen a person came here to work in. Until the
* projects answer there is nothing to redirect to, so the shell stands with an
* empty main rather than flashing a second screen on the way.
*/
const Landing = () => {
const projects = useProjects()
if (projects.phase === 'loading') return tracker(null)
const first = [...(projects.data ?? [])].sort((a, b) => a.name.localeCompare(b.name))[0]
return <Navigate to={first ? `/projects/${first._id}/issues` : '/issues'} replace />
}
const tracker = (view: ReactNode) => (
<Shell nav={<Nav />} aside={<Inbox />}>
{view}
</Shell>
)
const chat = (view: ReactNode) => (
<Shell nav={<Channels />} aside={<Inbox />}>
{view}
</Shell>
)
export const routes: RouteObject[] = [
{ path: '/', element: <Landing /> },
{ path: '/issues', element: tracker(<Tracker />) },
{ path: '/issues/:view', element: tracker(<Tracker />) },
{ path: '/mine', element: tracker(soon('My issues', 'Everything assigned to you, across every project.')) },
{ path: '/projects', element: tracker(soon('All projects', 'The projects you are in, and the ones you could join.')) },
{ path: '/projects/:id/issues', element: tracker(<Tracker />) },
{ path: '/projects/:id/issues/:view', element: tracker(<Tracker />) },
{ path: '/projects/:id/:page', element: tracker(<Page />) },
{ path: '/chat', element: chat(<Chat />) },
{ path: '/chat/:id', element: chat(<Chat />) },
{ path: '/calendar', element: <Shell nav={null}>{soon('Calendar', 'Meetings, deadlines and the week beside the work.')}</Shell> },
{ path: '/time', element: <Shell nav={null}>{soon('Time', 'What the work took, against what it was estimated at.')}</Shell> },
{ path: '/meet', element: <Shell nav={null}>{soon('Meet', 'Calls and huddles, from the room you are already in.')}</Shell> },
{ path: '*', element: <Navigate to="/issues" replace /> },
]