UI: refine panels, sidebar, chat dock, add BuyCredits page and style tweaks
This commit is contained in:
24
src/App.tsx
24
src/App.tsx
@@ -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">
|
||||
<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>
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
@@ -98,14 +93,14 @@ const AgentPanel: React.FC<AgentPanelProps> = ({ agents, activeAgentId, onChange
|
||||
|
||||
{/* Tooltip positioned outside rotating container */}
|
||||
<div
|
||||
className="absolute opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none z-50"
|
||||
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>
|
||||
);
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -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-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"
|
||||
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>
|
||||
<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>
|
||||
<h1 className="font-playfair text-[22px] leading-none font-bold text-cream pt-1">Credits & Usage History</h1>
|
||||
<div className="ml-auto">
|
||||
<button
|
||||
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="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>
|
||||
</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 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-lg font-bold text-cream">Current Balance</h3>
|
||||
<p className="text-cream/60 text-xs">Available credits</p>
|
||||
<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>
|
||||
<p className="text-2xl font-bold text-green-400">{currentBalance}</p>
|
||||
<div className="text-green-400 font-bold text-xl leading-none">{currentBalance}</div>
|
||||
</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 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-lg font-bold text-cream">Monthly Usage</h3>
|
||||
<p className="text-cream/60 text-xs">Credits spent this month</p>
|
||||
<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>
|
||||
<p className="text-2xl font-bold text-red-400">{monthlyUsage}</p>
|
||||
<div className="text-red-400 font-bold text-xl leading-none">{monthlyUsage}</div>
|
||||
</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 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-lg font-bold text-cream">Monthly Purchases</h3>
|
||||
<p className="text-cream/60 text-xs">Credits purchased this month</p>
|
||||
<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>
|
||||
<p className="text-2xl font-bold text-luxury-gold">{monthlyPurchases}</p>
|
||||
<div className="text-luxury-gold font-bold text-xl leading-none">{monthlyPurchases}</div>
|
||||
</div>
|
||||
</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>
|
||||
<h3 className="font-playfair text-lg font-bold text-cream leading-none">Transaction History</h3>
|
||||
</div>
|
||||
<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-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"
|
||||
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>
|
||||
<label className="block text-cream/80 text-sm mb-1">Filter by Month</label>
|
||||
<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-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"
|
||||
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">January</option>
|
||||
<option value="02">February</option>
|
||||
<option value="03">March</option>
|
||||
<option value="04">April</option>
|
||||
<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">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>
|
||||
<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>
|
||||
<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
|
||||
<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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -196,11 +196,11 @@ 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}
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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;
|
||||
|
||||
57
src/pages/BuyCreditsPage.tsx
Normal file
57
src/pages/BuyCreditsPage.tsx
Normal 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;
|
||||
@@ -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">
|
||||
{/* 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} />
|
||||
<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)]">
|
||||
{/* 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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -86,6 +98,8 @@ const LoginPage: React.FC = () => {
|
||||
src="/src/assets/Dark-LineLogo-cropped.svg"
|
||||
alt="Pennyzo Logo"
|
||||
className="h-full w-auto object-contain"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user