clean routes
This commit is contained in:
@@ -1,50 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
export default function AlphaBanner() {
|
||||
const [isDismissed, setIsDismissed] = useState(false);
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setIsMounted(true);
|
||||
const dismissed = sessionStorage.getItem('alpha-banner-dismissed');
|
||||
if (dismissed === 'true') {
|
||||
setIsDismissed(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleDismiss = () => {
|
||||
sessionStorage.setItem('alpha-banner-dismissed', 'true');
|
||||
setIsDismissed(true);
|
||||
};
|
||||
|
||||
if (!isMounted) return null;
|
||||
if (isDismissed) return null;
|
||||
|
||||
return (
|
||||
<div className="sticky top-0 z-50 bg-warning-amber/10 border-b border-warning-amber/20 backdrop-blur-sm">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-3">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<svg className="w-5 h-5 text-warning-amber flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
<p className="text-sm text-white">
|
||||
Alpha Version — Data resets on page reload. No persistent storage.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleDismiss}
|
||||
className="text-gray-400 hover:text-white transition-colors p-1"
|
||||
aria-label="Dismiss banner"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function AlphaFooter() {
|
||||
return (
|
||||
<footer className="mt-auto border-t border-charcoal-outline bg-deep-graphite">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="flex flex-col md:flex-row items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-2 text-sm text-gray-400">
|
||||
<span className="px-2 py-1 bg-warning-amber/10 text-warning-amber rounded border border-warning-amber/20 font-medium">
|
||||
Alpha
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-6 text-sm">
|
||||
<a
|
||||
href="https://discord.gg/gridpilot"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-gray-400 hover:text-primary-blue transition-colors"
|
||||
>
|
||||
Give Feedback
|
||||
</a>
|
||||
<a
|
||||
href="/docs/roadmap"
|
||||
className="text-gray-400 hover:text-primary-blue transition-colors"
|
||||
>
|
||||
Roadmap
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import UserPill from '@/components/profile/UserPill';
|
||||
import NotificationCenter from '@/components/notifications/NotificationCenter';
|
||||
import { useAuth } from '@/lib/auth/AuthContext';
|
||||
|
||||
type AlphaNavProps = Record<string, never>;
|
||||
const nonHomeLinks = [
|
||||
{ href: '/leagues', label: 'Leagues' },
|
||||
{ href: '/races', label: 'Races' },
|
||||
{ href: '/teams', label: 'Teams' },
|
||||
{ href: '/drivers', label: 'Drivers' },
|
||||
{ href: '/leaderboards', label: 'Leaderboards' },
|
||||
] as const;
|
||||
|
||||
export function AlphaNav({}: AlphaNavProps) {
|
||||
const pathname = usePathname();
|
||||
const { session } = useAuth();
|
||||
const isAuthenticated = !!session;
|
||||
|
||||
const navLinks = isAuthenticated
|
||||
? ([{ href: '/dashboard', label: 'Dashboard' } as const, ...nonHomeLinks] as const)
|
||||
: ([{ href: '/', label: 'Home' } as const, ...nonHomeLinks] as const);
|
||||
|
||||
const loginHref = '/auth/iracing/start?returnTo=/dashboard';
|
||||
|
||||
return (
|
||||
<nav className="sticky top-0 z-40 bg-deep-graphite/95 backdrop-blur-md border-b border-white/5">
|
||||
<div className="max-w-7xl mx-auto px-6">
|
||||
<div className="flex items-center justify-between h-14">
|
||||
<div className="flex items-baseline space-x-3">
|
||||
<Link
|
||||
href="/"
|
||||
className="text-xl font-semibold text-white hover:text-primary-blue transition-colors"
|
||||
>
|
||||
GridPilot
|
||||
</Link>
|
||||
<span className="text-xs text-gray-500 font-light">ALPHA</span>
|
||||
</div>
|
||||
|
||||
<div className="hidden md:flex items-center space-x-1">
|
||||
{navLinks.map((link) => {
|
||||
const isActive = pathname === link.href;
|
||||
return (
|
||||
<Link
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className={`
|
||||
relative px-4 py-2 text-sm font-medium transition-all duration-200
|
||||
${
|
||||
isActive
|
||||
? 'text-primary-blue'
|
||||
: 'text-gray-400 hover:text-white'
|
||||
}
|
||||
`}
|
||||
>
|
||||
{link.label}
|
||||
{isActive && (
|
||||
<span className="absolute bottom-0 left-0 right-0 h-0.5 bg-primary-blue rounded-full" />
|
||||
)}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="hidden md:flex items-center space-x-3">
|
||||
<NotificationCenter />
|
||||
<UserPill />
|
||||
</div>
|
||||
|
||||
<div className="md:hidden w-8" />
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import Button from '../ui/Button';
|
||||
import Card from '../ui/Card';
|
||||
|
||||
type CompanionRace = {
|
||||
track: string;
|
||||
car: string;
|
||||
scheduledAt: string | Date;
|
||||
sessionType: string;
|
||||
};
|
||||
|
||||
interface CompanionInstructionsProps {
|
||||
race: CompanionRace;
|
||||
leagueName?: string;
|
||||
}
|
||||
|
||||
export default function CompanionInstructions({ race, leagueName }: CompanionInstructionsProps) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const formatDateTime = (date: Date) => {
|
||||
return new Date(date).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
timeZoneName: 'short',
|
||||
});
|
||||
};
|
||||
|
||||
const scheduledAt = typeof race.scheduledAt === 'string' ? new Date(race.scheduledAt) : race.scheduledAt;
|
||||
|
||||
const raceDetails = `GridPilot Race: ${leagueName || 'League'}
|
||||
Track: ${race.track}
|
||||
Car: ${race.car}
|
||||
Date/Time: ${formatDateTime(scheduledAt)}
|
||||
Session Type: ${race.sessionType.charAt(0).toUpperCase() + race.sessionType.slice(1)}`;
|
||||
|
||||
const handleCopyDetails = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(raceDetails);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
} catch (err) {
|
||||
console.error('Failed to copy:', err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="border border-primary-blue/20 bg-iron-gray">
|
||||
<div className="flex items-start gap-3 mb-4">
|
||||
<div className="w-10 h-10 rounded-lg bg-primary-blue/10 flex items-center justify-center flex-shrink-0">
|
||||
<svg className="w-5 h-5 text-primary-blue" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-semibold text-white mb-1">Alpha Manual Workflow</h3>
|
||||
<p className="text-sm text-gray-400">
|
||||
Companion automation coming in production. For alpha, races are created manually.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 mb-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="flex items-center justify-center w-6 h-6 rounded-full bg-primary-blue/20 text-primary-blue text-xs font-semibold flex-shrink-0">
|
||||
1
|
||||
</span>
|
||||
<p className="text-sm text-gray-300 pt-0.5">
|
||||
Schedule race in GridPilot (completed)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="flex items-center justify-center w-6 h-6 rounded-full bg-charcoal-outline text-gray-400 text-xs font-semibold flex-shrink-0">
|
||||
2
|
||||
</span>
|
||||
<p className="text-sm text-gray-300 pt-0.5">
|
||||
Copy race details using button below
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="flex items-center justify-center w-6 h-6 rounded-full bg-charcoal-outline text-gray-400 text-xs font-semibold flex-shrink-0">
|
||||
3
|
||||
</span>
|
||||
<p className="text-sm text-gray-300 pt-0.5">
|
||||
Create hosted session manually in iRacing website
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="flex items-center justify-center w-6 h-6 rounded-full bg-charcoal-outline text-gray-400 text-xs font-semibold flex-shrink-0">
|
||||
4
|
||||
</span>
|
||||
<p className="text-sm text-gray-300 pt-0.5">
|
||||
Return to GridPilot after race completes
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="flex items-center justify-center w-6 h-6 rounded-full bg-charcoal-outline text-gray-400 text-xs font-semibold flex-shrink-0">
|
||||
5
|
||||
</span>
|
||||
<p className="text-sm text-gray-300 pt-0.5">
|
||||
Import results via CSV upload
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-4 border-t border-charcoal-outline">
|
||||
<div className="bg-deep-graphite rounded-lg p-3 mb-3">
|
||||
<pre className="text-xs text-gray-300 whitespace-pre-wrap font-mono">
|
||||
{raceDetails}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleCopyDetails}
|
||||
className="w-full"
|
||||
>
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||
</svg>
|
||||
{copied ? 'Copied!' : 'Copy Race Details'}
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
'use client';
|
||||
|
||||
interface CompanionStatusProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function CompanionStatus({ className = '' }: CompanionStatusProps) {
|
||||
// Alpha: always disconnected
|
||||
const isConnected = false;
|
||||
const statusMessage = "Companion app available in production";
|
||||
|
||||
return (
|
||||
<div className={`flex items-center gap-3 ${className}`}>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`w-2 h-2 rounded-full ${isConnected ? 'bg-performance-green' : 'bg-gray-500'}`} />
|
||||
<span className="text-sm text-gray-400">
|
||||
Companion App: <span className={isConnected ? 'text-performance-green' : 'text-gray-400'}>
|
||||
{isConnected ? 'Connected' : 'Disconnected'}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-xs text-gray-500">
|
||||
{statusMessage}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
'use client';
|
||||
|
||||
interface FeatureLimitationTooltipProps {
|
||||
message: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function FeatureLimitationTooltip({ message, children }: FeatureLimitationTooltipProps) {
|
||||
return (
|
||||
<div className="group relative inline-block">
|
||||
{children}
|
||||
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-3 py-2 bg-iron-gray border border-charcoal-outline rounded-lg text-sm text-gray-300 whitespace-nowrap opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 pointer-events-none z-50">
|
||||
<div className="flex items-center gap-2">
|
||||
<svg className="w-4 h-4 text-primary-blue flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span>{message}</span>
|
||||
</div>
|
||||
<div className="absolute top-full left-1/2 -translate-x-1/2 -mt-1 border-4 border-transparent border-t-iron-gray" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from '../leagues/ScheduleRaceForm';
|
||||
@@ -71,9 +71,22 @@ export default function DevToolbar() {
|
||||
// Determine login mode based on user email patterns
|
||||
const email = session.user.email?.toLowerCase() || '';
|
||||
const displayName = session.user.displayName?.toLowerCase() || '';
|
||||
const role = (session.user as any).role;
|
||||
|
||||
let mode: LoginMode = 'none';
|
||||
if (email.includes('sponsor') || displayName.includes('sponsor')) {
|
||||
|
||||
// First check session.role if available
|
||||
if (role) {
|
||||
if (role === 'sponsor') mode = 'sponsor';
|
||||
else if (role === 'league-owner') mode = 'league-owner';
|
||||
else if (role === 'league-steward') mode = 'league-steward';
|
||||
else if (role === 'league-admin') mode = 'league-admin';
|
||||
else if (role === 'system-owner') mode = 'system-owner';
|
||||
else if (role === 'super-admin') mode = 'super-admin';
|
||||
else if (role === 'driver') mode = 'driver';
|
||||
}
|
||||
// Fallback to email patterns
|
||||
else if (email.includes('sponsor') || displayName.includes('sponsor')) {
|
||||
mode = 'sponsor';
|
||||
} else if (email.includes('league-owner') || displayName.includes('owner')) {
|
||||
mode = 'league-owner';
|
||||
|
||||
@@ -13,22 +13,7 @@ import type { DriverViewModel } from '@/lib/view-models/DriverViewModel';
|
||||
import { DriverViewModel as DriverViewModelClass } from '@/lib/view-models/DriverViewModel';
|
||||
import { useServices } from '@/lib/services/ServiceProvider';
|
||||
|
||||
// Hook to detect sponsor mode
|
||||
function useSponsorMode(): boolean {
|
||||
const [isSponsor, setIsSponsor] = useState(false);
|
||||
useEffect(() => {
|
||||
const cookie = document.cookie
|
||||
.split('; ')
|
||||
.find(row => row.startsWith('gridpilot_demo_mode='));
|
||||
if (cookie) {
|
||||
const value = cookie.split('=')[1];
|
||||
setIsSponsor(value === 'sponsor');
|
||||
}
|
||||
}, []);
|
||||
return isSponsor;
|
||||
}
|
||||
|
||||
// Hook to detect demo user mode
|
||||
// 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 });
|
||||
@@ -42,21 +27,26 @@ function useDemoUserMode(): { isDemo: boolean; demoRole: string | null } {
|
||||
const email = session.user.email?.toLowerCase() || '';
|
||||
const displayName = session.user.displayName?.toLowerCase() || '';
|
||||
const primaryDriverId = (session.user as any).primaryDriverId || '';
|
||||
const role = (session.user as any).role;
|
||||
|
||||
// Check if this is a demo user
|
||||
if (email.includes('demo') ||
|
||||
displayName.includes('demo') ||
|
||||
primaryDriverId.startsWith('demo-')) {
|
||||
|
||||
let role = 'driver';
|
||||
if (email.includes('sponsor')) role = 'sponsor';
|
||||
else if (email.includes('league-owner') || displayName.includes('owner')) role = 'league-owner';
|
||||
else if (email.includes('league-steward') || displayName.includes('steward')) role = 'league-steward';
|
||||
else if (email.includes('league-admin') || displayName.includes('admin')) role = 'league-admin';
|
||||
else if (email.includes('system-owner') || displayName.includes('system owner')) role = 'system-owner';
|
||||
else if (email.includes('super-admin') || displayName.includes('super admin')) role = 'super-admin';
|
||||
// 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: role });
|
||||
setDemoMode({ isDemo: true, demoRole: roleToUse });
|
||||
} else {
|
||||
setDemoMode({ isDemo: false, demoRole: null });
|
||||
}
|
||||
@@ -149,7 +139,6 @@ export default function UserPill() {
|
||||
const { driverService, mediaService } = useServices();
|
||||
const [driver, setDriver] = useState<DriverViewModel | null>(null);
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
const isSponsorMode = useSponsorMode();
|
||||
const { isDemo, demoRole } = useDemoUserMode();
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
|
||||
@@ -236,8 +225,6 @@ export default function UserPill() {
|
||||
try {
|
||||
// Call the logout API
|
||||
await fetch('/api/auth/logout', { method: 'POST' });
|
||||
// Clear any demo mode cookies
|
||||
document.cookie = 'gridpilot_demo_mode=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT';
|
||||
// Redirect to home
|
||||
window.location.href = '/';
|
||||
} catch (error) {
|
||||
@@ -509,4 +496,4 @@ export default function UserPill() {
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { useRouter } from 'next/navigation';
|
||||
import Card from '@/components/ui/Card';
|
||||
import Button from '@/components/ui/Button';
|
||||
import { useServices } from '@/lib/services/ServiceProvider';
|
||||
import { useAuth } from '@/lib/auth/AuthContext';
|
||||
import {
|
||||
Eye,
|
||||
TrendingUp,
|
||||
@@ -445,18 +446,28 @@ export default function SponsorInsightsCard({
|
||||
// ============================================================================
|
||||
|
||||
export function useSponsorMode(): boolean {
|
||||
const { session } = useAuth();
|
||||
const [isSponsor, setIsSponsor] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (typeof document !== 'undefined') {
|
||||
const cookies = document.cookie.split(';');
|
||||
const demoModeCookie = cookies.find(c => c.trim().startsWith('gridpilot_demo_mode='));
|
||||
if (demoModeCookie) {
|
||||
const value = demoModeCookie.split('=')[1]?.trim();
|
||||
setIsSponsor(value === 'sponsor');
|
||||
}
|
||||
if (!session?.user) {
|
||||
setIsSponsor(false);
|
||||
return;
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Check session.user.role for sponsor
|
||||
const role = (session.user as any).role;
|
||||
if (role === 'sponsor') {
|
||||
setIsSponsor(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback: check email patterns
|
||||
const email = session.user.email?.toLowerCase() || '';
|
||||
const displayName = session.user.displayName?.toLowerCase() || '';
|
||||
|
||||
setIsSponsor(email.includes('sponsor') || displayName.includes('sponsor'));
|
||||
}, [session]);
|
||||
|
||||
return isSponsor;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user