Align frontend contracts, type safety, and portfolio status handling
This commit is contained in:
parent
8c160d6443
commit
b3b798ff2f
@ -6,9 +6,10 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
|
"typecheck": "tsc --noEmit",
|
||||||
|
"test": "npm run typecheck",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"start": "vite --host 0.0.0.0 --port 3001"
|
"start": "vite --host 0.0.0.0 --port 3001"
|
||||||
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^3.10.0",
|
"@hookform/resolvers": "^3.10.0",
|
||||||
|
|||||||
@ -1,21 +0,0 @@
|
|||||||
import { apiRequest } from "@/lib/queryClient";
|
|
||||||
|
|
||||||
export async function startStrategy(data) {
|
|
||||||
const res = await apiRequest("POST", "/strategy/start", data);
|
|
||||||
return res.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function stopStrategy() {
|
|
||||||
const res = await apiRequest("POST", "/strategy/stop");
|
|
||||||
return res.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function resumeStrategy() {
|
|
||||||
const res = await apiRequest("POST", "/strategy/resume");
|
|
||||||
return res.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getStrategyStatus() {
|
|
||||||
const res = await apiRequest("GET", "/strategy/status");
|
|
||||||
return res.json();
|
|
||||||
}
|
|
||||||
64
src/api/strategy.ts
Normal file
64
src/api/strategy.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { apiRequest } from "@/lib/queryClient";
|
||||||
|
|
||||||
|
export type StrategyFrequencyUnit = "days" | "minutes";
|
||||||
|
|
||||||
|
export type StrategyStartRequest = {
|
||||||
|
strategy_name: string;
|
||||||
|
sip_amount: number;
|
||||||
|
sip_frequency: {
|
||||||
|
value: number;
|
||||||
|
unit: StrategyFrequencyUnit;
|
||||||
|
};
|
||||||
|
mode: "LIVE" | "PAPER";
|
||||||
|
initial_cash?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type StrategyActionResponse = {
|
||||||
|
status: string;
|
||||||
|
run_id?: string | null;
|
||||||
|
message?: string;
|
||||||
|
redirect_url?: string | null;
|
||||||
|
broker?: string | null;
|
||||||
|
warning?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type StrategyStatusResponse = {
|
||||||
|
status?: string;
|
||||||
|
last_updated?: string | null;
|
||||||
|
last_execution_ts?: string | null;
|
||||||
|
next_eligible_ts?: string | null;
|
||||||
|
run_id?: string | null;
|
||||||
|
can_resume?: boolean;
|
||||||
|
can_restart?: boolean;
|
||||||
|
config?: {
|
||||||
|
strategy?: string | null;
|
||||||
|
sip_amount?: number | null;
|
||||||
|
sip_frequency?: {
|
||||||
|
value?: number | null;
|
||||||
|
unit?: StrategyFrequencyUnit | null;
|
||||||
|
} | null;
|
||||||
|
mode?: string | null;
|
||||||
|
broker?: string | null;
|
||||||
|
active?: boolean | null;
|
||||||
|
} | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function startStrategy(data: StrategyStartRequest): Promise<StrategyActionResponse> {
|
||||||
|
const res = await apiRequest("POST", "/strategy/start", data);
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function stopStrategy(): Promise<StrategyActionResponse> {
|
||||||
|
const res = await apiRequest("POST", "/strategy/stop");
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resumeStrategy(): Promise<StrategyActionResponse> {
|
||||||
|
const res = await apiRequest("POST", "/strategy/resume");
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getStrategyStatus(): Promise<StrategyStatusResponse> {
|
||||||
|
const res = await apiRequest("GET", "/strategy/status");
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
@ -33,9 +33,7 @@ type StrategyTimelineProps = {
|
|||||||
|
|
||||||
function parseTimestamp(value?: string | null) {
|
function parseTimestamp(value?: string | null) {
|
||||||
if (!value) return null;
|
if (!value) return null;
|
||||||
const hasTimezone = /Z|[+-]\d{2}:?\d{2}$/.test(value);
|
const parsed = new Date(value);
|
||||||
const normalized = hasTimezone ? value : `${value}Z`;
|
|
||||||
const parsed = new Date(normalized);
|
|
||||||
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -199,7 +197,7 @@ function VerboseStrategyTimeline() {
|
|||||||
prev.map((entry) => entry.seq).filter((seq) => typeof seq === "number"),
|
prev.map((entry) => entry.seq).filter((seq) => typeof seq === "number"),
|
||||||
);
|
);
|
||||||
const next = normalized.filter(
|
const next = normalized.filter(
|
||||||
(entry) => typeof entry.seq !== "number" || !seenSeq.has(entry.seq),
|
(entry: StrategyEvent) => typeof entry.seq !== "number" || !seenSeq.has(entry.seq),
|
||||||
);
|
);
|
||||||
return [...prev, ...next];
|
return [...prev, ...next];
|
||||||
});
|
});
|
||||||
@ -208,7 +206,7 @@ function VerboseStrategyTimeline() {
|
|||||||
latestSeqRef.current = data.latest_seq;
|
latestSeqRef.current = data.latest_seq;
|
||||||
} else {
|
} else {
|
||||||
const lastSeq = normalized.reduce(
|
const lastSeq = normalized.reduce(
|
||||||
(max, entry) => Math.max(max, entry.seq ?? 0),
|
(max: number, entry: StrategyEvent) => Math.max(max, entry.seq ?? 0),
|
||||||
latestSeqRef.current,
|
latestSeqRef.current,
|
||||||
);
|
);
|
||||||
latestSeqRef.current = lastSeq;
|
latestSeqRef.current = lastSeq;
|
||||||
@ -306,7 +304,7 @@ function VerboseStrategyTimeline() {
|
|||||||
<div className="mt-1 text-xs text-green-300/80">Reason: {reason}</div>
|
<div className="mt-1 text-xs text-green-300/80">Reason: {reason}</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="mt-3 space-y-3">
|
<div className="mt-3 space-y-3">
|
||||||
{run.events.map((entry, index) => {
|
{run.events.map((entry: StrategyEvent, index: number) => {
|
||||||
const badge = getBadge(entry);
|
const badge = getBadge(entry);
|
||||||
const timestamp = entry.ts ?? entry.timestamp;
|
const timestamp = entry.ts ?? entry.timestamp;
|
||||||
const message = entry.message ?? entry.event ?? "Unknown event";
|
const message = entry.message ?? entry.event ?? "Unknown event";
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import FinalCTA from "../landing/FinalCTA";
|
import FinalCTA from "../landing/FinalCTA";
|
||||||
|
|
||||||
export default function FinalCTAExample() {
|
export default function FinalCTAExample() {
|
||||||
return <FinalCTA />;
|
return <FinalCTA onExploreStrategies={() => {}} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import HeroSection from "../landing/HeroSection";
|
import HeroSection from "../landing/HeroSection";
|
||||||
|
|
||||||
export default function HeroSectionExample() {
|
export default function HeroSectionExample() {
|
||||||
return <HeroSection />;
|
return <HeroSection onExploreStrategies={() => {}} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -252,7 +252,7 @@ export default function AuthDialogs({ layout = "desktop" }: AuthDialogsProps) {
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="link"
|
variant="ghost"
|
||||||
className="w-full text-sm"
|
className="w-full text-sm"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setResetEmail(loginForm.email.trim());
|
setResetEmail(loginForm.email.trim());
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { GOLDEN_NIFTY_PATH } from "@/lib/routes";
|
import { GOLDEN_NIFTY_PATH } from "@/lib/routes";
|
||||||
|
|
||||||
export default function Footer() {
|
export default function Footer() {
|
||||||
const links = {
|
const links: Record<string, { label: string; href: string; newTab?: boolean }[]> = {
|
||||||
Product: [
|
Product: [
|
||||||
{ label: "Strategies", href: GOLDEN_NIFTY_PATH, newTab: true },
|
{ label: "Strategies", href: GOLDEN_NIFTY_PATH, newTab: true },
|
||||||
{ label: "Pricing", href: "#" },
|
{ label: "Pricing", href: "#" },
|
||||||
|
|||||||
@ -52,8 +52,10 @@ type PositionsResponse = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type MarketStatusResponse = {
|
type MarketStatusResponse = {
|
||||||
status?: "OPEN" | "CLOSED";
|
status?: "OPEN" | "CLOSED" | "HOLIDAY";
|
||||||
|
reason?: string;
|
||||||
checked_at?: string;
|
checked_at?: string;
|
||||||
|
next_open_at?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type FundsResponse = {
|
type FundsResponse = {
|
||||||
@ -446,16 +448,6 @@ export default function PortfolioSection() {
|
|||||||
enabled: !!brokerStatus?.connected,
|
enabled: !!brokerStatus?.connected,
|
||||||
retry: 1,
|
retry: 1,
|
||||||
retryDelay: 600,
|
retryDelay: 600,
|
||||||
onSuccess: (data) => {
|
|
||||||
setCachedHoldings(data?.holdings || []);
|
|
||||||
setSessionExpired(false);
|
|
||||||
setReconnectAttempted(false);
|
|
||||||
},
|
|
||||||
onError: () => {
|
|
||||||
if (brokerStatus?.connected) {
|
|
||||||
setSessionExpired(true);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const fundsQuery = useQuery<FundsResponse>({
|
const fundsQuery = useQuery<FundsResponse>({
|
||||||
@ -467,16 +459,6 @@ export default function PortfolioSection() {
|
|||||||
enabled: !!brokerStatus?.connected,
|
enabled: !!brokerStatus?.connected,
|
||||||
retry: 1,
|
retry: 1,
|
||||||
retryDelay: 600,
|
retryDelay: 600,
|
||||||
onSuccess: (data) => {
|
|
||||||
setCachedFunds(data?.funds ?? null);
|
|
||||||
setSessionExpired(false);
|
|
||||||
setReconnectAttempted(false);
|
|
||||||
},
|
|
||||||
onError: () => {
|
|
||||||
if (brokerStatus?.connected) {
|
|
||||||
setSessionExpired(true);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const [startDate, setStartDate] = useState(() =>
|
const [startDate, setStartDate] = useState(() =>
|
||||||
@ -617,16 +599,6 @@ export default function PortfolioSection() {
|
|||||||
enabled: !!brokerStatus?.connected,
|
enabled: !!brokerStatus?.connected,
|
||||||
retry: 1,
|
retry: 1,
|
||||||
retryDelay: 600,
|
retryDelay: 600,
|
||||||
onSuccess: (data) => {
|
|
||||||
setCachedEquityCurve(data ?? null);
|
|
||||||
setSessionExpired(false);
|
|
||||||
setReconnectAttempted(false);
|
|
||||||
},
|
|
||||||
onError: () => {
|
|
||||||
if (brokerStatus?.connected) {
|
|
||||||
setSessionExpired(true);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const positionsQuery = useQuery<PositionsResponse>({
|
const positionsQuery = useQuery<PositionsResponse>({
|
||||||
@ -638,16 +610,6 @@ export default function PortfolioSection() {
|
|||||||
enabled: !!brokerStatus?.connected,
|
enabled: !!brokerStatus?.connected,
|
||||||
retry: 1,
|
retry: 1,
|
||||||
retryDelay: 600,
|
retryDelay: 600,
|
||||||
onSuccess: (data) => {
|
|
||||||
setCachedPositions(data?.positions || []);
|
|
||||||
setSessionExpired(false);
|
|
||||||
setReconnectAttempted(false);
|
|
||||||
},
|
|
||||||
onError: () => {
|
|
||||||
if (brokerStatus?.connected) {
|
|
||||||
setSessionExpired(true);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const refreshBrokerData = useCallback(
|
const refreshBrokerData = useCallback(
|
||||||
@ -655,7 +617,7 @@ export default function PortfolioSection() {
|
|||||||
if (!brokerStatus?.connected) {
|
if (!brokerStatus?.connected) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const tasks = [
|
const tasks: Promise<unknown>[] = [
|
||||||
holdingsQuery.refetch(),
|
holdingsQuery.refetch(),
|
||||||
positionsQuery.refetch(),
|
positionsQuery.refetch(),
|
||||||
fundsQuery.refetch(),
|
fundsQuery.refetch(),
|
||||||
@ -678,11 +640,58 @@ export default function PortfolioSection() {
|
|||||||
|
|
||||||
const isConnected = !!brokerStatus?.connected;
|
const isConnected = !!brokerStatus?.connected;
|
||||||
const isAuthed = brokerStatus !== null;
|
const isAuthed = brokerStatus !== null;
|
||||||
const holdings = holdingsQuery.data ? holdingsQuery.data.holdings : cachedHoldings;
|
const holdings = holdingsQuery.data?.holdings ?? cachedHoldings;
|
||||||
const positions = positionsQuery.data ? positionsQuery.data.positions : cachedPositions;
|
const positions = positionsQuery.data?.positions ?? cachedPositions;
|
||||||
const fundsSnapshot = fundsQuery.data?.funds ?? cachedFunds;
|
const fundsSnapshot = fundsQuery.data?.funds ?? cachedFunds;
|
||||||
const noHoldings = holdings.length === 0;
|
const noHoldings = holdings.length === 0;
|
||||||
const noPositions = positions.length === 0;
|
const noPositions = positions.length === 0;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (holdingsQuery.data) {
|
||||||
|
setCachedHoldings(holdingsQuery.data.holdings ?? []);
|
||||||
|
setSessionExpired(false);
|
||||||
|
setReconnectAttempted(false);
|
||||||
|
}
|
||||||
|
}, [holdingsQuery.data]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (fundsQuery.data) {
|
||||||
|
setCachedFunds(fundsQuery.data.funds ?? null);
|
||||||
|
setSessionExpired(false);
|
||||||
|
setReconnectAttempted(false);
|
||||||
|
}
|
||||||
|
}, [fundsQuery.data]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (equityCurveQuery.data) {
|
||||||
|
setCachedEquityCurve(equityCurveQuery.data);
|
||||||
|
setSessionExpired(false);
|
||||||
|
setReconnectAttempted(false);
|
||||||
|
}
|
||||||
|
}, [equityCurveQuery.data]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (positionsQuery.data) {
|
||||||
|
setCachedPositions(positionsQuery.data.positions ?? []);
|
||||||
|
setSessionExpired(false);
|
||||||
|
setReconnectAttempted(false);
|
||||||
|
}
|
||||||
|
}, [positionsQuery.data]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
brokerStatus?.connected &&
|
||||||
|
(holdingsQuery.isError || fundsQuery.isError || equityCurveQuery.isError || positionsQuery.isError)
|
||||||
|
) {
|
||||||
|
setSessionExpired(true);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
brokerStatus?.connected,
|
||||||
|
holdingsQuery.isError,
|
||||||
|
fundsQuery.isError,
|
||||||
|
equityCurveQuery.isError,
|
||||||
|
positionsQuery.isError,
|
||||||
|
]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isConnected) {
|
if (!isConnected) {
|
||||||
setSessionExpired(false);
|
setSessionExpired(false);
|
||||||
|
|||||||
@ -43,8 +43,11 @@ async function getResponseErrorMessage(res: Response) {
|
|||||||
if (contentType.includes("application/json")) {
|
if (contentType.includes("application/json")) {
|
||||||
try {
|
try {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
const structuredDetail = typeof data?.detail === "object" && data?.detail !== null ? data.detail : null;
|
||||||
const detail =
|
const detail =
|
||||||
(typeof data?.detail === "string" && data.detail) ||
|
(typeof data?.detail === "string" && data.detail) ||
|
||||||
|
(typeof structuredDetail?.message === "string" && structuredDetail.message) ||
|
||||||
|
(typeof structuredDetail?.detail === "string" && structuredDetail.detail) ||
|
||||||
(typeof data?.message === "string" && data.message) ||
|
(typeof data?.message === "string" && data.message) ||
|
||||||
(typeof data?.error === "string" && data.error);
|
(typeof data?.error === "string" && data.error);
|
||||||
if (detail) {
|
if (detail) {
|
||||||
@ -161,7 +164,7 @@ export const queryClient = new QueryClient({
|
|||||||
queryFn: getQueryFn({ on401: "throw" }),
|
queryFn: getQueryFn({ on401: "throw" }),
|
||||||
refetchInterval: false,
|
refetchInterval: false,
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
staleTime: Infinity,
|
staleTime: 5_000,
|
||||||
retry: false,
|
retry: false,
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
|
|||||||
@ -43,7 +43,6 @@ const trustTimeline = [
|
|||||||
|
|
||||||
export default function About() {
|
export default function About() {
|
||||||
const prefersReducedMotion = useReducedMotion();
|
const prefersReducedMotion = useReducedMotion();
|
||||||
const heroMotion = prefersReducedMotion ? {} : pageEnterMotion;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-background text-foreground">
|
<div className="min-h-screen bg-background text-foreground">
|
||||||
@ -52,9 +51,9 @@ export default function About() {
|
|||||||
<main className="pt-24 pb-16">
|
<main className="pt-24 pb-16">
|
||||||
<motion.section
|
<motion.section
|
||||||
className="relative overflow-hidden"
|
className="relative overflow-hidden"
|
||||||
initial={heroMotion.initial}
|
{...(prefersReducedMotion
|
||||||
animate={heroMotion.animate}
|
? {}
|
||||||
transition={heroMotion.transition}
|
: { initial: pageEnterMotion.initial, animate: pageEnterMotion.animate })}
|
||||||
>
|
>
|
||||||
<div className="absolute inset-0 pointer-events-none">
|
<div className="absolute inset-0 pointer-events-none">
|
||||||
<div className="absolute -left-24 top-0 h-80 w-80 bg-gradient-to-br from-primary/20 via-primary/5 to-transparent blur-[120px]" />
|
<div className="absolute -left-24 top-0 h-80 w-80 bg-gradient-to-br from-primary/20 via-primary/5 to-transparent blur-[120px]" />
|
||||||
|
|||||||
@ -35,7 +35,6 @@ const posts: BlogPost[] = [
|
|||||||
|
|
||||||
export default function Blog() {
|
export default function Blog() {
|
||||||
const prefersReducedMotion = useReducedMotion();
|
const prefersReducedMotion = useReducedMotion();
|
||||||
const heroMotion = prefersReducedMotion ? {} : pageEnterMotion;
|
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -50,9 +49,9 @@ export default function Blog() {
|
|||||||
<main className="pt-24 pb-16">
|
<main className="pt-24 pb-16">
|
||||||
<motion.section
|
<motion.section
|
||||||
className="relative overflow-hidden px-6 py-16"
|
className="relative overflow-hidden px-6 py-16"
|
||||||
initial={heroMotion.initial}
|
{...(prefersReducedMotion
|
||||||
animate={heroMotion.animate}
|
? {}
|
||||||
transition={heroMotion.transition}
|
: { initial: pageEnterMotion.initial, animate: pageEnterMotion.animate })}
|
||||||
>
|
>
|
||||||
<div className="absolute inset-0 pointer-events-none">
|
<div className="absolute inset-0 pointer-events-none">
|
||||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_1px_1px,rgba(59,130,246,0.12),transparent_0)] [background-size:120px_120px] opacity-40" />
|
<div className="absolute inset-0 bg-[radial-gradient(circle_at_1px_1px,rgba(59,130,246,0.12),transparent_0)] [background-size:120px_120px] opacity-40" />
|
||||||
|
|||||||
@ -51,7 +51,6 @@ export default function Disclosures() {
|
|||||||
className="relative overflow-hidden px-6 py-16"
|
className="relative overflow-hidden px-6 py-16"
|
||||||
initial={prefersReducedMotion ? undefined : pageEnterMotion.initial}
|
initial={prefersReducedMotion ? undefined : pageEnterMotion.initial}
|
||||||
animate={prefersReducedMotion ? undefined : pageEnterMotion.animate}
|
animate={prefersReducedMotion ? undefined : pageEnterMotion.animate}
|
||||||
transition={prefersReducedMotion ? undefined : pageEnterMotion.transition}
|
|
||||||
>
|
>
|
||||||
<div className="absolute inset-0 pointer-events-none">
|
<div className="absolute inset-0 pointer-events-none">
|
||||||
<div className="absolute -left-24 top-0 h-80 w-80 bg-gradient-to-br from-primary/18 via-primary/5 to-transparent blur-[120px]" />
|
<div className="absolute -left-24 top-0 h-80 w-80 bg-gradient-to-br from-primary/18 via-primary/5 to-transparent blur-[120px]" />
|
||||||
|
|||||||
@ -59,7 +59,6 @@ export default function LearnMore() {
|
|||||||
className="relative overflow-hidden px-6 py-16"
|
className="relative overflow-hidden px-6 py-16"
|
||||||
initial={prefersReducedMotion ? undefined : pageEnterMotion.initial}
|
initial={prefersReducedMotion ? undefined : pageEnterMotion.initial}
|
||||||
animate={prefersReducedMotion ? undefined : pageEnterMotion.animate}
|
animate={prefersReducedMotion ? undefined : pageEnterMotion.animate}
|
||||||
transition={prefersReducedMotion ? undefined : pageEnterMotion.transition}
|
|
||||||
>
|
>
|
||||||
<div className="absolute inset-0 pointer-events-none">
|
<div className="absolute inset-0 pointer-events-none">
|
||||||
<div className="absolute -left-16 top-0 h-80 w-80 bg-gradient-to-br from-primary/20 via-primary/5 to-transparent blur-[120px]" />
|
<div className="absolute -left-16 top-0 h-80 w-80 bg-gradient-to-br from-primary/20 via-primary/5 to-transparent blur-[120px]" />
|
||||||
|
|||||||
@ -88,8 +88,10 @@ type PaperMtmResponse = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type MarketStatusResponse = {
|
type MarketStatusResponse = {
|
||||||
status?: "OPEN" | "CLOSED";
|
status?: "OPEN" | "CLOSED" | "HOLIDAY";
|
||||||
|
reason?: string;
|
||||||
checked_at?: string;
|
checked_at?: string;
|
||||||
|
next_open_at?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type EngineStatus = {
|
type EngineStatus = {
|
||||||
@ -166,7 +168,7 @@ function formatSignedCurrency(value: number, decimals = 2) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function formatTimestamp(value?: string) {
|
function formatTimestamp(value?: string) {
|
||||||
if (!value) return "—";
|
if (!value) return "-";
|
||||||
const parsed = new Date(value);
|
const parsed = new Date(value);
|
||||||
if (Number.isNaN(parsed.getTime())) {
|
if (Number.isNaN(parsed.getTime())) {
|
||||||
return value;
|
return value;
|
||||||
|
|||||||
@ -63,7 +63,6 @@ export default function Privacy() {
|
|||||||
className="relative overflow-hidden px-6 py-16"
|
className="relative overflow-hidden px-6 py-16"
|
||||||
initial={prefersReducedMotion ? undefined : pageEnterMotion.initial}
|
initial={prefersReducedMotion ? undefined : pageEnterMotion.initial}
|
||||||
animate={prefersReducedMotion ? undefined : pageEnterMotion.animate}
|
animate={prefersReducedMotion ? undefined : pageEnterMotion.animate}
|
||||||
transition={prefersReducedMotion ? undefined : pageEnterMotion.transition}
|
|
||||||
>
|
>
|
||||||
<div className="absolute inset-0 pointer-events-none">
|
<div className="absolute inset-0 pointer-events-none">
|
||||||
<div className="absolute -left-24 top-0 h-80 w-80 bg-gradient-to-br from-primary/18 via-primary/5 to-transparent blur-[120px]" />
|
<div className="absolute -left-24 top-0 h-80 w-80 bg-gradient-to-br from-primary/18 via-primary/5 to-transparent blur-[120px]" />
|
||||||
|
|||||||
@ -51,7 +51,6 @@ export default function Terms() {
|
|||||||
className="relative overflow-hidden px-6 py-16"
|
className="relative overflow-hidden px-6 py-16"
|
||||||
initial={prefersReducedMotion ? undefined : pageEnterMotion.initial}
|
initial={prefersReducedMotion ? undefined : pageEnterMotion.initial}
|
||||||
animate={prefersReducedMotion ? undefined : pageEnterMotion.animate}
|
animate={prefersReducedMotion ? undefined : pageEnterMotion.animate}
|
||||||
transition={prefersReducedMotion ? undefined : pageEnterMotion.transition}
|
|
||||||
>
|
>
|
||||||
<div className="absolute inset-0 pointer-events-none">
|
<div className="absolute inset-0 pointer-events-none">
|
||||||
<div className="absolute -left-24 top-0 h-80 w-80 bg-gradient-to-br from-primary/18 via-primary/5 to-transparent blur-[120px]" />
|
<div className="absolute -left-24 top-0 h-80 w-80 bg-gradient-to-br from-primary/18 via-primary/5 to-transparent blur-[120px]" />
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user