website refactor

This commit is contained in:
2026-01-15 17:12:24 +01:00
parent c3b308e960
commit f035cfe7ce
468 changed files with 24378 additions and 17324 deletions

View File

@@ -1,16 +1,28 @@
'use client';
import { useAuth } from '@/lib/auth/AuthContext';
import { AnimatePresence, motion, useReducedMotion } from 'framer-motion';
import { BarChart3, Building2, ChevronDown, CreditCard, Handshake, LogOut, Megaphone, Paintbrush, Settings, TrendingUp, Trophy, Shield } from 'lucide-react';
import Link from 'next/link';
import React, { useEffect, useMemo, useState } from 'react';
import { CapabilityGate } from '@/components/shared/CapabilityGate';
import { useEffectiveDriverId } from '@/lib/hooks/useEffectiveDriverId';
import type { DriverViewModel } from '@/lib/view-models/view-models/DriverViewModel';
import { DriverViewModel as DriverViewModelClass } from '@/lib/view-models/view-models/DriverViewModel';
import { useFindDriverById } from '@/lib/hooks/driver/useFindDriverById';
import { AnimatePresence, motion } from 'framer-motion';
import {
BarChart3,
ChevronDown,
CreditCard,
Handshake,
LogOut,
Megaphone,
Paintbrush,
Settings,
Shield
} from 'lucide-react';
import { useAuth } from '@/lib/auth/AuthContext';
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId';
import { DriverViewModel as DriverViewModelClass } from '@/lib/view-models/DriverViewModel';
import { useFindDriverById } from '@/hooks/driver/useFindDriverById';
import { routes } from '@/lib/routing/RouteConfig';
import { Box } from '@/ui/Box';
import { Text } from '@/ui/Text';
import { Icon } from '@/ui/Icon';
import { Link } from '@/ui/Link';
import { Image } from '@/ui/Image';
// Hook to detect demo user mode based on session
function useDemoUserMode(): { isDemo: boolean; demoRole: string | null } {
@@ -80,65 +92,10 @@ function useHasAdminAccess(): boolean {
displayName.includes('super admin');
}
// Sponsor Pill Component - matches the style of DriverSummaryPill
function SponsorSummaryPill({
onClick,
companyName = 'Acme Racing Co.',
activeSponsors = 7,
impressions = 127,
}: {
onClick: () => void;
companyName?: string;
activeSponsors?: number;
impressions?: number;
}) {
const shouldReduceMotion = useReducedMotion();
return (
<motion.button
onClick={onClick}
className="group flex items-center gap-3 rounded-full bg-gradient-to-r from-iron-gray to-deep-graphite border border-charcoal-outline px-3 py-1.5 hover:border-performance-green/50 transition-all duration-200"
whileHover={shouldReduceMotion ? {} : { scale: 1.02 }}
whileTap={shouldReduceMotion ? {} : { scale: 0.98 }}
>
{/* Avatar/Logo */}
<div className="relative">
<div className="w-8 h-8 rounded-full bg-gradient-to-br from-performance-green/20 to-performance-green/5 border border-performance-green/30 flex items-center justify-center">
<Building2 className="w-4 h-4 text-performance-green" />
</div>
{/* Active indicator */}
<div className="absolute -bottom-0.5 -right-0.5 w-3 h-3 rounded-full bg-performance-green border-2 border-deep-graphite" />
</div>
{/* Info */}
<div className="hidden sm:flex flex-col items-start">
<span className="text-xs font-semibold text-white truncate max-w-[100px]">
{companyName.split(' ')[0]}
</span>
<div className="flex items-center gap-1.5 text-[10px] text-gray-500">
<span className="flex items-center gap-0.5">
<Trophy className="w-2.5 h-2.5 text-performance-green" />
{activeSponsors}
</span>
<span className="text-gray-600"></span>
<span className="flex items-center gap-0.5">
<TrendingUp className="w-2.5 h-2.5 text-primary-blue" />
{impressions}k
</span>
</div>
</div>
{/* Chevron */}
<ChevronDown className="w-3.5 h-3.5 text-gray-500 group-hover:text-gray-300 transition-colors" />
</motion.button>
);
}
export default function UserPill() {
export function UserPill() {
const { session } = useAuth();
const [isMenuOpen, setIsMenuOpen] = useState(false);
const { isDemo, demoRole } = useDemoUserMode();
const shouldReduceMotion = useReducedMotion();
const primaryDriverId = useEffectiveDriverId();
@@ -153,43 +110,6 @@ export default function UserPill() {
return new DriverViewModelClass({ ...driverDto, avatarUrl: driverDto.avatarUrl ?? null });
}, [driverDto]);
const data = useMemo(() => {
if (!session?.user) {
return null;
}
// Demo users don't have real driver data
if (isDemo) {
return {
isDemo: true,
demoRole,
displayName: session.user.displayName,
email: session.user.email,
avatarUrl: session.user.avatarUrl,
};
}
if (!primaryDriverId || !driver) {
return null;
}
// Driver rating + rank are not exposed by the current API contract for the lightweight
// driver DTO used in the header. Keep it null until the API provides it.
const rating: number | null = null;
const rank: number | null = null;
const avatarSrc = driver.avatarUrl;
return {
driver,
avatarSrc,
rating,
rank,
isDemo: false,
demoRole: null,
};
}, [session, driver, primaryDriverId, isDemo, demoRole]);
// Close menu when clicking outside
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
@@ -223,20 +143,32 @@ export default function UserPill() {
// Handle unauthenticated users
if (!session) {
return (
<div className="flex items-center gap-2">
<Box display="flex" alignItems="center" gap={2}>
<Link
href=routes.auth.login
className="inline-flex items-center gap-2 rounded-full bg-iron-gray border border-charcoal-outline px-4 py-1.5 text-xs font-medium text-gray-300 hover:text-white hover:border-gray-500 transition-all"
href={routes.auth.login}
variant="secondary"
rounded="full"
px={4}
py={1.5}
size="xs"
hoverTextColor="text-white"
hoverBorderColor="border-gray-500"
>
Sign In
</Link>
<Link
href=routes.auth.signup
className="inline-flex items-center gap-2 rounded-full bg-primary-blue px-4 py-1.5 text-xs font-semibold text-white shadow-[0_0_12px_rgba(25,140,255,0.5)] hover:bg-primary-blue/90 hover:shadow-[0_0_18px_rgba(25,140,255,0.8)] transition-all"
href={routes.auth.signup}
variant="primary"
rounded="full"
px={4}
py={1.5}
size="xs"
shadow="0 0 12px rgba(25,140,255,0.5)"
hoverBg="rgba(25,140,255,0.9)"
>
Get Started
</Link>
</div>
</Box>
);
}
@@ -244,7 +176,7 @@ export default function UserPill() {
// 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 ? {
const roleLabel = isDemo ? ({
'driver': 'Driver',
'sponsor': 'Sponsor',
'league-owner': 'League Owner',
@@ -252,232 +184,294 @@ export default function UserPill() {
'league-admin': 'League Admin',
'system-owner': 'System Owner',
'super-admin': 'Super Admin',
}[demoRole || 'driver'] : null;
} as Record<string, string>)[demoRole || 'driver'] : null;
const roleColor = isDemo ? {
const roleColor = isDemo ? ({
'driver': 'text-primary-blue',
'sponsor': 'text-performance-green',
'league-owner': 'text-purple-400',
'league-steward': 'text-amber-400',
'league-steward': 'text-warning-amber',
'league-admin': 'text-red-400',
'system-owner': 'text-indigo-400',
'super-admin': 'text-pink-400',
}[demoRole || 'driver'] : null;
} as Record<string, string>)[demoRole || 'driver'] : null;
return (
<div className="relative inline-flex items-center" data-user-pill>
<motion.button
<Box position="relative" display="inline-flex" alignItems="center" data-user-pill>
<Box
as="button"
type="button"
onClick={() => setIsMenuOpen((open) => !open)}
className="group flex items-center gap-3 rounded-full bg-gradient-to-r from-iron-gray to-deep-graphite border border-charcoal-outline px-3 py-1.5 hover:border-primary-blue/50 transition-all duration-200"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
display="flex"
alignItems="center"
gap={3}
rounded="full"
border
px={3}
py={1.5}
transition
cursor="pointer"
bg="linear-gradient(to r, var(--iron-gray), var(--deep-graphite))"
borderColor={isMenuOpen ? 'border-primary-blue/50' : 'border-charcoal-outline'}
transform={isMenuOpen ? 'scale(1.02)' : 'scale(1)'}
>
{/* Avatar */}
<div className="relative">
<Box position="relative">
{avatarUrl ? (
<div className="w-8 h-8 rounded-full overflow-hidden bg-charcoal-outline flex items-center justify-center border border-charcoal-outline/80">
<img
<Box w="8" h="8" rounded="full" overflow="hidden" bg="bg-charcoal-outline" display="flex" alignItems="center" justifyContent="center" border borderColor="border-charcoal-outline/80">
<Image
src={avatarUrl}
alt={displayName}
className="w-full h-full object-cover"
objectFit="cover"
fill
/>
</div>
</Box>
) : (
<div className="w-8 h-8 rounded-full bg-gradient-to-br from-primary-blue/20 to-primary-blue/5 border border-primary-blue/30 flex items-center justify-center">
<span className="text-xs font-bold text-primary-blue">
<Box w="8" h="8" rounded="full" bg="bg-primary-blue/20" border borderColor="border-primary-blue/30" display="flex" alignItems="center" justifyContent="center">
<Text size="xs" weight="bold" color="text-primary-blue">
{displayName[0]?.toUpperCase() || 'U'}
</span>
</div>
</Text>
</Box>
)}
<div className="absolute -bottom-0.5 -right-0.5 w-3 h-3 rounded-full bg-primary-blue border-2 border-deep-graphite" />
</div>
<Box position="absolute" bottom="-0.5" right="-0.5" w="3" h="3" rounded="full" bg="bg-primary-blue" border borderColor="border-deep-graphite" borderWidth="2px" />
</Box>
{/* Info */}
<div className="hidden sm:flex flex-col items-start">
<span className="text-xs font-semibold text-white truncate max-w-[100px]">
<Box display={{ base: 'none', sm: 'flex' }} flexDirection="col" alignItems="start">
<Text size="xs" weight="semibold" color="text-white" truncate maxWidth="100px" block>
{displayName}
</span>
</Text>
{roleLabel && (
<span className={`text-[10px] ${roleColor} font-medium`}>
<Text size="xs" color={roleColor || 'text-gray-400'} weight="medium" fontSize="10px">
{roleLabel}
</span>
</Text>
)}
</div>
</Box>
{/* Chevron */}
<ChevronDown className="w-3.5 h-3.5 text-gray-500 group-hover:text-gray-300 transition-colors" />
</motion.button>
<Icon icon={ChevronDown} size={3.5} color="rgb(107, 114, 128)" groupHoverTextColor="text-gray-300" />
</Box>
<AnimatePresence>
{isMenuOpen && (
<motion.div
className="absolute right-0 top-full mt-2 w-56 rounded-xl bg-deep-graphite border border-charcoal-outline shadow-xl shadow-black/30 z-50 overflow-hidden"
<Box
as={motion.div}
initial={{ opacity: 0, y: -10, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: -10, scale: 0.95 }}
transition={{ duration: 0.15 }}
position="absolute"
right={0}
top="100%"
mt={2}
zIndex={50}
>
{/* Header */}
<div className={`p-4 bg-gradient-to-r ${isDemo ? 'from-primary-blue/10' : 'from-iron-gray/20'} to-transparent border-b border-charcoal-outline`}>
<div className="flex items-center gap-3">
{avatarUrl ? (
<div className="w-10 h-10 rounded-lg overflow-hidden bg-charcoal-outline flex items-center justify-center border border-charcoal-outline/80">
<img
src={avatarUrl}
alt={displayName}
className="w-full h-full object-cover"
/>
</div>
) : (
<div className="w-10 h-10 rounded-lg bg-gradient-to-br from-primary-blue/20 to-primary-blue/5 border border-primary-blue/30 flex items-center justify-center">
<span className="text-xs font-bold text-primary-blue">
{displayName[0]?.toUpperCase() || 'U'}
</span>
</div>
<Box w="56" rounded="xl" bg="bg-deep-graphite" border borderColor="border-charcoal-outline" shadow="xl" overflow="hidden">
{/* Header */}
<Box p={4} borderBottom borderColor="border-charcoal-outline" bg={`linear-gradient(to r, ${isDemo ? 'rgba(59, 130, 246, 0.1)' : 'rgba(38, 38, 38, 0.2)'}, transparent)`}>
<Box display="flex" alignItems="center" gap={3}>
{avatarUrl ? (
<Box w="10" h="10" rounded="lg" overflow="hidden" bg="bg-charcoal-outline" display="flex" alignItems="center" justifyContent="center" border borderColor="border-charcoal-outline/80">
<Image
src={avatarUrl}
alt={displayName}
objectFit="cover"
fill
/>
</Box>
) : (
<Box w="10" h="10" rounded="lg" bg="bg-primary-blue/20" border borderColor="border-primary-blue/30" display="flex" alignItems="center" justifyContent="center">
<Text size="xs" weight="bold" color="text-primary-blue">
{displayName[0]?.toUpperCase() || 'U'}
</Text>
</Box>
)}
<Box>
<Text size="sm" weight="semibold" color="text-white" block>{displayName}</Text>
{roleLabel && (
<Text size="xs" color={roleColor || 'text-gray-400'} block>{roleLabel}</Text>
)}
{isDemo && (
<Text size="xs" color="text-gray-500" block>Demo Account</Text>
)}
</Box>
</Box>
{isDemo && (
<Text size="xs" color="text-gray-500" block mt={2}>
Development account - not for production use
</Text>
)}
<div>
<p className="text-sm font-semibold text-white">{displayName}</p>
{roleLabel && (
<p className={`text-xs ${roleColor}`}>{roleLabel}</p>
)}
{isDemo && (
<p className="text-xs text-gray-500">Demo Account</p>
)}
</div>
</div>
{isDemo && (
<div className="mt-2 text-xs text-gray-500">
Development account - not for production use
</div>
)}
</div>
</Box>
{/* Menu Items */}
<div className="py-1 text-sm text-gray-200">
{/* Admin link for Owner/Super Admin users */}
{hasAdminAccess && (
{/* Menu Items */}
<Box py={1}>
{/* Admin link for Owner/Super Admin users */}
{hasAdminAccess && (
<Link
href="/admin"
block
px={4}
py={2.5}
onClick={() => setIsMenuOpen(false)}
>
<Icon icon={Shield} size={4} color="rgb(129, 140, 248)" mr={3} />
<Text size="sm" color="text-gray-200">Admin Area</Text>
</Link>
)}
{/* Sponsor portal link for demo sponsor users */}
{isDemo && demoRole === 'sponsor' && (
<>
<Link
href="/sponsor"
block
px={4}
py={2.5}
onClick={() => setIsMenuOpen(false)}
>
<Icon icon={BarChart3} size={4} color="rgb(16, 185, 129)" mr={3} />
<Text size="sm" color="text-gray-200">Dashboard</Text>
</Link>
<Link
href={routes.sponsor.campaigns}
block
px={4}
py={2.5}
onClick={() => setIsMenuOpen(false)}
>
<Icon icon={Megaphone} size={4} color="rgb(59, 130, 246)" mr={3} />
<Text size="sm" color="text-gray-200">My Sponsorships</Text>
</Link>
<Link
href={routes.sponsor.billing}
block
px={4}
py={2.5}
onClick={() => setIsMenuOpen(false)}
>
<Icon icon={CreditCard} size={4} color="rgb(245, 158, 11)" mr={3} />
<Text size="sm" color="text-gray-200">Billing</Text>
</Link>
<Link
href={routes.sponsor.settings}
block
px={4}
py={2.5}
onClick={() => setIsMenuOpen(false)}
>
<Icon icon={Settings} size={4} color="rgb(156, 163, 175)" mr={3} />
<Text size="sm" color="text-gray-200">Settings</Text>
</Link>
</>
)}
{/* Regular user profile links */}
<Link
href="/admin"
className="flex items-center gap-3 px-4 py-2.5 hover:bg-iron-gray/50 transition-colors"
href={routes.protected.profile}
block
px={4}
py={2.5}
onClick={() => setIsMenuOpen(false)}
>
<Shield className="h-4 w-4 text-indigo-400" />
<span>Admin Area</span>
<Text size="sm" color="text-gray-200">Profile</Text>
</Link>
)}
{/* Sponsor portal link for demo sponsor users */}
{isDemo && demoRole === 'sponsor' && (
<>
<Link
href="/sponsor"
className="flex items-center gap-3 px-4 py-2.5 hover:bg-iron-gray/50 transition-colors"
onClick={() => setIsMenuOpen(false)}
>
<BarChart3 className="h-4 w-4 text-performance-green" />
<span>Dashboard</span>
</Link>
<Link
href=routes.sponsor.campaigns
className="flex items-center gap-3 px-4 py-2.5 hover:bg-iron-gray/50 transition-colors"
onClick={() => setIsMenuOpen(false)}
>
<Megaphone className="h-4 w-4 text-primary-blue" />
<span>My Sponsorships</span>
</Link>
<Link
href=routes.sponsor.billing
className="flex items-center gap-3 px-4 py-2.5 hover:bg-iron-gray/50 transition-colors"
onClick={() => setIsMenuOpen(false)}
>
<CreditCard className="h-4 w-4 text-warning-amber" />
<span>Billing</span>
</Link>
<Link
href=routes.sponsor.settings
className="flex items-center gap-3 px-4 py-2.5 hover:bg-iron-gray/50 transition-colors"
onClick={() => setIsMenuOpen(false)}
>
<Settings className="h-4 w-4 text-gray-400" />
<span>Settings</span>
</Link>
</>
)}
{/* Regular user profile links */}
<Link
href=routes.protected.profile
className="block px-4 py-2.5 hover:bg-iron-gray/50 transition-colors"
onClick={() => setIsMenuOpen(false)}
>
Profile
</Link>
<Link
href=routes.protected.profileLeagues
className="block px-4 py-2.5 hover:bg-iron-gray/50 transition-colors"
onClick={() => setIsMenuOpen(false)}
>
Manage leagues
</Link>
<Link
href=routes.protected.profileLiveries
className="flex items-center gap-2 px-4 py-2.5 hover:bg-iron-gray/50 transition-colors"
onClick={() => setIsMenuOpen(false)}
>
<Paintbrush className="h-4 w-4" />
<span>Liveries</span>
</Link>
<Link
href=routes.protected.profileSponsorshipRequests
className="flex items-center gap-2 px-4 py-2.5 hover:bg-iron-gray/50 transition-colors"
onClick={() => setIsMenuOpen(false)}
>
<Handshake className="h-4 w-4 text-performance-green" />
<span>Sponsorship Requests</span>
</Link>
<Link
href=routes.protected.profileSettings
className="flex items-center gap-2 px-4 py-2.5 hover:bg-iron-gray/50 transition-colors"
onClick={() => setIsMenuOpen(false)}
>
<Settings className="h-4 w-4" />
<span>Settings</span>
</Link>
{/* Demo-specific info */}
{isDemo && (
<div className="px-4 py-2 text-xs text-gray-500 italic border-t border-charcoal-outline/50 mt-1">
Demo users have limited profile access
</div>
)}
</div>
{/* Footer */}
<div className="border-t border-charcoal-outline">
{isDemo ? (
<button
type="button"
onClick={handleLogout}
className="flex w-full items-center justify-between px-4 py-3 text-sm text-gray-500 hover:text-racing-red hover:bg-racing-red/5 transition-colors"
<Link
href={routes.protected.profileLeagues}
block
px={4}
py={2.5}
onClick={() => setIsMenuOpen(false)}
>
<span>Logout</span>
<LogOut className="h-4 w-4" />
</button>
) : (
<form action="/auth/logout" method="POST">
<button
type="submit"
className="flex w-full items-center justify-between px-4 py-3 text-sm text-gray-500 hover:text-red-400 hover:bg-red-500/10 transition-colors"
<Text size="sm" color="text-gray-200">Manage leagues</Text>
</Link>
<Link
href={routes.protected.profileLiveries}
block
px={4}
py={2.5}
onClick={() => setIsMenuOpen(false)}
>
<Icon icon={Paintbrush} size={4} mr={2} />
<Text size="sm" color="text-gray-200">Liveries</Text>
</Link>
<Link
href={routes.protected.profileSponsorshipRequests}
block
px={4}
py={2.5}
onClick={() => setIsMenuOpen(false)}
>
<Icon icon={Handshake} size={4} color="rgb(16, 185, 129)" mr={2} />
<Text size="sm" color="text-gray-200">Sponsorship Requests</Text>
</Link>
<Link
href={routes.protected.profileSettings}
block
px={4}
py={2.5}
onClick={() => setIsMenuOpen(false)}
>
<Icon icon={Settings} size={4} mr={2} />
<Text size="sm" color="text-gray-200">Settings</Text>
</Link>
{/* Demo-specific info */}
{isDemo && (
<Box px={4} py={2} borderTop borderColor="border-charcoal-outline/50" mt={1}>
<Text size="xs" color="text-gray-500" italic>Demo users have limited profile access</Text>
</Box>
)}
</Box>
{/* Footer */}
<Box borderTop borderColor="border-charcoal-outline">
{isDemo ? (
<Box
as="button"
type="button"
onClick={handleLogout}
display="flex"
alignItems="center"
justifyContent="between"
fullWidth
px={4}
py={3}
cursor="pointer"
transition
bg="transparent"
hoverBg="rgba(239, 68, 68, 0.05)"
hoverColor="text-racing-red"
>
<span>Logout</span>
<LogOut className="h-4 w-4" />
</button>
</form>
)}
</div>
</motion.div>
<Text size="sm" color="text-gray-500">Logout</Text>
<Icon icon={LogOut} size={4} color="rgb(107, 114, 128)" />
</Box>
) : (
<Box as="form" action="/auth/logout" method="POST">
<Box
as="button"
type="submit"
display="flex"
alignItems="center"
justifyContent="between"
fullWidth
px={4}
py={3}
cursor="pointer"
transition
bg="transparent"
hoverBg="rgba(239, 68, 68, 0.1)"
hoverColor="text-red-400"
>
<Text size="sm" color="text-gray-500">Logout</Text>
<Icon icon={LogOut} size={4} color="rgb(107, 114, 128)" />
</Box>
</Box>
)}
</Box>
</Box>
</Box>
)}
</AnimatePresence>
</div>
</Box>
);
}
}