UI: refine panels, sidebar, chat dock, add BuyCredits page and style tweaks

This commit is contained in:
2025-09-29 21:04:21 +05:30
parent c7a3220016
commit 77f8197fc1
11 changed files with 365 additions and 226 deletions

View File

@@ -1,7 +1,11 @@
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import LandingPage from './pages/LandingPage';
import LoginPage from './pages/LoginPage';
import ForgotPasswordPage from './pages/ForgotPasswordPage';
import { Suspense, lazy } from 'react';
// Route-level code splitting (improves first paint for initial /login route)
const LandingPage = lazy(() => import('./pages/LandingPage'));
const LoginPage = lazy(() => import('./pages/LoginPage'));
const ForgotPasswordPage = lazy(() => import('./pages/ForgotPasswordPage'));
const BuyCreditsPage = lazy(() => import('./pages/BuyCreditsPage'));
function App() {
return (
@@ -39,13 +43,25 @@ function App() {
/>
{/* Main Content - Highest z-index for interactivity */}
<main className="relative z-10 min-h-screen">
<Routes>
<Route path="/" element={<Navigate to="/login" replace />} />
<Route path="/home" element={<LandingPage />} />
<Route path="/login" element={<LoginPage />} />
<Route path="/forgot-password" element={<ForgotPasswordPage />} />
</Routes>
<main className="relative z-10 min-h-screen cv-auto">
<Suspense
fallback={
<div className="w-full h-screen flex items-center justify-center text-cream/70 font-montserrat animate-fadeInUp">
<div className="flex flex-col items-center gap-4">
<div className="w-10 h-10 rounded-full border-2 border-luxury-gold/40 border-t-luxury-gold animate-spin" />
<span className="tracking-wide text-sm uppercase">Loading…</span>
</div>
</div>
}
>
<Routes>
<Route path="/" element={<Navigate to="/login" replace />} />
<Route path="/home" element={<LandingPage />} />
<Route path="/buy-credits" element={<BuyCreditsPage />} />
<Route path="/login" element={<LoginPage />} />
<Route path="/forgot-password" element={<ForgotPasswordPage />} />
</Routes>
</Suspense>
</main>
</div>
</Router>

View File

@@ -29,23 +29,18 @@ const AgentPanel: React.FC<AgentPanelProps> = ({ agents, activeAgentId, onChange
], []);
return (
<div className="h-full w-full border border-luxury-gold/25 rounded-2xl p-4 md:p-6 bg-deep-forest/50 backdrop-blur-xl overflow-hidden flex flex-col shadow-luxury-inner">
<div className="flex items-center justify-between mb-4">
<h3 className="font-playfair text-xl text-cream">Personal Assistant</h3>
<div className="text-cream/80 text-sm font-montserrat">
{active.name}
</div>
</div>
<div className="h-full w-full rounded-xl bg-deep-forest/60 backdrop-blur-xl overflow-hidden flex flex-col shadow-inner">
{/* Removed heading & static active name per user request */}
<div className="relative rounded-xl bg-gradient-to-br from-deep-forest/60 to-rich-brown/40 border border-luxury-gold/25 overflow-hidden flex-1">
<div className={`absolute inset-0 opacity-40 bg-gradient-to-br ${active.color}`} />
<div className="relative flex-1">
<div className={`absolute inset-0 opacity-35 bg-gradient-to-br ${active.color}`} />
{/* Creative Orbital Agent Display */}
<div className="absolute inset-0 grid place-items-center">
{/* Central Hub */}
<div className="relative">
{/* Outer orbital ring */}
<div className="w-72 h-72 rounded-full border border-luxury-gold/20 animate-spin-ultra-slow relative">
<div className="w-72 h-72 rounded-full border border-luxury-gold/15 relative">
{/* Orbital path indicator */}
<div className="absolute inset-4 rounded-full border border-luxury-gold/10"></div>
<div className="absolute inset-8 rounded-full border border-luxury-gold/5"></div>
@@ -63,7 +58,7 @@ const AgentPanel: React.FC<AgentPanelProps> = ({ agents, activeAgentId, onChange
<div key={agent.id} className="contents">
<button
onClick={() => onChangeAgent(agent.id)}
className={`absolute w-14 h-14 rounded-2xl transition-all duration-500 hover:scale-125 focus:outline-none group ${
className={`absolute w-12 h-12 rounded-xl transition-all duration-300 hover:scale-125 focus:outline-none group ${
isActive
? 'bg-gradient-to-br from-luxury-gold/60 to-brass/60 border-2 border-luxury-gold shadow-[0_0_25px_rgba(197,157,89,0.8)] scale-110 z-10'
: 'bg-deep-forest/70 border border-luxury-gold/30 hover:border-luxury-gold/60 hover:bg-deep-forest/80'
@@ -71,7 +66,7 @@ const AgentPanel: React.FC<AgentPanelProps> = ({ agents, activeAgentId, onChange
style={{
left: '50%',
top: '50%',
transform: `translate(${x - 28}px, ${y - 28}px)`,
transform: `translate(${x - 24}px, ${y - 24}px)`,
}}
title={agent.name}
>
@@ -97,15 +92,15 @@ const AgentPanel: React.FC<AgentPanelProps> = ({ agents, activeAgentId, onChange
</button>
{/* Tooltip positioned outside rotating container */}
<div
className="absolute opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none z-50"
<div
className="absolute z-50 pointer-events-none"
style={{
left: '50%',
top: '50%',
transform: `translate(${x - 28}px, ${y + 40}px)`,
transform: `translate(${x - 24}px, ${y + 40}px)`,
}}
>
<div className="bg-deep-forest/95 text-cream text-sm px-3 py-2 rounded-xl border border-luxury-gold/40 whitespace-nowrap font-montserrat font-medium shadow-[0_4px_12px_rgba(0,0,0,0.4)] -translate-x-1/2">
<div className="opacity-0 group-hover:opacity-100 transition-opacity duration-150 bg-deep-forest/95 text-cream/90 text-[11px] px-2 py-1 rounded-md border border-luxury-gold/30 whitespace-nowrap font-montserrat font-medium shadow-[0_2px_4px_rgba(0,0,0,0.4)] -translate-x-1/2">
{agent.name}
</div>
</div>
@@ -115,36 +110,18 @@ const AgentPanel: React.FC<AgentPanelProps> = ({ agents, activeAgentId, onChange
</div>
{/* Central Control Hub */}
<div className="absolute top-1/2 left-1/2 w-20 h-20 -translate-x-1/2 -translate-y-1/2 z-20">
<div className="w-full h-full rounded-full bg-gradient-to-br from-deep-forest/80 to-rich-brown/60 border-2 border-luxury-gold/40 shadow-[0_0_20px_rgba(197,157,89,0.3)] flex items-center justify-center group cursor-pointer">
<div className="absolute top-1/2 left-1/2 w-16 h-16 -translate-x-1/2 -translate-y-1/2 z-20">
<div className="w-full h-full rounded-full bg-gradient-to-br from-deep-forest/80 to-rich-brown/60 border border-luxury-gold/30 flex items-center justify-center group cursor-pointer">
<div className="text-center">
<div className="text-2xl mb-1">🤖</div>
<div className="text-xs text-cream/80 font-montserrat font-medium">HUB</div>
<div className="text-xl mb-0.5">🤖</div>
<div className="text-[10px] text-cream/80 font-montserrat font-medium">HUB</div>
</div>
</div>
{/* Pulsing energy rings */}
<div className="absolute inset-0 rounded-full border border-luxury-gold/20 animate-ping"></div>
<div className="absolute inset-2 rounded-full border border-luxury-gold/10 animate-ping animation-delay-1000"></div>
{/* Removed pulsing rings for a calmer static display */}
</div>
</div>
</div>
{/* Floating particles for ambiance */}
<div className="absolute inset-0 pointer-events-none">
{[...Array(6)].map((_, i) => (
<div
key={i}
className="absolute w-1 h-1 bg-luxury-gold rounded-full animate-ping"
style={{
left: `${20 + Math.random() * 60}%`,
top: `${20 + Math.random() * 60}%`,
animationDelay: `${Math.random() * 2}s`,
animationDuration: `${1.5 + Math.random() * 1}s`
}}
></div>
))}
</div>
{/* Removed floating particle animations */}
</div>
</div>
);

View File

@@ -30,7 +30,7 @@ const ChatDock: React.FC<ChatDockProps> = ({ messages, onSend, placeholder = 'As
};
return (
<div className={`border-t border-luxury-gold/25 bg-deep-forest/80 backdrop-blur-2xl fixed bottom-0 right-0 ${leftOffsetClass} z-40 h-[140px] md:h-[160px] ${className}`}>
<div className={`border-t border-luxury-gold/25 bg-deep-forest/80 backdrop-blur-2xl fixed bottom-0 right-0 ${leftOffsetClass} z-40 h-[140px] md:h-[160px] transition-all duration-300 ease-in-out ${className}`}>
<div className="w-full h-full p-3 md:p-4 flex flex-col">
<div ref={listRef} className="flex-1 min-h-0 overflow-y-auto pr-1 space-y-3 custom-thin-scroll">
{messages.map((m) => (

View File

@@ -3,9 +3,10 @@ import React, { useState } from 'react';
interface CreditsScreenProps {
isOpen: boolean;
onClose: () => void;
sidebarOpen?: boolean; // controls left offset dynamically
}
const CreditsScreen: React.FC<CreditsScreenProps> = ({ isOpen, onClose }) => {
const CreditsScreen: React.FC<CreditsScreenProps> = ({ isOpen, onClose, sidebarOpen }) => {
const [selectedYear, setSelectedYear] = useState('2025');
const [selectedMonth, setSelectedMonth] = useState('09');
@@ -24,131 +25,139 @@ const CreditsScreen: React.FC<CreditsScreenProps> = ({ isOpen, onClose }) => {
const monthlyUsage = 16;
const monthlyPurchases = 100;
// Determine offsets: if sidebarOpen true use sidebar widths, else flush left.
const leftMobile = sidebarOpen ? 'left-16' : 'left-0';
const leftDesktop = sidebarOpen ? 'md:left-56' : 'md:left-0';
return (
<div className="fixed top-16 md:top-20 left-20 md:left-64 right-0 bottom-0 z-40 bg-gradient-to-br from-deep-forest/95 to-rich-brown/90 backdrop-blur-sm">
{/* Header */}
<div className="h-16 px-6 border-b border-luxury-gold/25 bg-deep-forest/90 backdrop-blur-xl flex items-center justify-between">
<div className="flex items-center gap-4">
<div className={`fixed top-16 md:top-20 ${leftMobile} ${leftDesktop} right-0 bottom-0 z-40 bg-gradient-to-br from-deep-forest/95 to-rich-brown/90 backdrop-blur-sm transition-all duration-300`}>
{/* Header (compact, subtitle removed) */}
<div className="h-14 px-5 border-b border-luxury-gold/20 bg-deep-forest/90 backdrop-blur-xl flex items-center gap-4">
<button
onClick={onClose}
className="w-9 h-9 rounded-lg bg-deep-forest/60 border border-luxury-gold/30 hover:bg-luxury-gold/10 hover:border-luxury-gold/50 transition-all flex items-center justify-center"
aria-label="Back"
>
<svg width="16" height="16" viewBox="0 0 24 24" className="text-cream fill-current">
<path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/>
</svg>
</button>
<h1 className="font-playfair text-[22px] leading-none font-bold text-cream pt-1">Credits & Usage History</h1>
<div className="ml-auto">
<button
onClick={onClose}
className="w-10 h-10 rounded-xl bg-deep-forest/60 border border-luxury-gold/30 hover:bg-luxury-gold/10 hover:border-luxury-gold/50 transition-all flex items-center justify-center"
onClick={() => {
// dispatch a custom event so the app can handle opening a buy flow
try {
window.dispatchEvent(new CustomEvent('openBuyCredits'));
} catch (e) {
// fallback
// eslint-disable-next-line no-alert
alert('Buy credits flow not implemented yet');
}
}}
className="inline-flex items-center gap-2 px-4 py-1.5 bg-gradient-to-r from-luxury-gold to-brass text-deep-forest font-semibold rounded-md shadow hover:scale-105 transition-transform text-sm"
aria-label="Buy Credits"
>
<svg width="16" height="16" viewBox="0 0 24 24" className="text-cream fill-current">
<path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/>
<svg width="16" height="16" viewBox="0 0 24 24" className="fill-current text-deep-forest">
<path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 2.18L18.56 6 12 8.82 5.44 6 12 3.18zM11 10h2v6h-2v-6z"/>
</svg>
Buy Credits
</button>
<div>
<h1 className="font-playfair text-xl md:text-2xl font-bold text-cream">Credits & Usage History</h1>
<p className="text-cream/60 text-sm">Manage your account balance and track usage patterns</p>
</div>
</div>
<div className="flex items-center gap-4">
<div className="bg-gradient-to-r from-luxury-gold/20 to-brass/20 border border-luxury-gold/30 px-4 py-2 rounded-xl">
<span className="text-luxury-gold font-bold text-lg">{currentBalance} Credits</span>
</div>
</div>
</div>
{/* Main Content */}
<div className="flex-1 p-4 md:p-6 overflow-auto h-[calc(100vh-12rem)] md:h-[calc(100vh-14rem)]">
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
<div className="bg-gradient-to-br from-deep-forest/60 to-rich-brown/40 border border-luxury-gold/25 rounded-xl p-4">
<div className="flex items-center gap-3 mb-3">
<div className="w-10 h-10 rounded-lg bg-gradient-to-br from-green-500/30 to-emerald-500/30 flex items-center justify-center">
<svg width="20" height="20" viewBox="0 0 24 24" className="text-green-400 fill-current">
<path d="M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4M11,16.5L6.5,12L7.91,10.59L11,13.67L16.59,8.09L18,9.5L11,16.5Z"/>
</svg>
</div>
<div>
<h3 className="font-playfair text-lg font-bold text-cream">Current Balance</h3>
<p className="text-cream/60 text-xs">Available credits</p>
<div className="flex-1 p-4 md:p-5 overflow-auto h-[calc(100vh-11rem)] md:h-[calc(100vh-12.5rem)] space-y-4">
{/* Stats Cards condensed with inline numbers */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
<div className="bg-gradient-to-br from-deep-forest/60 to-rich-brown/40 border border-luxury-gold/20 rounded-lg p-3 flex flex-col gap-2">
<div className="flex items-start justify-between gap-2">
<div className="flex items-center gap-3">
<div className="w-9 h-9 rounded-md bg-gradient-to-br from-green-500/30 to-emerald-500/30 flex items-center justify-center">
<svg width="18" height="18" viewBox="0 0 24 24" className="text-green-400 fill-current"><path d="M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4M11,16.5L6.5,12L7.91,10.59L11,13.67L16.59,8.09L18,9.5L11,16.5Z"/></svg>
</div>
<div>
<h3 className="font-playfair text-[15px] font-semibold text-cream">Current Balance</h3>
<p className="text-cream/55 text-[11px] uppercase tracking-wide">Available</p>
</div>
</div>
<div className="text-green-400 font-bold text-xl leading-none">{currentBalance}</div>
</div>
<p className="text-2xl font-bold text-green-400">{currentBalance}</p>
</div>
<div className="bg-gradient-to-br from-deep-forest/60 to-rich-brown/40 border border-luxury-gold/25 rounded-xl p-4">
<div className="flex items-center gap-3 mb-3">
<div className="w-10 h-10 rounded-lg bg-gradient-to-br from-red-500/30 to-pink-500/30 flex items-center justify-center">
<svg width="20" height="20" viewBox="0 0 24 24" className="text-red-400 fill-current">
<path d="M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z"/>
</svg>
</div>
<div>
<h3 className="font-playfair text-lg font-bold text-cream">Monthly Usage</h3>
<p className="text-cream/60 text-xs">Credits spent this month</p>
<div className="bg-gradient-to-br from-deep-forest/60 to-rich-brown/40 border border-luxury-gold/20 rounded-lg p-3 flex flex-col gap-2">
<div className="flex items-start justify-between gap-2">
<div className="flex items-center gap-3">
<div className="w-9 h-9 rounded-md bg-gradient-to-br from-red-500/30 to-pink-500/30 flex items-center justify-center">
<svg width="18" height="18" viewBox="0 0 24 24" className="text-red-400 fill-current"><path d="M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z"/></svg>
</div>
<div>
<h3 className="font-playfair text-[15px] font-semibold text-cream">Monthly Usage</h3>
<p className="text-cream/55 text-[11px] uppercase tracking-wide">Spent</p>
</div>
</div>
<div className="text-red-400 font-bold text-xl leading-none">{monthlyUsage}</div>
</div>
<p className="text-2xl font-bold text-red-400">{monthlyUsage}</p>
</div>
<div className="bg-gradient-to-br from-deep-forest/60 to-rich-brown/40 border border-luxury-gold/25 rounded-xl p-4">
<div className="flex items-center gap-3 mb-3">
<div className="w-10 h-10 rounded-lg bg-gradient-to-br from-luxury-gold/30 to-brass/30 flex items-center justify-center">
<svg width="20" height="20" viewBox="0 0 24 24" className="text-luxury-gold fill-current">
<path d="M7,15H9C9,16.08 10.37,17 12,17C13.63,17 15,16.08 15,15C15,13.9 13.96,13.5 11.76,12.97C9.64,12.44 7,11.78 7,9C7,7.21 8.47,5.69 10.5,5.18V3H13.5V5.18C15.53,5.69 17,7.21 17,9H15C15,7.92 13.63,7 12,7C10.37,7 9,7.92 9,9C9,10.1 10.04,10.5 12.24,11.03C14.36,11.56 17,12.22 17,15C17,16.79 15.53,18.31 13.5,18.82V21H10.5V18.82C8.47,18.31 7,16.79 7,15Z"/>
</svg>
</div>
<div>
<h3 className="font-playfair text-lg font-bold text-cream">Monthly Purchases</h3>
<p className="text-cream/60 text-xs">Credits purchased this month</p>
<div className="bg-gradient-to-br from-deep-forest/60 to-rich-brown/40 border border-luxury-gold/20 rounded-lg p-3 flex flex-col gap-2">
<div className="flex items-start justify-between gap-2">
<div className="flex items-center gap-3">
<div className="w-9 h-9 rounded-md bg-gradient-to-br from-luxury-gold/30 to-brass/30 flex items-center justify-center">
<svg width="18" height="18" viewBox="0 0 24 24" className="text-luxury-gold fill-current"><path d="M7,15H9C9,16.08 10.37,17 12,17C13.63,17 15,16.08 15,15C15,13.9 13.96,13.5 11.76,12.97C9.64,12.44 7,11.78 7,9C7,7.21 8.47,5.69 10.5,5.18V3H13.5V5.18C15.53,5.69 17,7.21 17,9H15C15,7.92 13.63,7 12,7C10.37,7 9,7.92 9,9C9,10.1 10.04,10.5 12.24,11.03C14.36,11.56 17,12.22 17,15C17,16.79 15.53,18.31 13.5,18.82V21H10.5V18.82C8.47,18.31 7,16.79 7,15Z"/></svg>
</div>
<div>
<h3 className="font-playfair text-[15px] font-semibold text-cream">Monthly Purchases</h3>
<p className="text-cream/55 text-[11px] uppercase tracking-wide">Bought</p>
</div>
</div>
<div className="text-luxury-gold font-bold text-xl leading-none">{monthlyPurchases}</div>
</div>
<p className="text-2xl font-bold text-luxury-gold">{monthlyPurchases}</p>
</div>
</div>
{/* Filters */}
<div className="bg-gradient-to-br from-deep-forest/60 to-rich-brown/40 border border-luxury-gold/25 rounded-xl p-4 mb-4">
<div className="flex flex-wrap items-end gap-4">
{/* History Table with inline filters */}
<div className="bg-gradient-to-br from-deep-forest/60 to-rich-brown/40 border border-luxury-gold/20 rounded-lg overflow-hidden">
<div className="px-4 py-3 border-b border-luxury-gold/15 flex flex-col md:flex-row md:items-center md:justify-between gap-3 bg-deep-forest/70">
<div>
<label className="block text-cream/80 text-sm mb-1">Filter by Year</label>
<select
value={selectedYear}
onChange={(e) => setSelectedYear(e.target.value)}
className="px-3 py-2 bg-deep-forest/70 border border-luxury-gold/40 rounded-lg text-cream focus:outline-none focus:ring-2 focus:ring-luxury-gold/40 text-sm"
>
<option value="2025">2025</option>
<option value="2024">2024</option>
<option value="2023">2023</option>
</select>
<h3 className="font-playfair text-lg font-bold text-cream leading-none">Transaction History</h3>
</div>
<div>
<label className="block text-cream/80 text-sm mb-1">Filter by Month</label>
<select
value={selectedMonth}
onChange={(e) => setSelectedMonth(e.target.value)}
className="px-3 py-2 bg-deep-forest/70 border border-luxury-gold/40 rounded-lg text-cream focus:outline-none focus:ring-2 focus:ring-luxury-gold/40 text-sm"
>
<option value="01">January</option>
<option value="02">February</option>
<option value="03">March</option>
<option value="04">April</option>
<option value="05">May</option>
<option value="06">June</option>
<option value="07">July</option>
<option value="08">August</option>
<option value="09">September</option>
<option value="10">October</option>
<option value="11">November</option>
<option value="12">December</option>
</select>
</div>
<div>
<button className="px-4 py-2 bg-gradient-to-r from-luxury-gold to-brass text-deep-forest font-semibold rounded-lg hover:scale-105 transition-transform text-sm">
Show Details
<div className="flex flex-wrap items-end gap-3">
<div className="flex flex-col">
<label className="text-cream/60 text-[11px] mb-1">Year</label>
<select
value={selectedYear}
onChange={(e) => setSelectedYear(e.target.value)}
className="px-2.5 py-1.5 bg-deep-forest/70 border border-luxury-gold/30 rounded-md text-cream focus:outline-none focus:ring-2 focus:ring-luxury-gold/30 text-xs"
>
<option value="2025">2025</option>
<option value="2024">2024</option>
<option value="2023">2023</option>
</select>
</div>
<div className="flex flex-col">
<label className="text-cream/60 text-[11px] mb-1">Month</label>
<select
value={selectedMonth}
onChange={(e) => setSelectedMonth(e.target.value)}
className="px-2.5 py-1.5 bg-deep-forest/70 border border-luxury-gold/30 rounded-md text-cream focus:outline-none focus:ring-2 focus:ring-luxury-gold/30 text-xs"
>
<option value="01">Jan</option>
<option value="02">Feb</option>
<option value="03">Mar</option>
<option value="04">Apr</option>
<option value="05">May</option>
<option value="06">Jun</option>
<option value="07">Jul</option>
<option value="08">Aug</option>
<option value="09">Sep</option>
<option value="10">Oct</option>
<option value="11">Nov</option>
<option value="12">Dec</option>
</select>
</div>
<button className="px-4 py-2 bg-gradient-to-r from-luxury-gold to-brass text-deep-forest font-semibold rounded-md text-xs md:text-sm hover:shadow-[0_0_0_2px_rgba(197,157,89,0.4)] transition-shadow">
Show
</button>
</div>
</div>
</div>
{/* History Table */}
<div className="bg-gradient-to-br from-deep-forest/60 to-rich-brown/40 border border-luxury-gold/25 rounded-xl overflow-hidden">
<div className="px-4 py-3 border-b border-luxury-gold/20 bg-gradient-to-r from-luxury-gold/10 to-transparent">
<h3 className="font-playfair text-lg font-bold text-cream">Transaction History</h3>
<p className="text-cream/60 text-sm">Detailed record of all credit transactions</p>
</div>
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-deep-forest/60">

View File

@@ -8,10 +8,10 @@ const DisplayPanel: React.FC<DisplayPanelProps> = ({ children }) => {
const [activeTab, setActiveTab] = useState<'instructions' | 'playground'>('instructions');
return (
<div className="h-full w-full border border-luxury-gold/25 rounded-2xl p-4 md:p-6 bg-deep-forest/50 backdrop-blur-xl overflow-hidden flex flex-col shadow-luxury-inner">
{/* Tab header */}
<div className="mb-3 flex items-center justify-between">
<div role="tablist" aria-label="Display panel tabs" className="inline-flex items-center rounded-xl bg-deep-forest/60 border border-luxury-gold/30 p-1 shadow-inner">
<div className="h-full w-full rounded-xl bg-deep-forest/60 backdrop-blur-xl overflow-hidden flex flex-col shadow-inner">
{/* Tab header condensed */}
<div className="px-3 pt-2 pb-1 flex items-center justify-start">
<div role="tablist" aria-label="Display panel tabs" className="inline-flex items-center rounded-lg bg-deep-forest/70 border border-luxury-gold/25 p-0.5 shadow-inner">
{[
{ id: 'instructions', label: 'Instructions' },
{ id: 'playground', label: 'Playground' },
@@ -23,10 +23,10 @@ const DisplayPanel: React.FC<DisplayPanelProps> = ({ children }) => {
role="tab"
aria-selected={active}
onClick={() => setActiveTab(t.id as 'instructions' | 'playground')}
className={`px-4 py-1.5 md:px-5 md:py-2 rounded-lg text-sm font-montserrat transition-all duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-luxury-gold/40 ${
className={`px-3 py-1 md:px-4 md:py-1.5 rounded-md text-xs md:text-sm font-montserrat transition-all duration-150 focus:outline-none focus-visible:ring-2 focus-visible:ring-luxury-gold/30 ${
active
? 'bg-gradient-to-r from-luxury-gold/25 to-brass/25 text-cream border border-luxury-gold/40 shadow-[0_2px_14px_rgba(197,157,89,0.25)]'
: 'text-cream/80 hover:text-cream hover:bg-luxury-gold/10 border border-transparent'
? 'bg-gradient-to-r from-luxury-gold/25 to-brass/25 text-cream border border-luxury-gold/30'
: 'text-cream/75 hover:text-cream hover:bg-luxury-gold/10 border border-transparent'
}`}
>
{t.label}
@@ -36,12 +36,9 @@ const DisplayPanel: React.FC<DisplayPanelProps> = ({ children }) => {
</div>
</div>
{/* Panel body */}
<div className="relative rounded-xl bg-gradient-to-br from-deep-forest/60 to-rich-brown/40 border border-luxury-gold/25 overflow-hidden flex-1">
{/* soft decorative gradient */}
<div className="pointer-events-none absolute inset-0 opacity-30 bg-gradient-to-tr from-luxury-gold/10 via-transparent to-bronze/10" />
<div className="absolute inset-0 p-4 md:p-5 overflow-auto custom-thin-scroll">
{/* Panel body condensed */}
<div className="relative flex-1">
<div className="absolute inset-0 p-3 md:p-4 overflow-auto custom-thin-scroll">
{activeTab === 'instructions' ? (
<div className="text-cream/85 font-montserrat text-sm leading-relaxed space-y-3">
<p className="text-cream/90">Welcome. This panel guides you on how to drive agents and view results.</p>
@@ -51,12 +48,12 @@ const DisplayPanel: React.FC<DisplayPanelProps> = ({ children }) => {
<li>Watch this panel for summaries, charts, or generated assets.</li>
<li>Refine with follow-ups. Use concise prompts for best results.</li>
</ol>
<div className="mt-3 grid grid-cols-1 sm:grid-cols-2 gap-3">
<div className="rounded-lg border border-luxury-gold/30 bg-deep-forest/60 p-3">
<div className="mt-3 grid grid-cols-1 sm:grid-cols-2 gap-2.5">
<div className="rounded-md border border-luxury-gold/25 bg-deep-forest/60 p-2.5">
<div className="text-cream/70 text-xs uppercase tracking-wide mb-1">Tip</div>
<div className="text-cream/90">Start prompts with a verb: “Summarize…”, “Compare…”, “Draft…”.</div>
</div>
<div className="rounded-lg border border-luxury-gold/30 bg-deep-forest/60 p-3">
<div className="rounded-md border border-luxury-gold/25 bg-deep-forest/60 p-2.5">
<div className="text-cream/70 text-xs uppercase tracking-wide mb-1">Format</div>
<div className="text-cream/90">Ask for tables, bullet points, or JSON if you need structured output.</div>
</div>
@@ -67,8 +64,8 @@ const DisplayPanel: React.FC<DisplayPanelProps> = ({ children }) => {
{children || (
<div className="space-y-3">
<div className="text-cream/80">This is your playground canvas. Render previews, charts, or content here.</div>
<div className="rounded-xl border border-luxury-gold/30 bg-deep-forest/60 p-4">
<div className="text-cream/70 text-xs uppercase tracking-wide mb-2">Empty Canvas</div>
<div className="rounded-lg border border-luxury-gold/30 bg-deep-forest/60 p-3">
<div className="text-cream/70 text-xs uppercase tracking-wide mb-1">Empty Canvas</div>
<div className="text-cream/90">Hook this area to your agent outputs to visualize results.</div>
</div>
</div>

View File

@@ -14,7 +14,7 @@ interface SidebarNavProps {
const SidebarNav: React.FC<SidebarNavProps> = ({ items, activeId, onSelect }) => {
return (
<aside className="w-20 md:w-64 border-r border-luxury-gold/25 bg-gradient-to-b from-deep-forest/90 via-deep-forest/80 to-rich-brown/70 backdrop-blur-xl sticky top-16 md:top-20 h-[calc(100vh-4rem)] md:h-[calc(100vh-5rem)] overflow-hidden">
<aside className="w-16 md:w-56 border-r border-luxury-gold/20 bg-gradient-to-b from-deep-forest/90 via-deep-forest/80 to-rich-brown/70 backdrop-blur-xl sticky top-16 md:top-20 h-[calc(100vh-4rem)] md:h-[calc(100vh-5rem)] overflow-hidden">
{/* Decorative background elements */}
<div className="absolute inset-0 pointer-events-none">
<div className="absolute -top-20 -left-10 w-40 h-40 bg-luxury-gold/5 rounded-full blur-3xl"></div>
@@ -25,11 +25,11 @@ const SidebarNav: React.FC<SidebarNavProps> = ({ items, activeId, onSelect }) =>
{/* Premium border accent */}
<div className="absolute top-0 left-0 w-full h-0.5 bg-gradient-to-r from-transparent via-luxury-gold/40 to-transparent"></div>
<nav className="relative p-3 md:p-6 space-y-2">
<nav className="relative p-2 md:p-4 space-y-1.5">
{/* Navigation title */}
<div className="hidden md:block mb-6">
<h3 className="text-luxury-gold/80 text-xs font-montserrat font-semibold uppercase tracking-wider mb-2">Navigation</h3>
<div className="w-8 h-0.5 bg-gradient-to-r from-luxury-gold to-transparent rounded-full"></div>
<div className="hidden md:block mb-3">
<h3 className="text-luxury-gold/70 text-[10px] font-montserrat font-medium uppercase tracking-wider mb-1">Navigation</h3>
<div className="w-6 h-0.5 bg-gradient-to-r from-luxury-gold to-transparent rounded-full"></div>
</div>
{items.map((it, index) => {
@@ -38,20 +38,20 @@ const SidebarNav: React.FC<SidebarNavProps> = ({ items, activeId, onSelect }) =>
<button
key={it.id}
onClick={() => onSelect(it.id)}
className={`group relative w-full flex items-center gap-3 md:gap-4 px-4 py-4 rounded-2xl transition-all duration-300 transform hover:scale-[1.02] ${
className={`group relative w-full flex items-center justify-start gap-2 md:gap-2.5 px-3 py-2.5 rounded-lg transition-colors duration-300 ${
isActive
? 'bg-gradient-to-r from-luxury-gold/25 to-brass/20 border border-luxury-gold/50 shadow-[0_8px_32px_rgba(197,157,89,0.15)] text-cream'
: 'border border-transparent hover:border-luxury-gold/30 hover:bg-gradient-to-r hover:from-luxury-gold/10 hover:to-transparent text-cream/80 hover:text-cream'
? 'bg-gradient-to-r from-luxury-gold/20 to-brass/20 border border-luxury-gold/50 text-cream shadow-[0_2px_8px_rgba(197,157,89,0.15)]'
: 'border border-transparent hover:border-luxury-gold/35 hover:bg-gradient-to-r hover:from-luxury-gold/10 hover:to-transparent text-cream/80 hover:text-cream'
}`}
style={{
animationDelay: `${index * 100}ms`
}}
>
{/* Icon container with enhanced styling */}
<div className={`relative w-8 h-8 rounded-xl flex items-center justify-center transition-all duration-300 ${
<div className={`relative w-8 h-8 rounded-md flex items-center justify-center transition-all duration-300 ${
isActive
? 'bg-gradient-to-br from-luxury-gold/30 to-brass/30 shadow-lg'
: 'bg-deep-forest/50 group-hover:bg-luxury-gold/20'
? 'bg-gradient-to-br from-luxury-gold/40 to-brass/40 ring-1 ring-luxury-gold/40'
: 'bg-deep-forest/60 group-hover:bg-luxury-gold/20'
}`}>
<span className={`text-lg transition-all duration-300 ${
isActive ? 'scale-110' : 'group-hover:scale-105'
@@ -65,37 +65,31 @@ const SidebarNav: React.FC<SidebarNavProps> = ({ items, activeId, onSelect }) =>
</div>
{/* Label with enhanced typography */}
<div className="hidden md:block flex-1">
<span className={`text-sm font-montserrat font-medium transition-all duration-300 ${
isActive ? 'font-semibold' : 'group-hover:font-medium'
<div className="hidden md:flex flex-1 min-w-0 items-center">
<span className={`truncate text-sm md:text-base leading-snug font-montserrat font-medium text-left tracking-wide transition-colors duration-300 ${
isActive ? 'text-cream' : 'text-cream/85 group-hover:text-cream'
}`}>
{it.label}
</span>
{/* Subtle underline for active item */}
{isActive && (
<div className="w-full h-0.5 bg-gradient-to-r from-luxury-gold/60 to-transparent rounded-full mt-1"></div>
<div className="absolute -bottom-1 left-0 w-5 h-0.5 bg-gradient-to-r from-luxury-gold/70 to-transparent rounded-full"></div>
)}
</div>
{/* Hover effect overlay */}
<div className="absolute inset-0 rounded-2xl bg-gradient-to-r from-luxury-gold/5 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none"></div>
<div className="absolute inset-0 rounded-lg bg-gradient-to-r from-luxury-gold/5 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none"></div>
{/* Active selection indicator */}
{isActive && (
<div className="absolute left-0 top-1/2 -translate-y-1/2 w-1 h-8 bg-gradient-to-b from-luxury-gold to-brass rounded-r-full shadow-[0_0_8px_rgba(197,157,89,0.4)]"></div>
<div className="absolute left-0 top-1/2 -translate-y-1/2 w-0.5 h-6 bg-gradient-to-b from-luxury-gold to-brass rounded-r-full shadow-[0_0_6px_rgba(197,157,89,0.45)]"></div>
)}
</button>
);
})}
{/* Bottom decorative element */}
<div className="hidden md:block mt-8 pt-6">
<div className="flex items-center justify-center">
<div className="w-12 h-12 rounded-2xl bg-gradient-to-br from-luxury-gold/10 to-bronze/5 border border-luxury-gold/20 flex items-center justify-center">
<div className="w-6 h-6 rounded-lg bg-luxury-gold/20 animate-pulse"></div>
</div>
</div>
</div>
{/* Removed large decorative bottom element for compactness */}
</nav>
{/* Bottom gradient fade */}

View File

@@ -196,15 +196,15 @@ const TopBar: React.FC<TopBarProps> = ({
<div className="relative" ref={profileRef}>
<button
onClick={() => setShowProfile((s) => !s)}
className="flex items-center gap-2 bg-deep-forest/60 border border-luxury-gold/30 pl-2 pr-3 py-2 rounded-full hover:bg-luxury-gold/10 transition-all duration-300 hover:border-luxury-gold/50 hover:shadow-[0_4px_12px_rgba(197,157,89,0.15)]"
className="flex items-center bg-deep-forest/60 border border-luxury-gold/30 p-1.5 rounded-full hover:bg-luxury-gold/10 transition-all duration-300 hover:border-luxury-gold/50 hover:shadow-[0_4px_12px_rgba(197,157,89,0.15)]"
aria-label="Profile menu"
>
{/* Enhanced Profile Picture Circle */}
<div className="relative">
<div className="w-9 h-9 rounded-full overflow-hidden border-2 border-luxury-gold/40 shadow-[0_0_12px_rgba(197,157,89,0.3)] hover:border-luxury-gold transition-all duration-300">
<div className="w-10 h-10 rounded-full overflow-hidden border-2 border-luxury-gold/40 shadow-[0_0_10px_rgba(197,157,89,0.25)] hover:border-luxury-gold transition-all duration-300">
{profileImage ? (
<img
src={profileImage}
alt="Profile"
<img
src={profileImage}
alt="Profile"
className="w-full h-full object-cover"
/>
) : (
@@ -213,13 +213,8 @@ const TopBar: React.FC<TopBarProps> = ({
</div>
)}
</div>
{/* Online status indicator */}
<div className="absolute -bottom-0.5 -right-0.5 w-3 h-3 bg-green-400 rounded-full border-2 border-deep-forest shadow-sm"></div>
</div>
<svg width="14" height="14" viewBox="0 0 24 24" className="text-cream/80 fill-current transition-transform duration-200 group-hover:rotate-180">
<path d="M7 10l5 5 5-5z"/>
</svg>
</button>
{showProfile && (

View File

@@ -221,6 +221,17 @@ h1, h2, h3, h4, h5, h6 {
}
}
/* Performance utility: defer rendering off-screen until scrolled into view */
.cv-auto {
content-visibility: auto;
contain-intrinsic-size: 800px 1200px; /* Reserve space to avoid layout shift */
}
/* Hint browser that frequent animation is intentional */
.will-change-transform {
will-change: transform;
}
/* Animation delay utilities */
.animation-delay-1000 {
animation-delay: 1s;

View File

@@ -0,0 +1,57 @@
import React from 'react';
import { Link } from 'react-router-dom';
const BuyCreditsPage: React.FC = () => {
return (
<div className="min-h-screen flex flex-col items-start md:items-center p-4 md:p-8">
<div className="w-full max-w-4xl bg-deep-forest/60 border border-luxury-gold/20 rounded-lg p-6 md:p-8 shadow-lg">
<header className="flex items-center justify-between mb-6">
<div>
<h1 className="text-2xl font-playfair text-cream">Buy Credits</h1>
<p className="text-sm text-cream/60 mt-1">Purchase credits to power your agents and tasks.</p>
</div>
<div className="flex items-center gap-3">
<span className="text-sm text-cream/80">Balance</span>
<div className="px-3 py-1 bg-luxury-gold/10 text-luxury-gold rounded-md">128</div>
</div>
</header>
<section className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="col-span-2 space-y-4">
<div className="bg-deep-forest/40 p-4 rounded-md border border-luxury-gold/10">
<h3 className="text-lg text-cream font-semibold">Quick Packs</h3>
<div className="mt-3 grid grid-cols-1 sm:grid-cols-3 gap-3">
<button className="w-full py-3 rounded-md bg-gradient-to-br from-luxury-gold to-brass text-deep-forest font-semibold">100 credits — $4</button>
<button className="w-full py-3 rounded-md bg-gradient-to-br from-bronze to-luxury-gold text-deep-forest font-semibold">500 credits — $18</button>
<button className="w-full py-3 rounded-md bg-gradient-to-br from-luxury-gold/80 to-bronze/80 text-deep-forest font-semibold">2000 credits — $55</button>
</div>
</div>
<div className="bg-deep-forest/40 p-4 rounded-md border border-luxury-gold/10">
<h3 className="text-lg text-cream font-semibold">Custom amount</h3>
<div className="mt-3 flex gap-2">
<input type="number" min={1} defaultValue={100} className="w-40 px-3 py-2 rounded-md bg-deep-forest/20 border border-luxury-gold/10 text-cream" />
<button className="px-4 py-2 rounded-md bg-luxury-gold text-deep-forest font-semibold">Buy</button>
</div>
<p className="text-sm text-cream/60 mt-2">This is a demo stub — replace with your payment integration.</p>
</div>
</div>
<aside className="col-span-1 bg-deep-forest/30 p-4 rounded-md border border-luxury-gold/10">
<h4 className="text-sm text-cream/80 mb-2">Why credits?</h4>
<ul className="text-cream/70 text-sm list-disc ml-5 space-y-1">
<li>Faster processing for heavy tasks</li>
<li>Priority agents & dedicated compute</li>
<li>Volume discounts on large purchases</li>
</ul>
<div className="mt-4">
<Link to="/home" className="text-sm text-luxury-gold/90 underline">Back to dashboard</Link>
</div>
</aside>
</section>
</div>
</div>
);
};
export default BuyCreditsPage;

View File

@@ -1,4 +1,5 @@
import React, { useMemo, useState, useEffect } from 'react';
import React, { useMemo, useState, useEffect, useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import TopBar from '../components/TopBar';
import SidebarNav from '../components/SidebarNav';
import AgentPanel from '../components/AgentPanel';
@@ -44,6 +45,8 @@ const LandingPage: React.FC = () => {
// Credits Modal
const [showCredits, setShowCredits] = useState(false);
const navigate = useNavigate();
// Listen for credits modal trigger from TopBar
useEffect(() => {
const handleOpenCredits = () => {
@@ -51,6 +54,8 @@ const LandingPage: React.FC = () => {
};
window.addEventListener('openCredits', handleOpenCredits);
const handleOpenBuyCredits = () => navigate('/buy-credits');
window.addEventListener('openBuyCredits', handleOpenBuyCredits as EventListener);
return () => window.removeEventListener('openCredits', handleOpenCredits);
}, []);
@@ -60,6 +65,29 @@ const LandingPage: React.FC = () => {
setMessages((prev) => [...prev, userMsg, assistantMsg]);
};
// Sidebar collapse / hover logic ----------------------------------------
const [sidebarCollapsed, setSidebarCollapsed] = useState<boolean>(() => {
try {
const stored = localStorage.getItem('pz.sidebarCollapsed');
return stored === 'true';
} catch { return false; }
});
const [sidebarHoverOpen, setSidebarHoverOpen] = useState(false);
const setCollapsed = useCallback((val: boolean) => {
setSidebarCollapsed(val);
try { localStorage.setItem('pz.sidebarCollapsed', String(val)); } catch {}
}, []);
const sidebarOpen = !sidebarCollapsed || sidebarHoverOpen; // actual visual open state
// Compute dynamic left offsets used by ChatDock & content padding
const sideOffsets = {
mobile: sidebarOpen ? 'pl-16' : 'pl-0', // matches compact sidebar w-16
desktop: sidebarOpen ? 'md:pl-56' : 'md:pl-0' // matches md sidebar w-56
};
const chatLeftOffset = sidebarOpen ? 'left-16 md:left-56' : 'left-0';
return (
<div className="min-h-screen flex flex-col overflow-hidden">
{/* Top bar */}
@@ -71,21 +99,61 @@ const LandingPage: React.FC = () => {
notifications={[{ id: 'n1', title: 'Welcome to Pennyzo!', time: 'Just now' }]}
/>
{/* Main area with sidebar + two panels */}
<div className="flex-1 min-h-0 grid grid-cols-[auto,1fr] gap-0 overflow-hidden">
<SidebarNav items={sidebarItems} activeId={activeNav} onSelect={setActiveNav} />
<div className="p-3 md:p-6 grid grid-cols-1 lg:grid-cols-2 gap-4 md:gap-6 min-h-0 overflow-hidden h-[calc(100vh-13rem)] md:h-[calc(100vh-15rem)]">
{/* Hover activation strip (only needed when collapsed) */}
{sidebarCollapsed && (
<div
className="fixed top-16 md:top-20 left-0 h-[calc(100vh-4rem)] md:h-[calc(100vh-5rem)] w-2 z-30"
onMouseEnter={() => setSidebarHoverOpen(true)}
onClick={() => setSidebarHoverOpen(true)}
>
{/* subtle gradient indicator */}
<div className="w-full h-full bg-gradient-to-r from-luxury-gold/10 to-transparent" />
</div>
)}
{/* Sliding Sidebar Wrapper */}
<div
onMouseLeave={() => { if (sidebarCollapsed) setSidebarHoverOpen(false); }}
className={`fixed top-16 md:top-20 left-0 h-[calc(100vh-4rem)] md:h-[calc(100vh-5rem)] z-40 transition-transform duration-300 ease-in-out ${sidebarOpen ? 'translate-x-0' : '-translate-x-full'} w-16 md:w-56`}
>
<div className="relative h-full">
<SidebarNav items={sidebarItems} activeId={activeNav} onSelect={setActiveNav} />
{/* Collapse button */}
<button
aria-label={sidebarCollapsed ? 'Pin sidebar open' : 'Collapse sidebar'}
onClick={() => {
if (sidebarCollapsed) {
// Pin open
setCollapsed(false);
setSidebarHoverOpen(false);
} else {
// Collapse immediately (no temporary open)
setCollapsed(true);
setSidebarHoverOpen(false);
}
}}
className={`absolute top-2 -right-3 w-8 h-8 rounded-full flex items-center justify-center border border-luxury-gold/40 text-luxury-gold bg-deep-forest/80 backdrop-blur-xl shadow-[0_4px_12px_rgba(0,0,0,0.4)] hover:border-luxury-gold/70 transition-all ${sidebarOpen ? 'opacity-100 pointer-events-auto' : 'opacity-0 pointer-events-none'}`}
>
{/* Icon changes: when open show « (collapse), when collapsed (visible via hover) show » (pin) */}
<span className="text-sm font-semibold leading-none">{sidebarCollapsed ? '»' : '«'}</span>
</button>
</div>
</div>
{/* Main content (padded when sidebar open) */}
<div className={`flex-1 min-h-0 w-full transition-[padding] duration-300 ease-in-out ${sideOffsets.mobile} ${sideOffsets.desktop} pt-2 md:pt-3`}>
<div className="p-0 grid grid-cols-1 lg:grid-cols-2 gap-2 md:gap-3 min-h-0 overflow-hidden h-[calc(100vh-13rem)] md:h-[calc(100vh-15rem)]">
<AgentPanel agents={agents} activeAgentId={activeAgent} onChangeAgent={setActiveAgent} />
<DisplayPanel />
</div>
</div>
{/* Chat dock at bottom (aligned to right of sidebar) */}
{/* Chat dock at bottom */}
<ChatDock
messages={messages}
onSend={handleSend}
leftOffsetClass="left-20 md:left-64"
className="w-[calc(100%-5rem)] md:w-[calc(100%-16rem)]"
leftOffsetClass={chatLeftOffset}
className={sidebarOpen ? 'w-[calc(100%-4rem)] md:w-[calc(100%-14rem)] transition-all duration-300 ease-in-out' : 'w-full transition-all duration-300 ease-in-out'}
placeholder="Ask anything..."
/>
@@ -93,6 +161,7 @@ const LandingPage: React.FC = () => {
<CreditsScreen
isOpen={showCredits}
onClose={() => setShowCredits(false)}
sidebarOpen={sidebarOpen}
/>
</div>
);

View File

@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useMemo, useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
const LoginPage: React.FC = () => {
@@ -26,6 +26,18 @@ const LoginPage: React.FC = () => {
}, 2000);
};
// Pre-generate particle positions once to avoid layout thrash on state updates
const particles = useMemo(
() => Array.from({ length: 12 }, (_, i) => ({
id: i,
left: Math.random() * 100,
top: Math.random() * 100,
delay: Math.random() * 3,
duration: 2 + Math.random() * 3,
})),
[]
);
return (
<div className="min-h-screen flex items-center justify-center px-4 relative overflow-hidden">
{/* Retro Grid Background */}
@@ -39,19 +51,19 @@ const LoginPage: React.FC = () => {
}}></div>
</div>
{/* Floating Particles */}
<div className="fixed inset-0 pointer-events-none">
{[...Array(15)].map((_, i) => (
{/* Floating Particles (memoized positions & reduced count) */}
<div className="fixed inset-0 pointer-events-none will-change-transform">
{particles.map(p => (
<div
key={i}
key={p.id}
className="absolute w-1 h-1 bg-luxury-gold rounded-full animate-pulse"
style={{
left: `${Math.random() * 100}%`,
top: `${Math.random() * 100}%`,
animationDelay: `${Math.random() * 3}s`,
animationDuration: `${2 + Math.random() * 3}s`
left: `${p.left}%`,
top: `${p.top}%`,
animationDelay: `${p.delay}s`,
animationDuration: `${p.duration}s`
}}
></div>
/>
))}
</div>
@@ -82,10 +94,12 @@ const LoginPage: React.FC = () => {
<div className="text-center mb-8">
<div className="flex items-center justify-center mb-4">
<div className="h-16 md:h-20 flex items-center">
<img
src="/src/assets/Dark-LineLogo-cropped.svg"
alt="Pennyzo Logo"
<img
src="/src/assets/Dark-LineLogo-cropped.svg"
alt="Pennyzo Logo"
className="h-full w-auto object-contain"
loading="lazy"
decoding="async"
/>
</div>
</div>