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>
151 lines
5.7 KiB
TypeScript
151 lines
5.7 KiB
TypeScript
"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>
|
||
);
|
||
}
|