website refactor

This commit is contained in:
2026-01-14 10:51:05 +01:00
parent 4522d41aef
commit 0d89ad027e
291 changed files with 6887 additions and 3685 deletions

View File

@@ -0,0 +1,70 @@
import { Eye, TrendingUp, Users, Star, Calendar, Zap } from 'lucide-react';
export interface SponsorMetric {
icon: React.ElementType;
label: string;
value: string | number;
color?: string;
trend?: {
value: number;
isPositive: boolean;
};
}
export const MetricBuilders = {
views: (value: number, label = 'Views'): SponsorMetric => ({
icon: Eye,
label,
value,
color: 'text-primary-blue',
}),
engagement: (value: number | string): SponsorMetric => ({
icon: TrendingUp,
label: 'Engagement',
value: typeof value === 'number' ? `${value}%` : value,
color: 'text-performance-green',
}),
reach: (value: number): SponsorMetric => ({
icon: Users,
label: 'Est. Reach',
value,
color: 'text-purple-400',
}),
rating: (value: number | string, label = 'Rating'): SponsorMetric => ({
icon: Star,
label,
value,
color: 'text-warning-amber',
}),
races: (value: number): SponsorMetric => ({
icon: Calendar,
label: 'Races',
value,
color: 'text-neon-aqua',
}),
members: (value: number): SponsorMetric => ({
icon: Users,
label: 'Members',
value,
color: 'text-purple-400',
}),
impressions: (value: number): SponsorMetric => ({
icon: Eye,
label: 'Impressions',
value,
color: 'text-primary-blue',
}),
sof: (value: number | string): SponsorMetric => ({
icon: Zap,
label: 'Avg SOF',
value,
color: 'text-warning-amber',
}),
};

View File

@@ -0,0 +1,63 @@
export interface SponsorshipSlot {
tier: 'main' | 'secondary';
available: boolean;
price: number;
currency?: string;
benefits: string[];
}
export const SlotTemplates = {
league: (mainAvailable: boolean, secondaryAvailable: number, mainPrice: number, secondaryPrice: number): SponsorshipSlot[] => [
{
tier: 'main',
available: mainAvailable,
price: mainPrice,
benefits: ['Hood placement', 'League banner', 'Prominent logo'],
},
{
tier: 'secondary',
available: secondaryAvailable > 0,
price: secondaryPrice,
benefits: ['Side logo placement', 'League page listing'],
},
{
tier: 'secondary',
available: secondaryAvailable > 1,
price: secondaryPrice,
benefits: ['Side logo placement', 'League page listing'],
},
],
race: (mainAvailable: boolean, mainPrice: number): SponsorshipSlot[] => [
{
tier: 'main',
available: mainAvailable,
price: mainPrice,
benefits: ['Race title sponsor', 'Stream overlay', 'Results banner'],
},
],
driver: (available: boolean, price: number): SponsorshipSlot[] => [
{
tier: 'main',
available,
price,
benefits: ['Suit logo', 'Helmet branding', 'Social mentions'],
},
],
team: (mainAvailable: boolean, secondaryAvailable: boolean, mainPrice: number, secondaryPrice: number): SponsorshipSlot[] => [
{
tier: 'main',
available: mainAvailable,
price: mainPrice,
benefits: ['Team name suffix', 'Car livery', 'All driver suits'],
},
{
tier: 'secondary',
available: secondaryAvailable,
price: secondaryPrice,
benefits: ['Team page logo', 'Minor livery placement'],
},
],
};

View File

