Some checks failed
Deploy to Production / deploy (push) Failing after 0s
- Complete dark-theme redesign inspired by ai-impress.com (navy + cyan + violet palette) - New Syne display font + gradient logo mark + SVG favicon - New Navigation: glass-morphism, gradient logo, Get Started CTA - New Hero: animated glow orbs, mock focus-group chat UI, stats row - New landing: Features grid, How-It-Works steps, CTA banner - New Footer: AImpress LTD branding, © AImpress LTD. All rights reserved. - New Login page: dark card, password visibility toggle, link to Register - New Register page: full form, benefits row, 50 free credits pitch - New VerifyEmail page: token verification flow with auto-redirect - Backend: email_service.py using Resend API for verification emails - Backend: /api/auth/register, /verify-email, /resend-verification endpoints - User model: email_verified, email_verify_token, email_verify_expires fields - Gitea Actions CI/CD: auto-deploy to aimpress server on push to main Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
275 lines
11 KiB
TypeScript
Executable file
275 lines
11 KiB
TypeScript
Executable file
import { useState, useEffect } from 'react';
|
|
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
|
import { Menu, X, LayoutDashboard, Users, MessageSquare, Home, LogIn, LogOut, ShieldCheck, CreditCard, Zap } from 'lucide-react';
|
|
import { cn } from '@/lib/utils';
|
|
import { useAuth } from '@/contexts/AuthContext';
|
|
import { billingApi } from '@/lib/api';
|
|
|
|
const LogoMark = () => (
|
|
<svg viewBox="0 0 36 36" fill="none" className="h-8 w-8 flex-shrink-0">
|
|
<defs>
|
|
<linearGradient id="nav-lg" x1="2" y1="2" x2="34" y2="34" gradientUnits="userSpaceOnUse">
|
|
<stop stopColor="#06B6D4" />
|
|
<stop offset="1" stopColor="#8B5CF6" />
|
|
</linearGradient>
|
|
</defs>
|
|
<path
|
|
d="M28 8C24.8 5.6 20.9 4 16.6 4C8.6 4 2 10.6 2 18.5C2 26.4 8.6 33 16.6 33C20.9 33 24.8 31.4 28 29"
|
|
stroke="url(#nav-lg)" strokeWidth="3.5" strokeLinecap="round" fill="none"
|
|
/>
|
|
<circle cx="28" cy="8" r="2.5" fill="#06B6D4" />
|
|
<circle cx="28" cy="29" r="2.5" fill="#8B5CF6" />
|
|
</svg>
|
|
);
|
|
|
|
export default function Navigation() {
|
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
|
const [creditsBalance, setCreditsBalance] = useState<number | null>(null);
|
|
const [scrolled, setScrolled] = useState(false);
|
|
const location = useLocation();
|
|
const navigate = useNavigate();
|
|
const { isAuthenticated, logout, user } = useAuth();
|
|
|
|
useEffect(() => {
|
|
const onScroll = () => setScrolled(window.scrollY > 12);
|
|
window.addEventListener('scroll', onScroll, { passive: true });
|
|
return () => window.removeEventListener('scroll', onScroll);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (!isAuthenticated) { setCreditsBalance(null); return; }
|
|
billingApi.getBalance().then(r => setCreditsBalance(r.data.credits_balance)).catch(() => {});
|
|
}, [isAuthenticated, location.pathname]);
|
|
|
|
const navigationItems = [
|
|
{ name: 'Home', href: '/', icon: Home },
|
|
{ name: 'Personas', href: '/synthetic-users', icon: Users },
|
|
{ name: 'Focus Groups', href: '/focus-groups', icon: MessageSquare },
|
|
{ name: 'Dashboard', href: '/dashboard', icon: LayoutDashboard },
|
|
{ name: 'Billing', href: '/billing', icon: CreditCard },
|
|
];
|
|
|
|
const isActive = (path: string) => location.pathname === path;
|
|
|
|
const handleAuthNavigation = (path: string) => {
|
|
if (path === '/synthetic-users') {
|
|
window.dispatchEvent(new CustomEvent('syntheticUsersNavigation'));
|
|
}
|
|
navigate(path);
|
|
};
|
|
|
|
return (
|
|
<header
|
|
className={cn(
|
|
'fixed top-0 left-0 right-0 z-50 transition-all duration-300',
|
|
scrolled
|
|
? 'bg-[hsl(222_47%_4%/0.95)] backdrop-blur-xl border-b border-[hsl(222_38%_16%)]'
|
|
: 'bg-transparent'
|
|
)}
|
|
>
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
<div className="flex h-16 items-center justify-between">
|
|
{/* Logo */}
|
|
<Link to="/" className="flex items-center gap-2.5 group">
|
|
<LogoMark />
|
|
<span
|
|
className="font-display font-bold text-xl tracking-tight"
|
|
style={{
|
|
background: 'linear-gradient(135deg, #06B6D4 0%, #8B5CF6 100%)',
|
|
WebkitBackgroundClip: 'text',
|
|
WebkitTextFillColor: 'transparent',
|
|
backgroundClip: 'text',
|
|
}}
|
|
>
|
|
Cohorta
|
|
</span>
|
|
</Link>
|
|
|
|
{/* Desktop Nav */}
|
|
<nav className="hidden md:flex items-center gap-1">
|
|
{navigationItems.map((item) => (
|
|
<div key={item.name}>
|
|
{item.href === '/' ? (
|
|
<Link
|
|
to={item.href}
|
|
className={cn(
|
|
'flex items-center gap-1.5 px-3 py-2 rounded-lg text-sm font-medium transition-all duration-200',
|
|
isActive(item.href)
|
|
? 'text-[#06B6D4] bg-[hsl(188_91%_44%/0.1)]'
|
|
: 'text-[hsl(215_20%_60%)] hover:text-[hsl(210_40%_90%)] hover:bg-[hsl(222_38%_11%)]'
|
|
)}
|
|
>
|
|
<item.icon className="h-3.5 w-3.5" />
|
|
{item.name}
|
|
</Link>
|
|
) : (
|
|
<button
|
|
onClick={() => handleAuthNavigation(item.href)}
|
|
className={cn(
|
|
'flex items-center gap-1.5 px-3 py-2 rounded-lg text-sm font-medium transition-all duration-200',
|
|
isActive(item.href)
|
|
? 'text-[#06B6D4] bg-[hsl(188_91%_44%/0.1)]'
|
|
: 'text-[hsl(215_20%_60%)] hover:text-[hsl(210_40%_90%)] hover:bg-[hsl(222_38%_11%)]'
|
|
)}
|
|
>
|
|
<item.icon className="h-3.5 w-3.5" />
|
|
{item.name}
|
|
</button>
|
|
)}
|
|
</div>
|
|
))}
|
|
|
|
{user?.role === 'admin' && (
|
|
<button
|
|
onClick={() => handleAuthNavigation('/admin')}
|
|
className={cn(
|
|
'flex items-center gap-1.5 px-3 py-2 rounded-lg text-sm font-medium transition-all duration-200',
|
|
isActive('/admin')
|
|
? 'text-[#06B6D4] bg-[hsl(188_91%_44%/0.1)]'
|
|
: 'text-[hsl(215_20%_60%)] hover:text-[hsl(210_40%_90%)] hover:bg-[hsl(222_38%_11%)]'
|
|
)}
|
|
>
|
|
<ShieldCheck className="h-3.5 w-3.5" />
|
|
Admin
|
|
</button>
|
|
)}
|
|
</nav>
|
|
|
|
{/* Right side */}
|
|
<div className="hidden md:flex items-center gap-3">
|
|
{isAuthenticated && creditsBalance !== null && (
|
|
<button
|
|
onClick={() => handleAuthNavigation('/billing')}
|
|
className="flex items-center gap-1.5 px-3 py-1.5 rounded-full text-sm font-medium bg-amber-500/10 text-amber-400 border border-amber-500/20 hover:bg-amber-500/15 transition-colors"
|
|
>
|
|
<Zap className="h-3.5 w-3.5" />
|
|
{creditsBalance} cr
|
|
</button>
|
|
)}
|
|
|
|
{isAuthenticated ? (
|
|
<button
|
|
onClick={() => { logout(); navigate('/login'); }}
|
|
className="flex items-center gap-1.5 px-4 py-2 rounded-lg text-sm font-medium text-[hsl(215_20%_60%)] hover:text-[hsl(210_40%_90%)] hover:bg-[hsl(222_38%_11%)] transition-all duration-200"
|
|
>
|
|
<LogOut className="h-4 w-4" />
|
|
Logout
|
|
</button>
|
|
) : (
|
|
<div className="flex items-center gap-2">
|
|
<Link
|
|
to="/login"
|
|
className="px-4 py-2 rounded-lg text-sm font-medium text-[hsl(215_20%_65%)] hover:text-white transition-colors"
|
|
>
|
|
Sign in
|
|
</Link>
|
|
<Link
|
|
to="/register"
|
|
className="px-4 py-2 rounded-lg text-sm font-semibold text-white btn-gradient"
|
|
style={{
|
|
background: 'linear-gradient(135deg, #06B6D4 0%, #7C3AED 100%)',
|
|
boxShadow: '0 0 20px hsl(188 91% 44% / 0.2)',
|
|
}}
|
|
>
|
|
Get Started
|
|
</Link>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Mobile toggle */}
|
|
<button
|
|
className="md:hidden p-2 rounded-lg text-[hsl(215_20%_60%)] hover:text-white hover:bg-[hsl(222_38%_11%)] transition-colors"
|
|
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
|
>
|
|
{mobileMenuOpen ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Mobile menu */}
|
|
{mobileMenuOpen && (
|
|
<div className="md:hidden bg-[hsl(222_47%_5%)] border-t border-[hsl(222_38%_16%)] animate-slide-down">
|
|
<div className="px-4 py-3 space-y-1">
|
|
{navigationItems.map((item) => (
|
|
<div key={item.name}>
|
|
{item.href === '/' ? (
|
|
<Link
|
|
to={item.href}
|
|
className={cn(
|
|
'flex items-center gap-3 px-4 py-2.5 rounded-lg text-sm font-medium transition-all duration-200',
|
|
isActive(item.href)
|
|
? 'text-[#06B6D4] bg-[hsl(188_91%_44%/0.1)]'
|
|
: 'text-[hsl(215_20%_60%)] hover:text-white hover:bg-[hsl(222_38%_11%)]'
|
|
)}
|
|
onClick={() => setMobileMenuOpen(false)}
|
|
>
|
|
<item.icon className="h-4 w-4" />
|
|
{item.name}
|
|
</Link>
|
|
) : (
|
|
<button
|
|
className={cn(
|
|
'w-full flex items-center gap-3 px-4 py-2.5 rounded-lg text-sm font-medium transition-all duration-200 text-left',
|
|
isActive(item.href)
|
|
? 'text-[#06B6D4] bg-[hsl(188_91%_44%/0.1)]'
|
|
: 'text-[hsl(215_20%_60%)] hover:text-white hover:bg-[hsl(222_38%_11%)]'
|
|
)}
|
|
onClick={() => { setMobileMenuOpen(false); handleAuthNavigation(item.href); }}
|
|
>
|
|
<item.icon className="h-4 w-4" />
|
|
{item.name}
|
|
</button>
|
|
)}
|
|
</div>
|
|
))}
|
|
|
|
{user?.role === 'admin' && (
|
|
<button
|
|
className={cn(
|
|
'w-full flex items-center gap-3 px-4 py-2.5 rounded-lg text-sm font-medium transition-all text-left',
|
|
isActive('/admin') ? 'text-[#06B6D4] bg-[hsl(188_91%_44%/0.1)]' : 'text-[hsl(215_20%_60%)] hover:text-white hover:bg-[hsl(222_38%_11%)]'
|
|
)}
|
|
onClick={() => { setMobileMenuOpen(false); handleAuthNavigation('/admin'); }}
|
|
>
|
|
<ShieldCheck className="h-4 w-4" />
|
|
Admin
|
|
</button>
|
|
)}
|
|
|
|
<div className="pt-2 border-t border-[hsl(222_38%_16%)]">
|
|
{isAuthenticated ? (
|
|
<button
|
|
onClick={() => { logout(); setMobileMenuOpen(false); navigate('/login'); }}
|
|
className="w-full flex items-center gap-3 px-4 py-2.5 rounded-lg text-sm font-medium text-[hsl(215_20%_60%)] hover:text-white hover:bg-[hsl(222_38%_11%)] transition-all"
|
|
>
|
|
<LogOut className="h-4 w-4" />
|
|
Logout
|
|
</button>
|
|
) : (
|
|
<div className="flex flex-col gap-2 pt-1">
|
|
<Link
|
|
to="/login"
|
|
className="flex items-center justify-center gap-2 px-4 py-2.5 rounded-lg text-sm font-medium text-[hsl(215_20%_70%)] hover:text-white border border-[hsl(222_38%_20%)] hover:border-[hsl(222_38%_28%)] transition-all"
|
|
onClick={() => setMobileMenuOpen(false)}
|
|
>
|
|
<LogIn className="h-4 w-4" />
|
|
Sign in
|
|
</Link>
|
|
<Link
|
|
to="/register"
|
|
className="flex items-center justify-center px-4 py-2.5 rounded-lg text-sm font-semibold text-white transition-all"
|
|
style={{ background: 'linear-gradient(135deg, #06B6D4 0%, #7C3AED 100%)' }}
|
|
onClick={() => setMobileMenuOpen(false)}
|
|
>
|
|
Get Started
|
|
</Link>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</header>
|
|
);
|
|
}
|