Last night

This commit is contained in:
Andras Schmelczer 2026-02-08 10:21:37 +00:00
parent 2906b01734
commit 42ee2d4c51
47 changed files with 848 additions and 478 deletions

View file

@ -15,7 +15,7 @@ export default function AuthModal({
}: {
onClose: () => void;
onLogin: (email: string, password: string) => Promise<void>;
onRegister: (email: string, password: string, name?: string) => Promise<void>;
onRegister: (email: string, password: string) => Promise<void>;
onForgotPassword: (email: string) => Promise<void>;
loading: boolean;
error: string | null;
@ -25,7 +25,6 @@ export default function AuthModal({
const [view, setView] = useState<View>(initialTab);
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [name, setName] = useState('');
const [resetSent, setResetSent] = useState(false);
const switchView = useCallback(
@ -45,7 +44,7 @@ export default function AuthModal({
await onLogin(email, password);
onClose();
} else if (view === 'register') {
await onRegister(email, password, name || undefined);
await onRegister(email, password);
onClose();
} else {
await onForgotPassword(email);
@ -55,7 +54,7 @@ export default function AuthModal({
// Error is handled by the hook
}
},
[view, email, password, name, onLogin, onRegister, onForgotPassword, onClose]
[view, email, password, onLogin, onRegister, onForgotPassword, onClose]
);
const title =
@ -107,21 +106,6 @@ export default function AuthModal({
{/* Form */}
<form onSubmit={handleSubmit} className="p-5 space-y-4">
{view === 'register' && (
<div>
<label className="block text-sm font-medium text-warm-700 dark:text-warm-300 mb-1">
Name
</label>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full px-3 py-2 text-sm rounded border border-warm-200 dark:border-warm-700 bg-white dark:bg-warm-900 text-navy-950 dark:text-white placeholder-warm-400 dark:placeholder-warm-500 outline-none focus:ring-2 ring-teal-400 dark:ring-teal-500"
placeholder="Your name (optional)"
/>
</div>
)}
<div>
<label className="block text-sm font-medium text-warm-700 dark:text-warm-300 mb-1">
Email

View file

@ -1,10 +1,12 @@
import { useState, useCallback } from 'react';
import { useState, useCallback, useEffect } from 'react';
import type { AuthUser } from '../../hooks/useAuth';
import { DownloadIcon } from './icons/DownloadIcon';
import { BookmarkIcon } from './icons/BookmarkIcon';
import { MapPinIcon } from './icons/MapPinIcon';
import { CheckIcon } from './icons/CheckIcon';
import { ClipboardIcon } from './icons/ClipboardIcon';
import { CloseIcon } from './icons/CloseIcon';
import { MenuIcon } from './icons/MenuIcon';
import { SunIcon } from './icons/SunIcon';
import { MoonIcon } from './icons/MoonIcon';
import { SpinnerIcon } from './icons/SpinnerIcon';
@ -25,6 +27,7 @@ export default function Header({
onLoginClick,
onRegisterClick,
onLogout,
isMobile,
}: {
activePage: Page;
onPageChange: (page: Page) => void;
@ -38,8 +41,25 @@ export default function Header({
onLoginClick: () => void;
onRegisterClick: () => void;
onLogout: () => void;
isMobile: boolean;
}) {
const [copied, setCopied] = useState(false);
const [menuOpen, setMenuOpen] = useState(false);
// Close menu on Escape
useEffect(() => {
if (!menuOpen) return;
const handler = (e: KeyboardEvent) => {
if (e.key === 'Escape') setMenuOpen(false);
};
window.addEventListener('keydown', handler);
return () => window.removeEventListener('keydown', handler);
}, [menuOpen]);
// Close menu when switching away from mobile
useEffect(() => {
if (!isMobile) setMenuOpen(false);
}, [isMobile]);
const handleShare = useCallback(() => {
const url = window.location.href;
@ -69,8 +89,24 @@ export default function Header({
: 'text-warm-300 hover:bg-navy-800 hover:text-white'
}`;
const mobileNavItem = (page: Page, label: string) => (
<button
key={page}
className={`w-full text-left px-4 py-3 text-base font-medium rounded ${
activePage === page ? 'bg-navy-700 text-white' : 'text-warm-300 hover:bg-navy-800 hover:text-white'
}`}
onClick={() => {
onPageChange(page);
setMenuOpen(false);
}}
>
{label}
</button>
);
return (
<header className="h-12 bg-navy-900 text-white flex items-center justify-between px-4 shrink-0">
<header className="h-12 bg-navy-900 text-white flex items-center px-4 shrink-0">
{/* Left: Logo + nav */}
<div className="flex items-center gap-4">
<button
className="flex items-center gap-2 hover:opacity-80 transition-opacity"
@ -79,25 +115,32 @@ export default function Header({
<MapPinIcon className="w-5 h-5 text-teal-400" />
<span className="font-semibold text-lg">Narrowit</span>
</button>
<nav className="flex items-center gap-2">
<button className={tabClass('dashboard')} onClick={() => onPageChange('dashboard')}>
Dashboard
</button>
<button className={tabClass('data-sources')} onClick={() => onPageChange('data-sources')}>
Data Sources
</button>
<button className={tabClass('faq')} onClick={() => onPageChange('faq')}>
FAQ
</button>
{user && (
<button className={tabClass('saved-searches')} onClick={() => onPageChange('saved-searches')}>
Saved
{/* Desktop nav */}
{!isMobile && (
<nav className="flex items-center gap-2">
<button className={tabClass('dashboard')} onClick={() => onPageChange('dashboard')}>
Dashboard
</button>
)}
</nav>
{user && (
<button className={tabClass('saved-searches')} onClick={() => onPageChange('saved-searches')}>
Saved
</button>
)}
<button className={tabClass('data-sources')} onClick={() => onPageChange('data-sources')}>
Data Sources
</button>
<button className={tabClass('faq')} onClick={() => onPageChange('faq')}>
FAQ
</button>
</nav>
)}
</div>
<div className="flex items-center gap-2">
{activePage === 'dashboard' && (
{/* Right side */}
<div className="flex items-center gap-2 ml-auto">
{/* Desktop-only dashboard actions */}
{!isMobile && activePage === 'dashboard' && (
<>
{onSaveSearch && (
<button
@ -140,32 +183,166 @@ export default function Header({
</button>
</>
)}
{user ? (
<UserMenu user={user} onLogout={onLogout} />
) : (
{/* Desktop-only auth */}
{!isMobile && (
<>
<button
onClick={onLoginClick}
className="px-3 py-1.5 rounded bg-navy-800 hover:bg-navy-700 transition-colors text-sm"
>
Log in
</button>
<button
onClick={onRegisterClick}
className="px-3 py-1.5 rounded bg-teal-600 hover:bg-teal-700 transition-colors text-sm font-medium"
>
Register
</button>
{user ? (
<UserMenu user={user} onLogout={onLogout} />
) : (
<>
<button
onClick={onLoginClick}
className="px-3 py-1.5 rounded bg-navy-800 hover:bg-navy-700 transition-colors text-sm"
>
Log in
</button>
<button
onClick={onRegisterClick}
className="px-3 py-1.5 rounded bg-teal-600 hover:bg-teal-700 transition-colors text-sm font-medium"
>
Register
</button>
</>
)}
</>
)}
<button
onClick={onToggleTheme}
className="flex items-center justify-center w-8 h-8 rounded bg-navy-800 hover:bg-navy-700 transition-colors"
title={`Theme: ${theme}`}
>
{theme === 'light' ? <SunIcon className="w-4 h-4" /> : <MoonIcon className="w-4 h-4" />}
</button>
{/* Mobile auth CTA (logged out only) */}
{isMobile && !user && (
<button
onClick={onRegisterClick}
className="px-4 py-1.5 rounded bg-teal-600 hover:bg-teal-700 transition-colors text-sm font-semibold"
>
Sign up
</button>
)}
{/* Theme toggle (desktop only) */}
{!isMobile && (
<button
onClick={onToggleTheme}
className="flex items-center justify-center w-8 h-8 rounded bg-navy-800 hover:bg-navy-700 transition-colors"
title={`Theme: ${theme}`}
>
{theme === 'light' ? <SunIcon className="w-4 h-4" /> : <MoonIcon className="w-4 h-4" />}
</button>
)}
{/* Mobile hamburger */}
{isMobile && (
<button
onClick={() => setMenuOpen(true)}
className="flex items-center justify-center w-10 h-10 -mr-2 rounded hover:bg-navy-800 transition-colors"
aria-label="Open menu"
>
<MenuIcon className="w-6 h-6" />
</button>
)}
</div>
{/* Mobile slide-in menu */}
{isMobile && menuOpen && (
<>
{/* Backdrop */}
<div
className="fixed inset-0 bg-black/50 z-40"
onClick={() => setMenuOpen(false)}
/>
{/* Menu panel */}
<div className="fixed top-0 right-0 bottom-0 w-64 bg-navy-900 z-50 flex flex-col shadow-xl">
<div className="flex items-center justify-between px-4 h-12 border-b border-navy-700">
<span className="font-semibold">Menu</span>
<button
onClick={() => setMenuOpen(false)}
className="flex items-center justify-center w-10 h-10 -mr-2 rounded hover:bg-navy-800 transition-colors"
aria-label="Close menu"
>
<CloseIcon className="w-5 h-5" />
</button>
</div>
<nav className="flex-1 flex flex-col gap-1 p-3 overflow-y-auto">
{mobileNavItem('dashboard', 'Dashboard')}
{user && mobileNavItem('saved-searches', 'Saved')}
{mobileNavItem('data-sources', 'Data Sources')}
{mobileNavItem('faq', 'FAQ')}
{/* Dashboard actions */}
{activePage === 'dashboard' && (
<div className="mt-3 pt-3 border-t border-navy-700 flex flex-col gap-1">
{onSaveSearch && (
<button
onClick={() => { onSaveSearch(); setMenuOpen(false); }}
disabled={savingSearch}
className="w-full flex items-center gap-2 px-4 py-3 text-base text-warm-300 hover:bg-navy-800 hover:text-white rounded disabled:opacity-50"
>
{savingSearch ? <SpinnerIcon className="w-5 h-5 animate-spin" /> : <BookmarkIcon className="w-5 h-5" />}
Save
</button>
)}
<button
onClick={() => { handleShare(); setMenuOpen(false); }}
className="w-full flex items-center gap-2 px-4 py-3 text-base text-warm-300 hover:bg-navy-800 hover:text-white rounded"
>
{copied ? <CheckIcon className="w-5 h-5" /> : <ClipboardIcon className="w-5 h-5" />}
{copied ? 'Copied!' : 'Share'}
</button>
<button
onClick={() => { onExport?.(); setMenuOpen(false); }}
disabled={!onExport || exporting}
className="w-full flex items-center gap-2 px-4 py-3 text-base text-warm-300 hover:bg-navy-800 hover:text-white rounded disabled:opacity-50"
>
<DownloadIcon className="w-5 h-5" />
{exporting ? 'Exporting...' : 'Export'}
</button>
</div>
)}
</nav>
{/* Theme toggle + Auth section at bottom */}
<div className="p-3 border-t border-navy-700 flex flex-col gap-3">
{/* Theme toggle */}
<button
onClick={() => { onToggleTheme(); }}
className="w-full flex items-center gap-3 px-4 py-3 text-base text-warm-300 hover:bg-navy-800 hover:text-white rounded transition-colors"
>
{theme === 'light' ? <SunIcon className="w-5 h-5" /> : <MoonIcon className="w-5 h-5" />}
<span>Theme: {theme === 'light' ? 'Light' : 'Dark'}</span>
</button>
{/* Auth buttons */}
<div>
{user ? (
<div className="flex items-center justify-between px-4 py-2">
<span className="text-sm text-warm-300 truncate">{user.email}</span>
<button
onClick={() => { onLogout(); setMenuOpen(false); }}
className="text-sm text-warm-400 hover:text-white"
>
Log out
</button>
</div>
) : (
<div className="flex gap-2">
<button
onClick={() => { onLoginClick(); setMenuOpen(false); }}
className="flex-1 px-3 py-2.5 rounded bg-navy-800 hover:bg-navy-700 transition-colors text-sm text-center"
>
Log in
</button>
<button
onClick={() => { onRegisterClick(); setMenuOpen(false); }}
className="flex-1 px-3 py-2.5 rounded bg-teal-600 hover:bg-teal-700 transition-colors text-sm font-medium text-center"
>
Register
</button>
</div>
)}
</div>
</div>
</div>
</>
)}
</header>
);
}

View file

@ -17,14 +17,14 @@ export default function UserMenu({ user, onLogout }: { user: AuthUser; onLogout:
return () => document.removeEventListener('mousedown', handleClick);
}, [open]);
const initial = (user.name || user.email)[0].toUpperCase();
const initial = user.email[0].toUpperCase();
return (
<div className="relative" ref={menuRef}>
<button
onClick={() => setOpen((prev) => !prev)}
className="flex items-center justify-center w-8 h-8 rounded-full bg-teal-600 text-white text-sm font-medium hover:bg-teal-700"
title={user.name || user.email}
title={user.email}
>
{initial}
</button>
@ -32,12 +32,7 @@ export default function UserMenu({ user, onLogout }: { user: AuthUser; onLogout:
{open && (
<div className="absolute right-0 top-10 w-56 bg-white dark:bg-warm-800 border border-warm-200 dark:border-warm-700 rounded-lg shadow-lg z-50">
<div className="px-4 py-3 border-b border-warm-200 dark:border-warm-700">
{user.name && (
<p className="text-sm font-medium text-navy-950 dark:text-warm-100 truncate">
{user.name}
</p>
)}
<p className="text-xs text-warm-500 dark:text-warm-400 truncate">{user.email}</p>
<p className="text-sm font-medium text-navy-950 dark:text-warm-100 truncate">{user.email}</p>
</div>
<div className="p-1">
<button

View file

@ -0,0 +1,17 @@
interface IconProps {
className?: string;
}
export function MenuIcon({ className = 'w-4 h-4' }: IconProps) {
return (
<svg
className={className}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M4 6h16M4 12h16M4 18h16" />
</svg>
);
}

View file

@ -5,3 +5,4 @@ export { PlusIcon } from './PlusIcon';
export { ChevronIcon } from './ChevronIcon';
export { FilterIcon } from './FilterIcon';
export { LightbulbIcon } from './LightbulbIcon';
export { MenuIcon } from './MenuIcon';