Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 36 additions & 21 deletions frontend/app/components/Landing.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
'use client';
import { MarketDataCenter } from "@/components/market-data-center";
import { Sidebar } from "@/components/sidebar";
import { StockTicker } from "@/components/stock-ticker";
import { ThemeToggle } from "@/components/theme-toggle";
import { TreasuryBillSection } from "@/components/treasury-bill-section";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { portfolioStocks } from "@/data/mock-data";
import clsx from "clsx";
import { type Variants } from "framer-motion";
import { BarChart2, ChartLine, CheckCircle, CircleDollarSign, LineChart, LogIn, PieChart, TrendingUp, XCircle } from "lucide-react";
import Link from "next/link";
import clsx from "clsx";
import { useEffect, useRef } from "react";
import { useState } from "react";
import { Sidebar } from "@/components/sidebar";
import { useRouter, usePathname } from "next/navigation";
import { motion, type Variants } from "framer-motion";
import { usePathname } from "next/navigation";
import { useEffect, useRef, useState } from "react";
import Testimonials from './Testimonials';
// import {MacbookScroll} from '../../components/ui/macbook-scroll'

Expand All @@ -34,7 +33,8 @@ export default function Landing() {

const [sidebarOpen, setSidebarOpen] = useState(false);
const [activeSection, setActiveSection] = useState("home");
const [scrollProgress, setScrollProgress] = useState(0);
const progressBarRef = useRef<HTMLDivElement | null>(null);
const scrollCurrentRef = useRef(0);

const [showScrollTop, setShowScrollTop] = useState(false);

Expand Down Expand Up @@ -119,16 +119,32 @@ export default function Landing() {
}, [pathname]);