@@ -2,26 +2,20 @@
import Button from '@/components/ui/Button';
import Card from '@/components/ui/Card';
import { useAuth } from '@/lib/auth/AuthContext';
import { useInject } from '@/lib/di/hooks/useInject';
import { SPONSORSHIP_SERVICE_TOKEN } from '@/lib/di/tokens';
import { SPONSOR_SERVICE_TOKEN } from '@/lib/di/tokens';
import {
Activity,
Calendar,
Check,
Eye,
Loader2,
MessageCircle,
Shield,
Star,
Target,
TrendingUp,
Trophy,
Users,
Zap
Target
} from 'lucide-react';
import { useRouter } from 'next/navigation';
import React, { useCallback, useState } from 'react';
import { SponsorMetric, SponsorshipSlot } from './SponsorInsightsCardTypes';
import { getTierStyles, getEntityLabel, getEntityIcon, getSponsorshipTagline } from './SponsorInsightsCardHelpers';
// ============================================================================
// TYPES
@@ -29,25 +23,6 @@ import React, { useCallback, useState } from 'react';
export type EntityType = 'league' | 'race' | 'driver' | 'team';
export interface SponsorMetric {
icon: React.ElementType;
label: string;
value: string | number;
color?: string;
trend?: {
value: number;
isPositive: boolean;
};
}
export interface SponsorshipSlot {
tier: 'main' | 'secondary';
available: boolean;
price: number;
currency?: string;
benefits: string[];
}
export interface SponsorInsightsProps {
// Entity info
entityType: EntityType;
@@ -85,55 +60,6 @@ export interface SponsorInsightsProps {
onSponsorshipRequested?: (tier: 'main' | 'secondary') => void;
}
// ============================================================================
// HELPER FUNCTIONS
// ============================================================================
function getTierStyles(tier: SponsorInsightsProps['tier']) {
switch (tier) {
case 'premium':
return {
badge: 'bg-yellow-500/20 text-yellow-400 border-yellow-500/30',
gradient: 'from-yellow-500/10 via-transparent to-transparent',
};
case 'standard':
return {
badge: 'bg-blue-500/20 text-blue-400 border-blue-500/30',
gradient: 'from-blue-500/10 via-transparent to-transparent',
};
default:
return {
badge: 'bg-gray-500/20 text-gray-400 border-gray-500/30',
gradient: 'from-gray-500/10 via-transparent to-transparent',
};
}
}
function getEntityLabel(type: EntityType): string {
switch (type) {
case 'league': return 'League';
case 'race': return 'Race';
case 'driver': return 'Driver';
case 'team': return 'Team';
}
}
function getEntityIcon(type: EntityType) {
switch (type) {
case 'league': return Trophy;
case 'race': return Zap;
case 'driver': return Users;
case 'team': return Users;
}
}
function getSponsorshipTagline(type: EntityType): string {
if (type === 'league') {
return 'Reach engaged sim racers by sponsoring a season in this league.';
}
return `Reach engaged sim racers by sponsoring this ${getEntityLabel(type).toLowerCase()}`;
}
// ============================================================================
// COMPONENT
// ============================================================================
@@ -156,7 +82,7 @@ export default function SponsorInsightsCard({
}: SponsorInsightsProps) {
// TODO components should not fetch any data
const router = useRouter();
const sponsorshipService = useInject(SPONSORSHIP_SERVICE_TOKEN);
const sponsorshipService = useInject(SPONSOR_SERVICE_TOKEN);
const tierStyles = getTierStyles(tier);
const EntityIcon = getEntityIcon(entityType);
@@ -254,9 +180,9 @@ export default function SponsorInsightsCard({
{/* Key Metrics Grid */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 mb-4">
{metrics.slice(0, 4).map((metric, index) => {
const Icon = metric.icon;
const Icon = metric.icon as React.ComponentType<{ className?: string }>;
return (
<div
<div
key={index}
className="bg-iron-gray/50 rounded-lg p-3 border border-charcoal-outline"
>
@@ -439,157 +365,4 @@ export default function SponsorInsightsCard({
</div>
</Card>
);
}
// ============================================================================
// HELPER HOOK: useSponsorMode
// ============================================================================
export function useSponsorMode(): boolean {
const { session } = useAuth();
const [isSponsor, setIsSponsor] = React.useState(false);
React.useEffect(() => {
if (!session?.user) {
setIsSponsor(false);
return;
}
// Check session.user.role for sponsor
const role = session.user?.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;
}
// ============================================================================
// COMMON METRIC BUILDERS
// ============================================================================
export const MetricBuilders = {
views: (value: number, label = 'Views'): SponsorMetric => ({
icon: Eye,
label,
value,
color: 'text-primary-blue',
}),
engagement: (value: number | string): SponsorMetric => ({
icon: TrendingUp,
label: 'Engagement',
value: typeof value === 'number' ? `${value}%` : value,
color: 'text-performance-green',
}),
reach: (value: number): SponsorMetric => ({
icon: Users,
label: 'Est. Reach',
value,
color: 'text-purple-400',
}),
rating: (value: number | string, label = 'Rating'): SponsorMetric => ({
icon: Star,
label,
value,
color: 'text-warning-amber',
}),
races: (value: number): SponsorMetric => ({
icon: Calendar,
label: 'Races',
value,
color: 'text-neon-aqua',
}),
members: (value: number): SponsorMetric => ({
icon: Users,
label: 'Members',
value,
color: 'text-purple-400',
}),
impressions: (value: number): SponsorMetric => ({
icon: Eye,
label: 'Impressions',
value,
color: 'text-primary-blue',
}),
sof: (value: number | string): SponsorMetric => ({
icon: Zap,
label: 'Avg SOF',
value,
color: 'text-warning-amber',
}),
};
// ============================================================================
// SLOT TEMPLATES
// ============================================================================
export const SlotTemplates = {
league: (mainAvailable: boolean, secondaryAvailable: number, mainPrice: number, secondaryPrice: number): SponsorshipSlot[] => [
{
tier: 'main',
available: mainAvailable,
price: mainPrice,
benefits: ['Hood placement', 'League banner', 'Prominent logo'],
},
{
tier: 'secondary',
available: secondaryAvailable > 0,
price: secondaryPrice,
benefits: ['Side logo placement', 'League page listing'],
},
{
tier: 'secondary',
available: secondaryAvailable > 1,
price: secondaryPrice,
benefits: ['Side logo placement', 'League page listing'],
},
],
race: (mainAvailable: boolean, mainPrice: number): SponsorshipSlot[] => [
{
tier: 'main',
available: mainAvailable,
price: mainPrice,
benefits: ['Race title sponsor', 'Stream overlay', 'Results banner'],
},
],
driver: (available: boolean, price: number): SponsorshipSlot[] => [
{
tier: 'main',
available,
price,
benefits: ['Suit logo', 'Helmet branding', 'Social mentions'],
},
],
team: (mainAvailable: boolean, secondaryAvailable: boolean, mainPrice: number, secondaryPrice: number): SponsorshipSlot[] => [
{
tier: 'main',
available: mainAvailable,
price: mainPrice,
benefits: ['Team name suffix', 'Car livery', 'All driver suits'],
},
{
tier: 'secondary',
available: secondaryAvailable,
price: secondaryPrice,
benefits: ['Team page logo', 'Minor livery placement'],
},
],
};
}

View File

@@ -0,0 +1,52 @@
import { EntityType } from './SponsorInsightsCard';
import { Trophy, Zap, Users, Eye, TrendingUp, Star, Calendar, MessageCircle, Activity, Shield, Target } from 'lucide-react';
export interface TierStyles {
badge: string;
gradient: string;
}
export function getTierStyles(tier: 'premium' | 'standard' | 'starter'): TierStyles {
switch (tier) {
case 'premium':
return {
badge: 'bg-yellow-500/20 text-yellow-400 border-yellow-500/30',
gradient: 'from-yellow-500/10 via-transparent to-transparent',
};
case 'standard':
return {
badge: 'bg-blue-500/20 text-blue-400 border-blue-500/30',
gradient: 'from-blue-500/10 via-transparent to-transparent',
};
default:
return {
badge: 'bg-gray-500/20 text-gray-400 border-gray-500/30',
gradient: 'from-gray-500/10 via-transparent to-transparent',
};
}
}
export function getEntityLabel(type: EntityType): string {
switch (type) {
case 'league': return 'League';
case 'race': return 'Race';
case 'driver': return 'Driver';
case 'team': return 'Team';
}
}
export function getEntityIcon(type: EntityType) {
switch (type) {
case 'league': return Trophy;
case 'race': return Zap;
case 'driver': return Users;
case 'team': return Users;
}
}
export function getSponsorshipTagline(type: EntityType): string {
if (type === 'league') {
return 'Reach engaged sim racers by sponsoring a season in this league.';
}
return `Reach engaged sim racers by sponsoring this ${getEntityLabel(type).toLowerCase()}`;
}

View File

@@ -0,0 +1,20 @@
import { ComponentType } from 'react';
export interface SponsorMetric {
icon: ComponentType;
label: string;
value: string | number;
color?: string;
trend?: {
value: number;
isPositive: boolean;
};
}
export interface SponsorshipSlot {
tier: 'main' | 'secondary';
available: boolean;
price: number;
currency?: string;
benefits: string[];
}

View File

@@ -0,0 +1,29 @@
import { useAuth } from '@/lib/auth/AuthContext';
import React from 'react';
export function useSponsorMode(): boolean {
const { session } = useAuth();
const [isSponsor, setIsSponsor] = React.useState(false);
React.useEffect(() => {
if (!session) {
setIsSponsor(false);
return;
}
// Check session.role for sponsor
const role = session.role;
if (role === 'sponsor') {
setIsSponsor(true);
return;
}
// Fallback: check email patterns
const email = session.email?.toLowerCase() || '';
const displayName = session.displayName?.toLowerCase() || '';
setIsSponsor(email.includes('sponsor') || displayName.includes('sponsor'));
}, [session]);
return isSponsor;
}