cohorta/src/components/Navigation.tsx
Vadym Samoilenko 5491d2d73d
Some checks failed
Deploy to Production / deploy (push) Failing after 0s
Rebrand to Cohorta + full UI redesign + registration with email verification
- 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>
2026-05-23 18:40:08 +01:00

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>
);
}