Compare commits
No commits in common. "b3b798ff2f86ab46e1f5fd28f8dc750c6b398abd" and "d10bb3dd7879c9cb232788f21af753b294fa91f7" have entirely different histories.
b3b798ff2f
...
d10bb3dd78
@ -6,10 +6,9 @@
|
|||||||
"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",
|
||||||
|
|||||||
21
src/api/strategy.js
Normal file
21
src/api/strategy.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
@ -1,64 +0,0 @@
|
|||||||
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,7 +33,9 @@ type StrategyTimelineProps = {
|
|||||||
|
|
||||||
function parseTimestamp(value?: string | null) {
|
function parseTimestamp(value?: string | null) {
|
||||||
if (!value) return null;
|
if (!value) return null;
|
||||||
const parsed = new Date(value);
|
const hasTimezone = /Z|[+-]\d{2}:?\d{2}$/.test(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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -197,7 +199,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: StrategyEvent) => typeof entry.seq !== "number" || !seenSeq.has(entry.seq),
|
(entry) => typeof entry.seq !== "number" || !seenSeq.has(entry.seq),
|
||||||
);
|
);
|
||||||
return [...prev, ...next];
|
return [...prev, ...next];
|
||||||
});
|
});
|
||||||
@ -206,7 +208,7 @@ function VerboseStrategyTimeline() {
|
|||||||
latestSeqRef.current = data.latest_seq;
|
latestSeqRef.current = data.latest_seq;
|
||||||
} else {
|
} else {
|
||||||
const lastSeq = normalized.reduce(
|
const lastSeq = normalized.reduce(
|
||||||
(max: number, entry: StrategyEvent) => Math.max(max, entry.seq ?? 0),
|
(max, entry) => Math.max(max, entry.seq ?? 0),
|
||||||
latestSeqRef.current,
|
latestSeqRef.current,
|
||||||
);
|
);
|
||||||
latestSeqRef.current = lastSeq;
|
latestSeqRef.current = lastSeq;
|
||||||
@ -304,7 +306,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: StrategyEvent, index: number) => {
|
{run.events.map((entry, index) => {
|
||||||
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 onExploreStrategies={() => {}} />;
|
return <FinalCTA />;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import HeroSection from "../landing/HeroSection";
|
import HeroSection from "../landing/HeroSection";
|
||||||
|
|
||||||
export default function HeroSectionExample() {
|
export default function HeroSectionExample() {
|
||||||
return <HeroSection onExploreStrategies={() => {}} />;
|
return <HeroSection />;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -252,7 +252,7 @@ export default function AuthDialogs({ layout = "desktop" }: AuthDialogsProps) {
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="link"
|
||||||
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: Record<string, { label: string; href: string; newTab?: boolean }[]> = {
|
const links = {
|
||||||
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,10 +52,8 @@ type PositionsResponse = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type MarketStatusResponse = {
|
type MarketStatusResponse = {
|
||||||
status?: "OPEN" | "CLOSED" | "HOLIDAY";
|
status?: "OPEN" | "CLOSED";
|
||||||
reason?: string;
|
|
||||||
checked_at?: string;
|
checked_at?: string;
|
||||||
next_open_at?: string | null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type FundsResponse = {
|
type FundsResponse = {
|
||||||
@ -197,16 +195,6 @@ function getNetQuantity(item: any) {
|
|||||||
return getEffectiveQuantity(item);
|
return getEffectiveQuantity(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatFrequencyLabel(value?: number | null, unit?: string | null) {
|
|
||||||
const safeValue = Number(value);
|
|
||||||
if (!Number.isFinite(safeValue) || safeValue <= 0 || !unit) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const normalizedUnit = unit === "minutes" ? "minute" : unit === "days" ? "day" : unit;
|
|
||||||
const pluralizedUnit = safeValue === 1 ? normalizedUnit : `${normalizedUnit}s`;
|
|
||||||
return `${safeValue} ${pluralizedUnit}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getAveragePrice(item: any) {
|
function getAveragePrice(item: any) {
|
||||||
return firstNumber(item?.average_price, item?.avg_price);
|
return firstNumber(item?.average_price, item?.avg_price);
|
||||||
}
|
}
|
||||||
@ -448,6 +436,16 @@ 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>({
|
||||||
@ -459,6 +457,16 @@ 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(() =>
|
||||||
@ -599,6 +607,16 @@ 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>({
|
||||||
@ -610,6 +628,16 @@ 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(
|
||||||
@ -617,7 +645,7 @@ export default function PortfolioSection() {
|
|||||||
if (!brokerStatus?.connected) {
|
if (!brokerStatus?.connected) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const tasks: Promise<unknown>[] = [
|
const tasks = [
|
||||||
holdingsQuery.refetch(),
|
holdingsQuery.refetch(),
|
||||||
positionsQuery.refetch(),
|
positionsQuery.refetch(),
|
||||||
fundsQuery.refetch(),
|
fundsQuery.refetch(),
|
||||||
@ -640,58 +668,11 @@ export default function PortfolioSection() {
|
|||||||
|
|
||||||
const isConnected = !!brokerStatus?.connected;
|
const isConnected = !!brokerStatus?.connected;
|
||||||
const isAuthed = brokerStatus !== null;
|
const isAuthed = brokerStatus !== null;
|
||||||
const holdings = holdingsQuery.data?.holdings ?? cachedHoldings;
|
const holdings = holdingsQuery.data ? holdingsQuery.data.holdings : cachedHoldings;
|
||||||
const positions = positionsQuery.data?.positions ?? cachedPositions;
|
const positions = positionsQuery.data ? 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);
|
||||||
@ -781,38 +762,6 @@ export default function PortfolioSection() {
|
|||||||
: strategyStatus === "WAITING"
|
: strategyStatus === "WAITING"
|
||||||
? "WAITING"
|
? "WAITING"
|
||||||
: "STOPPED";
|
: "STOPPED";
|
||||||
const savedStrategyAmount = Number(strategyDetails?.config?.sip_amount);
|
|
||||||
const savedStrategyFrequencyValue = Number(strategyDetails?.config?.sip_frequency?.value);
|
|
||||||
const savedStrategyFrequencyUnit =
|
|
||||||
strategyDetails?.config?.sip_frequency?.unit === "minutes"
|
|
||||||
? "minutes"
|
|
||||||
: strategyDetails?.config?.sip_frequency?.unit === "days"
|
|
||||||
? "days"
|
|
||||||
: null;
|
|
||||||
const savedStrategyCadence = formatFrequencyLabel(
|
|
||||||
savedStrategyFrequencyValue,
|
|
||||||
savedStrategyFrequencyUnit,
|
|
||||||
);
|
|
||||||
const savedStrategyAmountLabel =
|
|
||||||
Number.isFinite(savedStrategyAmount) && savedStrategyAmount > 0
|
|
||||||
? formatCurrency(savedStrategyAmount)
|
|
||||||
: null;
|
|
||||||
const savedStrategyMode =
|
|
||||||
strategyDetails?.config?.mode === "LIVE"
|
|
||||||
? "Live"
|
|
||||||
: strategyDetails?.config?.mode === "PAPER"
|
|
||||||
? "Paper"
|
|
||||||
: null;
|
|
||||||
const savedStrategyBroker =
|
|
||||||
strategyDetails?.config?.broker ? formatBrokerName(strategyDetails.config.broker) : null;
|
|
||||||
const savedStrategySummary = [
|
|
||||||
savedStrategyAmountLabel ? `${savedStrategyAmountLabel} SIP` : null,
|
|
||||||
savedStrategyCadence ? `every ${savedStrategyCadence}` : null,
|
|
||||||
savedStrategyMode,
|
|
||||||
savedStrategyBroker,
|
|
||||||
]
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(" · ");
|
|
||||||
const isStrategyActive =
|
const isStrategyActive =
|
||||||
normalizedStrategyStatus === "RUNNING" ||
|
normalizedStrategyStatus === "RUNNING" ||
|
||||||
normalizedStrategyStatus === "WAITING";
|
normalizedStrategyStatus === "WAITING";
|
||||||
@ -1534,12 +1483,6 @@ export default function PortfolioSection() {
|
|||||||
Minutes mode is for live testing only. The engine checks every few seconds in this mode.
|
Minutes mode is for live testing only. The engine checks every few seconds in this mode.
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{savedStrategySummary ? (
|
|
||||||
<div className="rounded-lg border border-border/60 bg-background/40 px-3 py-2 text-xs text-muted-foreground">
|
|
||||||
{isStrategyActive ? "Current run" : showResumeStrategy ? "Resume will continue" : "Saved strategy config"}
|
|
||||||
: <span className="font-medium text-foreground"> {savedStrategySummary}</span>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
{showResumeStrategy ? (
|
{showResumeStrategy ? (
|
||||||
<div className="text-xs text-muted-foreground">
|
<div className="text-xs text-muted-foreground">
|
||||||
Resume uses the previously saved SIP configuration. Choose restart to begin a fresh cycle.
|
Resume uses the previously saved SIP configuration. Choose restart to begin a fresh cycle.
|
||||||
|
|||||||
@ -43,11 +43,8 @@ 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) {
|
||||||
@ -164,7 +161,7 @@ export const queryClient = new QueryClient({
|
|||||||
queryFn: getQueryFn({ on401: "throw" }),
|
queryFn: getQueryFn({ on401: "throw" }),
|
||||||
refetchInterval: false,
|
refetchInterval: false,
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
staleTime: 5_000,
|
staleTime: Infinity,
|
||||||
retry: false,
|
retry: false,
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
|
|||||||
@ -43,6 +43,7 @@ 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">
|
||||||
@ -51,9 +52,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"
|
||||||
{...(prefersReducedMotion
|
initial={heroMotion.initial}
|
||||||
? {}
|
animate={heroMotion.animate}
|
||||||
: { initial: pageEnterMotion.initial, animate: pageEnterMotion.animate })}
|
transition={heroMotion.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/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,6 +35,7 @@ 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(() => {
|
||||||
@ -49,9 +50,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"
|
||||||
{...(prefersReducedMotion
|
initial={heroMotion.initial}
|
||||||
? {}
|
animate={heroMotion.animate}
|
||||||
: { initial: pageEnterMotion.initial, animate: pageEnterMotion.animate })}
|
transition={heroMotion.transition}
|
||||||
>
|
>
|
||||||
<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,6 +51,7 @@ 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,6 +59,7 @@ 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,10 +88,8 @@ type PaperMtmResponse = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type MarketStatusResponse = {
|
type MarketStatusResponse = {
|
||||||
status?: "OPEN" | "CLOSED" | "HOLIDAY";
|
status?: "OPEN" | "CLOSED";
|
||||||
reason?: string;
|
|
||||||
checked_at?: string;
|
checked_at?: string;
|
||||||
next_open_at?: string | null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type EngineStatus = {
|
type EngineStatus = {
|
||||||
@ -168,7 +166,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,6 +63,7 @@ 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,6 +51,7 @@ 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