10 שגיאות (כולן קיימות-מראש, לא מהפיצ'רים האחרונים): - react/no-unescaped-entities (3): legal-arguments-panel, precedent-edit-sheet — escaping של מרכאות ב-JSX (“/") - react-hooks/set-state-in-effect (6): documents-panel, chair-editor, content-checklists, discussion-rules, golden-ratios, documents.ts — disable-comment לדפוסי sync/reset לגיטימיים (false-positive ידוע) - React Compiler reassign (1): subject-donut — refactor לחישוב prefix-sums ללא mutable accumulator ניקוי: הסרת 5 eslint-disable directives מיותרים (halacha-review-panel, precedent-upload-sheet). תוצאה: 0 errors (היה 10), 24→ warnings (היה 29). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
98 lines
3.0 KiB
TypeScript
98 lines
3.0 KiB
TypeScript
"use client";
|
||
|
||
import { useEffect, useRef, useState } from "react";
|
||
import { useSaveChairPosition } from "@/lib/api/research";
|
||
|
||
/*
|
||
* Chair-position editor for a single threshold claim or issue.
|
||
*
|
||
* Autosaves on blur, with an optimistic in-memory "last saved" value so the
|
||
* user sees immediate feedback. No debounced per-keystroke save — the user
|
||
* writes in long paragraphs and the backend writes to a file, so per-blur
|
||
* is the right granularity (matches the vanilla UI's behavior).
|
||
*/
|
||
|
||
type SaveState =
|
||
| { kind: "idle" }
|
||
| { kind: "saving" }
|
||
| { kind: "saved"; at: Date }
|
||
| { kind: "error"; message: string };
|
||
|
||
export function ChairEditor({
|
||
caseNumber,
|
||
sectionId,
|
||
initialValue,
|
||
}: {
|
||
caseNumber: string;
|
||
sectionId: string;
|
||
initialValue: string;
|
||
}) {
|
||
const [value, setValue] = useState(initialValue);
|
||
const [state, setState] = useState<SaveState>({ kind: "idle" });
|
||
const lastSaved = useRef(initialValue);
|
||
const mutate = useSaveChairPosition(caseNumber);
|
||
|
||
/* Reset when the upstream analysis refetches (e.g. after initial load) */
|
||
useEffect(() => {
|
||
// eslint-disable-next-line react-hooks/set-state-in-effect -- sync from server
|
||
setValue(initialValue);
|
||
lastSaved.current = initialValue;
|
||
}, [initialValue]);
|
||
|
||
const save = async () => {
|
||
const trimmed = value.trim();
|
||
if (trimmed === lastSaved.current.trim()) return;
|
||
setState({ kind: "saving" });
|
||
try {
|
||
await mutate.mutateAsync({ sectionId, position: trimmed });
|
||
lastSaved.current = trimmed;
|
||
setState({ kind: "saved", at: new Date() });
|
||
} catch (e) {
|
||
setState({
|
||
kind: "error",
|
||
message: e instanceof Error ? e.message : "שגיאה בשמירה",
|
||
});
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div className="space-y-2">
|
||
<div className="flex items-center justify-between">
|
||
<span className="text-[0.78rem] font-semibold text-navy">
|
||
עמדת ועדת הערר
|
||
</span>
|
||
<SaveIndicator state={state} />
|
||
</div>
|
||
<textarea
|
||
value={value}
|
||
onChange={(e) => setValue(e.target.value)}
|
||
onBlur={save}
|
||
rows={6}
|
||
dir="rtl"
|
||
placeholder="כתבי כאן את עמדתך לגבי סוגיה זו. הטקסט נשמר אוטומטית כשעוזבת את השדה."
|
||
className="
|
||
w-full resize-y rounded border border-rule bg-parchment
|
||
px-3 py-2 text-sm leading-relaxed text-ink
|
||
shadow-inner focus:border-gold focus:outline-none
|
||
focus:ring-2 focus:ring-gold/30
|
||
"
|
||
/>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function SaveIndicator({ state }: { state: SaveState }) {
|
||
if (state.kind === "idle") return null;
|
||
if (state.kind === "saving") {
|
||
return <span className="text-[0.72rem] text-ink-muted">⏳ שומר…</span>;
|
||
}
|
||
if (state.kind === "saved") {
|
||
const time = state.at.toLocaleTimeString("he-IL", {
|
||
hour: "2-digit",
|
||
minute: "2-digit",
|
||
});
|
||
return <span className="text-[0.72rem] text-success">✓ נשמר {time}</span>;
|
||
}
|
||
return <span className="text-[0.72rem] text-danger">⚠ {state.message}</span>;
|
||
}
|