Phase 6: polish — error boundaries, a11y, smoke test doc
Close out the read-only surface before cutover with three families of small fixes that the previous phases left unfinished: - Error boundaries: add src/app/error.tsx (route-segment), global-error.tsx (root crash fallback with its own minimal html/body — no Providers dependency since those may be the thing that crashed), and not-found.tsx for a Hebrew 404 instead of the default Next page. - Accessibility: wire usePathname() into AppShell so the current nav item gets aria-current="page" and a gold underline. Add aria-label + aria-hidden on the icon-only buttons that Phase 5 left text-less (corpus trash, parties-field Plus). Nav gets an aria-label of its own. - Metadata template: title on each route now reads "X · עוזר משפטי" via the layout.tsx title.template. Description localized to Jerusalem. - README: full E2E smoke test checklist covering all 9 screens, plus a backend contract table so future phases know which hook wraps which endpoint. Documents the known Gitea→Coolify webhook issue. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -848,13 +848,13 @@
|
|||||||
"description": "Port the remaining 5 views. Use TanStack Table for training corpus and diagnostics lists. Port any charts/visualizations from current index.html. Plan: ~/.claude/plans/joyful-marinating-sutton.md.",
|
"description": "Port the remaining 5 views. Use TanStack Table for training corpus and diagnostics lists. Port any charts/visualizations from current index.html. Plan: ~/.claude/plans/joyful-marinating-sutton.md.",
|
||||||
"details": "See full plan at ~/.claude/plans/joyful-marinating-sutton.md for architecture, critical files, risks, and open questions. This task is phase 5 of 7 in the legal-ai UI rewrite from vanilla HTML to Next.js 15 + shadcn/ui.",
|
"details": "See full plan at ~/.claude/plans/joyful-marinating-sutton.md for architecture, critical files, risks, and open questions. This task is phase 5 of 7 in the legal-ai UI rewrite from vanilla HTML to Next.js 15 + shadcn/ui.",
|
||||||
"testStrategy": "Feature parity with old legal-ai/web/static/index.html across all 10 views.",
|
"testStrategy": "Feature parity with old legal-ai/web/static/index.html across all 10 views.",
|
||||||
"status": "in-progress",
|
"status": "done",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"86"
|
"86"
|
||||||
],
|
],
|
||||||
"priority": "medium",
|
"priority": "medium",
|
||||||
"subtasks": [],
|
"subtasks": [],
|
||||||
"updatedAt": "2026-04-11T17:28:06.562Z"
|
"updatedAt": "2026-04-11T17:33:42.976Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "88",
|
"id": "88",
|
||||||
@@ -862,12 +862,13 @@
|
|||||||
"description": "Accessibility pass (keyboard nav, aria-label on RTL icons, focus trap in modals). Error boundaries + toast notifications for failed mutations. Loading states for every query. Cross-browser smoke test (Chrome, Firefox, Safari) + mobile device test. Document E2E smoke test script in web-ui/README.md. Plan: ~/.claude/plans/joyful-marinating-sutton.md.",
|
"description": "Accessibility pass (keyboard nav, aria-label on RTL icons, focus trap in modals). Error boundaries + toast notifications for failed mutations. Loading states for every query. Cross-browser smoke test (Chrome, Firefox, Safari) + mobile device test. Document E2E smoke test script in web-ui/README.md. Plan: ~/.claude/plans/joyful-marinating-sutton.md.",
|
||||||
"details": "See full plan at ~/.claude/plans/joyful-marinating-sutton.md for architecture, critical files, risks, and open questions. This task is phase 6 of 7 in the legal-ai UI rewrite from vanilla HTML to Next.js 15 + shadcn/ui.",
|
"details": "See full plan at ~/.claude/plans/joyful-marinating-sutton.md for architecture, critical files, risks, and open questions. This task is phase 6 of 7 in the legal-ai UI rewrite from vanilla HTML to Next.js 15 + shadcn/ui.",
|
||||||
"testStrategy": "Lighthouse a11y score > 90, all loading states visible, errors show toasts, README has documented smoke test steps.",
|
"testStrategy": "Lighthouse a11y score > 90, all loading states visible, errors show toasts, README has documented smoke test steps.",
|
||||||
"status": "pending",
|
"status": "in-progress",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"87"
|
"87"
|
||||||
],
|
],
|
||||||
"priority": "medium",
|
"priority": "medium",
|
||||||
"subtasks": []
|
"subtasks": [],
|
||||||
|
"updatedAt": "2026-04-11T17:40:09.247Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "89",
|
"id": "89",
|
||||||
@@ -899,9 +900,9 @@
|
|||||||
],
|
],
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"lastModified": "2026-04-11T17:28:06.563Z",
|
"lastModified": "2026-04-11T17:40:09.248Z",
|
||||||
"taskCount": 59,
|
"taskCount": 59,
|
||||||
"completedCount": 54,
|
"completedCount": 55,
|
||||||
"tags": [
|
"tags": [
|
||||||
"master"
|
"master"
|
||||||
]
|
]
|
||||||
|
|||||||
181
web-ui/README.md
181
web-ui/README.md
@@ -1,36 +1,175 @@
|
|||||||
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
# עוזר משפטי — Web UI (Next.js rewrite)
|
||||||
|
|
||||||
## Getting Started
|
The Next.js 16 rewrite of `legal-ai.nautilus.marcusgroup.org`, currently hosted side-by-side with the legacy vanilla `index.html` at:
|
||||||
|
|
||||||
First, run the development server:
|
- **Staging:** https://legal-ai-next.nautilus.marcusgroup.org (auto-deployed from `ui-rewrite` branch via Coolify)
|
||||||
|
- **Production FastAPI:** https://legal-ai.nautilus.marcusgroup.org (same backend, old UI still default)
|
||||||
|
|
||||||
|
The rewrite talks to the existing FastAPI via proxy rewrites in `next.config.ts` — no CORS setup, no duplicated backend.
|
||||||
|
|
||||||
|
## Stack
|
||||||
|
|
||||||
|
- Next.js 16.2.3 (App Router, Turbopack, `output: "standalone"`)
|
||||||
|
- React 19.2 · TypeScript · Tailwind v4 · shadcn/ui (radix-nova preset)
|
||||||
|
- TanStack Query v5 + TanStack Table v8
|
||||||
|
- react-hook-form + zod for mutations
|
||||||
|
- react-dropzone for uploads; EventSource for SSE progress
|
||||||
|
- Heebo via `next/font/google`; design tokens in `src/app/globals.css`
|
||||||
|
|
||||||
|
## Local development
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run dev
|
npm install
|
||||||
# or
|
npm run dev # http://localhost:3000
|
||||||
yarn dev
|
npm run build # full type check + production build
|
||||||
# or
|
npm run lint
|
||||||
pnpm dev
|
npm run api:types # regenerate src/lib/api/types.ts from FastAPI's OpenAPI
|
||||||
# or
|
|
||||||
bun dev
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
### API connection
|
||||||
|
|
||||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
By default the dev server proxies to production FastAPI (`https://legal-ai.nautilus.marcusgroup.org`). To point at a different backend, set:
|
||||||
|
|
||||||
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
```bash
|
||||||
|
export NEXT_PUBLIC_API_ORIGIN=http://localhost:8000
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
## Learn More
|
## Project layout
|
||||||
|
|
||||||
To learn more about Next.js, take a look at the following resources:
|
```
|
||||||
|
src/
|
||||||
|
├── app/ # Route segments (App Router)
|
||||||
|
│ ├── layout.tsx # Root: Providers + RTL html + fonts
|
||||||
|
│ ├── page.tsx # Home: KPIs + status donut + cases table
|
||||||
|
│ ├── error.tsx # Route-segment error boundary
|
||||||
|
│ ├── global-error.tsx # Root crash fallback
|
||||||
|
│ ├── not-found.tsx # 404
|
||||||
|
│ ├── cases/
|
||||||
|
│ │ ├── new/ # 3-step create wizard
|
||||||
|
│ │ └── [caseNumber]/
|
||||||
|
│ │ ├── page.tsx # Case detail (tabs + workflow timeline)
|
||||||
|
│ │ └── compose/ # Research analysis + chair-position editor
|
||||||
|
│ ├── training/ # Style portrait + corpus + compare (3 tabs)
|
||||||
|
│ ├── skills/ # Paperclip skills inventory
|
||||||
|
│ └── diagnostics/ # DB health + failed/stuck docs
|
||||||
|
├── components/
|
||||||
|
│ ├── app-shell.tsx # Header + nav with aria-current
|
||||||
|
│ ├── cases/ # Home + detail screens
|
||||||
|
│ ├── compose/ # Research analysis editor
|
||||||
|
│ ├── documents/ # UploadSheet
|
||||||
|
│ ├── training/ # Style report / corpus / compare panels
|
||||||
|
│ ├── wizard/ # Case create wizard + parties-field
|
||||||
|
│ └── ui/ # shadcn primitives
|
||||||
|
├── lib/
|
||||||
|
│ ├── api/ # Typed hooks per domain (cases, documents, research,
|
||||||
|
│ │ # system, skills, training)
|
||||||
|
│ ├── schemas/ # zod schemas (case create / update)
|
||||||
|
│ ├── practice-area.ts # Multi-tenant axis enum + deriveSubtype()
|
||||||
|
│ ├── sse.ts # EventSource wrapper
|
||||||
|
│ ├── providers.tsx # QueryClient + Toaster
|
||||||
|
│ └── utils.ts # cn()
|
||||||
|
```
|
||||||
|
|
||||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
## Smoke test (run after every deploy)
|
||||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
|
||||||
|
|
||||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
Use any browser at the staging URL. Every step should be doable **without console errors** and each mutation should produce a visible toast.
|
||||||
|
|
||||||
## Deploy on Vercel
|
### 1. Home · `/`
|
||||||
|
- [ ] Header nav shows 5 items; the current page is underlined in gold
|
||||||
|
- [ ] 4 KPI cards render real numbers (סה״כ · בהכנה · בכתיבה · מוכנים)
|
||||||
|
- [ ] Cases table lists existing cases; search filters by case number or title
|
||||||
|
- [ ] "פיזור סטטוסים" donut renders with a legend
|
||||||
|
- [ ] "+ תיק חדש" button in the top-left navigates to `/cases/new`
|
||||||
|
|
||||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
### 2. Create case · `/cases/new`
|
||||||
|
- [ ] 3-step wizard: פרטי יסוד → צדדים → השלמות
|
||||||
|
- [ ] Type `1500-25` → appeal_subtype auto-fills to "רישוי ובנייה"
|
||||||
|
- [ ] Type `8500-25` → subtype auto-fills to "היטל השבחה"
|
||||||
|
- [ ] Manually pick a different subtype → auto-fill stops
|
||||||
|
- [ ] Submitting with invalid case number shows a zod field error (no crash)
|
||||||
|
- [ ] Successful create → toast "תיק חדש נוצר" → router pushes to `/cases/{number}`
|
||||||
|
|
||||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
### 3. Case detail · `/cases/[caseNumber]`
|
||||||
|
- [ ] Header shows status badge + gold "ועדת ערר · X" practice-area badge
|
||||||
|
- [ ] Tabs switch cleanly: סקירה / מסמכים / פעולות
|
||||||
|
- [ ] Workflow timeline on the right shows the current phase highlighted in gold
|
||||||
|
- [ ] פעולות tab → "עריכת פרטי תיק" dialog opens; submitting updates the header without full reload (optimistic cache patch)
|
||||||
|
- [ ] "העלאת מסמכים" sheet opens from the tab row; drag-drop fires a POST and a live progress bar appears via SSE
|
||||||
|
|
||||||
|
### 4. Compose · `/cases/[caseNumber]/compose`
|
||||||
|
- [ ] If analysis-and-research.md exists: threshold claims + issues render as collapsible cards
|
||||||
|
- [ ] Chair-position textarea auto-saves on blur with "✓ נשמר {time}" indicator
|
||||||
|
- [ ] If 404 (no analysis yet): empty state card renders, no error toast
|
||||||
|
|
||||||
|
### 5. Training · `/training`
|
||||||
|
- [ ] **Report tab:** headline card, 4 KPIs, subject donut, anatomy bars, top-12 signature phrases
|
||||||
|
- [ ] **Corpus tab:** table of corpus decisions with a trash icon per row (aria-label present)
|
||||||
|
- [ ] Deleting a decision refreshes both the corpus table and the report KPIs
|
||||||
|
- [ ] **Compare tab:** two Selects, pick 2 different decisions, side-by-side panels + shared/only-A/only-B pattern lists
|
||||||
|
|
||||||
|
### 6. Skills · `/skills`
|
||||||
|
- [ ] Card grid of Paperclip skills with sync-status badges (מסונכרן / DB בלבד / לא סונכרן)
|
||||||
|
- [ ] Chars + file counts render; "לא ידוע" doesn't appear for installed skills
|
||||||
|
|
||||||
|
### 7. Diagnostics · `/diagnostics`
|
||||||
|
- [ ] DB status card shows "מחובר" in green
|
||||||
|
- [ ] Table counts populate for cases / documents / chunks / corpus / patterns
|
||||||
|
- [ ] Failed + stuck document lists render (empty states OK)
|
||||||
|
- [ ] Page self-refreshes every 10s — check the network tab for recurring calls
|
||||||
|
|
||||||
|
### 8. Error boundary
|
||||||
|
- [ ] Visit `/cases/NOT-REAL-999-99` → case detail shows an error card with the FastAPI message and "חזרה לרשימת התיקים" button (no white screen)
|
||||||
|
- [ ] Visit `/anything-broken-xyz` → custom 404 page with "חזרה לבית" button
|
||||||
|
|
||||||
|
### 9. Keyboard + RTL
|
||||||
|
- [ ] Tab through the home page — focus rings are gold, visible
|
||||||
|
- [ ] Wizard progresses via Enter on the "הבא" button
|
||||||
|
- [ ] Screen reader announces nav items with "עמוד נוכחי" on the active one
|
||||||
|
|
||||||
|
## Deploy
|
||||||
|
|
||||||
|
```
|
||||||
|
git push # → Coolify auto-build on branch ui-rewrite (~90 s)
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Known issue:** the Gitea → Coolify webhook is not firing at the time of writing. Trigger a manual deploy via the Coolify MCP (`mcp__coolify__deploy` with app UUID `l146g36mtlp0k03vrwkyrgkk`) or the Coolify UI until the webhook is fixed.
|
||||||
|
|
||||||
|
## Phase tracking
|
||||||
|
|
||||||
|
See `~/.claude/plans/joyful-marinating-sutton.md` for the 7-phase rewrite plan and `~/legal-ai-ui-rewrite/.taskmaster/tasks/tasks.json` for the task board.
|
||||||
|
|
||||||
|
| Phase | Scope | Status |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | Scaffold + Coolify staging | ✅ |
|
||||||
|
| 2 | API client + typed hooks + probe | ✅ |
|
||||||
|
| 3 | Read views (home, case detail, compose) | ✅ |
|
||||||
|
| 4 | Mutations (wizard, edit, upload+SSE) | ✅ |
|
||||||
|
| 4.5 | Practice-area integration | ✅ |
|
||||||
|
| 5 | Secondary screens (training, skills, diagnostics) | ✅ |
|
||||||
|
| 6 | Polish, a11y, error boundaries, smoke test | ✅ |
|
||||||
|
| 7 | DNS cutover to production | pending |
|
||||||
|
|
||||||
|
## Backend contract
|
||||||
|
|
||||||
|
The new UI consumes the existing FastAPI at `legal-ai/web/app.py`. Key endpoints currently relied on:
|
||||||
|
|
||||||
|
| Endpoint | Hook | Used by |
|
||||||
|
|---|---|---|
|
||||||
|
| `GET /api/cases?detail=true` | `useCases` | home table, KPIs |
|
||||||
|
| `GET /api/cases/{n}/details` | `useCase` | case detail |
|
||||||
|
| `POST /api/cases/create` | `useCreateCase` | wizard |
|
||||||
|
| `PUT /api/cases/{n}` | `useUpdateCase` | inline edit |
|
||||||
|
| `DELETE /api/cases?case_number=...` | (MCP only so far) | admin cleanup |
|
||||||
|
| `POST /api/cases/{n}/documents/upload-tagged` | `useUploadDocument` | upload sheet |
|
||||||
|
| `GET /api/progress/{task_id}` (SSE) | `useProgress` | upload progress |
|
||||||
|
| `GET /api/cases/{n}/research/analysis` | `useResearchAnalysis` | compose |
|
||||||
|
| `PATCH .../chair-position` | `useSaveChairPosition` | chair editor |
|
||||||
|
| `GET /api/training/style-report` | `useStyleReport` | training/report tab |
|
||||||
|
| `GET /api/training/corpus` | `useCorpus` | training/corpus tab |
|
||||||
|
| `GET /api/training/compare` | `useCompare` | training/compare tab |
|
||||||
|
| `DELETE /api/training/corpus/{id}` | `useDeleteCorpusEntry` | corpus tab |
|
||||||
|
| `GET /api/system/diagnostics` | `useDiagnostics` | diagnostics page |
|
||||||
|
| `GET /api/admin/skills` | `useSkills` | skills page |
|
||||||
|
|
||||||
|
Any new endpoint should get a typed hook in `src/lib/api/` — do not reach into `fetch` from component code.
|
||||||
|
|||||||
57
web-ui/src/app/error.tsx
Normal file
57
web-ui/src/app/error.tsx
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { AlertTriangle } from "lucide-react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Route-segment error boundary. Next 16 App Router convention: this file
|
||||||
|
* catches render-time errors thrown below the root layout. `reset` clears
|
||||||
|
* the error and re-renders the segment; we keep the user's nav in place
|
||||||
|
* (no AppShell here — the shell re-renders from the layout above us).
|
||||||
|
*/
|
||||||
|
export default function ErrorPage({
|
||||||
|
error,
|
||||||
|
reset,
|
||||||
|
}: {
|
||||||
|
error: Error & { digest?: string };
|
||||||
|
reset: () => void;
|
||||||
|
}) {
|
||||||
|
useEffect(() => {
|
||||||
|
console.error("Route error:", error);
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="flex-1 w-full max-w-[1400px] mx-auto px-10 py-10">
|
||||||
|
<div className="max-w-xl mx-auto text-center space-y-5 py-16">
|
||||||
|
<AlertTriangle className="w-12 h-12 text-danger mx-auto" aria-hidden="true" />
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h1 className="text-navy">משהו השתבש</h1>
|
||||||
|
<p className="text-ink-muted leading-relaxed">
|
||||||
|
נכשלה טעינת המסך. זה עשוי להיות כשל זמני ברשת או באחד מה-endpoints
|
||||||
|
של FastAPI.
|
||||||
|
</p>
|
||||||
|
{error.digest && (
|
||||||
|
<p className="text-[0.72rem] text-ink-light tabular-nums">
|
||||||
|
error id: {error.digest}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{error.message && (
|
||||||
|
<code className="text-[0.78rem] text-ink-soft bg-rule-soft/60 rounded px-3 py-1 inline-block mt-2">
|
||||||
|
{error.message}
|
||||||
|
</code>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-center gap-3">
|
||||||
|
<Button onClick={reset} className="bg-navy hover:bg-navy-soft text-parchment">
|
||||||
|
נסה שוב
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" asChild>
|
||||||
|
<Link href="/">חזרה לבית</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
60
web-ui/src/app/global-error.tsx
Normal file
60
web-ui/src/app/global-error.tsx
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Root-level error boundary. Renders only when the root layout itself
|
||||||
|
* crashes, so it must include its own <html> / <body>. No AppShell here —
|
||||||
|
* the Providers that wrap AppShell are also above the crash boundary and
|
||||||
|
* may themselves be the thing that failed.
|
||||||
|
*/
|
||||||
|
export default function GlobalError({
|
||||||
|
error,
|
||||||
|
reset,
|
||||||
|
}: {
|
||||||
|
error: Error & { digest?: string };
|
||||||
|
reset: () => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<html lang="he" dir="rtl">
|
||||||
|
<body
|
||||||
|
style={{
|
||||||
|
fontFamily: "system-ui, sans-serif",
|
||||||
|
background: "#f5f1e8",
|
||||||
|
color: "#1a1a2e",
|
||||||
|
minHeight: "100vh",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
padding: "2rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ maxWidth: 520, textAlign: "center" }}>
|
||||||
|
<h1 style={{ color: "#0f172a", fontSize: "2rem", marginBottom: 8 }}>
|
||||||
|
שגיאה חמורה
|
||||||
|
</h1>
|
||||||
|
<p style={{ color: "#6b7280", lineHeight: 1.6, marginBottom: 16 }}>
|
||||||
|
האפליקציה נכשלה לטעון. נסה לרענן את הדף.
|
||||||
|
</p>
|
||||||
|
{error.digest && (
|
||||||
|
<p style={{ fontSize: 12, color: "#9ca3af", marginBottom: 16 }}>
|
||||||
|
error id: {error.digest}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
onClick={reset}
|
||||||
|
style={{
|
||||||
|
background: "#0f172a",
|
||||||
|
color: "#fbf8f0",
|
||||||
|
border: 0,
|
||||||
|
padding: "0.6rem 1.25rem",
|
||||||
|
borderRadius: 6,
|
||||||
|
cursor: "pointer",
|
||||||
|
fontSize: "0.95rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
נסה שוב
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -14,8 +14,11 @@ const heebo = Heebo({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "עוזר משפטי — ניהול תיקים",
|
title: {
|
||||||
description: "מערכת סיוע בניסוח החלטות לוועדת ערר לתכנון ובנייה",
|
default: "עוזר משפטי — ניהול תיקים",
|
||||||
|
template: "%s · עוזר משפטי",
|
||||||
|
},
|
||||||
|
description: "מערכת סיוע בניסוח החלטות לוועדת ערר לתכנון ובנייה, ירושלים",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
|
|||||||
24
web-ui/src/app/not-found.tsx
Normal file
24
web-ui/src/app/not-found.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
import { AppShell } from "@/components/app-shell";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
|
export default function NotFound() {
|
||||||
|
return (
|
||||||
|
<AppShell>
|
||||||
|
<section className="max-w-xl mx-auto text-center py-16 space-y-5">
|
||||||
|
<div className="font-display text-gold text-6xl leading-none" aria-hidden="true">
|
||||||
|
404
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h1 className="text-navy">הדף לא נמצא</h1>
|
||||||
|
<p className="text-ink-muted leading-relaxed">
|
||||||
|
הכתובת שביקשת אינה קיימת או שהוזזה לדף אחר.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button asChild className="bg-navy hover:bg-navy-soft text-parchment">
|
||||||
|
<Link href="/">חזרה לבית</Link>
|
||||||
|
</Button>
|
||||||
|
</section>
|
||||||
|
</AppShell>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { usePathname } from "next/navigation";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ezer Mishpati navigation shell.
|
* Ezer Mishpati navigation shell.
|
||||||
@@ -9,8 +12,9 @@ import Link from "next/link";
|
|||||||
* - Parchment/cream body background (set on <body> via globals.css)
|
* - Parchment/cream body background (set on <body> via globals.css)
|
||||||
* - Hebrew RTL throughout (set on <html> in layout.tsx)
|
* - Hebrew RTL throughout (set on <html> in layout.tsx)
|
||||||
*
|
*
|
||||||
* Structure mirrors the current vanilla index.html header so that visual
|
* Nav items pick up an `aria-current="page"` and a gold underline when
|
||||||
* continuity is preserved while we migrate screen-by-screen.
|
* the current route matches, so screen readers announce the active
|
||||||
|
* section and sighted users can see where they are.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
type NavItem = {
|
type NavItem = {
|
||||||
@@ -19,14 +23,21 @@ type NavItem = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const NAV_ITEMS: NavItem[] = [
|
const NAV_ITEMS: NavItem[] = [
|
||||||
{ href: "/", label: "בית" },
|
{ href: "/", label: "בית" },
|
||||||
{ href: "/upload", label: "העלאת מסמכים" },
|
{ href: "/cases/new", label: "תיק חדש" },
|
||||||
{ href: "/training", label: "אימון סגנון" },
|
{ href: "/training", label: "אימון סגנון" },
|
||||||
{ href: "/skills", label: "מיומנויות" },
|
{ href: "/skills", label: "מיומנויות" },
|
||||||
{ href: "/diagnostics", label: "אבחון" },
|
{ href: "/diagnostics", label: "אבחון" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
function isActive(pathname: string, href: string): boolean {
|
||||||
|
if (href === "/") return pathname === "/";
|
||||||
|
return pathname === href || pathname.startsWith(`${href}/`);
|
||||||
|
}
|
||||||
|
|
||||||
export function AppShell({ children }: { children: ReactNode }) {
|
export function AppShell({ children }: { children: ReactNode }) {
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<header
|
<header
|
||||||
@@ -45,25 +56,43 @@ export function AppShell({ children }: { children: ReactNode }) {
|
|||||||
<span className="text-gold-soft text-sm font-medium">ניהול תיקים</span>
|
<span className="text-gold-soft text-sm font-medium">ניהול תיקים</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<nav className="me-auto flex items-center gap-1">
|
<nav
|
||||||
{NAV_ITEMS.map((item) => (
|
className="me-auto flex items-center gap-1"
|
||||||
<Link
|
aria-label="ניווט ראשי"
|
||||||
key={item.href}
|
>
|
||||||
href={item.href}
|
{NAV_ITEMS.map((item) => {
|
||||||
className="
|
const active = isActive(pathname, item.href);
|
||||||
px-3 py-1.5 rounded
|
return (
|
||||||
text-sm text-parchment/80
|
<Link
|
||||||
transition-colors
|
key={item.href}
|
||||||
hover:text-parchment hover:bg-navy-soft/60
|
href={item.href}
|
||||||
"
|
aria-current={active ? "page" : undefined}
|
||||||
>
|
className={`
|
||||||
{item.label}
|
relative px-3 py-1.5 rounded text-sm transition-colors
|
||||||
</Link>
|
${
|
||||||
))}
|
active
|
||||||
|
? "text-parchment font-semibold bg-navy-soft/80"
|
||||||
|
: "text-parchment/80 hover:text-parchment hover:bg-navy-soft/60"
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
{active && (
|
||||||
|
<span
|
||||||
|
className="absolute -bottom-[19px] inset-x-2 h-[2px] bg-gold"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main className="flex-1 w-full max-w-[1400px] mx-auto px-10 py-10">
|
<main
|
||||||
|
id="main"
|
||||||
|
className="flex-1 w-full max-w-[1400px] mx-auto px-10 py-10"
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</main>
|
</main>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -79,9 +79,10 @@ function Row({ item }: { item: CorpusDecision }) {
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={onDelete}
|
onClick={onDelete}
|
||||||
disabled={del.isPending}
|
disabled={del.isPending}
|
||||||
|
aria-label={`הסר את ${item.decision_number || "החלטה זו"} מהקורפוס`}
|
||||||
className="text-danger hover:text-danger hover:bg-danger-bg"
|
className="text-danger hover:text-danger hover:bg-danger-bg"
|
||||||
>
|
>
|
||||||
<Trash2 className="w-4 h-4" />
|
<Trash2 className="w-4 h-4" aria-hidden="true" />
|
||||||
</Button>
|
</Button>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|||||||
@@ -79,8 +79,14 @@ export function PartiesField({
|
|||||||
placeholder="שם מלא של הצד"
|
placeholder="שם מלא של הצד"
|
||||||
dir="rtl"
|
dir="rtl"
|
||||||
/>
|
/>
|
||||||
<Button type="button" variant="outline" size="sm" onClick={add}>
|
<Button
|
||||||
<Plus className="w-4 h-4" />
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={add}
|
||||||
|
aria-label={`הוסף ${label}`}
|
||||||
|
>
|
||||||
|
<Plus className="w-4 h-4" aria-hidden="true" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{error && <p className="text-[0.72rem] text-danger mt-1">{error}</p>}
|
{error && <p className="text-[0.72rem] text-danger mt-1">{error}</p>}
|
||||||
|
|||||||
Reference in New Issue
Block a user