Features: - FastAPI backend with stocks, news, signals, watchlist, analytics endpoints - React frontend with TailwindCSS dark mode trading dashboard - Celery workers for news fetching, sentiment analysis, pattern detection - TimescaleDB schema for time-series stock data - Docker Compose setup for all services - OpenAI integration for sentiment analysis
173 lines
6.1 KiB
TypeScript
173 lines
6.1 KiB
TypeScript
import { ReactNode, useState } from 'react'
|
|
import { Link, useLocation } from 'react-router-dom'
|
|
import { motion } from 'framer-motion'
|
|
import {
|
|
HomeIcon,
|
|
BellAlertIcon,
|
|
ChartBarIcon,
|
|
NewspaperIcon,
|
|
StarIcon,
|
|
ChartPieIcon,
|
|
Bars3Icon,
|
|
XMarkIcon,
|
|
} from '@heroicons/react/24/outline'
|
|
import clsx from 'clsx'
|
|
|
|
interface LayoutProps {
|
|
children: ReactNode
|
|
}
|
|
|
|
const navigation = [
|
|
{ name: 'Dashboard', href: '/', icon: HomeIcon },
|
|
{ name: 'Buy Signals', href: '/signals', icon: BellAlertIcon },
|
|
{ name: 'Stocks', href: '/stocks', icon: ChartBarIcon },
|
|
{ name: 'News', href: '/news', icon: NewspaperIcon },
|
|
{ name: 'Watchlist', href: '/watchlist', icon: StarIcon },
|
|
{ name: 'Analytics', href: '/analytics', icon: ChartPieIcon },
|
|
]
|
|
|
|
export default function Layout({ children }: LayoutProps) {
|
|
const location = useLocation()
|
|
const [sidebarOpen, setSidebarOpen] = useState(false)
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gradient-to-br from-gray-950 via-gray-900 to-gray-950">
|
|
{/* Mobile sidebar backdrop */}
|
|
{sidebarOpen && (
|
|
<div
|
|
className="fixed inset-0 z-40 bg-black/50 backdrop-blur-sm lg:hidden"
|
|
onClick={() => setSidebarOpen(false)}
|
|
/>
|
|
)}
|
|
|
|
{/* Sidebar */}
|
|
<aside
|
|
className={clsx(
|
|
'fixed inset-y-0 left-0 z-50 w-64 bg-gray-900/95 backdrop-blur-xl border-r border-gray-800 transform transition-transform duration-300 lg:translate-x-0',
|
|
sidebarOpen ? 'translate-x-0' : '-translate-x-full'
|
|
)}
|
|
>
|
|
<div className="flex flex-col h-full">
|
|
{/* Logo */}
|
|
<div className="flex items-center justify-between h-16 px-4 border-b border-gray-800">
|
|
<Link to="/" className="flex items-center space-x-3">
|
|
<div className="w-10 h-10 bg-gradient-to-br from-green-500 to-emerald-600 rounded-xl flex items-center justify-center">
|
|
<ChartBarIcon className="w-6 h-6 text-white" />
|
|
</div>
|
|
<div>
|
|
<span className="font-bold text-lg gradient-text">MarketScanner</span>
|
|
<p className="text-xs text-gray-500">Buy the Fear</p>
|
|
</div>
|
|
</Link>
|
|
<button
|
|
onClick={() => setSidebarOpen(false)}
|
|
className="lg:hidden p-1 rounded-lg hover:bg-gray-800"
|
|
>
|
|
<XMarkIcon className="w-6 h-6" />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Navigation */}
|
|
<nav className="flex-1 px-3 py-4 space-y-1 overflow-y-auto">
|
|
{navigation.map((item) => {
|
|
const isActive = location.pathname === item.href
|
|
return (
|
|
<Link
|
|
key={item.name}
|
|
to={item.href}
|
|
onClick={() => setSidebarOpen(false)}
|
|
className={clsx(
|
|
'flex items-center px-3 py-2.5 rounded-lg transition-all duration-200',
|
|
isActive
|
|
? 'bg-green-500/20 text-green-400 border border-green-500/30'
|
|
: 'text-gray-400 hover:text-white hover:bg-gray-800'
|
|
)}
|
|
>
|
|
<item.icon className={clsx('w-5 h-5 mr-3', isActive && 'text-green-400')} />
|
|
{item.name}
|
|
{item.name === 'Buy Signals' && (
|
|
<span className="ml-auto badge-success">3</span>
|
|
)}
|
|
</Link>
|
|
)
|
|
})}
|
|
</nav>
|
|
|
|
{/* Bottom section */}
|
|
<div className="p-4 border-t border-gray-800">
|
|
<div className="card p-3">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<span className="text-xs text-gray-500">Market Sentiment</span>
|
|
<span className="badge-danger">Fear</span>
|
|
</div>
|
|
<div className="h-2 bg-gray-800 rounded-full overflow-hidden">
|
|
<motion.div
|
|
className="h-full bg-gradient-to-r from-red-500 to-orange-500"
|
|
initial={{ width: 0 }}
|
|
animate={{ width: '35%' }}
|
|
transition={{ duration: 1, ease: 'easeOut' }}
|
|
/>
|
|
</div>
|
|
<div className="flex justify-between mt-1 text-xs text-gray-600">
|
|
<span>Extreme Fear</span>
|
|
<span>Greed</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
|
|
{/* Main content */}
|
|
<div className="lg:pl-64">
|
|
{/* Top bar */}
|
|
<header className="sticky top-0 z-30 h-16 bg-gray-900/80 backdrop-blur-xl border-b border-gray-800">
|
|
<div className="flex items-center justify-between h-full px-4">
|
|
<button
|
|
onClick={() => setSidebarOpen(true)}
|
|
className="lg:hidden p-2 rounded-lg hover:bg-gray-800"
|
|
>
|
|
<Bars3Icon className="w-6 h-6" />
|
|
</button>
|
|
|
|
<div className="flex items-center space-x-4">
|
|
{/* Search */}
|
|
<div className="hidden md:block">
|
|
<input
|
|
type="text"
|
|
placeholder="Search stocks..."
|
|
className="input w-64"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-4">
|
|
{/* Market status */}
|
|
<div className="flex items-center space-x-2">
|
|
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse" />
|
|
<span className="text-sm text-gray-400">Market Open</span>
|
|
</div>
|
|
|
|
{/* Time */}
|
|
<div className="hidden sm:block text-sm text-gray-500">
|
|
{new Date().toLocaleTimeString()}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
{/* Page content */}
|
|
<main className="p-4 md:p-6 lg:p-8">
|
|
<motion.div
|
|
key={location.pathname}
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.3 }}
|
|
>
|
|
{children}
|
|
</motion.div>
|
|
</main>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|