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>
96 lines
2.5 KiB
TypeScript
96 lines
2.5 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { X, Plus } from "lucide-react";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
/*
|
|
* Minimal tag-style editor for a list of party names (appellants / respondents).
|
|
* Backed by a controlled string[] — submits as the same shape the FastAPI
|
|
* CaseCreateRequest expects. Enter adds the current draft; X removes a chip.
|
|
*/
|
|
|
|
export function PartiesField({
|
|
label,
|
|
value,
|
|
onChange,
|
|
error,
|
|
}: {
|
|
label: string;
|
|
value: string[];
|
|
onChange: (next: string[]) => void;
|
|
error?: string;
|
|
}) {
|
|
const [draft, setDraft] = useState("");
|
|
|
|
const add = () => {
|
|
const trimmed = draft.trim();
|
|
if (!trimmed) return;
|
|
if (value.includes(trimmed)) {
|
|
setDraft("");
|
|
return;
|
|
}
|
|
onChange([...value, trimmed]);
|
|
setDraft("");
|
|
};
|
|
|
|
const remove = (name: string) => {
|
|
onChange(value.filter((v) => v !== name));
|
|
};
|
|
|
|
return (
|
|
<div>
|
|
<label className="block text-sm font-medium text-navy mb-1.5">{label}</label>
|
|
{value.length > 0 && (
|
|
<ul className="flex flex-wrap gap-2 mb-2">
|
|
{value.map((name) => (
|
|
<li
|
|
key={name}
|
|
className="
|
|
inline-flex items-center gap-1.5 rounded-full
|
|
bg-gold-wash text-gold-deep border border-gold/40
|
|
px-3 py-1 text-sm
|
|
"
|
|
>
|
|
<span>{name}</span>
|
|
<button
|
|
type="button"
|
|
onClick={() => remove(name)}
|
|
className="hover:text-danger transition-colors"
|
|
aria-label={`הסר ${name}`}
|
|
>
|
|
<X className="w-3.5 h-3.5" />
|
|
</button>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
<div className="flex gap-2">
|
|
<Input
|
|
value={draft}
|
|
onChange={(e) => setDraft(e.target.value)}
|
|
onKeyDown={(e) => {
|
|
if (e.key === "Enter") {
|
|
e.preventDefault();
|
|
add();
|
|
}
|
|
}}
|
|
placeholder="שם מלא של הצד"
|
|
dir="rtl"
|
|
/>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={add}
|
|
aria-label={`הוסף ${label}`}
|
|
>
|
|
<Plus className="w-4 h-4" aria-hidden="true" />
|
|
</Button>
|
|
</div>
|
|
{error && <p className="text-[0.72rem] text-danger mt-1">{error}</p>}
|
|
</div>
|
|
);
|
|
}
|