Recreate Next.js source — shadcn components, chart, types, hooks

Source files were untracked and lost during reset. Recreated:
- page.tsx (full 3-tab dashboard), layout.tsx, globals.css (shadcn theme)
- types.ts, api.ts, utils.ts
- strategy-card.tsx, equity-chart.tsx, positions-panel.tsx
- ui/ (7 shadcn wrappers: card, badge, button, table, tabs, sheet, collapsible, select)
- Config files: next.config.ts, tsconfig.json, postcss.config.mjs, components.json
This commit is contained in:
ramseshk
2026-08-05 05:05:26 +00:00
parent ac1b33a014
commit a09954017e
23 changed files with 1354 additions and 0 deletions
@@ -0,0 +1,80 @@
"use client";
import { useEffect, useRef } from "react";
import { createChart, ColorType } from "lightweight-charts";
interface EquityChartProps {
data: { t: number; v: number }[];
color?: string;
height?: number;
}
export function EquityChart({ data, color = "#3b82f6", height = 260 }: EquityChartProps) {
const containerRef = useRef<HTMLDivElement>(null);
const chartRef = useRef<ReturnType<typeof createChart> | null>(null);
const seriesRef = useRef<any>(null);
const seriesColor = color.startsWith("#") ? color : "#3b82f6";
useEffect(() => {
if (!containerRef.current || data.length === 0) return;
const chart = createChart(containerRef.current, {
layout: {
background: { type: ColorType.Solid, color: "transparent" },
textColor: "#b0b7c0",
},
grid: {
vertLines: { color: "rgba(255,255,255,0.03)" },
horzLines: { color: "rgba(255,255,255,0.03)" },
},
rightPriceScale: { borderColor: "rgba(255,255,255,0.08)" },
timeScale: { borderColor: "rgba(255,255,255,0.08)", timeVisible: true },
crosshair: { mode: 0 },
width: containerRef.current.clientWidth,
height,
});
const series = chart.addAreaSeries({
lineColor: seriesColor,
topColor: `${seriesColor}26`,
bottomColor: `${seriesColor}05`,
lineWidth: 2,
});
const pts = data.map((d) => ({
time: d.t as import("lightweight-charts").UTCTimestamp,
value: d.v,
}));
series.setData(pts);
chart.timeScale().fitContent();
chartRef.current = chart;
seriesRef.current = series;
const handleResize = () => {
if (containerRef.current && chartRef.current) {
chartRef.current.applyOptions({ width: containerRef.current.clientWidth });
}
};
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
chart.remove();
};
}, [data, seriesColor, height]);
useEffect(() => {
if (seriesRef.current && data.length > 0) {
const pts = data.map((d) => ({
time: d.t as import("lightweight-charts").UTCTimestamp,
value: d.v,
}));
seriesRef.current.setData(pts);
chartRef.current?.timeScale().fitContent();
}
}, [data]);
return <div ref={containerRef} style={{ width: "100%", height }} />;
}
@@ -0,0 +1,77 @@
"use client";
import { Card } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import type { Position, Order } from "@/lib/types";
interface Props {
positions: Position[];
orders: Order[];
}
export function PositionsPanel({ positions, orders }: Props) {
return (
<Card className="p-0 overflow-hidden border-border">
{positions.length > 0 && (
<div className="p-4 pb-0">
<p className="text-[10px] font-semibold text-muted-foreground uppercase tracking-wider mb-2">Positions ({positions.length})</p>
<Table>
<TableHeader>
<TableRow className="border-border/50">
<TableHead className="text-[9px] h-7">Strategy</TableHead>
<TableHead className="text-[9px] h-7">Size</TableHead>
<TableHead className="text-[9px] h-7">Entry</TableHead>
<TableHead className="text-[9px] h-7 text-right">PnL</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{positions.map((p, i) => (
<TableRow key={i} className="border-border/50">
<TableCell className="text-[10px] py-1.5">{p.strategy}</TableCell>
<TableCell className="text-[10px] py-1.5 font-mono">{p.size}</TableCell>
<TableCell className="text-[10px] py-1.5 font-mono">${p.entry_px?.toFixed(1) ?? "—"}</TableCell>
<TableCell className={`text-[10px] py-1.5 font-mono text-right ${(p.pnl ?? 0) >= 0 ? "text-green-500" : "text-red-500"}`}>
{(p.pnl ?? 0) >= 0 ? "+" : ""}${(p.pnl ?? 0).toFixed(4)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)}
{orders.length > 0 && (
<div className="p-4 pb-4">
<p className="text-[10px] font-semibold text-muted-foreground uppercase tracking-wider mb-2">Orders ({orders.length})</p>
<Table>
<TableHeader>
<TableRow className="border-border/50">
<TableHead className="text-[9px] h-7">Coin</TableHead>
<TableHead className="text-[9px] h-7">Side</TableHead>
<TableHead className="text-[9px] h-7">Size</TableHead>
<TableHead className="text-[9px] h-7">Limit</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{orders.map((o, i) => (
<TableRow key={i} className="border-border/50">
<TableCell className="text-[10px] py-1.5">{o.coin}</TableCell>
<TableCell className="text-[10px] py-1.5">
<Badge variant="outline" className={`text-[9px] h-4 px-1.5 border-0 ${o.side === "B" ? "bg-green-500/10 text-green-500" : "bg-red-500/10 text-red-500"}`}>
{o.side === "B" ? "BUY" : "SELL"}
</Badge>
</TableCell>
<TableCell className="text-[10px] py-1.5 font-mono">{o.sz}</TableCell>
<TableCell className="text-[10px] py-1.5 font-mono">${o.limitPx ?? "—"}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)}
{positions.length === 0 && orders.length === 0 && (
<p className="text-xs text-muted-foreground text-center py-8">No open positions or orders</p>
)}
</Card>
);
}
@@ -0,0 +1,95 @@
"use client";
import { Card } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import type { Strategy } from "@/lib/types";
import { TrendingUp, TrendingDown } from "lucide-react";
interface StrategyCardProps {
name: string;
strategy?: Strategy;
tab: "live" | "paper" | "backtest" | "historical";
onClick: () => void;
badge?: string;
stats?: { label: string; value: string; negative?: boolean }[];
pnlPct?: number;
status?: string;
}
export function StrategyCard({ name, strategy, tab, onClick, badge, stats, pnlPct, status }: StrategyCardProps) {
if (strategy) {
const equity = strategy.allocation + (strategy.pnl ?? 0);
const isUp = equity >= strategy.allocation;
const pnl = strategy.pnl ?? 0;
const pnlPctVal = strategy.pnl_pct ?? 0;
return (
<Card
className="p-4 cursor-pointer hover:border-primary/50 hover:shadow-md transition-all duration-200 hover:-translate-y-0.5 border-border"
onClick={onClick}
>
<div className="flex items-start justify-between mb-2">
<div>
<p className="text-xs font-semibold leading-tight">{name}</p>
<p className="text-[9px] text-muted-foreground mt-0.5">
${strategy.allocation} · {strategy.type}
</p>
</div>
<div className="flex gap-1">
<Badge variant={strategy.status === "running" ? "default" : "secondary"} className="text-[8px] h-4 px-1.5">
{strategy.status?.toUpperCase()}
</Badge>
<Badge variant="outline" className="text-[8px] h-4 px-1.5 border-border">
{strategy.fee_model?.toUpperCase()}
</Badge>
</div>
</div>
<div className={`text-xl font-mono font-bold mb-2 flex items-center gap-1 ${isUp ? "text-green-500" : "text-red-500"}`}>
{isUp ? <TrendingUp className="w-4 h-4" /> : <TrendingDown className="w-4 h-4" />}
${equity.toFixed(2)}
</div>
<div className="flex gap-3 text-[9px] text-muted-foreground flex-wrap">
<span>PnL: <b className={pnlPctVal >= 0 ? "text-green-500" : "text-red-500"}>{pnl >= 0 ? "+" : ""}{pnl.toFixed(2)} ({pnlPctVal >= 0 ? "+" : ""}{pnlPctVal.toFixed(2)}%)</b></span>
<span>Trades: <b>{strategy.trades_today ?? 0}</b></span>
<span>Win: <b>{Math.round((strategy.win_rate ?? 0) * 100)}%</b></span>
<span>Pos: <b>{(strategy.position ?? 0).toFixed(4)}</b></span>
</div>
<div className="mt-2 pt-2 border-t border-border/50 text-[9px] text-muted-foreground leading-relaxed">
<b>Alloc:</b> ${strategy.allocation} · <b>Max pos:</b> {strategy.max_position ?? "—"} · <b>Stop:</b> {strategy.stop_loss ?? "—"}
</div>
</Card>
);
}
// Backtest / Historical card
return (
<Card
className="p-4 cursor-pointer hover:border-primary/50 hover:shadow-md transition-all duration-200 hover:-translate-y-0.5 border-border"
onClick={onClick}
>
<div className="flex items-start justify-between mb-2">
<div>
<p className="text-xs font-semibold leading-tight">{name}</p>
<p className="text-[9px] text-muted-foreground mt-0.5">{badge ?? "Backtest"}</p>
</div>
<Badge variant="secondary" className="text-[8px] h-4 px-1.5">{status ?? "BACKTEST"}</Badge>
</div>
<div className={`text-xl font-mono font-bold mb-2 flex items-center gap-1 ${(pnlPct ?? 0) >= 0 ? "text-green-500" : "text-red-500"}`}>
{(pnlPct ?? 0) >= 0 ? <TrendingUp className="w-4 h-4" /> : <TrendingDown className="w-4 h-4" />}
{(pnlPct ?? 0) >= 0 ? "+" : ""}{(pnlPct ?? 0).toFixed(2)}%
</div>
{stats && (
<div className="flex gap-3 text-[9px] text-muted-foreground flex-wrap">
{stats.map((s) => (
<span key={s.label}>
{s.label}: <b className={s.negative ? "text-red-500" : ""}>{s.value}</b>
</span>
))}
</div>
)}
</Card>
);
}
@@ -0,0 +1,29 @@
import * as React from "react";
import { cn } from "@/lib/utils";
const badgeVariants = {
default: "border-transparent bg-primary text-primary-foreground",
secondary: "border-transparent bg-secondary text-secondary-foreground",
destructive: "border-transparent bg-destructive text-destructive-foreground",
outline: "text-foreground",
};
function Badge({
className,
variant = "default",
...props
}: React.ComponentProps<"div"> & { variant?: keyof typeof badgeVariants }) {
return (
<div
data-slot="badge"
className={cn(
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
badgeVariants[variant],
className,
)}
{...props}
/>
);
}
export { Badge };
@@ -0,0 +1,37 @@
import * as React from "react";
import { cn } from "@/lib/utils";
function Button({
className,
variant = "default",
size = "default",
...props
}: React.ComponentProps<"button"> & {
variant?: "default" | "ghost" | "outline";
size?: "default" | "sm" | "icon";
}) {
const variants: Record<string, string> = {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
ghost: "hover:bg-accent hover:text-accent-foreground",
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
};
const sizes: Record<string, string> = {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
icon: "h-9 w-9",
};
return (
<button
data-slot="button"
className={cn(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
variants[variant],
sizes[size],
className,
)}
{...props}
/>
);
}
export { Button };
+37
View File
@@ -0,0 +1,37 @@
import * as React from "react";
import { cn } from "@/lib/utils";
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card"
className={cn(
"group/card flex flex-col gap-[--card-spacing] overflow-hidden rounded-xl bg-card text-sm text-card-foreground ring-1 ring-foreground/10 [--card-spacing:--spacing(4)] has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(3)] data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
className,
)}
{...props}
/>
);
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return <div data-slot="card-header" className={cn("flex flex-col gap-1.5 px-[--card-spacing] pt-[--card-spacing]", className)} {...props} />;
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return <div data-slot="card-title" className={cn("font-semibold leading-none tracking-tight", className)} {...props} />;
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return <div data-slot="card-description" className={cn("text-muted-foreground text-sm", className)} {...props} />;
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return <div data-slot="card-content" className={cn("px-[--card-spacing]", className)} {...props} />;
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return <div data-slot="card-footer" className={cn("flex items-center px-[--card-spacing] pb-[--card-spacing]", className)} {...props} />;
}
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
@@ -0,0 +1,47 @@
"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
interface CollapsibleContextType {
open: boolean;
onOpenChange: (open: boolean) => void;
}
const CollapsibleContext = React.createContext<CollapsibleContextType | null>(null);
function Collapsible({ open, onOpenChange, className, children, ...props }: React.ComponentProps<"div"> & CollapsibleContextType) {
return (
<CollapsibleContext.Provider value={{ open, onOpenChange }}>
<div data-slot="collapsible" className={cn("flex flex-col gap-2", className)} {...props}>
{children}
</div>
</CollapsibleContext.Provider>
);
}
function CollapsibleTrigger({ className, children, ...props }: React.ComponentProps<"button">) {
const ctx = React.useContext(CollapsibleContext);
return (
<button
data-slot="collapsible-trigger"
className={cn("flex items-center gap-2 text-sm font-medium [&[data-state=open]>svg]:rotate-180", className)}
onClick={() => ctx?.onOpenChange(!ctx?.open)}
data-state={ctx?.open ? "open" : "closed"}
{...props}
>
{children}
</button>
);
}
function CollapsibleContent({ className, children, ...props }: React.ComponentProps<"div">) {
const ctx = React.useContext(CollapsibleContext);
if (!ctx?.open) return null;
return (
<div data-slot="collapsible-content" className={cn("overflow-hidden data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down", className)} data-state={ctx.open ? "open" : "closed"} {...props}>
{children}
</div>
);
}
export { Collapsible, CollapsibleTrigger, CollapsibleContent };
@@ -0,0 +1,26 @@
"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
import { ChevronDown } from "lucide-react";
function Select({ value, onChange, className, children, ...props }: React.ComponentProps<"select"> & { onChange?: (e: React.ChangeEvent<HTMLSelectElement>) => void }) {
return (
<select
data-slot="select"
value={value}
onChange={onChange}
className={cn(
"border-input file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground flex h-9 w-full rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className,
)}
{...props}
>
{children}
</select>
);
}
export { Select };
@@ -0,0 +1,42 @@
"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
import { X } from "lucide-react";
function Sheet({ open, onOpenChange, children }: { open: boolean; onOpenChange: (open: boolean) => void; children: React.ReactNode }) {
if (!open) return null;
return (
<div className="fixed inset-0 z-50 bg-background/80 backdrop-blur-sm" onClick={() => onOpenChange(false)}>
<div className="fixed inset-y-0 right-0 z-50 flex" onClick={(e) => e.stopPropagation()}>
{children}
</div>
</div>
);
}
function SheetContent({ side = "right", className, children, ...props }: React.ComponentProps<"div"> & { side?: "right" | "left" }) {
return (
<div
data-slot="sheet-content"
className={cn(
"bg-background flex h-full flex-col shadow-lg",
side === "right" ? "ml-auto" : "mr-auto",
className,
)}
{...props}
>
{children}
</div>
);
}
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
return <div data-slot="sheet-header" className={cn("flex flex-col gap-1.5 p-4", className)} {...props} />;
}
function SheetTitle({ className, ...props }: React.ComponentProps<"h2">) {
return <h2 data-slot="sheet-title" className={cn("text-lg font-semibold", className)} {...props} />;
}
export { Sheet, SheetContent, SheetHeader, SheetTitle };
@@ -0,0 +1,28 @@
import * as React from "react";
import { cn } from "@/lib/utils";
function Table({ className, ...props }: React.ComponentProps<"table">) {
return <table data-slot="table" className={cn("w-full caption-bottom text-sm", className)} {...props} />;
}
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
return <thead data-slot="table-header" className={cn("[&_tr]:border-b", className)} {...props} />;
}
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
return <tbody data-slot="table-body" className={cn("[&_tr:last-child]:border-0", className)} {...props} />;
}
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
return <tr data-slot="table-row" className={cn("border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted", className)} {...props} />;
}
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
return <th data-slot="table-head" className={cn("h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0", className)} {...props} />;
}
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
return <td data-slot="table-cell" className={cn("p-2 align-middle [&:has([role=checkbox])]:pr-0", className)} {...props} />;
}
export { Table, TableHeader, TableBody, TableRow, TableHead, TableCell };
+51
View File
@@ -0,0 +1,51 @@
"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
const TabsContext = React.createContext<{ value: string; onValueChange: (v: string) => void } | null>(null);
function Tabs({ value, onValueChange, className, children, ...props }: React.ComponentProps<"div"> & { value: string; onValueChange: (v: string) => void }) {
return (
<TabsContext.Provider value={{ value, onValueChange }}>
<div data-slot="tabs" className={cn("flex flex-col", className)} {...props}>
{children}
</div>
</TabsContext.Provider>
);
}
function TabsList({ className, ...props }: React.ComponentProps<"div">) {
return <div data-slot="tabs-list" className={cn("inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground", className)} {...props} />;
}
function TabsTrigger({ value, className, children, ...props }: React.ComponentProps<"button"> & { value: string }) {
const ctx = React.useContext(TabsContext);
const active = ctx?.value === value;
return (
<button
data-slot="tabs-trigger"
data-state={active ? "active" : "inactive"}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
className,
)}
onClick={() => ctx?.onValueChange(value)}
{...props}
>
{children}
</button>
);
}
function TabsContent({ value, className, children, ...props }: React.ComponentProps<"div"> & { value: string }) {
const ctx = React.useContext(TabsContext);
if (ctx?.value !== value) return null;
return (
<div data-slot="tabs-content" className={cn("flex-1 outline-none", className)} {...props}>
{children}
</div>
);
}
export { Tabs, TabsList, TabsTrigger, TabsContent };