// scroll animation
// scroll animation with rAF for smooth, continuous progress (lerped)
useEffect(() => {
const updateScroll = () => {
const currentScroll = window.scrollY;
const totalHeight = document.body.scrollHeight - window.innerHeight;
setScrollProgress((currentScroll / totalHeight) * 100);
let rafId: number | null = null;
const update = () => {
const doc = document.documentElement;
const currentScroll = doc.scrollTop || window.scrollY || 0;
const totalHeight = (doc.scrollHeight - doc.clientHeight) || 1;
const target = Math.max(0, Math.min(1, currentScroll / totalHeight));
// Exponential moving average for smoothing
const smoothing = 0.18; // 0..1 higher = snappier
scrollCurrentRef.current = scrollCurrentRef.current + (target - scrollCurrentRef.current) * smoothing;
if (progressBarRef.current) {
progressBarRef.current.style.transform = `scaleX(${scrollCurrentRef.current})`;
}
rafId = requestAnimationFrame(update);
};
const onResize = () => {
// Trigger a recalculation on resize
if (rafId === null) rafId = requestAnimationFrame(update);
};
rafId = requestAnimationFrame(update);
window.addEventListener('resize', onResize, { passive: true });
return () => {
if (rafId) cancelAnimationFrame(rafId);
window.removeEventListener('resize', onResize);
};

window.addEventListener("scroll", updateScroll);
return () => window.removeEventListener("scroll", updateScroll);
}, []);


Expand Down Expand Up @@ -159,11 +175,10 @@ export default function Landing() {

{/* Progress Bar */}
<div
className="h-1 rounded-full
bg-gradient-to-r from-cyan-400 via-blue-400 to-purple-500
shadow-[0_0_10px_rgba(59,130,246,0.8),0_0_20px_rgba(139,92,246,0.6)]
transition-all duration-200"
style={{ width: `${scrollProgress}%` }}
ref={progressBarRef}
className="h-1 w-full origin-left will-change-transform transform-gpu rounded-full bg-gradient-to-r from-cyan-400 via-blue-400 to-purple-500 shadow-[0_0_10px_rgba(59,130,246,0.8),0_0_20px_rgba(139,92,246,0.6)]"
style={{ transform: 'scaleX(0)' }}
aria-hidden="true"
/>

<div className="container mx-auto px-4 py-4 flex justify-between items-center">
Expand Down Expand Up @@ -255,7 +270,7 @@ export default function Landing() {


{/* Stock Ticker */}
<StockTicker />
<StockTicker speedPxPerSecond={35} />


{/* Hero Section */}
Expand Down
55 changes: 16 additions & 39 deletions frontend/app/components/Testimonials.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import { useState } from 'react';

const Testimonials = () => {
const [isPaused, setIsPaused] = useState(false);
Expand Down Expand Up @@ -61,7 +61,7 @@ const Testimonials = () => {
];

const TestimonialCard = ({ testimonial }) => (
<div className="flex-shrink-0 w-96 mx-4 bg-gradient-to-br from-slate-800/50 to-slate-900/50 backdrop-blur-sm border border-slate-700/50 rounded-2xl p-6 hover:border-blue-500/50 transition-all duration-300 group">
<div className="flex-shrink-0 w-[72vw] sm:w-64 md:w-72 lg:w-80 xl:w-96 mx-2 sm:mx-3 md:mx-4 bg-card/70 dark:from-slate-800/50 dark:to-slate-900/50 backdrop-blur-sm border border-border rounded-2xl p-4 sm:p-6 hover:border-primary/50 transition-all duration-300 group">
<div className="flex items-start space-x-4">
<div className="flex-shrink-0">
<div className="w-12 h-12 bg-gradient-to-br from-blue-500 to-purple-600 rounded-full flex items-center justify-center text-white font-semibold text-sm shadow-lg">
Expand All @@ -76,21 +76,21 @@ const Testimonials = () => {
</svg>
))}
</div>
<p className="text-slate-300 text-sm leading-relaxed mb-4 group-hover:text-white transition-colors duration-300">
<p className="text-muted-foreground text-sm leading-relaxed mb-4 group-hover:text-foreground transition-colors duration-300">
"{testimonial.content}"
</p>
<div>
<h4 className="font-semibold text-white text-sm">{testimonial.name}</h4>
<p className="text-slate-400 text-xs">{testimonial.role}</p>
<p className="text-blue-400 text-xs font-medium">{testimonial.company}</p>
<h4 className="font-semibold text-foreground text-sm">{testimonial.name}</h4>
<p className="text-muted-foreground text-xs">{testimonial.role}</p>
<p className="text-primary text-xs font-medium">{testimonial.company}</p>
</div>
</div>
</div>
</div>
);

return (
<div className="bg-slate-950 py-16 overflow-hidden">
<div className="py-16 overflow-hidden bg-background">
<div className="max-w-7xl mx-auto px-6 mb-12">
<div className="text-center">
<h2 className="text-4xl font-bold bg-gradient-to-r from-blue-400 via-purple-400 to-pink-400 bg-clip-text text-transparent mb-4">
Expand All @@ -104,14 +104,14 @@ const Testimonials = () => {

<div className="relative">
{/* Gradient overlays for smooth edges */}
<div className="absolute left-0 top-0 w-32 h-full bg-gradient-to-r from-slate-950 to-transparent z-10 pointer-events-none"></div>
<div className="absolute right-0 top-0 w-32 h-full bg-gradient-to-l from-slate-950 to-transparent z-10 pointer-events-none"></div>
<div className="absolute left-0 top-0 w-8 sm:w-16 md:w-24 h-full bg-gradient-to-r from-background to-transparent z-10 pointer-events-none" aria-hidden="true"></div>
<div className="absolute right-0 top-0 w-8 sm:w-16 md:w-24 h-full bg-gradient-to-l from-background to-transparent z-10 pointer-events-none" aria-hidden="true"></div>

{/* First row - moving right */}
<div
className="flex animate-marquee-right hover:pause-animation"
className="inline-flex gap-3 sm:gap-4 px-1 sm:px-2 will-change-transform animate-marquee-right hover:pause-animation"
style={{
animationDuration: '60s',
animationDuration: '28s',
animationPlayState: isPaused ? 'paused' : 'running'
}}
onMouseEnter={() => setIsPaused(true)}
Expand All @@ -129,9 +129,9 @@ const Testimonials = () => {

{/* Second row - moving left */}
<div
className="flex animate-marquee-left mt-8 hover:pause-animation"
className="inline-flex gap-3 sm:gap-4 px-1 sm:px-2 will-change-transform animate-marquee-left mt-8 hover:pause-animation"
style={{
animationDuration: '60s',
animationDuration: '28s',
animationPlayState: isPaused ? 'paused' : 'running'
}}
onMouseEnter={() => setIsPaused(true)}
Expand All @@ -148,35 +148,12 @@ const Testimonials = () => {
</div>
</div>

{/* Interactive controls */}
<div className="text-center mt-12">
<button
onClick={() => setIsPaused(!isPaused)}
className="inline-flex items-center space-x-2 px-6 py-3 bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700 text-white rounded-lg transition-all duration-300 shadow-lg hover:shadow-xl transform hover:scale-105"
>
{isPaused ? (
<>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<polygon points="5,3 19,12 5,21"></polygon>
</svg>
<span>Resume</span>
</>
) : (
<>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<rect x="6" y="4" width="4" height="16"></rect>
<rect x="14" y="4" width="4" height="16"></rect>
</svg>
<span>Pause</span>
</>
)}
</button>
</div>
{/* Removed interactive pause/resume button; hover still pauses */}

<style jsx>{`
@keyframes marquee-right {
from {
transform: translateX(-100%);
transform: translateX(-50%);
}
to {
transform: translateX(0%);
Expand All @@ -188,7 +165,7 @@ const Testimonials = () => {
transform: translateX(0%);
}
to {
transform: translateX(-100%);
transform: translateX(-50%);
}
}

Expand Down
25 changes: 15 additions & 10 deletions frontend/app/components/dashboard.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
import { portfolioStocks, portfolioSummary, recentActivities } from "@/data/mock-data";
// import { useLocation } from "react-router-dom";
import { PerformanceSection } from "./performance-section";
import { PortfolioAllocation } from "./portfolio-allocation";
import { PortfolioSection } from "./portfolio-section";
import { MultiPortfolioSection } from "./multi-portfolio-section";
import {
BarChart2,
BriefcaseBusiness, ChartNoAxesCombined, CircleDollarSign,
LineChart,
Settings,
TrendingUp,
Wallet
} from "lucide-react";
import dynamic from "next/dynamic";
import { RecentActivity } from "./recent-activity";
import Screener from "./screener";
import { SettingsSection } from "./settings-section";
import { StockCard } from "./stock-card";
import { StockChart } from "./stock-chart";
import { SummaryCard } from "./summary-card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "./ui/tabs";
import Screener from "./screener";
import {
BarChart2, ChevronLeft, ChevronRight, Home, LineChart,
Settings, Wallet,BriefcaseBusiness , ChartNoAxesCombined ,CircleDollarSign, TrendingUp
} from "lucide-react";
const PerformanceSection = dynamic(() => import("./performance-section").then(m => m.PerformanceSection), { ssr: false });
const PortfolioAllocation = dynamic(() => import("./portfolio-allocation").then(m => m.PortfolioAllocation), { ssr: false });
const PortfolioSection = dynamic(() => import("./portfolio-section").then(m => m.PortfolioSection), { ssr: false });
const MultiPortfolioSection = dynamic(() => import("./multi-portfolio-section").then(m => m.MultiPortfolioSection), { ssr: false });
const StockChart = dynamic(() => import("./stock-chart").then(m => m.StockChart), { ssr: false });
// Define the Activity type to match what RecentActivity component expects
type Activity = {
id: number;
Expand Down
7 changes: 4 additions & 3 deletions frontend/app/components/performance-section.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
'use client';
"use client";

import { marketIndices } from "@/data/mock-data";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "./ui/card";
Expand All @@ -12,9 +12,10 @@ import {
ChartTooltipContent,
} from "@/components/ui/chart";
import { TrendingUp } from "lucide-react";
import dynamic from 'next/dynamic';
import { Bar, BarChart, CartesianGrid, Cell, LabelList } from "recharts";
import { Overview } from './overview';
import { RecentSales } from './recent-sales';
const Overview = dynamic(() => import('./overview').then(m => m.Overview), { ssr: false });
const RecentSales = dynamic(() => import('./recent-sales').then(m => m.RecentSales), { ssr: false });
const chartData = [
{ month: "January", visitors: 186 },
{ month: "February", visitors: 205 },
Expand Down
27 changes: 24 additions & 3 deletions frontend/app/components/stock-card.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@

import { StockData } from "@/data/mock-data";
import { cn } from "@/lib/utils";
import { ArrowDownIcon, ArrowUpIcon } from "lucide-react";
import React from "react";
import { Card, CardContent, CardHeader, CardTitle } from "./ui/card";
import { cn } from "@/lib/utils";
import { StockData } from "@/data/mock-data";

interface StockCardProps {
stock: StockData;
className?: string;
style?: React.CSSProperties;
}

export function StockCard({ stock, className, style }: StockCardProps) {
function StockCardInner({ stock, className, style }: StockCardProps) {
const isPositive = stock.change >= 0;

return (
Expand Down Expand Up @@ -55,3 +56,23 @@ export function StockCard({ stock, className, style }: StockCardProps) {
</Card>
);
}

export const StockCard = React.memo(
StockCardInner,
(prevProps, nextProps) => {
// Shallow compare fields used in render to avoid unnecessary rerenders
const p = prevProps.stock;
const n = nextProps.stock;
const sameStock =
p.symbol === n.symbol &&
p.name === n.name &&
p.shares === n.shares &&
p.price === n.price &&
p.change === n.change &&
p.changePercent === n.changePercent &&
p.value === n.value &&
p.marketCap === n.marketCap;
const sameStyle = prevProps.className === nextProps.className && JSON.stringify(prevProps.style) === JSON.stringify(nextProps.style);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using JSON.stringify for prop comparison in React.memo can be inefficient, especially for larger objects, as it requires serializing the object on every render. For style objects, a shallow comparison is generally more performant. While the performance impact is likely minimal here since the style object is small, it's a good practice to avoid JSON.stringify in memoization comparators for performance-critical components.

return sameStock && sameStyle;
}
);
15 changes: 9 additions & 6 deletions frontend/app/components/stock-chart.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";
import { StockData, timeRanges } from "@/data/mock-data";
import { cn } from "@/lib/utils";
import { useState } from "react";
import { useMemo, useState } from "react";
import {
Area,
AreaChart,
Expand All @@ -24,22 +24,25 @@ interface StockChartProps {
}

export function StockChart({ data, title = "Stock Performance", className, style }: StockChartProps) {
const [selectedStock, setSelectedStock] = useState<string>(data[0]?.symbol || "");
const initialSymbol = data[0]?.symbol || "";
const [selectedStock, setSelectedStock] = useState<string>(initialSymbol);
const [timeRange, setTimeRange] = useState("1m");
const { theme } = useTheme();

const isDarkMode = theme === "dark";

const selectedStockData = data.find((stock) => stock.symbol === selectedStock) || data[0];
const selectedStockData = useMemo(() => {
return data.find((stock) => stock.symbol === selectedStock) || data[0];
}, [data, selectedStock]);

// Filter history based on time range (for simplicity, we'll just use the full history)
const chartData = selectedStockData?.history || [];
const chartData = useMemo(() => selectedStockData?.history || [], [selectedStockData]);

// Format currency for tooltip
const formatCurrency = (value: number) => `$${value.toFixed(2)}`;

// Custom chart colors based on theme
const chartColors = {
const chartColors = useMemo(() => ({
line: isDarkMode ? "#38BDF8" : "#3b82f6",
gradient: {
start: isDarkMode ? "rgba(56, 189, 248, 0.6)" : "rgba(59, 130, 246, 0.6)",
Expand All @@ -50,7 +53,7 @@ export function StockChart({ data, title = "Stock Performance", className, style
bg: isDarkMode ? "rgba(15, 23, 42, 0.8)" : "rgba(255, 255, 255, 0.8)",
border: isDarkMode ? "rgba(51, 65, 85, 0.5)" : "rgba(229, 231, 235, 0.5)"
}
};
}), [isDarkMode]);

return (
<Card className={cn("col-span-3 backdrop-blur-lg bg-card/80 border-primary/10 transition-all hover:shadow-lg", className)} style={style}>
Expand Down
Loading