Files
legal-ai/web-ui/src/app/skills/page.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

151 lines
5.7 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 { AppShell } from "@/components/app-shell";
import { Card } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { useSkills, type Skill } from "@/lib/api/skills";
import { formatIsoDate } from "@/lib/format-date";
function formatChars(skill: Skill): string {
// Mockup column = "גודל (תווים)" — the DB markdown char count, grouped.
const n = skill.db_markdown_chars ?? 0;
return n.toLocaleString("en-US");
}
function formatUpdated(iso: string | null): string {
// ISO-style date to match the mockup (2026-06-09), tabular — Israel-time day.
return formatIsoDate(iso);
}
/**
* Sync chip — colored dot + label, faithful to mockup 14:
* - מסונכרן (success) when present in DB + on disk
* - DB בלבד (info) when in DB but no disk copy
* - לא מסונכרן (warn) when missing from the DB
*/
function SyncChip({ skill }: { skill: Skill }) {
let tone: { wrap: string; dot: string };
let label: string;
if (skill.not_in_db) {
tone = { wrap: "bg-warn-bg text-warn", dot: "bg-warn" };
label = "לא מסונכרן";
} else if (skill.db_markdown_chars > 0 && skill.disk_exists) {
tone = { wrap: "bg-success-bg text-success", dot: "bg-success" };
label = "מסונכרן";
} else if (skill.db_markdown_chars > 0) {
tone = { wrap: "bg-info-bg text-info", dot: "bg-info" };
label = "DB בלבד";
} else {
tone = { wrap: "bg-rule-soft text-ink-muted", dot: "bg-ink-muted" };
label = "לא ידוע";
}
return (
<span
className={`inline-flex items-center gap-1.5 rounded-full px-2.5 py-[3px] text-[0.75rem] font-semibold ${tone.wrap}`}
>
<span className={`h-1.5 w-1.5 rounded-full ${tone.dot}`} aria-hidden />
{label}
</span>
);
}
export default function SkillsPage() {
const { data, isPending, error } = useSkills();
return (
<AppShell>
<section className="space-y-6">
<header>
<nav className="text-[0.78rem] text-ink-muted mb-1">
<Link href="/" className="hover:text-gold-deep">בית</Link>
<span aria-hidden> · </span>
<span className="text-navy">מיומנויות</span>
</nav>
<h1 className="text-navy mb-0">מיומנויות</h1>
<p className="text-ink-muted text-sm mt-1 max-w-2xl">
סקילים מותקנים בפלטפורמה שם, גודל, מועד עדכון ומצב הסנכרון בין ה-DB
לדיסק.
</p>
</header>
<div className="h-[2px] bg-gradient-to-l from-transparent via-gold to-transparent" />
{error ? (
<Card className="bg-danger-bg border-danger/40 px-6 py-6 text-center text-danger">
{error.message}
</Card>
) : isPending ? (
<Skeleton className="h-80 w-full rounded-lg" />
) : data?.length === 0 ? (
<Card className="bg-surface border-rule px-6 py-12 text-center text-ink-muted">
<div className="text-gold text-3xl mb-2" aria-hidden></div>
אין skills מותקנים
</Card>
) : (
<Card className="bg-surface border-rule shadow-sm overflow-hidden p-0">
<Table>
<TableHeader>
<TableRow className="bg-parchment hover:bg-parchment border-rule">
<TableHead className="text-start text-[0.75rem] font-semibold text-ink-muted px-5 py-3.5">
סלאג
</TableHead>
<TableHead className="text-start text-[0.75rem] font-semibold text-ink-muted px-5 py-3.5">
שם תצוגה
</TableHead>
<TableHead className="text-start text-[0.75rem] font-semibold text-ink-muted px-5 py-3.5">
גודל (תווים)
</TableHead>
<TableHead className="text-start text-[0.75rem] font-semibold text-ink-muted px-5 py-3.5">
עודכן
</TableHead>
<TableHead className="text-start text-[0.75rem] font-semibold text-ink-muted px-5 py-3.5">
סנכרון
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data?.map((s) => (
<TableRow
key={s.slug}
className="border-rule-soft hover:bg-gold-wash"
>
<TableCell className="px-5 py-4">
<code
className="font-mono text-[0.81rem] font-semibold text-navy"
dir="ltr"
>
{s.slug}
</code>
</TableCell>
<TableCell className="px-5 py-4 font-semibold text-navy text-[0.9rem]">
{s.name || s.slug}
</TableCell>
<TableCell className="px-5 py-4 text-ink-soft tabular-nums">
{formatChars(s)}
</TableCell>
<TableCell className="px-5 py-4 text-ink-muted text-[0.81rem] tabular-nums">
{formatUpdated(s.updated_at)}
</TableCell>
<TableCell className="px-5 py-4">
<SyncChip skill={s} />
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Card>
)}
</section>
</AppShell>
);
}