Add React/Next.js + shadcn/ui frontend spec
This commit is contained in:
245
docs/web-spec.md
Normal file
245
docs/web-spec.md
Normal file
@@ -0,0 +1,245 @@
|
||||
Create a React/Next.js frontend with shadcn/ui for the Tessera ticketing system.
|
||||
|
||||
The backend API server lives at /home/gjermund/projects/tessera/src/ (Hono/Bun, serves on port 9876).
|
||||
The frontend will live at /home/gjermund/projects/tessera/web/ (Next.js 15 App Router, dev on port 5173).
|
||||
|
||||
## Stack
|
||||
- Next.js 15 (App Router, TypeScript, strict mode)
|
||||
- shadcn/ui (component library — code lives in web/src/components/ui/)
|
||||
- TanStack Table v8 (for data tables — sorting, filtering, pagination)
|
||||
- Tailwind CSS v4 (via shadcn/ui)
|
||||
- React Hook Form + Zod (form validation)
|
||||
- lucide-react (icons)
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Create Next.js app
|
||||
```bash
|
||||
cd /home/gjermund/projects/tessera
|
||||
npx create-next-app@latest web --typescript --tailwind --eslint --app --src-dir --import-alias "@/*" --no-turbopack
|
||||
cd web
|
||||
```
|
||||
|
||||
### 2. Install shadcn/ui
|
||||
```bash
|
||||
npx shadcn@latest init
|
||||
```
|
||||
- Style: Default
|
||||
- Base color: Neutral
|
||||
- CSS variables: Yes
|
||||
|
||||
### 3. Install shadcn/ui components
|
||||
```bash
|
||||
npx shadcn@latest add button input select textarea dialog tabs table card badge label separator dropdown-menu
|
||||
npx shadcn@latest add form
|
||||
npx shadcn@latest add data-table
|
||||
npx shadcn@latest add sheet
|
||||
npx shadcn@latest add tooltip
|
||||
```
|
||||
|
||||
Also install:
|
||||
```bash
|
||||
bun add @tanstack/react-table zod react-hook-form @hookform/resolvers lucide-react date-fns
|
||||
```
|
||||
|
||||
### 4. Proxy config
|
||||
In `web/next.config.ts`:
|
||||
```typescript
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
async rewrites() {
|
||||
return [
|
||||
{
|
||||
source: '/api/:path*',
|
||||
destination: 'http://127.0.0.1:9876/api/:path*',
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
```
|
||||
|
||||
### 5. API Client
|
||||
File: `web/src/lib/api.ts`
|
||||
|
||||
A typed fetch wrapper for all Tessera endpoints. Every function returns `{ data, error }`. On HTTP error, return `{ data: null, error: message }`. Use proper TypeScript types.
|
||||
|
||||
Functions:
|
||||
- `getTickets(params?: { queue_id?: string; status?: string }): Promise<{ data: Ticket[] | null; error: string | null }>`
|
||||
- `getTicket(id: string): Promise<{ data: Ticket | null; error: string | null }>`
|
||||
- `createTicket(data: { subject: string; queue_id: string }): Promise<{ data: Ticket | null; error: string | null }>`
|
||||
- `updateTicket(id: string, data: { subject?: string; status?: string }): Promise<{ data: UpdateResult | null; error: string | null }>`
|
||||
- `previewTicket(id: string, data: { status?: string }): Promise<{ data: PreviewResult | null; error: string | null }>`
|
||||
- `getTicketTransactions(id: string): Promise<{ data: Transaction[] | null; error: string | null }>`
|
||||
- `getQueues(): Promise<{ data: Queue[] | null; error: string | null }>`
|
||||
- `createQueue(data: { name: string; description?: string }): Promise<{ data: Queue | null; error: string | null }>`
|
||||
- `getScrips(): Promise<{ data: Scrip[] | null; error: string | null }>`
|
||||
- `createScrip(data): Promise<{ data: Scrip | null; error: string | null }>`
|
||||
- `updateScrip(id: string, data): Promise<{ data: Scrip | null; error: string | null }>`
|
||||
- `getLifecycles(): Promise<{ data: Lifecycle[] | null; error: string | null }>`
|
||||
- `createLifecycle(data): Promise<{ data: Lifecycle | null; error: string | null }>`
|
||||
- `getCustomFields(): Promise<{ data: CustomField[] | null; error: string | null }>`
|
||||
- `createCustomField(data): Promise<{ data: CustomField | null; error: string | null }>`
|
||||
|
||||
### 6. Types
|
||||
File: `web/src/lib/types.ts`
|
||||
|
||||
TypeScript interfaces:
|
||||
```typescript
|
||||
interface Ticket {
|
||||
id: string; subject: string; queue_id: string; status: string;
|
||||
owner_id: string | null; creator_id: string;
|
||||
created_at: string; updated_at: string;
|
||||
started_at: string | null; resolved_at: string | null;
|
||||
custom_fields?: CustomFieldValue[];
|
||||
}
|
||||
interface Queue { id: string; name: string; description: string | null; lifecycle_id: string | null; }
|
||||
interface Transaction { id: string; ticket_id: string; transaction_type: string; field: string | null; old_value: string | null; new_value: string | null; data: unknown; creator_id: string; created_at: string; }
|
||||
interface Scrip { id: string; queue_id: string | null; name: string; condition_type: string; action_type: string; action_config: Record<string,unknown>; template_id: string | null; stage: string; sort_order: number; disabled: boolean; }
|
||||
interface Template { id: string; name: string; queue_id: string | null; subject_template: string; body_template: string; }
|
||||
interface Lifecycle { id: string; name: string; definition: LifecycleDefinition; }
|
||||
interface LifecycleDefinition { statuses: { initial: string[]; active: string[]; inactive: string[] }; transitions: Record<string, string[]>; }
|
||||
interface CustomField { id: string; name: string; field_type: string; values: unknown | null; max_values: number; }
|
||||
interface CustomFieldValue { id: string; custom_field_id: string; ticket_id: string; value: string; custom_field?: CustomField; }
|
||||
interface PreviewResult { prepared_scrips: PreparedScrip[]; }
|
||||
interface PreparedScrip { scripId: string; scripName: string; actionType: string; actionPayload: unknown; dryRun: boolean; }
|
||||
interface UpdateResult { ticket: Ticket; scrip_results: ScripResult[]; }
|
||||
interface ScripResult { scripId: string; success: boolean; message: string; }
|
||||
```
|
||||
|
||||
### 7. Layout
|
||||
File: `web/src/app/layout.tsx`
|
||||
|
||||
- Root layout with Inter font (from next/font/google)
|
||||
- Body: bg-neutral-950 text-neutral-100, antialiased
|
||||
- Nav bar at top: "Tessera" text on left (font-bold text-lg), nav links on right: "Tickets" (/) and "Admin" (/admin)
|
||||
- Nav bar styling: bg-neutral-900 border-b border-neutral-800, h-14, px-6, flex items-center justify-between
|
||||
- Active link: text-white font-medium. Inactive: text-neutral-400 hover:text-neutral-200
|
||||
- Main content below: container mx-auto px-6 py-8
|
||||
|
||||
### 8. Ticket List page
|
||||
File: `web/src/app/page.tsx` (root = ticket list)
|
||||
|
||||
- Fetch tickets + queues on load (useEffect)
|
||||
- Filter bar: queue dropdown (populated from queues, with "All queues" default), status dropdown (All, new, open, in_progress, resolved, closed), "Filter" button
|
||||
- Data table using shadcn/ui Table component:
|
||||
- Columns: ID (first 8 chars, monospace), Subject (link to /tickets/[id]), Queue (badge), Status (badge with color), Created (formatted date)
|
||||
- Status badges: new=default, open=blue, in_progress=yellow, resolved=green, closed=neutral
|
||||
- Clicking a row navigates to `/tickets/${id}`
|
||||
- Loading state: skeleton rows
|
||||
- Empty state: centered text "No tickets yet." with a "Create your first ticket" link
|
||||
- "New Ticket" button (top right) → opens Dialog with CreateTicketForm
|
||||
|
||||
CreateTicketForm (dialog):
|
||||
- Subject input (required)
|
||||
- Queue select dropdown (required, populated from queues)
|
||||
- Submit button
|
||||
- On success: close dialog, refresh ticket list
|
||||
|
||||
### 9. Ticket Detail page
|
||||
File: `web/src/app/tickets/[id]/page.tsx`
|
||||
|
||||
- Fetch ticket, transactions on load
|
||||
- Back link "← All tickets" at top
|
||||
|
||||
**Ticket info card** (Card component):
|
||||
- Subject (text-xl font-semibold)
|
||||
- Status badge (same colors as list)
|
||||
- Queue name, Created date, Updated date
|
||||
- Owner (or "Unassigned")
|
||||
|
||||
**Status change section** (Card, below info):
|
||||
- Status dropdown: populated from lifecycle definition. The PATCH /:id endpoint validates transitions.
|
||||
- "Preview" button: calls previewTicket, shows results in a dialog
|
||||
- Dialog shows: list of scrips that would fire (name, action type, "would execute X")
|
||||
- "Apply" button: calls updateTicket
|
||||
- Shows scrip results after: each result with success/error and message
|
||||
- Refreshes ticket data and transaction timeline
|
||||
|
||||
**Transaction timeline** (Card, below status):
|
||||
- List of transactions, newest first
|
||||
- Each transaction: icon + type badge + description + timestamp
|
||||
- Types: Create (green Plus icon), StatusChange (blue ArrowRightLeft icon), Comment (gray MessageSquare icon), CustomField (purple Pencil icon)
|
||||
- Show: field changed, old_value → new_value
|
||||
- Timestamps relative ("2 minutes ago") with date-fns formatDistanceToNow
|
||||
|
||||
**Custom fields** (Card, if any exist):
|
||||
- List of name: value pairs
|
||||
- Editable if CF permissions allow (MVP: read-only)
|
||||
|
||||
### 10. Admin page
|
||||
File: `web/src/app/admin/page.tsx`
|
||||
|
||||
Tabs component with 4 tabs: Queues, Lifecycles, Scrips, Custom Fields
|
||||
|
||||
Each tab: list table + "Add" button → dialog with form.
|
||||
|
||||
**Queues tab:**
|
||||
- Table: Name, Description, Actions
|
||||
- Add form: name (required), description (optional textarea)
|
||||
|
||||
**Lifecycles tab:**
|
||||
- Table: Name, Actions
|
||||
- Add form: name (required), definition (JSON textarea with placeholder example)
|
||||
- Example placeholder:
|
||||
```json
|
||||
{
|
||||
"statuses": {
|
||||
"initial": ["new"],
|
||||
"active": ["open", "in_progress"],
|
||||
"inactive": ["resolved", "closed"]
|
||||
},
|
||||
"transitions": {
|
||||
"new": ["open"],
|
||||
"open": ["in_progress", "resolved"],
|
||||
"in_progress": ["resolved"],
|
||||
"*": ["closed"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Scrips tab:**
|
||||
- Table: Name, Queue, Condition, Action, Enabled, Actions
|
||||
- Enabled toggle: switch/checkbox
|
||||
- Add form: name (required), queue_id (select dropdown, "Global" option), condition_type (select: OnCreate, OnStatusChange, OnResolve), action_type (select: SendEmail, Webhook, SetCustomField), action_config (JSON textarea), stage (select: TransactionCreate, TransactionBatch), sort_order (number input)
|
||||
|
||||
**Custom Fields tab:**
|
||||
- Table: Name, Type, Max Values, Actions
|
||||
- Add form: name (required), field_type (select: Freeform, SelectSingle, SelectMulti), values (JSON array textarea for select types), max_values (number, 0=unlimited)
|
||||
|
||||
**Admin page tables**: use shadcn/ui Table component, not TanStack Table (simpler, no sorting needed for small admin tables)
|
||||
|
||||
### 11. Theme
|
||||
shadcn/ui default neutral theme. Enhance:
|
||||
- Add dark-only mode (force dark)
|
||||
- Root layout: add `class="dark"` to html element
|
||||
- Tailwind config: `darkMode: 'class'`
|
||||
- Background: bg-neutral-950 (darkest)
|
||||
- Cards: bg-neutral-900 border-neutral-800
|
||||
- Inputs/Selects: bg-neutral-800 border-neutral-700 focus:ring-neutral-600 text-neutral-100
|
||||
|
||||
### 12. Navigation
|
||||
Use Next.js Link component and usePathname for active state. The "Tickets" link at `/` should be active, "Admin" at `/admin`.
|
||||
|
||||
## Verification
|
||||
1. Start backend: `cd /home/gjermund/projects/tessera && bun run src/index.ts` (port 9876)
|
||||
2. Start frontend: `cd /home/gjermund/projects/tessera/web && bun run dev` (port 5173)
|
||||
3. Open http://localhost:5173 — should show ticket list with dark theme, nav bar
|
||||
4. Navigate to /admin — should show 4 tabs
|
||||
5. Create a queue via Admin → Queues → Add
|
||||
6. Create a ticket via Tickets page → New Ticket
|
||||
7. Click ticket → detail page with timeline
|
||||
8. Change status → preview → apply → verify scrip results and timeline update
|
||||
9. All pages must render without errors
|
||||
|
||||
## Rules
|
||||
- Use `bun` for package management (bun add, bun install, bun run dev)
|
||||
- All TypeScript, strict mode, no `any` unless truly unavoidable
|
||||
- Zero ESLint errors on build
|
||||
- All shadcn/ui components must work — test each import
|
||||
- Proper loading states everywhere (no blank pages during fetch)
|
||||
- Error states with retry buttons
|
||||
- Commit after each major page is complete
|
||||
- The backend must be running for API calls to work — handle connection refused gracefully
|
||||
Reference in New Issue
Block a user