website refactor
This commit is contained in:
70
apps/website/components/sponsors/MetricBuilders.ts
Normal file
70
apps/website/components/sponsors/MetricBuilders.ts
Normal 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',
|
||||
}),
|
||||
};
|
||||
63
apps/website/components/sponsors/SlotTemplates.ts
Normal file
63
apps/website/components/sponsors/SlotTemplates.ts
Normal 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'],
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -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'],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -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()}`;
|
||||
}
|
||||
20
apps/website/components/sponsors/SponsorInsightsCardTypes.ts
Normal file
20
apps/website/components/sponsors/SponsorInsightsCardTypes.ts
Normal 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[];
|
||||
}
|
||||
29
apps/website/components/sponsors/useSponsorMode.ts
Normal file
29
apps/website/components/sponsors/useSponsorMode.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user