initial commit
This commit is contained in:
134
apps/frontend/src/features/dashboard/DashboardPage.tsx
Normal file
134
apps/frontend/src/features/dashboard/DashboardPage.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
import { Minus, RefreshCw, TrendingDown, TrendingUp } from "lucide-react";
|
||||
import { RelativeTime } from "@/lib/time";
|
||||
import type { Quote } from "@/lib/api";
|
||||
import { useQuotes, useFetchQuotes } from "@/lib/queries";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
|
||||
const priceFormatter = new Intl.NumberFormat("es-AR", {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
});
|
||||
|
||||
function QuoteCard({ quote, title, to, variant }: { quote: Quote | undefined; title: string; to?: string; variant?: "default" | "minimal" }) {
|
||||
const navigate = useNavigate();
|
||||
const diff = quote ? Number(quote.difference) : 0;
|
||||
const pct = quote ? Number(quote.percentage) : 0;
|
||||
const isUp = diff > 0;
|
||||
const isDown = diff < 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="rounded-lg border p-4 cursor-pointer"
|
||||
onClick={() => to && navigate({ to })}
|
||||
onKeyDown={(e) => { if (e.key === "Enter" && to) navigate({ to }); }}
|
||||
role={to ? "button" : undefined}
|
||||
tabIndex={to ? 0 : undefined}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<p className="text-sm text-muted-foreground">{title}</p>
|
||||
{quote && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger className="cursor-pointer inline-flex">
|
||||
{isUp ? (
|
||||
<TrendingUp className="h-4 w-4 text-emerald-500" />
|
||||
) : isDown ? (
|
||||
<TrendingDown className="h-4 w-4 text-red-500" />
|
||||
) : (
|
||||
<Minus className="h-4 w-4 text-muted-foreground/50" />
|
||||
)}
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
{isUp
|
||||
? `Subió $${priceFormatter.format(diff)} (${pct.toFixed(2)}%)`
|
||||
: isDown
|
||||
? `Bajó $${priceFormatter.format(Math.abs(diff))} (${pct.toFixed(2)}%)`
|
||||
: "Sin cambios"}
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</div>
|
||||
{quote && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
<RelativeTime date={quote.timeStamp} />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{quote ? (
|
||||
variant === "minimal" ? (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger className="cursor-default">
|
||||
<p className="text-2xl font-bold tabular-nums text-left">${priceFormatter.format(Number(quote.buy))}</p>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
<p>Compra: ${priceFormatter.format(Number(quote.buy))}</p>
|
||||
<p>Venta: ${priceFormatter.format(Number(quote.sell))}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm">
|
||||
Compra: <span className="text-lg font-bold">${priceFormatter.format(Number(quote.buy))}</span>
|
||||
</p>
|
||||
<p className="text-sm">
|
||||
Venta: <span className="text-lg font-bold">${priceFormatter.format(Number(quote.sell))}</span>
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">Sin datos</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function DashboardPage() {
|
||||
const { data: quotes, isLoading } = useQuotes();
|
||||
const { mutate: doFetchQuotes, isPending } = useFetchQuotes();
|
||||
|
||||
const belo = quotes?.find((q) => q.type === "BELO");
|
||||
const blue = quotes?.find((q) => q.type === "BLUE");
|
||||
|
||||
const isFetching = isLoading || isPending;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-2xl font-semibold">Panel</h1>
|
||||
<button
|
||||
type="button"
|
||||
disabled={isFetching}
|
||||
onClick={() => doFetchQuotes()}
|
||||
className="inline-flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors cursor-pointer"
|
||||
>
|
||||
<RefreshCw className={`h-4 w-4 ${isFetching ? "animate-spin" : ""}`} />
|
||||
Actualizar
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid gap-4 md:grid-cols-4">
|
||||
<div className="rounded-lg border p-4">
|
||||
<p className="text-sm text-muted-foreground">Total ingresos</p>
|
||||
<p className="text-2xl font-bold">$0.00</p>
|
||||
</div>
|
||||
<div className="rounded-lg border p-4">
|
||||
<p className="text-sm text-muted-foreground">Gastos del mes</p>
|
||||
<p className="text-2xl font-bold">$0.00</p>
|
||||
</div>
|
||||
<QuoteCard quote={belo} title="BELO" to="/quotes/belo" variant="minimal" />
|
||||
<QuoteCard quote={blue} title="BLUE" variant="minimal" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user