Files
temetro/frontend/components/analysis/trend-card.tsx
T
Khalid Abdi d237504af9 frontend: add Somali, Arabic (RTL) & German languages
Add three UI locales (so/ar/de) with full ~1,660-key translations alongside
en/fr, selectable in Settings → Profile. Arabic gets full right-to-left support:

- config.ts registers the locales and exports a `dirFor` helper; an inline
  <head> script in layout.tsx sets <html dir/lang> before first paint (no RTL
  flash), and i18n-provider keeps them in sync on language change.
- ~160 physical direction utilities converted to logical (ms/me/ps/pe/
  start/end/text-start/text-end); directional chevrons/arrows get rtl:rotate-180;
  chat-bubble align variants fixed to logical.
- IBM Plex Sans Arabic appended to the sans/heading font stacks for
  per-character Arabic fallback.
- Language persists to the backend user_settings and re-applies on sign-in so it
  roams across devices (localStorage stays the offline source of truth).
- New scripts/check-locales.mjs (npm run check-locales) enforces key/placeholder
  parity and Arabic CLDR plural completeness.

Bump to 0.3.0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 23:03:13 +03:00

92 lines
2.8 KiB
TypeScript

"use client";
import { useState } from "react";
import { Sparkline } from "@/components/chat/sparkline";
import { Card } from "@/components/ui/card";
import {
Dialog,
DialogDescription,
DialogHeader,
DialogPanel,
DialogPopup,
DialogTitle,
} from "@/components/ui/dialog";
import type { TrendPoint } from "@/lib/analytics";
// A KPI card with an inline line chart (Sparkline). Clicking it opens a dialog
// with the full line chart and a per-point breakdown. Used on the Analysis page.
export function TrendCard({
title,
description,
points,
emptyLabel,
detailsLabel,
}: {
title: string;
description: string;
points: TrendPoint[];
emptyLabel: string;
detailsLabel: string;
}) {
const [open, setOpen] = useState(false);
const values = points.map((p) => p.count);
const total = values.reduce((a, b) => a + b, 0);
const hasData = points.length > 0;
return (
<>
<button
className="w-full text-start"
disabled={!hasData}
onClick={() => setOpen(true)}
type="button"
>
<Card className="gap-3 p-4 transition-colors hover:border-ring/40 hover:bg-accent/30">
<div className="flex items-baseline justify-between gap-2">
<span className="text-muted-foreground text-sm">{title}</span>
<span className="font-semibold text-foreground text-xl tabular-nums">
{total}
</span>
</div>
{hasData ? (
<Sparkline className="h-12" points={values} />
) : (
<p className="py-4 text-center text-muted-foreground text-xs">
{emptyLabel}
</p>
)}
{hasData && (
<span className="text-muted-foreground text-xs">{detailsLabel}</span>
)}
</Card>
</button>
<Dialog onOpenChange={setOpen} open={open}>
<DialogPopup className="sm:max-w-lg">
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<DialogPanel className="flex flex-col gap-5">
<Sparkline className="h-36" points={values} />
<dl className="grid grid-cols-2 gap-x-6 gap-y-1 text-sm sm:grid-cols-3">
{points.map((p) => (
<div
className="flex items-center justify-between border-border/60 border-b py-1"
key={p.label}
>
<dt className="text-muted-foreground">{p.label}</dt>
<dd className="font-medium text-foreground tabular-nums">
{p.count}
</dd>
</div>
))}
</dl>
</DialogPanel>
</DialogPopup>
</Dialog>
</>
);
}