Files
legal-ai/web-ui/src/components/digests/digest-card.tsx
Chaim 67c2c43777
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 3s
Lint — undefined names / undefined-names (pull_request) Successful in 10s
fix(web-ui): display all human-facing timestamps in Asia/Jerusalem (deterministic)
Storage stays UTC (DB TIMESTAMPTZ, API ISO-UTC) — only the display layer is
localized, and now deterministically: every timestamp renders pinned to
Asia/Jerusalem via a single Intl-based formatter, so SSR (UTC container) and
the browser agree on any runtime. No layout/visible-format change — only the tz.

- New single date formatter web-ui/src/lib/format-date.ts (G2): formatDate /
  formatDateShort / formatDateLong / formatDateTime / formatDateTimeFull /
  formatTime / formatIsoDate + Israel helpers getIsraelYear / israelDayKey /
  israelMidnightMs / israelParts + formatRelative (long/short/tight wording).
- Routed all ad-hoc toLocaleDateString/toLocaleTimeString/toLocaleString +
  hand-rolled new Date(...).get*() / toISOString().slice(0,10) timestamp call
  sites (30 files) through it. Number toLocaleString left untouched.
- Spec: added INV-UI9 to docs/spec/X6 (UTC storage, Asia/Jerusalem display).

Display-only; no layout/design/IA change → Claude Design gate N/A.
Invariants: G2 (single date formatter, no parallel ad-hoc formatting), INV-UI9.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 17:18:09 +00:00

131 lines
4.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import Link from "next/link";
import type { ReactNode } from "react";
import { Badge } from "@/components/ui/badge";
import { practiceAreaLabel } from "@/components/precedents/practice-area";
import type { Digest } from "@/lib/api/digests";
import { formatDateShort as formatDate } from "@/lib/format-date";
/**
* Presentational card for a single digest ("כל יום" radar entry).
*
* A digest is a SECONDARY source that POINTS at a ruling — the card makes the
* pointer-not-citation nature explicit: the underlying ruling's citation is
* shown with a link-status badge (linked → the ruling is in the precedent
* library; unlinked → an open knowledge gap, INV-DIG3).
*/
export function DigestCard({
digest,
score,
actions,
}: {
digest: Digest;
score?: number;
actions?: ReactNode;
}) {
const linked = Boolean(digest.linked_case_law_id);
const pending = digest.extraction_status !== "completed";
return (
<div className="flex flex-col rounded-lg border border-rule bg-surface shadow-sm p-4">
{/* top row — yomon-number + date at start, concept tag pushed to end */}
<div className="flex items-center gap-2.5 mb-2.5 flex-wrap">
{digest.yomon_number && (
<span className="text-navy font-bold text-[0.95rem]">
יומון {digest.yomon_number}
</span>
)}
{digest.digest_date && (
<span className="text-ink-muted text-[0.78rem]">· {formatDate(digest.digest_date)}</span>
)}
{digest.publication && digest.publication !== "כל יום" && (
<Badge variant="outline" className="bg-rule-soft text-ink-muted text-[0.65rem]">
{digest.publication}
</Badge>
)}
{digest.digest_kind === "announcement" && (
<Badge variant="outline" className="bg-sky-50 text-sky-700 border-sky-300 text-[0.65rem]">
עדכון
</Badge>
)}
{digest.digest_kind === "article" && (
<Badge variant="outline" className="bg-amber-50 text-amber-700 border-amber-300 text-[0.65rem]">
מאמר
</Badge>
)}
{digest.concept_tag && (
<span className="ms-auto rounded-full bg-info-bg text-info text-[0.72rem] font-semibold px-2.5 py-0.5">
{digest.concept_tag}
</span>
)}
{typeof score === "number" && (
<span className="ms-1 tabular-nums text-ink-muted text-[0.78rem]">
דירוג {score.toFixed(2)}
</span>
)}
</div>
{/* holding — the digest headline */}
{digest.headline_holding && (
<p className="text-ink font-medium text-[0.92rem] leading-6 mb-2.5" dir="rtl">
{digest.headline_holding}
</p>
)}
{/* source ruling — "מקור:" + citation, start-bordered (mockup `.ruling`) */}
<div className="border-s-2 border-rule ps-3 text-[0.8rem] text-ink-muted leading-6" dir="rtl">
<b className="text-ink-soft font-semibold">מקור:</b>{" "}
{digest.underlying_citation || "—"}
</div>
{digest.summary && (
<p className="text-ink-soft text-[0.82rem] leading-relaxed mt-2" dir="rtl">
{digest.summary}
</p>
)}
{digest.practice_area && (
<span className="text-ink-muted text-[0.72rem] mt-2">
{practiceAreaLabel(digest.practice_area)}
</span>
)}
{digest.subject_tags?.length > 0 && (
<div className="flex flex-wrap gap-1 mt-2">
{digest.subject_tags.map((t) => (
<Badge key={t} variant="outline" className="text-[0.65rem] bg-surface">
{t}
</Badge>
))}
</div>
)}
{/* foot — link-status chip + passive "ממתין לעיבוד" label (mockup `.foot`) */}
<div className="mt-3 pt-3 border-t border-rule-soft flex items-center gap-2 flex-wrap">
{linked ? (
<Link href={`/precedents/${digest.linked_case_law_id}`}>
<span className="rounded-full bg-success-bg text-success text-[0.72rem] font-semibold px-3 py-0.5 hover:opacity-90">
מקושר לפסיקה
</span>
</Link>
) : (
<span className="rounded-full bg-rule-soft text-ink-muted text-[0.72rem] font-semibold px-3 py-0.5">
לא מקושר
</span>
)}
{pending && (
/* passive status label — a dot + text, deliberately NOT a button */
<span className="ms-auto inline-flex items-center gap-1.5 text-[0.72rem] font-medium text-ink-muted">
<span className="h-[7px] w-[7px] rounded-full bg-warn/60" aria-hidden />
{digest.extraction_status === "pending" ? "ממתין לעיבוד" : digest.extraction_status}
</span>
)}
</div>
{actions && (
<div className="flex items-center gap-2 pt-3">{actions}</div>
)}
</div>
);
}