Files
gridpilot.gg/apps/website/components/profile/UserPill.tsx
2026-01-18 22:55:55 +01:00

297 lines
11 KiB
TypeScript

'use client';
import { useAuth } from '@/components/auth/AuthContext';
import { useFindDriverById } from '@/hooks/driver/useFindDriverById';
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId';
import { routes } from '@/lib/routing/RouteConfig';
import { DriverViewModel as DriverViewModelClass } from '@/lib/view-models/DriverViewModel';
import { Icon } from '@/ui/Icon';
import { Image } from '@/ui/Image';
import { Link } from '@/ui/Link';
import { Text } from '@/ui/Text';
import { UserDropdown, UserDropdownHeader, UserDropdownItem, UserDropdownFooter } from '@/ui/UserDropdown';
import { AnimatePresence, motion } from 'framer-motion';
import {
BarChart3,
ChevronDown,
CreditCard,
Handshake,
LogOut,
Megaphone,
Paintbrush,
Settings,
Shield
} from 'lucide-react';
import React, { useEffect, useMemo, useState } from 'react';
// Hook to detect demo user mode based on session
function useDemoUserMode(): { isDemo: boolean; demoRole: string | null } {
const { session } = useAuth();
const [demoMode, setDemoMode] = useState({ isDemo: false, demoRole: null as string | null });
// Check if this is a demo user
useEffect(() => {
if (!session?.user) {
setDemoMode({ isDemo: false, demoRole: null });
return;
}
const email = session.user.email?.toLowerCase() || '';
const displayName = session.user.displayName?.toLowerCase() || '';
const primaryDriverId = session.user.primaryDriverId || '';
const role = 'role' in session.user ? (session.user as { role?: string }).role : undefined;
// Check if this is a demo user
if (email.includes('demo') ||
displayName.includes('demo') ||
primaryDriverId.startsWith('demo-')) {
// Use role from session if available, otherwise derive from email
let roleToUse = role;
if (!roleToUse) {
if (email.includes('sponsor')) roleToUse = 'sponsor';
else if (email.includes('league-owner') || displayName.includes('owner')) roleToUse = 'league-owner';
else if (email.includes('league-steward') || displayName.includes('steward')) roleToUse = 'league-steward';
else if (email.includes('league-admin') || displayName.includes('admin')) roleToUse = 'league-admin';
else if (email.includes('system-owner') || displayName.includes('system owner')) roleToUse = 'system-owner';
else if (email.includes('super-admin') || displayName.includes('super admin')) roleToUse = 'super-admin';
else roleToUse = 'driver';
}
setDemoMode({ isDemo: true, demoRole: roleToUse });
} else {
setDemoMode({ isDemo: false, demoRole: null });
}
}, [session]);
return demoMode;
}
// Helper to check if user has admin access (Owner or Super Admin)
function useHasAdminAccess(): boolean {
const { session } = useAuth();
const { isDemo, demoRole } = useDemoUserMode();
// Demo users with system-owner or super-admin roles
if (isDemo && (demoRole === 'system-owner' || demoRole === 'super-admin')) {
return true;
}
// Real users - would need role information from session
// For now, we'll check if the user has any admin-related capabilities
// This can be enhanced when the API includes role information
if (!session?.user) return false;
// Check for admin-related email patterns as a temporary measure
const email = session.user.email?.toLowerCase() || '';
const displayName = session.user.displayName?.toLowerCase() || '';
return email.includes('system-owner') ||
email.includes('super-admin') ||
displayName.includes('system owner') ||
displayName.includes('super admin');
}
export function UserPill() {
const { session } = useAuth();
const [isMenuOpen, setIsMenuOpen] = useState(false);
const { isDemo, demoRole } = useDemoUserMode();
const primaryDriverId = useEffectiveDriverId();
// Use React-Query hook for driver data (only for non-demo users)
const { data: driverDto } = useFindDriverById(primaryDriverId || '', {
enabled: !!primaryDriverId && !isDemo,
});
// Transform DTO to ViewModel
const driver = useMemo(() => {
if (!driverDto) return null;
return new DriverViewModelClass({ ...driverDto, avatarUrl: driverDto.avatarUrl ?? null });
}, [driverDto]);
// Close menu when clicking outside
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
if (isMenuOpen) {
const target = e.target as HTMLElement;
if (!target.closest('[data-user-pill]')) {
setIsMenuOpen(false);
}
}
};
document.addEventListener('click', handleClickOutside);
return () => document.removeEventListener('click', handleClickOutside);
}, [isMenuOpen]);
// Logout handler for demo users
const handleLogout = async () => {
try {
// Call the logout API
await fetch('/api/auth/logout', { method: 'POST' });
// Redirect to home
window.location.href = '/';
} catch (error) {
console.error('Logout failed:', error);
window.location.href = '/';
}
};
// Call hooks unconditionally before any returns
const hasAdminAccess = useHasAdminAccess();
// Handle unauthenticated users
if (!session) {
return (
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<Link
href={routes.auth.login}
variant="secondary"
>
Sign In
</Link>
<Link
href={routes.auth.signup}
variant="primary"
>
Get Started
</Link>
</div>
);
}
// For all authenticated users (demo or regular), show the user pill
// Determine what to show in the pill
const displayName = driver?.name || session.user.displayName || session.user.email || 'User';
const avatarUrl = session.user.avatarUrl;
const roleLabel = isDemo ? ({
'driver': 'Driver',
'sponsor': 'Sponsor',
'league-owner': 'League Owner',
'league-steward': 'League Steward',
'league-admin': 'League Admin',
'system-owner': 'System Owner',
'super-admin': 'Super Admin',
} as Record<string, string>)[demoRole || 'driver'] : null;
const roleIntent = isDemo ? ({
'driver': 'primary',
'sponsor': 'success',
'league-owner': 'primary',
'league-steward': 'warning',
'league-admin': 'critical',
'system-owner': 'primary',
'super-admin': 'primary',
} as Record<string, 'primary' | 'success' | 'warning' | 'critical'>)[demoRole || 'driver'] : 'low';
return (
<div style={{ position: 'relative', display: 'inline-flex', alignItems: 'center' }} data-user-pill>
<button
type="button"
onClick={() => setIsMenuOpen((open) => !open)}
style={{
display: 'flex',
alignItems: 'center',
gap: '0.75rem',
borderRadius: '9999px',
border: `1px solid ${isMenuOpen ? 'var(--ui-color-intent-primary)' : 'var(--ui-color-border-default)'}`,
padding: '0.375rem 0.75rem',
background: 'var(--ui-color-bg-surface-muted)',
cursor: 'pointer'
}}
>
{/* Avatar */}
<div style={{ position: 'relative' }}>
{avatarUrl ? (
<div style={{ width: '2rem', height: '2rem', borderRadius: '9999px', overflow: 'hidden', border: '1px solid var(--ui-color-border-default)' }}>
<Image
src={avatarUrl}
alt={displayName}
objectFit="cover"
/>
</div>
) : (
<div style={{ width: '2rem', height: '2rem', borderRadius: '9999px', backgroundColor: 'var(--ui-color-intent-primary)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Text size="xs" weight="bold" variant="high">
{displayName[0]?.toUpperCase() || 'U'}
</Text>
</div>
)}
</div>
{/* Info */}
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'start' }}>
<Text size="xs" weight="semibold" variant="high" truncate style={{ maxWidth: '100px' }}>
{displayName}
</Text>
{roleLabel && (
<Text size="xs" variant={roleIntent as any} weight="medium" style={{ fontSize: '10px' }}>
{roleLabel}
</Text>
)}
</div>
{/* Chevron */}
<Icon icon={ChevronDown} size={3.5} intent="low" />
</button>
<UserDropdown isOpen={isMenuOpen}>
<UserDropdownHeader variant={isDemo ? 'demo' : 'default'}>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
{avatarUrl ? (
<div style={{ width: '2.5rem', height: '2.5rem', borderRadius: '0.5rem', overflow: 'hidden', border: '1px solid var(--ui-color-border-default)' }}>
<Image
src={avatarUrl}
alt={displayName}
objectFit="cover"
/>
</div>
) : (
<div style={{ width: '2.5rem', height: '2.5rem', borderRadius: '0.5rem', backgroundColor: 'var(--ui-color-intent-primary)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Text size="xs" weight="bold" variant="high">
{displayName[0]?.toUpperCase() || 'U'}
</Text>
</div>
)}
<div>
<Text size="sm" weight="semibold" variant="high" block>{displayName}</Text>
{roleLabel && (
<Text size="xs" variant="low" block>{roleLabel}</Text>
)}
</div>
</div>
</UserDropdownHeader>
<div style={{ padding: '0.25rem 0' }}>
{hasAdminAccess && (
<UserDropdownItem href="/admin" icon={Shield} label="Admin Area" intent="primary" onClick={() => setIsMenuOpen(false)} />
)}
{isDemo && demoRole === 'sponsor' && (
<React.Fragment>
<UserDropdownItem href="/sponsor" icon={BarChart3} label="Dashboard" onClick={() => setIsMenuOpen(false)} />
<UserDropdownItem href={routes.sponsor.campaigns} icon={Megaphone} label="My Sponsorships" onClick={() => setIsMenuOpen(false)} />
<UserDropdownItem href={routes.sponsor.billing} icon={CreditCard} label="Billing" onClick={() => setIsMenuOpen(false)} />
</React.Fragment>
)}
<UserDropdownItem href={routes.protected.profile} label="Profile" onClick={() => setIsMenuOpen(false)} />
<UserDropdownItem href={routes.protected.profileLeagues} label="Manage leagues" onClick={() => setIsMenuOpen(false)} />
<UserDropdownItem href={routes.protected.profileLiveries} icon={Paintbrush} label="Liveries" onClick={() => setIsMenuOpen(false)} />
<UserDropdownItem href={routes.protected.profileSponsorshipRequests} icon={Handshake} label="Sponsorship Requests" intent="success" onClick={() => setIsMenuOpen(false)} />
<UserDropdownItem href={routes.protected.profileSettings} icon={Settings} label="Settings" onClick={() => setIsMenuOpen(false)} />
</div>
<UserDropdownFooter>
<UserDropdownItem
icon={LogOut}
label="Logout"
intent="critical"
onClick={isDemo ? handleLogout : undefined}
/>
</UserDropdownFooter>
</UserDropdown>
</div>
);
}