Improve mobile layout across landing and portfolio pages
This commit is contained in:
parent
d63202261a
commit
e183f9ddce
@ -8,11 +8,11 @@ type ChatButtonProps = {
|
|||||||
|
|
||||||
export default function ChatButton({ open, onToggle }: ChatButtonProps) {
|
export default function ChatButton({ open, onToggle }: ChatButtonProps) {
|
||||||
return (
|
return (
|
||||||
<div className="fixed bottom-6 right-6 z-[60] flex flex-col items-end gap-2">
|
<div className="fixed bottom-4 right-4 z-[60] flex flex-col items-end gap-2 sm:bottom-6 sm:right-6">
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 10 }}
|
initial={{ opacity: 0, y: 10 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
className="rounded-full bg-white/10 px-3 py-1 text-xs text-slate-100 shadow-lg backdrop-blur"
|
className="hidden rounded-full bg-white/10 px-3 py-1 text-xs text-slate-100 shadow-lg backdrop-blur sm:block"
|
||||||
>
|
>
|
||||||
Need help?
|
Need help?
|
||||||
</motion.div>
|
</motion.div>
|
||||||
@ -20,7 +20,7 @@ export default function ChatButton({ open, onToggle }: ChatButtonProps) {
|
|||||||
whileHover={{ scale: 1.05 }}
|
whileHover={{ scale: 1.05 }}
|
||||||
whileTap={{ scale: 0.96 }}
|
whileTap={{ scale: 0.96 }}
|
||||||
onClick={onToggle}
|
onClick={onToggle}
|
||||||
className="relative flex h-14 w-14 items-center justify-center rounded-full bg-gradient-to-br from-sky-500 to-cyan-400 text-white shadow-xl"
|
className="relative flex h-12 w-12 items-center justify-center rounded-full bg-gradient-to-br from-sky-500 to-cyan-400 text-white shadow-xl sm:h-14 sm:w-14"
|
||||||
aria-label={open ? "Close chat" : "Open chat"}
|
aria-label={open ? "Close chat" : "Open chat"}
|
||||||
>
|
>
|
||||||
<motion.span
|
<motion.span
|
||||||
@ -28,7 +28,7 @@ export default function ChatButton({ open, onToggle }: ChatButtonProps) {
|
|||||||
animate={{ scale: [1, 1.4, 1], opacity: [0.6, 0, 0.6] }}
|
animate={{ scale: [1, 1.4, 1], opacity: [0.6, 0, 0.6] }}
|
||||||
transition={{ duration: 2.2, repeat: Infinity, ease: "easeInOut" }}
|
transition={{ duration: 2.2, repeat: Infinity, ease: "easeInOut" }}
|
||||||
/>
|
/>
|
||||||
<MessageCircle className="relative z-10 h-6 w-6" />
|
<MessageCircle className="relative z-10 h-5 w-5 sm:h-6 sm:w-6" />
|
||||||
</motion.button>
|
</motion.button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -91,7 +91,7 @@ export default function ChatWidget() {
|
|||||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||||
exit={{ opacity: 0, y: 20, scale: 0.98 }}
|
exit={{ opacity: 0, y: 20, scale: 0.98 }}
|
||||||
transition={{ duration: 0.25, ease: "easeOut" }}
|
transition={{ duration: 0.25, ease: "easeOut" }}
|
||||||
className="fixed bottom-6 right-4 top-20 z-[60] w-[372px] max-w-[calc(100vw-2rem)]"
|
className="fixed bottom-20 left-3 right-3 z-[60] h-[min(68vh,32rem)] sm:bottom-6 sm:left-auto sm:right-4 sm:top-20 sm:h-auto sm:w-[372px] sm:max-w-[calc(100vw-2rem)]"
|
||||||
>
|
>
|
||||||
<div className="flex h-full flex-col overflow-hidden rounded-3xl border border-white/10 bg-slate-900/70 shadow-2xl backdrop-blur-xl">
|
<div className="flex h-full flex-col overflow-hidden rounded-3xl border border-white/10 bg-slate-900/70 shadow-2xl backdrop-blur-xl">
|
||||||
<div className="flex items-center justify-between border-b border-white/10 px-5 py-4">
|
<div className="flex items-center justify-between border-b border-white/10 px-5 py-4">
|
||||||
|
|||||||
@ -22,6 +22,7 @@ type BrokerConnectDialogProps = {
|
|||||||
layout?: "desktop" | "mobile";
|
layout?: "desktop" | "mobile";
|
||||||
open?: boolean;
|
open?: boolean;
|
||||||
onOpenChange?: (open: boolean) => void;
|
onOpenChange?: (open: boolean) => void;
|
||||||
|
triggerClassName?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type SessionUser = Pick<User, "id" | "username">;
|
type SessionUser = Pick<User, "id" | "username">;
|
||||||
@ -48,6 +49,7 @@ export default function BrokerConnectDialog({
|
|||||||
layout = "desktop",
|
layout = "desktop",
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
|
triggerClassName: triggerClassNameProp,
|
||||||
}: BrokerConnectDialogProps) {
|
}: BrokerConnectDialogProps) {
|
||||||
const [connectOpenInternal, setConnectOpenInternal] = useState(false);
|
const [connectOpenInternal, setConnectOpenInternal] = useState(false);
|
||||||
const isControlled = open !== undefined;
|
const isControlled = open !== undefined;
|
||||||
@ -243,7 +245,7 @@ export default function BrokerConnectDialog({
|
|||||||
<Dialog open={connectOpen} onOpenChange={setConnectOpen}>
|
<Dialog open={connectOpen} onOpenChange={setConnectOpen}>
|
||||||
<Button
|
<Button
|
||||||
variant={connected ? "secondary" : "secondary"}
|
variant={connected ? "secondary" : "secondary"}
|
||||||
className={triggerClassName}
|
className={[triggerClassName, triggerClassNameProp].filter(Boolean).join(" ")}
|
||||||
disabled={connected}
|
disabled={connected}
|
||||||
onClick={handleConnectClick}
|
onClick={handleConnectClick}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -40,12 +40,12 @@ export default function FinalCTA({ onExploreStrategies }: FinalCTAProps) {
|
|||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
ref={sectionRef}
|
ref={sectionRef}
|
||||||
className="py-32 px-6 relative overflow-hidden"
|
className="relative overflow-hidden px-4 py-20 sm:px-6 sm:py-24 md:py-32"
|
||||||
data-testid="section-final-cta"
|
data-testid="section-final-cta"
|
||||||
onMouseMove={handleMouseMove}
|
onMouseMove={handleMouseMove}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[500px] h-[500px] transition-all duration-1000 ${
|
className={`absolute top-1/2 left-1/2 h-[280px] w-[280px] -translate-x-1/2 -translate-y-1/2 transition-all duration-1000 sm:h-[360px] sm:w-[360px] md:h-[500px] md:w-[500px] ${
|
||||||
isVisible ? "opacity-100 scale-100" : "opacity-0 scale-50"
|
isVisible ? "opacity-100 scale-100" : "opacity-0 scale-50"
|
||||||
}`}
|
}`}
|
||||||
style={{
|
style={{
|
||||||
@ -79,7 +79,7 @@ export default function FinalCTA({ onExploreStrategies }: FinalCTAProps) {
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<h2
|
<h2
|
||||||
className="text-4xl md:text-5xl font-bold mb-6 leading-tight"
|
className="mb-6 text-3xl font-bold leading-tight sm:text-4xl md:text-5xl"
|
||||||
data-testid="text-final-cta-headline"
|
data-testid="text-final-cta-headline"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@ -99,7 +99,7 @@ export default function FinalCTA({ onExploreStrategies }: FinalCTAProps) {
|
|||||||
</span>
|
</span>
|
||||||
</h2>
|
</h2>
|
||||||
<p
|
<p
|
||||||
className={`text-xl text-muted-foreground mb-12 max-w-xl mx-auto transition-all duration-700 delay-400 ${
|
className={`mx-auto mb-10 max-w-xl text-base text-muted-foreground transition-all duration-700 delay-400 sm:mb-12 sm:text-lg md:text-xl ${
|
||||||
isVisible ? "opacity-100 translate-y-0" : "opacity-0 translate-y-4"
|
isVisible ? "opacity-100 translate-y-0" : "opacity-0 translate-y-4"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@ -113,7 +113,7 @@ export default function FinalCTA({ onExploreStrategies }: FinalCTAProps) {
|
|||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
size="lg"
|
size="lg"
|
||||||
className="rounded-xl px-10 py-6 text-lg group relative overflow-hidden"
|
className="group relative mx-auto w-full max-w-[20rem] overflow-hidden rounded-xl px-6 py-5 text-base sm:max-w-none sm:px-10 sm:py-6 sm:text-lg"
|
||||||
data-testid="button-final-cta"
|
data-testid="button-final-cta"
|
||||||
onClick={onExploreStrategies}
|
onClick={onExploreStrategies}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -14,12 +14,27 @@ export default function HeroSection({ onExploreStrategies }: HeroSectionProps) {
|
|||||||
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
|
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
|
||||||
const [isLoaded, setIsLoaded] = useState(false);
|
const [isLoaded, setIsLoaded] = useState(false);
|
||||||
const [scrollY, setScrollY] = useState(0);
|
const [scrollY, setScrollY] = useState(0);
|
||||||
|
const [isCompactViewport, setIsCompactViewport] = useState(false);
|
||||||
const [, navigate] = useLocation();
|
const [, navigate] = useLocation();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsLoaded(true);
|
setIsLoaded(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const mediaQuery = window.matchMedia("(max-width: 639px)");
|
||||||
|
const updateViewportMode = () => setIsCompactViewport(mediaQuery.matches);
|
||||||
|
updateViewportMode();
|
||||||
|
|
||||||
|
if (mediaQuery.addEventListener) {
|
||||||
|
mediaQuery.addEventListener("change", updateViewportMode);
|
||||||
|
return () => mediaQuery.removeEventListener("change", updateViewportMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaQuery.addListener(updateViewportMode);
|
||||||
|
return () => mediaQuery.removeListener(updateViewportMode);
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
setScrollY(window.scrollY);
|
setScrollY(window.scrollY);
|
||||||
@ -29,6 +44,10 @@ export default function HeroSection({ onExploreStrategies }: HeroSectionProps) {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (isCompactViewport) {
|
||||||
|
setMousePosition({ x: 0, y: 0 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
const handleMouseMove = (e: MouseEvent) => {
|
const handleMouseMove = (e: MouseEvent) => {
|
||||||
if (!containerRef.current) return;
|
if (!containerRef.current) return;
|
||||||
const rect = containerRef.current.getBoundingClientRect();
|
const rect = containerRef.current.getBoundingClientRect();
|
||||||
@ -40,16 +59,17 @@ export default function HeroSection({ onExploreStrategies }: HeroSectionProps) {
|
|||||||
const container = containerRef.current;
|
const container = containerRef.current;
|
||||||
container?.addEventListener("mousemove", handleMouseMove);
|
container?.addEventListener("mousemove", handleMouseMove);
|
||||||
return () => container?.removeEventListener("mousemove", handleMouseMove);
|
return () => container?.removeEventListener("mousemove", handleMouseMove);
|
||||||
}, []);
|
}, [isCompactViewport]);
|
||||||
|
|
||||||
const parallaxY = scrollY * 0.5;
|
const parallaxY = isCompactViewport ? scrollY * 0.18 : scrollY * 0.5;
|
||||||
const videoScale = 1 + scrollY * 0.0003;
|
const videoScale = 1 + scrollY * (isCompactViewport ? 0.00012 : 0.0003);
|
||||||
const videoOpacity = Math.max(0.4 - scrollY * 0.0005, 0.15);
|
const videoOpacity = Math.max((isCompactViewport ? 0.3 : 0.4) - scrollY * 0.0005, 0.15);
|
||||||
|
const particleCount = isCompactViewport ? 10 : 25;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
className="relative min-h-screen flex items-center justify-center overflow-hidden"
|
className="relative flex min-h-[100svh] items-center justify-center overflow-hidden px-4 sm:px-0"
|
||||||
data-testid="section-hero"
|
data-testid="section-hero"
|
||||||
style={{ perspective: "1000px" }}
|
style={{ perspective: "1000px" }}
|
||||||
>
|
>
|
||||||
@ -86,7 +106,7 @@ export default function HeroSection({ onExploreStrategies }: HeroSectionProps) {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`absolute top-1/4 left-1/4 w-96 h-96 bg-primary/20 rounded-full blur-3xl transition-all duration-1000 ${
|
className={`absolute left-[12%] top-[18%] h-56 w-56 rounded-full bg-primary/20 blur-3xl transition-all duration-1000 sm:top-1/4 sm:left-1/4 sm:h-96 sm:w-96 ${
|
||||||
isLoaded ? "opacity-100 scale-100" : "opacity-0 scale-50"
|
isLoaded ? "opacity-100 scale-100" : "opacity-0 scale-50"
|
||||||
}`}
|
}`}
|
||||||
style={{
|
style={{
|
||||||
@ -94,7 +114,7 @@ export default function HeroSection({ onExploreStrategies }: HeroSectionProps) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className={`absolute bottom-1/3 right-1/4 w-80 h-80 bg-chart-2/20 rounded-full blur-3xl transition-all duration-1000 delay-200 ${
|
className={`absolute bottom-[18%] right-[8%] h-48 w-48 rounded-full bg-chart-2/20 blur-3xl transition-all duration-1000 delay-200 sm:bottom-1/3 sm:right-1/4 sm:h-80 sm:w-80 ${
|
||||||
isLoaded ? "opacity-100 scale-100" : "opacity-0 scale-50"
|
isLoaded ? "opacity-100 scale-100" : "opacity-0 scale-50"
|
||||||
}`}
|
}`}
|
||||||
style={{
|
style={{
|
||||||
@ -102,7 +122,7 @@ export default function HeroSection({ onExploreStrategies }: HeroSectionProps) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className={`absolute top-1/2 right-1/3 w-64 h-64 bg-chart-3/15 rounded-full blur-3xl transition-all duration-1000 delay-500 ${
|
className={`absolute right-[10%] top-[46%] h-40 w-40 rounded-full bg-chart-3/15 blur-3xl transition-all duration-1000 delay-500 sm:top-1/2 sm:right-1/3 sm:h-64 sm:w-64 ${
|
||||||
isLoaded ? "opacity-100 scale-100" : "opacity-0 scale-50"
|
isLoaded ? "opacity-100 scale-100" : "opacity-0 scale-50"
|
||||||
}`}
|
}`}
|
||||||
style={{
|
style={{
|
||||||
@ -112,7 +132,7 @@ export default function HeroSection({ onExploreStrategies }: HeroSectionProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="absolute inset-0 overflow-hidden z-10">
|
<div className="absolute inset-0 overflow-hidden z-10">
|
||||||
{[...Array(25)].map((_, i) => (
|
{[...Array(particleCount)].map((_, i) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className={`absolute rounded-full transition-all duration-700 ${
|
className={`absolute rounded-full transition-all duration-700 ${
|
||||||
@ -144,7 +164,7 @@ export default function HeroSection({ onExploreStrategies }: HeroSectionProps) {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`absolute top-20 left-[15%] w-20 h-20 transition-all duration-1000 ${
|
className={`absolute left-[8%] top-24 h-12 w-12 transition-all duration-1000 sm:top-20 sm:left-[15%] sm:h-20 sm:w-20 ${
|
||||||
isLoaded ? "opacity-100 rotate-12" : "opacity-0 rotate-0"
|
isLoaded ? "opacity-100 rotate-12" : "opacity-0 rotate-0"
|
||||||
}`}
|
}`}
|
||||||
style={{
|
style={{
|
||||||
@ -156,7 +176,7 @@ export default function HeroSection({ onExploreStrategies }: HeroSectionProps) {
|
|||||||
<DollarSign className="w-full h-full text-primary/25 drop-shadow-sm" strokeWidth={1.25} />
|
<DollarSign className="w-full h-full text-primary/25 drop-shadow-sm" strokeWidth={1.25} />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={`absolute bottom-32 right-[20%] w-16 h-16 transition-all duration-1000 delay-300 ${
|
className={`absolute bottom-28 right-[12%] h-10 w-10 transition-all duration-1000 delay-300 sm:bottom-32 sm:right-[20%] sm:h-16 sm:w-16 ${
|
||||||
isLoaded ? "opacity-100" : "opacity-0"
|
isLoaded ? "opacity-100" : "opacity-0"
|
||||||
}`}
|
}`}
|
||||||
style={{
|
style={{
|
||||||
@ -168,7 +188,7 @@ export default function HeroSection({ onExploreStrategies }: HeroSectionProps) {
|
|||||||
<IndianRupee className="w-full h-full text-chart-2/25 drop-shadow-sm" strokeWidth={1.25} />
|
<IndianRupee className="w-full h-full text-chart-2/25 drop-shadow-sm" strokeWidth={1.25} />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={`absolute top-1/3 right-[10%] w-12 h-12 transition-all duration-1000 delay-500 ${
|
className={`absolute right-[10%] top-[28%] h-8 w-8 transition-all duration-1000 delay-500 sm:top-1/3 sm:h-12 sm:w-12 ${
|
||||||
isLoaded ? "opacity-100 rotate-45" : "opacity-0 rotate-0"
|
isLoaded ? "opacity-100 rotate-45" : "opacity-0 rotate-0"
|
||||||
}`}
|
}`}
|
||||||
style={{
|
style={{
|
||||||
@ -182,7 +202,7 @@ export default function HeroSection({ onExploreStrategies }: HeroSectionProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="relative z-20 max-w-4xl mx-auto px-6 text-center"
|
className="relative z-20 mx-auto max-w-4xl px-4 text-center sm:px-6"
|
||||||
style={{
|
style={{
|
||||||
transform: `translate3d(${mousePosition.x * 0.15}px, ${mousePosition.y * 0.15}px, 100px)`,
|
transform: `translate3d(${mousePosition.x * 0.15}px, ${mousePosition.y * 0.15}px, 100px)`,
|
||||||
transformStyle: "preserve-3d",
|
transformStyle: "preserve-3d",
|
||||||
@ -194,7 +214,7 @@ export default function HeroSection({ onExploreStrategies }: HeroSectionProps) {
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<h1
|
<h1
|
||||||
className="text-5xl md:text-7xl font-bold tracking-tight mb-6 leading-tight"
|
className="mb-6 text-4xl font-bold leading-tight tracking-tight sm:text-5xl md:text-7xl"
|
||||||
data-testid="text-hero-headline"
|
data-testid="text-hero-headline"
|
||||||
style={{ transform: "translateZ(20px)" }}
|
style={{ transform: "translateZ(20px)" }}
|
||||||
>
|
>
|
||||||
@ -215,7 +235,7 @@ export default function HeroSection({ onExploreStrategies }: HeroSectionProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p
|
<p
|
||||||
className={`text-xl md:text-2xl text-muted-foreground max-w-2xl mx-auto mb-12 leading-relaxed transition-all duration-1000 delay-700 ${
|
className={`mx-auto mb-10 max-w-2xl text-base leading-relaxed text-muted-foreground transition-all duration-1000 delay-700 sm:mb-12 sm:text-lg md:text-2xl ${
|
||||||
isLoaded ? "opacity-100 translate-y-0" : "opacity-0 translate-y-8"
|
isLoaded ? "opacity-100 translate-y-0" : "opacity-0 translate-y-8"
|
||||||
}`}
|
}`}
|
||||||
data-testid="text-hero-subheadline"
|
data-testid="text-hero-subheadline"
|
||||||
@ -226,14 +246,14 @@ export default function HeroSection({ onExploreStrategies }: HeroSectionProps) {
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={`flex flex-col sm:flex-row items-center justify-center gap-4 transition-all duration-1000 delay-1000 ${
|
className={`flex flex-col items-stretch justify-center gap-4 transition-all duration-1000 delay-1000 sm:flex-row sm:items-center ${
|
||||||
isLoaded ? "opacity-100 translate-y-0" : "opacity-0 translate-y-8"
|
isLoaded ? "opacity-100 translate-y-0" : "opacity-0 translate-y-8"
|
||||||
}`}
|
}`}
|
||||||
style={{ transform: "translateZ(30px)" }}
|
style={{ transform: "translateZ(30px)" }}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
size="lg"
|
size="lg"
|
||||||
className="rounded-xl px-8 py-6 text-lg group relative overflow-hidden shadow-lg shadow-primary/20"
|
className="group relative mx-auto w-full max-w-[20rem] overflow-hidden rounded-xl px-6 py-5 text-base shadow-lg shadow-primary/20 sm:max-w-none sm:px-8 sm:py-6 sm:text-lg"
|
||||||
data-testid="button-explore-strategies"
|
data-testid="button-explore-strategies"
|
||||||
onClick={onExploreStrategies}
|
onClick={onExploreStrategies}
|
||||||
>
|
>
|
||||||
@ -246,7 +266,7 @@ export default function HeroSection({ onExploreStrategies }: HeroSectionProps) {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="lg"
|
size="lg"
|
||||||
className="rounded-xl px-8 py-6 text-lg text-muted-foreground backdrop-blur-sm"
|
className="mx-auto w-full max-w-[20rem] rounded-xl px-6 py-5 text-base text-muted-foreground backdrop-blur-sm sm:max-w-none sm:px-8 sm:py-6 sm:text-lg"
|
||||||
data-testid="button-learn-more"
|
data-testid="button-learn-more"
|
||||||
onClick={() => navigate("/learn-more")}
|
onClick={() => navigate("/learn-more")}
|
||||||
>
|
>
|
||||||
@ -256,7 +276,7 @@ export default function HeroSection({ onExploreStrategies }: HeroSectionProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={`absolute bottom-8 left-1/2 -translate-x-1/2 z-20 transition-all duration-1000 delay-1500 ${
|
className={`absolute bottom-8 left-1/2 z-20 hidden -translate-x-1/2 transition-all duration-1000 delay-1500 sm:block ${
|
||||||
isLoaded ? "opacity-100 translate-y-0" : "opacity-0 translate-y-4"
|
isLoaded ? "opacity-100 translate-y-0" : "opacity-0 translate-y-4"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -66,10 +66,10 @@ export default function HowItWorks() {
|
|||||||
<section
|
<section
|
||||||
ref={sectionRef}
|
ref={sectionRef}
|
||||||
id="how-it-works"
|
id="how-it-works"
|
||||||
className="py-32 px-6 relative overflow-hidden"
|
className="relative overflow-hidden px-4 py-20 sm:px-6 sm:py-24 md:py-32"
|
||||||
data-testid="section-how-it-works"
|
data-testid="section-how-it-works"
|
||||||
>
|
>
|
||||||
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-[800px] h-[800px] opacity-30">
|
<div className="absolute left-1/2 top-1/2 h-[420px] w-[420px] -translate-x-1/2 -translate-y-1/2 opacity-20 sm:h-[560px] sm:w-[560px] md:h-[800px] md:w-[800px] md:opacity-30">
|
||||||
<div
|
<div
|
||||||
className="absolute inset-0 rounded-full border border-primary/20"
|
className="absolute inset-0 rounded-full border border-primary/20"
|
||||||
style={{ animation: "pulse-ring 4s ease-out infinite" }}
|
style={{ animation: "pulse-ring 4s ease-out infinite" }}
|
||||||
@ -89,20 +89,20 @@ export default function HowItWorks() {
|
|||||||
<p className="text-sm uppercase tracking-widest text-muted-foreground mb-4">
|
<p className="text-sm uppercase tracking-widest text-muted-foreground mb-4">
|
||||||
How It Works
|
How It Works
|
||||||
</p>
|
</p>
|
||||||
<h2 className="text-4xl md:text-5xl font-bold mb-4" data-testid="text-how-it-works-headline">
|
<h2 className="mb-4 text-3xl font-bold sm:text-4xl md:text-5xl" data-testid="text-how-it-works-headline">
|
||||||
Four Simple Steps
|
Four Simple Steps
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-xl text-muted-foreground">
|
<p className="text-base text-muted-foreground sm:text-lg md:text-xl">
|
||||||
Start your investment journey in minutes.
|
Start your investment journey in minutes.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid md:grid-cols-2 gap-8">
|
<div className="grid gap-6 md:grid-cols-2 md:gap-8">
|
||||||
{steps.map((step, index) => (
|
{steps.map((step, index) => (
|
||||||
<div
|
<div
|
||||||
key={step.title}
|
key={step.title}
|
||||||
data-step={index}
|
data-step={index}
|
||||||
className={`flex gap-5 transition-all duration-700 ease-out group cursor-pointer ${
|
className={`group flex items-start gap-4 transition-all duration-700 ease-out sm:gap-5 ${
|
||||||
visibleSteps.includes(index)
|
visibleSteps.includes(index)
|
||||||
? "opacity-100 translate-y-0"
|
? "opacity-100 translate-y-0"
|
||||||
: "opacity-0 translate-y-8"
|
: "opacity-0 translate-y-8"
|
||||||
@ -111,7 +111,7 @@ export default function HowItWorks() {
|
|||||||
onMouseEnter={() => setActiveStep(index)}
|
onMouseEnter={() => setActiveStep(index)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`flex-shrink-0 w-14 h-14 rounded-xl flex items-center justify-center transition-all duration-500 ${
|
className={`flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-xl transition-all duration-500 sm:h-14 sm:w-14 ${
|
||||||
activeStep === index
|
activeStep === index
|
||||||
? "bg-primary text-primary-foreground scale-110 shadow-lg shadow-primary/25"
|
? "bg-primary text-primary-foreground scale-110 shadow-lg shadow-primary/25"
|
||||||
: "bg-primary/10 text-primary"
|
: "bg-primary/10 text-primary"
|
||||||
@ -131,7 +131,7 @@ export default function HowItWorks() {
|
|||||||
0{index + 1}
|
0{index + 1}
|
||||||
</span>
|
</span>
|
||||||
<h3
|
<h3
|
||||||
className={`text-lg font-semibold transition-colors duration-300 ${
|
className={`text-base font-semibold transition-colors duration-300 sm:text-lg ${
|
||||||
activeStep === index ? "text-foreground" : "text-foreground/80"
|
activeStep === index ? "text-foreground" : "text-foreground/80"
|
||||||
}`}
|
}`}
|
||||||
data-testid={`text-step-title-${index}`}
|
data-testid={`text-step-title-${index}`}
|
||||||
|
|||||||
@ -68,6 +68,7 @@ export default function Navigation() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleNavClick = (href: string) => {
|
const handleNavClick = (href: string) => {
|
||||||
|
setIsMobileMenuOpen(false);
|
||||||
const hash = href.startsWith("#") ? href.slice(1) : "";
|
const hash = href.startsWith("#") ? href.slice(1) : "";
|
||||||
if (href.startsWith("/") && !href.startsWith("#")) {
|
if (href.startsWith("/") && !href.startsWith("#")) {
|
||||||
navigate(href);
|
navigate(href);
|
||||||
@ -130,12 +131,18 @@ export default function Navigation() {
|
|||||||
return location === href || location.startsWith(`${href}/`);
|
return location === href || location.startsWith(`${href}/`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const LinkItem = ({ link }: { link: { label: string; href: string; newTab?: boolean } }) => (
|
const LinkItem = ({
|
||||||
|
link,
|
||||||
|
mobile = false,
|
||||||
|
}: {
|
||||||
|
link: { label: string; href: string; newTab?: boolean };
|
||||||
|
mobile?: boolean;
|
||||||
|
}) => (
|
||||||
<button
|
<button
|
||||||
onClick={() => handleNavClick(link.href)}
|
onClick={() => handleNavClick(link.href)}
|
||||||
className={`group relative whitespace-nowrap text-sm font-medium transition-colors duration-300 ${
|
className={`group relative text-sm font-medium transition-colors duration-300 ${
|
||||||
isActiveLink(link.href) ? "text-foreground" : "text-muted-foreground hover:text-foreground"
|
isActiveLink(link.href) ? "text-foreground" : "text-muted-foreground hover:text-foreground"
|
||||||
}`}
|
} ${mobile ? "w-full py-1.5 text-left text-base" : "whitespace-nowrap"}`}
|
||||||
data-testid={`link-nav-${link.label.toLowerCase().replace(/\s/g, "-")}`}
|
data-testid={`link-nav-${link.label.toLowerCase().replace(/\s/g, "-")}`}
|
||||||
>
|
>
|
||||||
{link.label}
|
{link.label}
|
||||||
@ -154,11 +161,11 @@ export default function Navigation() {
|
|||||||
isScrolled ? "bg-background/80 backdrop-blur-xl border-b border-border" : "bg-transparent"
|
isScrolled ? "bg-background/80 backdrop-blur-xl border-b border-border" : "bg-transparent"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="max-w-6xl mx-auto px-6 py-4">
|
<div className="mx-auto max-w-6xl px-4 py-3 sm:px-6 sm:py-4">
|
||||||
<div className="flex items-center justify-between gap-4">
|
<div className="flex items-center justify-between gap-4">
|
||||||
<button
|
<button
|
||||||
onClick={() => handleNavClick("#")}
|
onClick={() => handleNavClick("#")}
|
||||||
className="text-xl font-bold tracking-tight text-foreground"
|
className="text-lg font-bold tracking-tight text-foreground sm:text-xl"
|
||||||
data-testid="link-logo"
|
data-testid="link-logo"
|
||||||
>
|
>
|
||||||
QuantFortune
|
QuantFortune
|
||||||
@ -173,9 +180,9 @@ export default function Navigation() {
|
|||||||
<div className="hidden md:flex items-center gap-3">
|
<div className="hidden md:flex items-center gap-3">
|
||||||
{sessionUser ? (
|
{sessionUser ? (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center gap-2 rounded-full border border-primary/40 bg-primary/10 px-3 py-1 text-sm font-medium text-primary">
|
<div className="flex max-w-[11rem] items-center gap-2 rounded-full border border-primary/40 bg-primary/10 px-3 py-1 text-sm font-medium text-primary lg:max-w-[16rem]">
|
||||||
<span className="h-2 w-2 rounded-full bg-primary" />
|
<span className="h-2 w-2 rounded-full bg-primary" />
|
||||||
{sessionUser.username}
|
<span className="truncate">{sessionUser.username}</span>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@ -195,27 +202,28 @@ export default function Navigation() {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="md:hidden"
|
className="shrink-0 text-foreground md:hidden"
|
||||||
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
|
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
|
||||||
data-testid="button-mobile-menu"
|
data-testid="button-mobile-menu"
|
||||||
|
aria-label={isMobileMenuOpen ? "Close navigation menu" : "Open navigation menu"}
|
||||||
>
|
>
|
||||||
{isMobileMenuOpen ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
|
{isMobileMenuOpen ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isMobileMenuOpen && (
|
{isMobileMenuOpen && (
|
||||||
<div className="md:hidden mt-4 pb-4 border-t border-border pt-4">
|
<div className="mt-3 rounded-2xl border border-border bg-background/95 px-3 pb-4 pt-4 shadow-xl backdrop-blur-xl md:hidden">
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
{navLinks.map((link) => (
|
{navLinks.map((link) => (
|
||||||
<LinkItem key={link.label} link={link} />
|
<LinkItem key={link.label} link={link} mobile />
|
||||||
))}
|
))}
|
||||||
<div className="pt-2 border-t border-border">
|
<div className="border-t border-border pt-2">
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
{sessionUser ? (
|
{sessionUser ? (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center gap-2 rounded-xl border border-primary/40 bg-primary/10 px-3 py-2 text-sm font-medium text-primary">
|
<div className="flex items-center gap-2 rounded-xl border border-primary/40 bg-primary/10 px-3 py-2 text-sm font-medium text-primary">
|
||||||
<span className="h-2 w-2 rounded-full bg-primary" />
|
<span className="h-2 w-2 rounded-full bg-primary" />
|
||||||
{sessionUser.username}
|
<span className="truncate">{sessionUser.username}</span>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|||||||
@ -84,7 +84,7 @@ export default function PerformanceChart() {
|
|||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
ref={sectionRef}
|
ref={sectionRef}
|
||||||
className="py-32 px-6 bg-card/30 relative overflow-hidden"
|
className="relative overflow-hidden bg-card/30 px-4 py-20 sm:px-6 sm:py-24 md:py-32"
|
||||||
data-testid="section-performance"
|
data-testid="section-performance"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@ -103,7 +103,7 @@ export default function PerformanceChart() {
|
|||||||
The Power of Compounding
|
The Power of Compounding
|
||||||
</p>
|
</p>
|
||||||
<h2
|
<h2
|
||||||
className={`text-4xl md:text-5xl font-bold mb-4 transition-all duration-700 delay-100 ${
|
className={`mb-4 text-3xl font-bold transition-all duration-700 delay-100 sm:text-4xl md:text-5xl ${
|
||||||
isVisible ? "opacity-100 translate-y-0" : "opacity-0 translate-y-4"
|
isVisible ? "opacity-100 translate-y-0" : "opacity-0 translate-y-4"
|
||||||
}`}
|
}`}
|
||||||
data-testid="text-performance-headline"
|
data-testid="text-performance-headline"
|
||||||
@ -111,7 +111,7 @@ export default function PerformanceChart() {
|
|||||||
Watch Your Wealth Grow
|
Watch Your Wealth Grow
|
||||||
</h2>
|
</h2>
|
||||||
<p
|
<p
|
||||||
className={`text-xl text-muted-foreground transition-all duration-700 delay-200 ${
|
className={`text-base text-muted-foreground transition-all duration-700 delay-200 sm:text-lg md:text-xl ${
|
||||||
isVisible ? "opacity-100 translate-y-0" : "opacity-0 translate-y-4"
|
isVisible ? "opacity-100 translate-y-0" : "opacity-0 translate-y-4"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@ -120,13 +120,15 @@ export default function PerformanceChart() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={`relative bg-card rounded-2xl p-8 border border-card-border transition-all duration-700 delay-300 ${
|
className={`relative rounded-2xl border border-card-border bg-card p-4 transition-all duration-700 delay-300 sm:p-6 md:p-8 ${
|
||||||
isVisible ? "opacity-100 translate-y-0" : "opacity-0 translate-y-8"
|
isVisible ? "opacity-100 translate-y-0" : "opacity-0 translate-y-8"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
<div className="overflow-x-auto pb-2">
|
||||||
|
<div className="min-w-[560px]">
|
||||||
<svg
|
<svg
|
||||||
viewBox={`0 0 ${chartWidth} ${chartHeight}`}
|
viewBox={`0 0 ${chartWidth} ${chartHeight}`}
|
||||||
className="w-full h-auto"
|
className="h-auto w-full"
|
||||||
preserveAspectRatio="xMidYMid meet"
|
preserveAspectRatio="xMidYMid meet"
|
||||||
>
|
>
|
||||||
<defs>
|
<defs>
|
||||||
@ -341,8 +343,10 @@ export default function PerformanceChart() {
|
|||||||
</g>
|
</g>
|
||||||
)}
|
)}
|
||||||
</svg>
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-center gap-8 mt-8 flex-wrap">
|
<div className="mt-8 flex flex-wrap items-center justify-center gap-4 sm:gap-8">
|
||||||
<div
|
<div
|
||||||
className={`flex items-center gap-3 transition-all duration-500 ${
|
className={`flex items-center gap-3 transition-all duration-500 ${
|
||||||
animationPhase >= 1 ? "opacity-100 translate-x-0" : "opacity-0 -translate-x-4"
|
animationPhase >= 1 ? "opacity-100 translate-x-0" : "opacity-0 -translate-x-4"
|
||||||
|
|||||||
@ -1012,7 +1012,7 @@ export default function PortfolioSection() {
|
|||||||
<section
|
<section
|
||||||
ref={sectionRef}
|
ref={sectionRef}
|
||||||
id="portfolio"
|
id="portfolio"
|
||||||
className={`relative overflow-hidden py-32 px-6 pb-32 bg-gradient-to-b from-background to-background/80 ${revealTransition} ${sectionRevealClass}`}
|
className={`relative overflow-hidden bg-gradient-to-b from-background to-background/80 px-4 py-20 pb-20 sm:px-6 sm:py-24 sm:pb-24 md:py-32 md:pb-32 ${revealTransition} ${sectionRevealClass}`}
|
||||||
>
|
>
|
||||||
<LoginRequiredDialog
|
<LoginRequiredDialog
|
||||||
open={loginPromptOpen}
|
open={loginPromptOpen}
|
||||||
@ -1044,12 +1044,12 @@ export default function PortfolioSection() {
|
|||||||
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<p className="text-sm uppercase tracking-[0.25em] text-muted-foreground">Portfolio</p>
|
<p className="text-sm uppercase tracking-[0.25em] text-muted-foreground">Portfolio</p>
|
||||||
<h3 className="text-3xl md:text-4xl font-bold">Your holdings & live positions</h3>
|
<h3 className="text-2xl font-bold sm:text-3xl md:text-4xl">Your holdings & live positions</h3>
|
||||||
<p className="text-muted-foreground max-w-2xl">
|
<p className="text-muted-foreground max-w-2xl">
|
||||||
Connect your broker to sync holdings. When disconnected, values stay at zero and you will see a prompt to connect.
|
Connect your broker to sync holdings. When disconnected, values stay at zero and you will see a prompt to connect.
|
||||||
</p>
|
</p>
|
||||||
{isConnected && (brokerStatus?.userName || brokerStatus?.broker) ? (
|
{isConnected && (brokerStatus?.userName || brokerStatus?.broker) ? (
|
||||||
<div className="inline-flex items-center gap-2 rounded-full border border-primary/50 bg-primary/10 px-3 py-1 text-sm font-medium text-primary">
|
<div className="inline-flex max-w-full flex-wrap items-center gap-2 rounded-full border border-primary/50 bg-primary/10 px-3 py-1 text-sm font-medium text-primary sm:flex-nowrap">
|
||||||
<Wallet className="h-4 w-4" />
|
<Wallet className="h-4 w-4" />
|
||||||
{brokerStatus?.userName ? (
|
{brokerStatus?.userName ? (
|
||||||
<>
|
<>
|
||||||
@ -1063,9 +1063,13 @@ export default function PortfolioSection() {
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex w-full flex-col gap-2 sm:w-auto sm:flex-row sm:flex-wrap">
|
||||||
<BrokerConnectDialog open={brokerDialogOpen} onOpenChange={setBrokerDialogOpen} />
|
<BrokerConnectDialog
|
||||||
<Button variant="secondary" asChild>
|
open={brokerDialogOpen}
|
||||||
|
onOpenChange={setBrokerDialogOpen}
|
||||||
|
triggerClassName="w-full sm:w-auto"
|
||||||
|
/>
|
||||||
|
<Button variant="secondary" asChild className="w-full sm:w-auto">
|
||||||
<a href="/portfolio/paper" target="_blank" rel="noreferrer">
|
<a href="/portfolio/paper" target="_blank" rel="noreferrer">
|
||||||
Paper Trading Portfolio
|
Paper Trading Portfolio
|
||||||
</a>
|
</a>
|
||||||
@ -1075,6 +1079,7 @@ export default function PortfolioSection() {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={handleRefreshBrokerData}
|
onClick={handleRefreshBrokerData}
|
||||||
disabled={holdingsQuery.isFetching || equityCurveQuery.isFetching}
|
disabled={holdingsQuery.isFetching || equityCurveQuery.isFetching}
|
||||||
|
className="w-full sm:w-auto"
|
||||||
>
|
>
|
||||||
<RefreshCcw className="h-4 w-4" />
|
<RefreshCcw className="h-4 w-4" />
|
||||||
{holdingsQuery.isFetching || equityCurveQuery.isFetching
|
{holdingsQuery.isFetching || equityCurveQuery.isFetching
|
||||||
@ -1086,6 +1091,7 @@ export default function PortfolioSection() {
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={handleReconnectClick}
|
onClick={handleReconnectClick}
|
||||||
|
className="w-full sm:w-auto"
|
||||||
>
|
>
|
||||||
<PlugZap className="h-4 w-4" />
|
<PlugZap className="h-4 w-4" />
|
||||||
Reconnect broker
|
Reconnect broker
|
||||||
@ -1096,6 +1102,7 @@ export default function PortfolioSection() {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={handleDisconnectBroker}
|
onClick={handleDisconnectBroker}
|
||||||
disabled={disconnectBrokerMutation.isPending}
|
disabled={disconnectBrokerMutation.isPending}
|
||||||
|
className="w-full sm:w-auto"
|
||||||
>
|
>
|
||||||
{disconnectBrokerMutation.isPending ? "Disconnecting..." : "Disconnect broker"}
|
{disconnectBrokerMutation.isPending ? "Disconnecting..." : "Disconnect broker"}
|
||||||
</Button>
|
</Button>
|
||||||
@ -1128,7 +1135,7 @@ export default function PortfolioSection() {
|
|||||||
className={`rounded-2xl border border-border/60 bg-card/70 shadow-xl overflow-hidden ${hoverLift} ${revealTransition} ${cardRevealClass}`}
|
className={`rounded-2xl border border-border/60 bg-card/70 shadow-xl overflow-hidden ${hoverLift} ${revealTransition} ${cardRevealClass}`}
|
||||||
style={prefersReducedMotion ? undefined : { transitionDelay: "500ms" }}
|
style={prefersReducedMotion ? undefined : { transitionDelay: "500ms" }}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between px-6 py-4 border-b border-border/50">
|
<div className="flex flex-col gap-3 border-b border-border/50 px-4 py-4 sm:flex-row sm:items-center sm:justify-between sm:px-6">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<p className="text-sm font-semibold">Holdings</p>
|
<p className="text-sm font-semibold">Holdings</p>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
@ -1161,9 +1168,9 @@ export default function PortfolioSection() {
|
|||||||
) : (
|
) : (
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
{showSessionExpired ? (
|
{showSessionExpired ? (
|
||||||
<div className="flex flex-wrap items-center justify-between gap-2 px-6 py-3 text-xs text-amber-200 bg-amber-500/10 border-b border-amber-400/20">
|
<div className="flex flex-wrap items-center justify-between gap-2 border-b border-amber-400/20 bg-amber-500/10 px-4 py-3 text-xs text-amber-200 sm:px-6">
|
||||||
<span>Session expired. Showing the last known holdings. Reconnect to refresh.</span>
|
<span>Session expired. Showing the last known holdings. Reconnect to refresh.</span>
|
||||||
<Button size="sm" variant="secondary" onClick={handleReconnectClick}>
|
<Button size="sm" variant="secondary" onClick={handleReconnectClick} className="w-full sm:w-auto">
|
||||||
Reconnect broker
|
Reconnect broker
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@ -1171,11 +1178,11 @@ export default function PortfolioSection() {
|
|||||||
<table className="min-w-full text-sm">
|
<table className="min-w-full text-sm">
|
||||||
<thead className="bg-muted/40 text-muted-foreground">
|
<thead className="bg-muted/40 text-muted-foreground">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-6 py-3 text-left font-medium">Symbol</th>
|
<th className="px-4 py-3 text-left font-medium sm:px-6">Symbol</th>
|
||||||
<th className="px-6 py-3 text-left font-medium">Qty</th>
|
<th className="px-4 py-3 text-left font-medium sm:px-6">Qty</th>
|
||||||
<th className="px-6 py-3 text-left font-medium">Avg price</th>
|
<th className="px-4 py-3 text-left font-medium sm:px-6">Avg price</th>
|
||||||
<th className="px-6 py-3 text-left font-medium">LTP</th>
|
<th className="px-4 py-3 text-left font-medium sm:px-6">LTP</th>
|
||||||
<th className="px-6 py-3 text-left font-medium">P&L</th>
|
<th className="px-4 py-3 text-left font-medium sm:px-6">P&L</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-border/60">
|
<tbody className="divide-y divide-border/60">
|
||||||
@ -1188,7 +1195,7 @@ export default function PortfolioSection() {
|
|||||||
const pnl = getDisplayPnl(item);
|
const pnl = getDisplayPnl(item);
|
||||||
return (
|
return (
|
||||||
<tr key={`${item.tradingsymbol || item.instrument_token || idx}`}>
|
<tr key={`${item.tradingsymbol || item.instrument_token || idx}`}>
|
||||||
<td className="px-6 py-3">
|
<td className="px-4 py-3 sm:px-6">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="font-semibold">
|
<span className="font-semibold">
|
||||||
{item.tradingsymbol || item.symbol || "Instrument"}
|
{item.tradingsymbol || item.symbol || "Instrument"}
|
||||||
@ -1196,7 +1203,7 @@ export default function PortfolioSection() {
|
|||||||
<Badge variant="outline">{item.exchange || item.exchange_type || "N/A"}</Badge>
|
<Badge variant="outline">{item.exchange || item.exchange_type || "N/A"}</Badge>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-3">
|
<td className="px-4 py-3 sm:px-6">
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span>{qty}</span>
|
<span>{qty}</span>
|
||||||
{t1Qty > 0 && settledQty <= 0 ? (
|
{t1Qty > 0 && settledQty <= 0 ? (
|
||||||
@ -1204,9 +1211,9 @@ export default function PortfolioSection() {
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-3">{formatCurrency(avg, { decimals: 2 })}</td>
|
<td className="px-4 py-3 sm:px-6">{formatCurrency(avg, { decimals: 2 })}</td>
|
||||||
<td className="px-6 py-3">{ltp ? formatCurrency(ltp, { decimals: 2 }) : "-"}</td>
|
<td className="px-4 py-3 sm:px-6">{ltp ? formatCurrency(ltp, { decimals: 2 }) : "-"}</td>
|
||||||
<td className="px-6 py-3">
|
<td className="px-4 py-3 sm:px-6">
|
||||||
<span className={pnl >= 0 ? "text-emerald-500" : "text-red-500"}>
|
<span className={pnl >= 0 ? "text-emerald-500" : "text-red-500"}>
|
||||||
{formatCurrency(pnl, { decimals: 2 })}
|
{formatCurrency(pnl, { decimals: 2 })}
|
||||||
</span>
|
</span>
|
||||||
@ -1225,14 +1232,14 @@ export default function PortfolioSection() {
|
|||||||
className={`rounded-2xl border border-border/60 bg-card/70 shadow-xl overflow-hidden ${hoverLift} ${revealTransition} ${cardRevealClass}`}
|
className={`rounded-2xl border border-border/60 bg-card/70 shadow-xl overflow-hidden ${hoverLift} ${revealTransition} ${cardRevealClass}`}
|
||||||
style={prefersReducedMotion ? undefined : { transitionDelay: "600ms" }}
|
style={prefersReducedMotion ? undefined : { transitionDelay: "600ms" }}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between px-6 py-4 border-b border-border/50">
|
<div className="flex flex-col gap-3 border-b border-border/50 px-4 py-4 sm:flex-row sm:items-center sm:justify-between sm:px-6">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<p className="text-sm font-semibold">Strategy control</p>
|
<p className="text-sm font-semibold">Strategy control</p>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
Start or stop the Golden Nifty SIP engine from the dashboard.
|
Start or stop the Golden Nifty SIP engine from the dashboard.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<Badge variant="outline" className={livenessBadgeClass}>
|
<Badge variant="outline" className={livenessBadgeClass}>
|
||||||
{livenessBadgeLabel}
|
{livenessBadgeLabel}
|
||||||
</Badge>
|
</Badge>
|
||||||
@ -1241,7 +1248,7 @@ export default function PortfolioSection() {
|
|||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-6 space-y-4">
|
<div className="space-y-4 p-4 sm:p-6">
|
||||||
<div className="rounded-lg border border-border/60 bg-background/40 px-4 py-3">
|
<div className="rounded-lg border border-border/60 bg-background/40 px-4 py-3">
|
||||||
<div className="text-xs uppercase tracking-[0.18em] text-muted-foreground">
|
<div className="text-xs uppercase tracking-[0.18em] text-muted-foreground">
|
||||||
Next eligible SIP
|
Next eligible SIP
|
||||||
@ -1311,13 +1318,13 @@ export default function PortfolioSection() {
|
|||||||
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.
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-col gap-2 sm:flex-row sm:flex-wrap">
|
||||||
{showResumeStrategy ? (
|
{showResumeStrategy ? (
|
||||||
<MotionButton
|
<MotionButton
|
||||||
{...ctaMotionProps}
|
{...ctaMotionProps}
|
||||||
onClick={handleResume}
|
onClick={handleResume}
|
||||||
disabled={isResuming || !canArmStrategy}
|
disabled={isResuming || !canArmStrategy}
|
||||||
className="shimmer"
|
className="w-full shimmer sm:w-auto"
|
||||||
>
|
>
|
||||||
{isResuming ? "Resuming..." : "Resume Strategy"}
|
{isResuming ? "Resuming..." : "Resume Strategy"}
|
||||||
</MotionButton>
|
</MotionButton>
|
||||||
@ -1327,18 +1334,18 @@ export default function PortfolioSection() {
|
|||||||
{...ctaMotionProps}
|
{...ctaMotionProps}
|
||||||
onClick={handleStart}
|
onClick={handleStart}
|
||||||
disabled={isStarting || !canArmStrategy || isStrategyActive}
|
disabled={isStarting || !canArmStrategy || isStrategyActive}
|
||||||
className="shimmer"
|
className="w-full shimmer sm:w-auto"
|
||||||
>
|
>
|
||||||
{isStarting ? "Starting..." : "Start Strategy"}
|
{isStarting ? "Starting..." : "Start Strategy"}
|
||||||
</MotionButton>
|
</MotionButton>
|
||||||
) : null}
|
) : null}
|
||||||
{showRestartStrategy ? (
|
{showRestartStrategy ? (
|
||||||
<Button variant="outline" onClick={() => setFreshStartRequested(true)}>
|
<Button variant="outline" onClick={() => setFreshStartRequested(true)} className="w-full sm:w-auto">
|
||||||
Restart Strategy
|
Restart Strategy
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
{isStrategyActive ? (
|
{isStrategyActive ? (
|
||||||
<Button variant="outline" onClick={handleStop} disabled={isStopping}>
|
<Button variant="outline" onClick={handleStop} disabled={isStopping} className="w-full sm:w-auto">
|
||||||
{isStopping ? "Stopping..." : "Stop Strategy"}
|
{isStopping ? "Stopping..." : "Stop Strategy"}
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
@ -1351,14 +1358,14 @@ export default function PortfolioSection() {
|
|||||||
className={`rounded-2xl border border-border/60 bg-card/70 shadow-xl p-6 space-y-4 ${hoverLift} ${revealTransition} ${cardRevealClass}`}
|
className={`rounded-2xl border border-border/60 bg-card/70 shadow-xl p-6 space-y-4 ${hoverLift} ${revealTransition} ${cardRevealClass}`}
|
||||||
style={prefersReducedMotion ? undefined : { transitionDelay: "700ms" }}
|
style={prefersReducedMotion ? undefined : { transitionDelay: "700ms" }}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-3">
|
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-semibold">Equity curve</p>
|
<p className="text-sm font-semibold">Equity curve</p>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
Track exact stored daily broker snapshots from the day recording began.
|
Track exact stored daily broker snapshots from the day recording began.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
|
||||||
<Label htmlFor="equity-start" className="text-xs text-muted-foreground">
|
<Label htmlFor="equity-start" className="text-xs text-muted-foreground">
|
||||||
From
|
From
|
||||||
</Label>
|
</Label>
|
||||||
@ -1368,7 +1375,7 @@ export default function PortfolioSection() {
|
|||||||
value={startDate}
|
value={startDate}
|
||||||
max={formatDateInput(new Date())}
|
max={formatDateInput(new Date())}
|
||||||
onChange={(e) => setStartDate(e.target.value)}
|
onChange={(e) => setStartDate(e.target.value)}
|
||||||
className="w-[180px]"
|
className="w-full sm:w-[180px]"
|
||||||
disabled={!isConnected}
|
disabled={!isConnected}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -1383,7 +1390,7 @@ export default function PortfolioSection() {
|
|||||||
) : equityCurvePoints.length === 0 ? (
|
) : equityCurvePoints.length === 0 ? (
|
||||||
<ZeroState message="No exact equity snapshots yet. The curve starts from the first recorded daily snapshot." />
|
<ZeroState message="No exact equity snapshots yet. The curve starts from the first recorded daily snapshot." />
|
||||||
) : (
|
) : (
|
||||||
<div className="h-80">
|
<div className="h-72 sm:h-80">
|
||||||
<ChartContainer
|
<ChartContainer
|
||||||
config={{
|
config={{
|
||||||
equity: { label: "Equity", color: "hsl(var(--chart-1))" },
|
equity: { label: "Equity", color: "hsl(var(--chart-1))" },
|
||||||
|
|||||||
@ -70,11 +70,11 @@ export default function StrategiesSection() {
|
|||||||
<section
|
<section
|
||||||
ref={sectionRef}
|
ref={sectionRef}
|
||||||
id="strategies"
|
id="strategies"
|
||||||
className="py-32 px-6 relative overflow-hidden"
|
className="relative overflow-hidden px-4 py-20 sm:px-6 sm:py-24 md:py-32"
|
||||||
data-testid="section-strategies"
|
data-testid="section-strategies"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] h-[600px] bg-primary/5 rounded-full blur-3xl transition-all duration-1000 ${
|
className={`absolute top-1/2 left-1/2 h-[360px] w-[360px] -translate-x-1/2 -translate-y-1/2 rounded-full bg-primary/5 blur-3xl transition-all duration-1000 sm:h-[460px] sm:w-[460px] md:h-[600px] md:w-[600px] ${
|
||||||
isVisible ? "opacity-100 scale-100" : "opacity-0 scale-50"
|
isVisible ? "opacity-100 scale-100" : "opacity-0 scale-50"
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
@ -89,7 +89,7 @@ export default function StrategiesSection() {
|
|||||||
Strategies
|
Strategies
|
||||||
</p>
|
</p>
|
||||||
<h2
|
<h2
|
||||||
className={`text-4xl md:text-5xl font-bold mb-4 transition-all duration-700 delay-100 ${
|
className={`mb-4 text-3xl font-bold transition-all duration-700 delay-100 sm:text-4xl md:text-5xl ${
|
||||||
isVisible ? "opacity-100 translate-y-0" : "opacity-0 translate-y-4"
|
isVisible ? "opacity-100 translate-y-0" : "opacity-0 translate-y-4"
|
||||||
}`}
|
}`}
|
||||||
data-testid="text-strategies-headline"
|
data-testid="text-strategies-headline"
|
||||||
@ -97,7 +97,7 @@ export default function StrategiesSection() {
|
|||||||
Choose Your Path
|
Choose Your Path
|
||||||
</h2>
|
</h2>
|
||||||
<p
|
<p
|
||||||
className={`text-xl text-muted-foreground max-w-2xl mx-auto transition-all duration-700 delay-200 ${
|
className={`mx-auto max-w-2xl text-base text-muted-foreground transition-all duration-700 delay-200 sm:text-lg md:text-xl ${
|
||||||
isVisible ? "opacity-100 translate-y-0" : "opacity-0 translate-y-4"
|
isVisible ? "opacity-100 translate-y-0" : "opacity-0 translate-y-4"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@ -106,7 +106,7 @@ export default function StrategiesSection() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<motion.div
|
<motion.div
|
||||||
className="grid md:grid-cols-3 gap-6"
|
className="grid gap-6 md:grid-cols-3"
|
||||||
variants={cardContainer}
|
variants={cardContainer}
|
||||||
initial="hidden"
|
initial="hidden"
|
||||||
animate={isVisible ? "show" : "hidden"}
|
animate={isVisible ? "show" : "hidden"}
|
||||||
|
|||||||
@ -67,7 +67,7 @@ export default function StrategySelectorModal({ open, onClose }: StrategySelecto
|
|||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{open && (
|
{open && (
|
||||||
<motion.div
|
<motion.div
|
||||||
className="fixed inset-0 z-[90] flex items-center justify-center px-6 py-10"
|
className="fixed inset-0 z-[90] flex items-end justify-center px-3 py-4 sm:items-center sm:px-6 sm:py-10"
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
exit={{ opacity: 0 }}
|
exit={{ opacity: 0 }}
|
||||||
@ -88,14 +88,14 @@ export default function StrategySelectorModal({ open, onClose }: StrategySelecto
|
|||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-labelledby="strategy-modal-title"
|
aria-labelledby="strategy-modal-title"
|
||||||
aria-describedby="strategy-modal-subtitle"
|
aria-describedby="strategy-modal-subtitle"
|
||||||
className="relative z-[91] w-full max-w-4xl rounded-3xl border border-white/10 bg-gradient-to-br from-white/10 via-background/80 to-background/90 p-8 shadow-2xl"
|
className="relative z-[91] max-h-[90vh] w-full max-w-4xl overflow-y-auto rounded-[1.75rem] border border-white/10 bg-gradient-to-br from-white/10 via-background/80 to-background/90 p-4 shadow-2xl sm:rounded-3xl sm:p-8"
|
||||||
initial={{ opacity: 0, scale: 0.95, y: 16 }}
|
initial={{ opacity: 0, scale: 0.95, y: 16 }}
|
||||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||||
exit={{ opacity: 0, scale: 0.95, y: 16 }}
|
exit={{ opacity: 0, scale: 0.95, y: 16 }}
|
||||||
transition={{ duration: 0.3, ease: "easeOut" }}
|
transition={{ duration: 0.3, ease: "easeOut" }}
|
||||||
onClick={(event) => event.stopPropagation()}
|
onClick={(event) => event.stopPropagation()}
|
||||||
>
|
>
|
||||||
<div className="flex items-start justify-between gap-6">
|
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between sm:gap-6">
|
||||||
<div>
|
<div>
|
||||||
<h2 id="strategy-modal-title" className="text-2xl font-semibold text-foreground">
|
<h2 id="strategy-modal-title" className="text-2xl font-semibold text-foreground">
|
||||||
Choose Your Strategy
|
Choose Your Strategy
|
||||||
@ -107,13 +107,13 @@ export default function StrategySelectorModal({ open, onClose }: StrategySelecto
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="rounded-full border border-white/10 bg-white/10 px-3 py-1 text-xs text-muted-foreground hover:text-foreground"
|
className="rounded-full border border-white/10 bg-white/10 px-3 py-1 text-xs text-muted-foreground hover:text-foreground sm:self-start"
|
||||||
>
|
>
|
||||||
Close
|
Close
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-8 grid gap-4 md:grid-cols-3">
|
<div className="mt-6 grid gap-4 md:mt-8 md:grid-cols-3">
|
||||||
<StrategyCard
|
<StrategyCard
|
||||||
title="Golden Nifty"
|
title="Golden Nifty"
|
||||||
description="Balanced Nifty + Gold allocation for stability."
|
description="Balanced Nifty + Gold allocation for stability."
|
||||||
|
|||||||
@ -736,13 +736,13 @@ function PaperTradingPortfolio() {
|
|||||||
<Navigation />
|
<Navigation />
|
||||||
<PageEnter>
|
<PageEnter>
|
||||||
<main className="pt-24 pb-40">
|
<main className="pt-24 pb-40">
|
||||||
<section className="max-w-6xl mx-auto px-6 py-10 space-y-8">
|
<section className="mx-auto max-w-6xl space-y-8 px-4 py-8 sm:px-6 sm:py-10">
|
||||||
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<p className="text-sm uppercase tracking-[0.25em] text-muted-foreground">
|
<p className="text-sm uppercase tracking-[0.25em] text-muted-foreground">
|
||||||
Paper Trading Portfolio
|
Paper Trading Portfolio
|
||||||
</p>
|
</p>
|
||||||
<h2 className="text-3xl md:text-4xl font-bold">Paper trading (simulated)</h2>
|
<h2 className="text-2xl font-bold sm:text-3xl md:text-4xl">Paper trading (simulated)</h2>
|
||||||
<p className="text-muted-foreground max-w-2xl">
|
<p className="text-muted-foreground max-w-2xl">
|
||||||
This dashboard mirrors live execution flow with simulated orders and balances.
|
This dashboard mirrors live execution flow with simulated orders and balances.
|
||||||
</p>
|
</p>
|
||||||
@ -768,14 +768,14 @@ function PaperTradingPortfolio() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rounded-2xl border border-border/60 bg-card/70 shadow-xl overflow-hidden">
|
<div className="rounded-2xl border border-border/60 bg-card/70 shadow-xl overflow-hidden">
|
||||||
<div className="flex items-center justify-between px-6 py-4 border-b border-border/50">
|
<div className="flex flex-col gap-3 border-b border-border/50 px-4 py-4 sm:flex-row sm:items-center sm:justify-between sm:px-6">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<p className="text-sm font-semibold">Strategy control</p>
|
<p className="text-sm font-semibold">Strategy control</p>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
Start or stop the Golden Nifty SIP engine from the dashboard.
|
Start or stop the Golden Nifty SIP engine from the dashboard.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<Badge variant="outline" className={livenessBadgeClass}>
|
<Badge variant="outline" className={livenessBadgeClass}>
|
||||||
{livenessBadgeLabel}
|
{livenessBadgeLabel}
|
||||||
</Badge>
|
</Badge>
|
||||||
@ -789,7 +789,7 @@ function PaperTradingPortfolio() {
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-6 space-y-4">
|
<div className="space-y-4 p-4 sm:p-6">
|
||||||
<div className="rounded-lg border border-border/60 bg-background/40 px-4 py-3">
|
<div className="rounded-lg border border-border/60 bg-background/40 px-4 py-3">
|
||||||
<div className="text-xs uppercase tracking-[0.18em] text-muted-foreground">
|
<div className="text-xs uppercase tracking-[0.18em] text-muted-foreground">
|
||||||
Next eligible SIP
|
Next eligible SIP
|
||||||
@ -926,13 +926,13 @@ function PaperTradingPortfolio() {
|
|||||||
Resume uses the previously saved paper SIP configuration. Choose restart to begin a fresh cycle.
|
Resume uses the previously saved paper SIP configuration. Choose restart to begin a fresh cycle.
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-col gap-2 sm:flex-row sm:flex-wrap">
|
||||||
{showResumeStrategy ? (
|
{showResumeStrategy ? (
|
||||||
<MotionButton
|
<MotionButton
|
||||||
{...ctaMotionProps}
|
{...ctaMotionProps}
|
||||||
onClick={handleResume}
|
onClick={handleResume}
|
||||||
disabled={isResuming || !canArmStrategy}
|
disabled={isResuming || !canArmStrategy}
|
||||||
className="shimmer"
|
className="w-full shimmer sm:w-auto"
|
||||||
>
|
>
|
||||||
{isResuming ? "Resuming..." : "Resume Strategy"}
|
{isResuming ? "Resuming..." : "Resume Strategy"}
|
||||||
</MotionButton>
|
</MotionButton>
|
||||||
@ -942,7 +942,7 @@ function PaperTradingPortfolio() {
|
|||||||
{...ctaMotionProps}
|
{...ctaMotionProps}
|
||||||
onClick={handleStart}
|
onClick={handleStart}
|
||||||
disabled={!canStartFresh || isStarting || isResuming || isStopping || isResetting || !canArmStrategy || isStrategyActive}
|
disabled={!canStartFresh || isStarting || isResuming || isStopping || isResetting || !canArmStrategy || isStrategyActive}
|
||||||
className="shimmer"
|
className="w-full shimmer sm:w-auto"
|
||||||
>
|
>
|
||||||
{isStarting ? "Starting..." : "Start Strategy"}
|
{isStarting ? "Starting..." : "Start Strategy"}
|
||||||
</MotionButton>
|
</MotionButton>
|
||||||
@ -952,6 +952,7 @@ function PaperTradingPortfolio() {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => setFreshStartRequested(true)}
|
onClick={() => setFreshStartRequested(true)}
|
||||||
disabled={isStarting || isResuming || isStopping || isResetting}
|
disabled={isStarting || isResuming || isStopping || isResetting}
|
||||||
|
className="w-full sm:w-auto"
|
||||||
>
|
>
|
||||||
Restart Strategy
|
Restart Strategy
|
||||||
</Button>
|
</Button>
|
||||||
@ -961,6 +962,7 @@ function PaperTradingPortfolio() {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={handleStop}
|
onClick={handleStop}
|
||||||
disabled={!canStop || isStarting || isResuming || isStopping || isResetting}
|
disabled={!canStop || isStarting || isResuming || isStopping || isResetting}
|
||||||
|
className="w-full sm:w-auto"
|
||||||
>
|
>
|
||||||
{isStopping ? "Stopping..." : "Stop Strategy"}
|
{isStopping ? "Stopping..." : "Stop Strategy"}
|
||||||
</Button>
|
</Button>
|
||||||
@ -969,6 +971,7 @@ function PaperTradingPortfolio() {
|
|||||||
variant="destructive"
|
variant="destructive"
|
||||||
onClick={handleReset}
|
onClick={handleReset}
|
||||||
disabled={isStarting || isResuming || isStopping || isResetting}
|
disabled={isStarting || isResuming || isStopping || isResetting}
|
||||||
|
className="w-full sm:w-auto"
|
||||||
>
|
>
|
||||||
{isResetting ? "Resetting..." : "Reset Paper Account"}
|
{isResetting ? "Resetting..." : "Reset Paper Account"}
|
||||||
</Button>
|
</Button>
|
||||||
@ -1013,14 +1016,14 @@ function PaperTradingPortfolio() {
|
|||||||
<p className="text-xs text-muted-foreground">Delivery SIP · No leverage</p>
|
<p className="text-xs text-muted-foreground">Delivery SIP · No leverage</p>
|
||||||
|
|
||||||
<div className="rounded-2xl border border-border/60 bg-card/70 shadow-xl overflow-hidden">
|
<div className="rounded-2xl border border-border/60 bg-card/70 shadow-xl overflow-hidden">
|
||||||
<div className="flex items-center justify-between px-6 py-4 border-b border-border/50">
|
<div className="flex flex-col gap-3 border-b border-border/50 px-4 py-4 sm:flex-row sm:items-center sm:justify-between sm:px-6">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<p className="text-sm font-semibold">Positions</p>
|
<p className="text-sm font-semibold">Positions</p>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
Current paper positions and unrealized P&L.
|
Current paper positions and unrealized P&L.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
<span className="text-xs text-muted-foreground">
|
<span className="text-xs text-muted-foreground">
|
||||||
Live prices · 5s refresh
|
Live prices · 5s refresh
|
||||||
</span>
|
</span>
|
||||||
@ -1028,9 +1031,9 @@ function PaperTradingPortfolio() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{positionsQuery.isLoading && displayPositions.length === 0 ? (
|
{positionsQuery.isLoading && displayPositions.length === 0 ? (
|
||||||
<div className="px-6 py-6 text-sm text-muted-foreground">Loading positions...</div>
|
<div className="px-4 py-6 text-sm text-muted-foreground sm:px-6">Loading positions...</div>
|
||||||
) : displayPositions.length === 0 ? (
|
) : displayPositions.length === 0 ? (
|
||||||
<div className="px-6 py-6 text-sm text-muted-foreground">
|
<div className="px-4 py-6 text-sm text-muted-foreground sm:px-6">
|
||||||
No paper positions yet.
|
No paper positions yet.
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@ -1038,21 +1041,21 @@ function PaperTradingPortfolio() {
|
|||||||
<table className="min-w-full text-sm">
|
<table className="min-w-full text-sm">
|
||||||
<thead className="bg-muted/40 text-muted-foreground">
|
<thead className="bg-muted/40 text-muted-foreground">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-6 py-3 text-left font-medium">Symbol</th>
|
<th className="px-4 py-3 text-left font-medium sm:px-6">Symbol</th>
|
||||||
<th className="px-6 py-3 text-left font-medium">Qty</th>
|
<th className="px-4 py-3 text-left font-medium sm:px-6">Qty</th>
|
||||||
<th className="px-6 py-3 text-left font-medium">Avg price</th>
|
<th className="px-4 py-3 text-left font-medium sm:px-6">Avg price</th>
|
||||||
<th className="px-6 py-3 text-left font-medium">LTP</th>
|
<th className="px-4 py-3 text-left font-medium sm:px-6">LTP</th>
|
||||||
<th className="px-6 py-3 text-left font-medium">P&L</th>
|
<th className="px-4 py-3 text-left font-medium sm:px-6">P&L</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-border/60">
|
<tbody className="divide-y divide-border/60">
|
||||||
{displayPositions.map((pos) => (
|
{displayPositions.map((pos) => (
|
||||||
<tr key={pos.symbol}>
|
<tr key={pos.symbol}>
|
||||||
<td className="px-6 py-3 font-semibold">{pos.symbol}</td>
|
<td className="px-4 py-3 font-semibold sm:px-6">{pos.symbol}</td>
|
||||||
<td className="px-6 py-3">{pos.qty}</td>
|
<td className="px-4 py-3 sm:px-6">{pos.qty}</td>
|
||||||
<td className="px-6 py-3">{formatCurrency(pos.avg_price, 2)}</td>
|
<td className="px-4 py-3 sm:px-6">{formatCurrency(pos.avg_price, 2)}</td>
|
||||||
<td className="px-6 py-3">{formatCurrency(pos.last_price, 2)}</td>
|
<td className="px-4 py-3 sm:px-6">{formatCurrency(pos.last_price, 2)}</td>
|
||||||
<td className="px-6 py-3">
|
<td className="px-4 py-3 sm:px-6">
|
||||||
<span className={pos.pnl >= 0 ? "text-emerald-400" : "text-red-400"}>
|
<span className={pos.pnl >= 0 ? "text-emerald-400" : "text-red-400"}>
|
||||||
{formatCurrency(pos.pnl, 2)} ({pos.pnl_pct.toFixed(2)}%)
|
{formatCurrency(pos.pnl, 2)} ({pos.pnl_pct.toFixed(2)}%)
|
||||||
</span>
|
</span>
|
||||||
@ -1066,7 +1069,7 @@ function PaperTradingPortfolio() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rounded-2xl border border-border/60 bg-card/70 shadow-xl overflow-hidden">
|
<div className="rounded-2xl border border-border/60 bg-card/70 shadow-xl overflow-hidden">
|
||||||
<div className="flex items-center justify-between px-6 py-4 border-b border-border/50">
|
<div className="flex flex-col gap-3 border-b border-border/50 px-4 py-4 sm:flex-row sm:items-center sm:justify-between sm:px-6">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<p className="text-sm font-semibold">Orders</p>
|
<p className="text-sm font-semibold">Orders</p>
|
||||||
<p className="text-xs text-muted-foreground">Paper order history.</p>
|
<p className="text-xs text-muted-foreground">Paper order history.</p>
|
||||||
@ -1074,9 +1077,9 @@ function PaperTradingPortfolio() {
|
|||||||
<Badge variant="outline">{orders.length} orders</Badge>
|
<Badge variant="outline">{orders.length} orders</Badge>
|
||||||
</div>
|
</div>
|
||||||
{ordersQuery.isLoading ? (
|
{ordersQuery.isLoading ? (
|
||||||
<div className="px-6 py-6 text-sm text-muted-foreground">Loading orders...</div>
|
<div className="px-4 py-6 text-sm text-muted-foreground sm:px-6">Loading orders...</div>
|
||||||
) : orders.length === 0 ? (
|
) : orders.length === 0 ? (
|
||||||
<div className="px-6 py-6 text-sm text-muted-foreground">
|
<div className="px-4 py-6 text-sm text-muted-foreground sm:px-6">
|
||||||
No paper orders yet.
|
No paper orders yet.
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@ -1084,27 +1087,27 @@ function PaperTradingPortfolio() {
|
|||||||
<table className="min-w-full text-sm">
|
<table className="min-w-full text-sm">
|
||||||
<thead className="bg-muted/40 text-muted-foreground">
|
<thead className="bg-muted/40 text-muted-foreground">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-6 py-3 text-left font-medium">Order ID</th>
|
<th className="px-4 py-3 text-left font-medium sm:px-6">Order ID</th>
|
||||||
<th className="px-6 py-3 text-left font-medium">Time</th>
|
<th className="px-4 py-3 text-left font-medium sm:px-6">Time</th>
|
||||||
<th className="px-6 py-3 text-left font-medium">Symbol</th>
|
<th className="px-4 py-3 text-left font-medium sm:px-6">Symbol</th>
|
||||||
<th className="px-6 py-3 text-left font-medium">Side</th>
|
<th className="px-4 py-3 text-left font-medium sm:px-6">Side</th>
|
||||||
<th className="px-6 py-3 text-left font-medium">Qty</th>
|
<th className="px-4 py-3 text-left font-medium sm:px-6">Qty</th>
|
||||||
<th className="px-6 py-3 text-left font-medium">Price</th>
|
<th className="px-4 py-3 text-left font-medium sm:px-6">Price</th>
|
||||||
<th className="px-6 py-3 text-left font-medium">Status</th>
|
<th className="px-4 py-3 text-left font-medium sm:px-6">Status</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-border/60">
|
<tbody className="divide-y divide-border/60">
|
||||||
{orders.map((order) => (
|
{orders.map((order) => (
|
||||||
<tr key={order.id}>
|
<tr key={order.id}>
|
||||||
<td className="px-6 py-3 font-mono text-xs">{order.id}</td>
|
<td className="px-4 py-3 font-mono text-xs sm:px-6">{order.id}</td>
|
||||||
<td className="px-6 py-3 text-xs text-muted-foreground">
|
<td className="px-4 py-3 text-xs text-muted-foreground sm:px-6">
|
||||||
{formatTimestamp(order.timestamp)}
|
{formatTimestamp(order.timestamp)}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-3">{order.symbol}</td>
|
<td className="px-4 py-3 sm:px-6">{order.symbol}</td>
|
||||||
<td className="px-6 py-3">{order.side}</td>
|
<td className="px-4 py-3 sm:px-6">{order.side}</td>
|
||||||
<td className="px-6 py-3">{order.qty}</td>
|
<td className="px-4 py-3 sm:px-6">{order.qty}</td>
|
||||||
<td className="px-6 py-3">{formatCurrency(order.price, 2)}</td>
|
<td className="px-4 py-3 sm:px-6">{formatCurrency(order.price, 2)}</td>
|
||||||
<td className="px-6 py-3">
|
<td className="px-4 py-3 sm:px-6">
|
||||||
<Badge variant="outline">{order.status}</Badge>
|
<Badge variant="outline">{order.status}</Badge>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user