Files
lemonspace_app/components/dashboard/recent-transactions.tsx
Matthias Meister 79d9092d43 Implement internationalization support across components
- Integrated `next-intl` for toast messages and locale handling in various components, including `Providers`, `CanvasUserMenu`, and `CreditOverview`.
- Replaced hardcoded strings with translation keys to enhance localization capabilities.
- Updated `RootLayout` to dynamically set the language attribute based on the user's locale.
- Ensured consistent user feedback through localized toast messages in actions such as sign-out, canvas operations, and billing notifications.
2026-04-01 18:16:52 +02:00

169 lines
6.4 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 { useAuthQuery } from "@/hooks/use-auth-query";
import { useFormatter } from "next-intl";
import { Activity, Coins } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { api } from "@/convex/_generated/api";
import { cn } from "@/lib/utils";
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function statusBadge(status: string) {
switch (status) {
case "committed":
return <Badge variant="secondary" className="text-xs font-normal">Abgeschlossen</Badge>;
case "reserved":
return (
<Badge variant="outline" className="border-amber-300 text-xs font-normal text-amber-700 dark:border-amber-700 dark:text-amber-400">
Reserviert
</Badge>
);
case "released":
return (
<Badge variant="secondary" className="text-xs font-normal text-emerald-600 dark:text-emerald-400 px-3">
Rückerstattet
</Badge>
);
case "failed":
return <Badge variant="destructive" className="text-xs font-normal">Fehlgeschlagen</Badge>;
default:
return <Badge variant="secondary" className="text-xs font-normal">Unbekannt</Badge>;
}
}
function truncatedDescription(text: string, maxLen = 40) {
if (text.length <= maxLen) return text;
return text.slice(0, maxLen) + "…";
}
// ---------------------------------------------------------------------------
// Component
// ---------------------------------------------------------------------------
export function RecentTransactions() {
const format = useFormatter();
const transactions = useAuthQuery(api.credits.getRecentTransactions, {
limit: 10,
});
const formatEurFromCents = (cents: number) =>
format.number(cents / 100, { style: "currency", currency: "EUR" });
const formatRelativeTime = (timestamp: number) => {
const now = Date.now();
const diff = now - timestamp;
const minutes = Math.floor(diff / 60000);
const hours = Math.floor(diff / 3600000);
const days = Math.floor(diff / 86400000);
if (minutes < 1) return "Gerade eben";
if (minutes < 60) return `vor ${minutes} Min.`;
if (hours < 24) return `vor ${hours} Std.`;
if (days < 7) return days === 1 ? "vor 1 Tag" : `vor ${days} Tagen`;
return format.dateTime(timestamp, { day: "numeric", month: "short" });
};
// ── Loading State ──────────────────────────────────────────────────────
if (transactions === undefined) {
return (
<div className="rounded-xl border bg-card p-6 shadow-sm shadow-foreground/3">
<div className="mb-4 flex items-center gap-2 text-sm font-medium">
<Activity className="size-3.5 text-muted-foreground" />
Letzte Aktivität
</div>
<div className="divide-y">
{Array.from({ length: 5 }).map((_, i) => (
<div key={i} className="flex items-center gap-6 px-1 py-3.5">
<div className="h-2.5 w-2.5 animate-pulse rounded-full bg-muted" />
<div className="flex-1 space-y-1.5">
<div className="h-3.5 w-48 animate-pulse rounded bg-muted" />
<div className="h-3 w-32 animate-pulse rounded bg-muted" />
</div>
<div className="h-3.5 w-16 animate-pulse rounded bg-muted" />
</div>
))}
</div>
</div>
);
}
// ── Empty State ────────────────────────────────────────────────────────
if (transactions.length === 0) {
return (
<div className="rounded-xl border bg-card p-6 shadow-sm shadow-foreground/3">
<div className="mb-4 flex items-center gap-2 text-sm font-medium">
<Activity className="size-3.5 text-muted-foreground" />
Letzte Aktivität
</div>
<div className="flex flex-col items-center justify-center py-10 text-center">
<Coins className="mb-3 size-10 text-muted-foreground/40" />
<p className="text-sm font-medium text-muted-foreground">
Noch keine Aktivität
</p>
<p className="mt-1 text-xs text-muted-foreground/70">
Erstelle dein erstes Bild im Canvas (Prompt-Knoten)
</p>
</div>
</div>
);
}
// ── Transaction List ───────────────────────────────────────────────────
return (
<div className="rounded-xl border bg-card shadow-sm shadow-foreground/3">
<div className="flex items-center gap-2 px-5 pt-5 pb-3 text-sm font-medium">
<Activity className="size-3.5 text-muted-foreground" />
Letzte Aktivität
</div>
<div className="divide-y">
{transactions.map((t: NonNullable<typeof transactions>[number]) => {
const isCredit = t.amount > 0;
return (
<div
key={t._id}
className="flex items-center gap-6 px-5 py-3.5"
>
{/* Status Indicator */}
<div className="shrink-0">
{statusBadge(t.status)}
</div>
{/* Description */}
<div className="min-w-0 flex-1">
<p
className="truncate text-sm font-medium"
title={t.description}
>
{truncatedDescription(t.description)}
</p>
<p className="mt-0.5 text-xs text-muted-foreground">
{formatRelativeTime(t._creationTime)}
</p>
</div>
{/* Credits */}
<div className="shrink-0 text-right">
<span
className={cn(
"text-sm tabular-nums font-medium",
isCredit
? "text-emerald-600 dark:text-emerald-400"
: "text-foreground",
)}
>
{isCredit ? "+" : ""}
{formatEurFromCents(Math.abs(t.amount))}
</span>
</div>
</div>
);
})}
</div>
</div>
);
}