website landing page
This commit is contained in:
@@ -3,69 +3,87 @@
|
||||
## Role
|
||||
You are **Ken Thompson**.
|
||||
You write minimal, correct code from precise objectives.
|
||||
You never explain methods.
|
||||
You never output anything except test-driven results.
|
||||
You never explain *how* you solved something.
|
||||
But you DO report **what changed**, **what passed**, and **what the system state is** — clearly and concisely.
|
||||
|
||||
You:
|
||||
- Follow strict TDD (RED → GREEN → Refactor).
|
||||
- Write the smallest code that works.
|
||||
- Use short, readable names (no abbreviations).
|
||||
- Keep every file single-purpose.
|
||||
- Remove all debug traces.
|
||||
You speak briefly, directly, and only in facts.
|
||||
|
||||
## Team Micro-Dialogue (Optional)
|
||||
Before producing your result, you may output a **tiny expert exchange**:
|
||||
- Booch: architecture insight (max 1 line)
|
||||
- Carmack: stability / correctness insight (max 1 line)
|
||||
- Thompson: implementation stance (max 1 line)
|
||||
|
||||
Maximum 3 lines.
|
||||
No fluff. No reasoning.
|
||||
Only insight.
|
||||
|
||||
Example style:
|
||||
- Booch: “Boundary consistent.”
|
||||
- Carmack: “Behavior stable.”
|
||||
- Thompson: “Applied minimal change.”
|
||||
|
||||
## Mission
|
||||
Given an objective, you deliver **one cohesive implementation package**:
|
||||
You deliver **one cohesive implementation package**:
|
||||
- one behavior
|
||||
- one change set
|
||||
- one reasoning flow
|
||||
- test-driven and minimal
|
||||
- one code change
|
||||
- one test cycle (RED → GREEN → Refactor)
|
||||
- nothing beyond the objective
|
||||
|
||||
You implement only what the objective requires — nothing else.
|
||||
You implement only what is required.
|
||||
|
||||
## Output Rules
|
||||
You output **one** compact `attempt_completion` with:
|
||||
|
||||
- `actions` — ≤ 140 chars (RED → GREEN → Refactor summary)
|
||||
- `tests` — ≤ 120 chars (relevant pass/fail summary)
|
||||
- `files` — list of affected files (each ≤ 60 chars)
|
||||
- `context` — ≤ 120 chars (area touched)
|
||||
- `notes` — max 2 bullets, each ≤ 100 chars
|
||||
- `actions` — ≤ 140 chars (what changed: RED→GREEN→REF)
|
||||
- `tests` — ≤ 120 chars (summary of pass/fail)
|
||||
- `files` — affected files (each ≤ 60 chars)
|
||||
- `context` — ≤ 120 chars (where the change applies)
|
||||
- `notes` — max 2 bullets (≤ 100 chars) with factual, non-method details
|
||||
|
||||
You must not:
|
||||
- output logs
|
||||
- output long text
|
||||
- output commentary
|
||||
- describe technique or reasoning
|
||||
- generate architecture
|
||||
- produce multi-purpose files
|
||||
You ARE allowed to say:
|
||||
- “added test for …”
|
||||
- “implemented missing behavior …”
|
||||
- “refactored selector logic …”
|
||||
- “aligned domain model …”
|
||||
- “removed unused paths …”
|
||||
|
||||
Only minimal, factual results.
|
||||
You are NOT allowed to:
|
||||
- explain how
|
||||
- write narrative
|
||||
- produce code explanations
|
||||
- justify design
|
||||
- include logs or verbose text
|
||||
|
||||
## Information Sweep
|
||||
You check only:
|
||||
- the objective
|
||||
- related tests
|
||||
- relevant files
|
||||
- previous expert output
|
||||
- tests that define the behavior
|
||||
- files touched by that behavior
|
||||
- results from previous experts
|
||||
|
||||
Stop once you know:
|
||||
1. what behavior to test
|
||||
2. what behavior to implement
|
||||
3. which files it touches
|
||||
1. what behavior to encode in RED
|
||||
2. what minimal change makes GREEN
|
||||
3. which files to touch
|
||||
|
||||
## File Discipline
|
||||
- One function/class per file.
|
||||
- Files must remain focused and compact.
|
||||
- Split immediately if a file grows beyond a single purpose.
|
||||
- Keep code small, clear, direct.
|
||||
- Keep files compact.
|
||||
- Split if a file grows beyond one purpose.
|
||||
- Maintain minimal, direct code.
|
||||
|
||||
## Constraints
|
||||
- No comments, scaffolding, or TODOs.
|
||||
- No speculative design.
|
||||
- No unnecessary abstractions.
|
||||
- Never silence lint/type errors — fix at the source.
|
||||
- Zero excess. Everything minimal.
|
||||
- No comments, TODOs, scaffolding.
|
||||
- No speculative abstractions.
|
||||
- Fix lint/type errors at source.
|
||||
- Zero excess.
|
||||
|
||||
## Completion
|
||||
You emit one compact `attempt_completion` with RED/GREEN/refactor results.
|
||||
You emit one compact `attempt_completion` containing:
|
||||
- what changed
|
||||
- what passed
|
||||
- what files moved
|
||||
- what context applied
|
||||
|
||||
Nothing else.
|
||||
@@ -20,6 +20,12 @@ NEXT_PUBLIC_GRIDPILOT_MODE=pre-launch
|
||||
# Site URL (for metadata and OG tags)
|
||||
NEXT_PUBLIC_SITE_URL=https://gridpilot.com
|
||||
|
||||
# Discord Community
|
||||
# Discord invite URL for the community CTA
|
||||
# Get this from: Discord Server Settings -> Invites -> Create Invite
|
||||
# Example: https://discord.gg/your-invite-code
|
||||
NEXT_PUBLIC_DISCORD_URL=https://discord.gg/your-invite-code
|
||||
|
||||
# Example for post-launch mode:
|
||||
# GRIDPILOT_MODE=post-launch
|
||||
# NEXT_PUBLIC_GRIDPILOT_MODE=post-launch
|
||||
@@ -1,147 +0,0 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { validateEmail, isDisposableEmail } from '@/lib/email-validation';
|
||||
import { checkRateLimit, getClientIp } from '@/lib/rate-limit';
|
||||
|
||||
const SIGNUP_LIST_KEY = 'signups:emails';
|
||||
const isDev = !process.env.KV_REST_API_URL;
|
||||
|
||||
// In-memory fallback for development
|
||||
const devSignups = new Map<string, { email: string; timestamp: string; ip: string }>();
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { email } = body;
|
||||
|
||||
if (!email || typeof email !== 'string') {
|
||||
return NextResponse.json(
|
||||
{ error: "That email doesn't look right." },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const validation = validateEmail(email);
|
||||
if (!validation.success) {
|
||||
return NextResponse.json(
|
||||
{ error: "That email doesn't look right." },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const sanitizedEmail = validation.email!;
|
||||
|
||||
if (isDisposableEmail(sanitizedEmail)) {
|
||||
return NextResponse.json(
|
||||
{ error: "That email doesn't look right." },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const clientIp = getClientIp(request);
|
||||
const rateLimitResult = await checkRateLimit(clientIp);
|
||||
|
||||
if (!rateLimitResult.allowed) {
|
||||
const retrySeconds = Math.ceil((rateLimitResult.resetAt - Date.now()) / 1000);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Too fast. Try again in a minute.',
|
||||
resetAt: rateLimitResult.resetAt,
|
||||
retryAfter: retrySeconds,
|
||||
},
|
||||
{
|
||||
status: 429,
|
||||
headers: {
|
||||
'Retry-After': retrySeconds.toString(),
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (isDev) {
|
||||
console.warn('[DEV MODE] Using in-memory signup storage - data will not persist');
|
||||
|
||||
if (devSignups.has(sanitizedEmail)) {
|
||||
return NextResponse.json(
|
||||
{ error: "Already got you. I'll keep you posted." },
|
||||
{ status: 409 }
|
||||
);
|
||||
}
|
||||
|
||||
const signupData = {
|
||||
email: sanitizedEmail,
|
||||
timestamp: new Date().toISOString(),
|
||||
ip: clientIp,
|
||||
};
|
||||
|
||||
devSignups.set(sanitizedEmail, signupData);
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
message: 'Thanks. That means a lot.',
|
||||
},
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
'X-RateLimit-Remaining': rateLimitResult.remaining.toString(),
|
||||
'X-RateLimit-Reset': rateLimitResult.resetAt.toString(),
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Production: Use Vercel KV
|
||||
const { kv } = await import('@vercel/kv');
|
||||
|
||||
const existingSignup = await kv.hget(SIGNUP_LIST_KEY, sanitizedEmail);
|
||||
|
||||
if (existingSignup) {
|
||||
return NextResponse.json(
|
||||
{ error: "Already got you. I'll keep you posted." },
|
||||
{ status: 409 }
|
||||
);
|
||||
}
|
||||
|
||||
const signupData = {
|
||||
email: sanitizedEmail,
|
||||
timestamp: new Date().toISOString(),
|
||||
ip: clientIp,
|
||||
};
|
||||
|
||||
await kv.hset(SIGNUP_LIST_KEY, {
|
||||
[sanitizedEmail]: JSON.stringify(signupData),
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
message: 'Thanks. That means a lot.',
|
||||
},
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
'X-RateLimit-Remaining': rateLimitResult.remaining.toString(),
|
||||
'X-RateLimit-Reset': rateLimitResult.resetAt.toString(),
|
||||
},
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Signup error:', error);
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: 'Something broke. Try again?' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/signup
|
||||
* Return 405 Method Not Allowed
|
||||
*/
|
||||
export async function GET() {
|
||||
return NextResponse.json(
|
||||
{ error: 'Method not allowed' },
|
||||
{ status: 405 }
|
||||
);
|
||||
}
|
||||
@@ -13,10 +13,61 @@
|
||||
--color-performance-green: #6FE37A;
|
||||
--color-warning-amber: #FFC556;
|
||||
--color-neon-aqua: #43C9E6;
|
||||
--sat: env(safe-area-inset-top);
|
||||
--sar: env(safe-area-inset-right);
|
||||
--sab: env(safe-area-inset-bottom);
|
||||
--sal: env(safe-area-inset-left);
|
||||
}
|
||||
|
||||
* {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
-webkit-touch-callout: none;
|
||||
}
|
||||
|
||||
html {
|
||||
overscroll-behavior: none;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-deep-graphite text-white antialiased;
|
||||
overscroll-behavior: none;
|
||||
}
|
||||
|
||||
button, a {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
.scroll-container {
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
/* Mobile typography optimization - lighter and more spacious */
|
||||
@media (max-width: 640px) {
|
||||
h1 {
|
||||
font-size: clamp(1.5rem, 6vw, 2rem);
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: clamp(1.125rem, 4.5vw, 1.5rem);
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 0.8125rem; /* 13px */
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,4 +75,159 @@
|
||||
.animate-spring {
|
||||
transition-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
|
||||
/* Racing stripe patterns */
|
||||
.racing-stripes {
|
||||
background: linear-gradient(
|
||||
45deg,
|
||||
transparent 25%,
|
||||
rgba(25, 140, 255, 0.03) 25%,
|
||||
rgba(25, 140, 255, 0.03) 50%,
|
||||
transparent 50%,
|
||||
transparent 75%,
|
||||
rgba(25, 140, 255, 0.03) 75%
|
||||
);
|
||||
background-size: 60px 60px;
|
||||
}
|
||||
|
||||
/* Checkered flag pattern */
|
||||
.checkered-pattern {
|
||||
background-image:
|
||||
linear-gradient(45deg, rgba(255,255,255,0.02) 25%, transparent 25%),
|
||||
linear-gradient(-45deg, rgba(255,255,255,0.02) 25%, transparent 25%),
|
||||
linear-gradient(45deg, transparent 75%, rgba(255,255,255,0.02) 75%),
|
||||
linear-gradient(-45deg, transparent 75%, rgba(255,255,255,0.02) 75%);
|
||||
background-size: 20px 20px;
|
||||
background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
|
||||
}
|
||||
|
||||
/* Speed lines animation */
|
||||
@keyframes speed-lines {
|
||||
0% {
|
||||
transform: translateX(0) scaleX(0);
|
||||
opacity: 0;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.3;
|
||||
}
|
||||
100% {
|
||||
transform: translateX(100px) scaleX(1);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-speed-lines {
|
||||
animation: speed-lines 1.5s ease-out infinite;
|
||||
}
|
||||
|
||||
/* Racing accent line */
|
||||
.racing-accent {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.racing-accent::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -16px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 3px;
|
||||
background: linear-gradient(to bottom, #FF0000, #198CFF);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* Carbon fiber texture */
|
||||
.carbon-fiber {
|
||||
background-image:
|
||||
linear-gradient(27deg, rgba(255,255,255,0.02) 5%, transparent 5%),
|
||||
linear-gradient(207deg, rgba(255,255,255,0.02) 5%, transparent 5%),
|
||||
linear-gradient(27deg, rgba(0,0,0,0.05) 5%, transparent 5%),
|
||||
linear-gradient(207deg, rgba(0,0,0,0.05) 5%, transparent 5%);
|
||||
background-size: 10px 10px;
|
||||
}
|
||||
|
||||
/* Racing red-white-blue animated gradient */
|
||||
@keyframes racing-gradient {
|
||||
0% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-racing-gradient {
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
#DC0000 0%,
|
||||
#FFFFFF 25%,
|
||||
#0066FF 50%,
|
||||
#DC0000 75%,
|
||||
#FFFFFF 100%
|
||||
);
|
||||
background-size: 300% 100%;
|
||||
animation: racing-gradient 12s linear infinite;
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
/* Static red-white-blue gradient (no animation) */
|
||||
.static-racing-gradient {
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
#DC0000 0%,
|
||||
#FFFFFF 50%,
|
||||
#2563eb 100%
|
||||
);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.animate-racing-gradient {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Entrance animations */
|
||||
@keyframes fade-in-up {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-fade-in-up {
|
||||
animation: fade-in-up 0.6s ease-out forwards;
|
||||
}
|
||||
|
||||
.animate-fade-in {
|
||||
animation: fade-in 0.4s ease-out forwards;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.animate-fade-in-up,
|
||||
.animate-fade-in {
|
||||
animation: none;
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,18 @@ import './globals.css';
|
||||
export const metadata: Metadata = {
|
||||
title: 'GridPilot - iRacing League Racing Platform',
|
||||
description: 'The dedicated home for serious iRacing leagues. Automatic results, standings, team racing, and professional race control.',
|
||||
viewport: {
|
||||
width: 'device-width',
|
||||
initialScale: 1,
|
||||
maximumScale: 1,
|
||||
userScalable: false,
|
||||
viewportFit: 'cover',
|
||||
},
|
||||
themeColor: '#0a0a0a',
|
||||
appleWebApp: {
|
||||
capable: true,
|
||||
statusBarStyle: 'black-translucent',
|
||||
},
|
||||
openGraph: {
|
||||
title: 'GridPilot - iRacing League Racing Platform',
|
||||
description: 'Structure over chaos. The professional platform for iRacing league racing.',
|
||||
@@ -22,8 +34,11 @@ export default function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="en" className="scroll-smooth">
|
||||
<body className="antialiased">
|
||||
<html lang="en" className="scroll-smooth overflow-x-hidden">
|
||||
<head>
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
</head>
|
||||
<body className="antialiased overflow-x-hidden">
|
||||
<header className="fixed top-0 left-0 right-0 z-50 bg-deep-graphite/80 backdrop-blur-sm border-b border-white/5">
|
||||
<div className="max-w-7xl mx-auto px-6 py-4">
|
||||
<div className="flex items-baseline space-x-3">
|
||||
|
||||
@@ -2,13 +2,14 @@ import { ModeGuard } from '@/components/shared/ModeGuard';
|
||||
import Hero from '@/components/landing/Hero';
|
||||
import AlternatingSection from '@/components/landing/AlternatingSection';
|
||||
import FeatureGrid from '@/components/landing/FeatureGrid';
|
||||
import EmailCapture from '@/components/landing/EmailCapture';
|
||||
import DiscordCTA from '@/components/landing/DiscordCTA';
|
||||
import FAQ from '@/components/landing/FAQ';
|
||||
import Footer from '@/components/landing/Footer';
|
||||
import CareerProgressionMockup from '@/components/mockups/CareerProgressionMockup';
|
||||
import RaceHistoryMockup from '@/components/mockups/RaceHistoryMockup';
|
||||
import CompanionAutomationMockup from '@/components/mockups/CompanionAutomationMockup';
|
||||
import SimPlatformMockup from '@/components/mockups/SimPlatformMockup';
|
||||
import MockupStack from '@/components/ui/MockupStack';
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
@@ -19,25 +20,47 @@ export default function HomePage() {
|
||||
{/* Section 1: A Persistent Identity */}
|
||||
<AlternatingSection
|
||||
heading="A Persistent Identity"
|
||||
backgroundVideo="/gameplay.mp4"
|
||||
description={
|
||||
<>
|
||||
<p>
|
||||
Your races, your seasons, your progress — finally in one place.
|
||||
</p>
|
||||
<ul className="space-y-2 mt-4">
|
||||
<li className="flex items-center gap-2">
|
||||
<span className="text-primary-blue">•</span>
|
||||
<span>Lifetime stats and season history across all your leagues</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<span className="text-primary-blue">•</span>
|
||||
<span>Track your performance, consistency, and team contributions</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<span className="text-primary-blue">•</span>
|
||||
<span>Your own rating that reflects real league competition</span>
|
||||
</li>
|
||||
</ul>
|
||||
<div className="space-y-3 mt-4">
|
||||
<div className="group relative overflow-hidden rounded-lg bg-gradient-to-r from-slate-900/60 via-slate-800/40 to-slate-900/60 p-4 border border-slate-700/40 hover:border-primary-blue/50 transition-all duration-300 hover:shadow-[0_0_25px_rgba(59,130,246,0.15)]">
|
||||
<div className="absolute top-0 left-0 w-full h-0.5 bg-gradient-to-r from-transparent via-primary-blue/60 to-transparent opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex-shrink-0 w-9 h-9 rounded-lg bg-gradient-to-br from-primary-blue/20 to-blue-900/20 border border-primary-blue/30 flex items-center justify-center shadow-lg group-hover:scale-105 transition-transform">
|
||||
<svg className="w-5 h-5 text-primary-blue" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<span className="text-slate-200 leading-relaxed font-light">Lifetime stats and season history across all your leagues</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="group relative overflow-hidden rounded-lg bg-gradient-to-r from-slate-900/60 via-slate-800/40 to-slate-900/60 p-4 border border-slate-700/40 hover:border-primary-blue/50 transition-all duration-300 hover:shadow-[0_0_25px_rgba(59,130,246,0.15)]">
|
||||
<div className="absolute top-0 left-0 w-full h-0.5 bg-gradient-to-r from-transparent via-primary-blue/60 to-transparent opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex-shrink-0 w-9 h-9 rounded-lg bg-gradient-to-br from-primary-blue/20 to-blue-900/20 border border-primary-blue/30 flex items-center justify-center shadow-lg group-hover:scale-105 transition-transform">
|
||||
<svg className="w-5 h-5 text-primary-blue" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<span className="text-slate-200 leading-relaxed font-light">Track your performance, consistency, and team contributions</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="group relative overflow-hidden rounded-lg bg-gradient-to-r from-slate-900/60 via-slate-800/40 to-slate-900/60 p-4 border border-slate-700/40 hover:border-primary-blue/50 transition-all duration-300 hover:shadow-[0_0_25px_rgba(59,130,246,0.15)]">
|
||||
<div className="absolute top-0 left-0 w-full h-0.5 bg-gradient-to-r from-transparent via-primary-blue/60 to-transparent opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex-shrink-0 w-9 h-9 rounded-lg bg-gradient-to-br from-primary-blue/20 to-blue-900/20 border border-primary-blue/30 flex items-center justify-center shadow-lg group-hover:scale-105 transition-transform">
|
||||
<svg className="w-5 h-5 text-primary-blue" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<span className="text-slate-200 leading-relaxed font-light">Your own rating that reflects real league competition</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-4">
|
||||
iRacing gives you physics. GridPilot gives you a career.
|
||||
</p>
|
||||
@@ -55,29 +78,50 @@ export default function HomePage() {
|
||||
backgroundImage="/images/ff1600.jpeg"
|
||||
description={
|
||||
<>
|
||||
<p>
|
||||
<p className="text-sm md:text-base leading-relaxed">
|
||||
Every race you run stays with you.
|
||||
</p>
|
||||
<ul className="space-y-2 mt-4">
|
||||
<li className="flex items-center gap-2">
|
||||
<span className="text-primary-blue">•</span>
|
||||
<span>Your stats, your team, your story — all connected</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<span className="text-primary-blue">•</span>
|
||||
<span>One race result updates your profile, team points, rating, and season history</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<span className="text-primary-blue">•</span>
|
||||
<span>No more fragmented data across spreadsheets and forums</span>
|
||||
</li>
|
||||
</ul>
|
||||
<p className="mt-4">
|
||||
<div className="space-y-3 mt-4 md:mt-6">
|
||||
<div className="group relative overflow-hidden rounded-lg bg-gradient-to-r from-slate-900/60 via-slate-800/40 to-slate-900/60 p-3.5 md:p-4 border border-slate-700/40 hover:border-red-600/50 transition-all duration-300 hover:shadow-[0_0_25px_rgba(220,38,38,0.15)]">
|
||||
<div className="absolute top-0 right-0 w-12 h-12 bg-gradient-to-bl from-red-600/10 to-transparent rounded-bl-3xl opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||
<div className="flex items-start gap-2.5 md:gap-3">
|
||||
<div className="flex-shrink-0 w-8 h-8 md:w-9 md:h-9 rounded-lg bg-gradient-to-br from-red-600/20 to-red-900/20 border border-red-600/30 flex items-center justify-center shadow-lg group-hover:scale-105 transition-transform">
|
||||
<svg className="w-4 h-4 md:w-5 md:h-5 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" />
|
||||
</svg>
|
||||
</div>
|
||||
<span className="text-slate-200 text-sm md:text-base leading-relaxed font-light">Your stats, your team, your story — all connected</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="group relative overflow-hidden rounded-lg bg-gradient-to-r from-slate-900/60 via-slate-800/40 to-slate-900/60 p-3.5 md:p-4 border border-slate-700/40 hover:border-red-600/50 transition-all duration-300 hover:shadow-[0_0_25px_rgba(220,38,38,0.15)]">
|
||||
<div className="absolute top-0 right-0 w-12 h-12 bg-gradient-to-bl from-red-600/10 to-transparent rounded-bl-3xl opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||
<div className="flex items-start gap-2.5 md:gap-3">
|
||||
<div className="flex-shrink-0 w-8 h-8 md:w-9 md:h-9 rounded-lg bg-gradient-to-br from-red-600/20 to-red-900/20 border border-red-600/30 flex items-center justify-center shadow-lg group-hover:scale-105 transition-transform">
|
||||
<svg className="w-4 h-4 md:w-5 md:h-5 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
</div>
|
||||
<span className="text-slate-200 text-sm md:text-base leading-relaxed font-light">One race result updates your profile, team points, rating, and season history</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="group relative overflow-hidden rounded-lg bg-gradient-to-r from-slate-900/60 via-slate-800/40 to-slate-900/60 p-3.5 md:p-4 border border-slate-700/40 hover:border-red-600/50 transition-all duration-300 hover:shadow-[0_0_25px_rgba(220,38,38,0.15)]">
|
||||
<div className="absolute top-0 right-0 w-12 h-12 bg-gradient-to-bl from-red-600/10 to-transparent rounded-bl-3xl opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||
<div className="flex items-start gap-2.5 md:gap-3">
|
||||
<div className="flex-shrink-0 w-8 h-8 md:w-9 md:h-9 rounded-lg bg-gradient-to-br from-red-600/20 to-red-900/20 border border-red-600/30 flex items-center justify-center shadow-lg group-hover:scale-105 transition-transform">
|
||||
<svg className="w-4 h-4 md:w-5 md:h-5 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<span className="text-slate-200 text-sm md:text-base leading-relaxed font-light">No more fragmented data across spreadsheets and forums</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-4 md:mt-6 text-sm md:text-base leading-relaxed">
|
||||
Your racing career, finally in one place.
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
mockup={<RaceHistoryMockup />}
|
||||
mockup={<MockupStack index={1}><RaceHistoryMockup /></MockupStack>}
|
||||
layout="text-right"
|
||||
/>
|
||||
|
||||
@@ -86,28 +130,50 @@ export default function HomePage() {
|
||||
heading="Automatic Session Creation"
|
||||
description={
|
||||
<>
|
||||
<p>
|
||||
<p className="text-sm md:text-base leading-relaxed">
|
||||
Setting up league races used to mean clicking through iRacing's wizard 20 times.
|
||||
</p>
|
||||
<ul className="space-y-2 mt-4">
|
||||
<li className="flex items-center gap-2">
|
||||
<span className="text-primary-blue">•</span>
|
||||
<span>Our companion app syncs with your league schedule</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<span className="text-primary-blue">•</span>
|
||||
<span>When it's race time, it creates the iRacing session automatically</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<span className="text-primary-blue">•</span>
|
||||
<span>No clicking through wizards. No manual setup</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<span className="text-primary-blue">•</span>
|
||||
<span>Runs on your machine, totally transparent, completely safe</span>
|
||||
</li>
|
||||
</ul>
|
||||
<p className="mt-4">
|
||||
<div className="space-y-3 mt-4 md:mt-6">
|
||||
<div className="group relative overflow-hidden rounded-lg bg-gradient-to-br from-slate-900/70 to-slate-800/50 p-3.5 md:p-4 border border-slate-700/50 hover:border-primary-blue/60 transition-all duration-300 hover:shadow-[0_0_30px_rgba(59,130,246,0.2)]">
|
||||
<div className="absolute -top-12 -right-12 w-24 h-24 bg-gradient-to-br from-primary-blue/10 to-transparent rounded-full blur-2xl opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||
<div className="flex items-start gap-2.5 md:gap-3 relative">
|
||||
<div className="flex-shrink-0 w-9 h-9 md:w-10 md:h-10 rounded-xl bg-gradient-to-br from-primary-blue/25 to-blue-900/25 border border-primary-blue/40 flex items-center justify-center shadow-lg group-hover:shadow-primary-blue/20 group-hover:scale-110 transition-all">
|
||||
<span className="text-primary-blue font-bold text-sm">1</span>
|
||||
</div>
|
||||
<span className="text-slate-200 text-sm md:text-base leading-relaxed font-light">Our companion app syncs with your league schedule</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="group relative overflow-hidden rounded-lg bg-gradient-to-br from-slate-900/70 to-slate-800/50 p-3.5 md:p-4 border border-slate-700/50 hover:border-primary-blue/60 transition-all duration-300 hover:shadow-[0_0_30px_rgba(59,130,246,0.2)]">
|
||||
<div className="absolute -top-12 -right-12 w-24 h-24 bg-gradient-to-br from-primary-blue/10 to-transparent rounded-full blur-2xl opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||
<div className="flex items-start gap-2.5 md:gap-3 relative">
|
||||
<div className="flex-shrink-0 w-9 h-9 md:w-10 md:h-10 rounded-xl bg-gradient-to-br from-primary-blue/25 to-blue-900/25 border border-primary-blue/40 flex items-center justify-center shadow-lg group-hover:shadow-primary-blue/20 group-hover:scale-110 transition-all">
|
||||
<span className="text-primary-blue font-bold text-sm">2</span>
|
||||
</div>
|
||||
<span className="text-slate-200 text-sm md:text-base leading-relaxed font-light">When it's race time, it creates the iRacing session automatically</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="group relative overflow-hidden rounded-lg bg-gradient-to-br from-slate-900/70 to-slate-800/50 p-3.5 md:p-4 border border-slate-700/50 hover:border-primary-blue/60 transition-all duration-300 hover:shadow-[0_0_30px_rgba(59,130,246,0.2)]">
|
||||
<div className="absolute -top-12 -right-12 w-24 h-24 bg-gradient-to-br from-primary-blue/10 to-transparent rounded-full blur-2xl opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||
<div className="flex items-start gap-2.5 md:gap-3 relative">
|
||||
<div className="flex-shrink-0 w-9 h-9 md:w-10 md:h-10 rounded-xl bg-gradient-to-br from-primary-blue/25 to-blue-900/25 border border-primary-blue/40 flex items-center justify-center shadow-lg group-hover:shadow-primary-blue/20 group-hover:scale-110 transition-all">
|
||||
<span className="text-primary-blue font-bold text-sm">3</span>
|
||||
</div>
|
||||
<span className="text-slate-200 text-sm md:text-base leading-relaxed font-light">No clicking through wizards. No manual setup</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="group relative overflow-hidden rounded-lg bg-gradient-to-br from-slate-900/70 to-slate-800/50 p-3.5 md:p-4 border border-slate-700/50 hover:border-green-600/60 transition-all duration-300 hover:shadow-[0_0_30px_rgba(34,197,94,0.2)]">
|
||||
<div className="absolute -top-12 -right-12 w-24 h-24 bg-gradient-to-br from-green-600/10 to-transparent rounded-full blur-2xl opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||
<div className="flex items-start gap-2.5 md:gap-3 relative">
|
||||
<div className="flex-shrink-0 w-9 h-9 md:w-10 md:h-10 rounded-xl bg-gradient-to-br from-green-600/25 to-green-900/25 border border-green-600/40 flex items-center justify-center shadow-lg group-hover:shadow-green-600/20 group-hover:scale-110 transition-all">
|
||||
<svg className="w-4 h-4 md:w-5 md:h-5 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
||||
</svg>
|
||||
</div>
|
||||
<span className="text-slate-200 text-sm md:text-base leading-relaxed font-light">Runs on your machine, totally transparent, completely safe</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-4 md:mt-6 text-sm md:text-base leading-relaxed">
|
||||
Automation instead of repetition.
|
||||
</p>
|
||||
</>
|
||||
@@ -122,16 +188,16 @@ export default function HomePage() {
|
||||
backgroundImage="/images/lmp3.jpeg"
|
||||
description={
|
||||
<>
|
||||
<p>
|
||||
<p className="text-sm md:text-base leading-relaxed">
|
||||
Right now, we're focused on making iRacing league racing better.
|
||||
</p>
|
||||
<p className="mt-4">
|
||||
<p className="mt-4 md:mt-6 text-sm md:text-base leading-relaxed">
|
||||
But sims come and go. Your leagues, your teams, your rating — those stay.
|
||||
</p>
|
||||
<p className="mt-4">
|
||||
<p className="mt-4 md:mt-6 text-sm md:text-base leading-relaxed">
|
||||
GridPilot is built to outlast any single platform.
|
||||
</p>
|
||||
<p className="mt-4">
|
||||
<p className="mt-4 md:mt-6 text-sm md:text-base leading-relaxed">
|
||||
When the next sim arrives, your competitive identity moves with you.
|
||||
</p>
|
||||
</>
|
||||
@@ -140,7 +206,7 @@ export default function HomePage() {
|
||||
layout="text-right"
|
||||
/>
|
||||
|
||||
<EmailCapture />
|
||||
<DiscordCTA />
|
||||
<FAQ />
|
||||
<Footer />
|
||||
</main>
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { ReactNode } from 'react';
|
||||
'use client';
|
||||
|
||||
import { useRef, ReactNode } from 'react';
|
||||
import Container from '@/components/ui/Container';
|
||||
import Heading from '@/components/ui/Heading';
|
||||
import { useScrollProgress, useParallax } from '@/hooks/useScrollProgress';
|
||||
|
||||
interface AlternatingSectionProps {
|
||||
heading: string;
|
||||
@@ -7,6 +11,7 @@ interface AlternatingSectionProps {
|
||||
mockup: ReactNode;
|
||||
layout: 'text-left' | 'text-right';
|
||||
backgroundImage?: string;
|
||||
backgroundVideo?: string;
|
||||
}
|
||||
|
||||
export default function AlternatingSection({
|
||||
@@ -14,73 +19,88 @@ export default function AlternatingSection({
|
||||
description,
|
||||
mockup,
|
||||
layout,
|
||||
backgroundImage
|
||||
backgroundImage,
|
||||
backgroundVideo
|
||||
}: AlternatingSectionProps) {
|
||||
const sectionRef = useRef<HTMLElement>(null);
|
||||
|
||||
const bgParallax = useParallax(sectionRef, 0.2);
|
||||
|
||||
return (
|
||||
<section className="relative overflow-hidden bg-deep-graphite px-6 py-24 sm:py-32 lg:px-8">
|
||||
{backgroundImage && (
|
||||
<div
|
||||
className="absolute inset-0 bg-cover bg-center"
|
||||
style={{
|
||||
backgroundImage: `url(${backgroundImage})`,
|
||||
maskImage: 'radial-gradient(ellipse at center, rgba(0,0,0,0.12) 0%, rgba(0,0,0,0.06) 40%, transparent 70%)',
|
||||
WebkitMaskImage: 'radial-gradient(ellipse at center, rgba(0,0,0,0.12) 0%, rgba(0,0,0,0.06) 40%, transparent 70%)'
|
||||
}}
|
||||
/>
|
||||
<section ref={sectionRef} className="relative overflow-hidden bg-deep-graphite px-[calc(1rem+var(--sal))] pr-[calc(1rem+var(--sar))] py-20 sm:py-24 md:py-32 md:px-[calc(2rem+var(--sal))] md:pr-[calc(2rem+var(--sar))] lg:px-8">
|
||||
{backgroundVideo && (
|
||||
<>
|
||||
<video
|
||||
autoPlay
|
||||
loop
|
||||
muted
|
||||
playsInline
|
||||
className="absolute inset-0 w-full h-full object-cover opacity-20 md:opacity-30"
|
||||
style={{
|
||||
maskImage: 'radial-gradient(ellipse at center, black 0%, rgba(0,0,0,0.8) 40%, transparent 70%)',
|
||||
WebkitMaskImage: 'radial-gradient(ellipse at center, black 0%, rgba(0,0,0,0.8) 40%, transparent 70%)',
|
||||
}}
|
||||
>
|
||||
<source src={backgroundVideo} type="video/mp4" />
|
||||
</video>
|
||||
{/* Racing red accent for sections with background videos */}
|
||||
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-red-500/30 to-transparent" />
|
||||
</>
|
||||
)}
|
||||
{backgroundImage && !backgroundVideo && (
|
||||
<>
|
||||
<div
|
||||
className="absolute inset-0 bg-cover bg-center"
|
||||
style={{
|
||||
backgroundImage: `url(${backgroundImage})`,
|
||||
maskImage: 'radial-gradient(ellipse at center, rgba(0,0,0,0.18) 0%, rgba(0,0,0,0.1) 40%, transparent 70%)',
|
||||
WebkitMaskImage: 'radial-gradient(ellipse at center, rgba(0,0,0,0.18) 0%, rgba(0,0,0,0.1) 40%, transparent 70%)',
|
||||
transform: `translateY(${bgParallax * 0.3}px)`
|
||||
}}
|
||||
/>
|
||||
{/* Racing red accent for sections with background images */}
|
||||
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-red-500/30 to-transparent" />
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Carbon fiber texture on sections without images or videos */}
|
||||
{!backgroundImage && !backgroundVideo && (
|
||||
<div className="absolute inset-0 carbon-fiber opacity-30" />
|
||||
)}
|
||||
|
||||
{/* Checkered pattern accent */}
|
||||
<div className="absolute inset-0 checkered-pattern opacity-10" />
|
||||
|
||||
<Container size="lg" className="relative z-10">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-16 items-center">
|
||||
{layout === 'text-left' ? (
|
||||
<>
|
||||
{/* Text Content - Left */}
|
||||
<div className="space-y-6">
|
||||
<h2 className="text-3xl sm:text-4xl font-semibold text-white leading-tight">
|
||||
{heading}
|
||||
</h2>
|
||||
<div className="text-lg text-slate-400 font-light leading-relaxed space-y-4">
|
||||
{description}
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 md:gap-12 lg:gap-16 items-center">
|
||||
{/* Text Content - Always first on mobile, respects layout on desktop */}
|
||||
<div
|
||||
className={`space-y-4 md:space-y-6 lg:space-y-8 ${layout === 'text-right' ? 'lg:order-2' : ''}`}
|
||||
style={{
|
||||
opacity: 1,
|
||||
transform: 'translateX(0)'
|
||||
}}
|
||||
>
|
||||
<Heading level={2} className="text-xl md:text-2xl lg:text-3xl xl:text-4xl bg-gradient-to-r from-red-600 via-white to-blue-600 bg-clip-text text-transparent font-medium drop-shadow-[0_0_15px_rgba(220,0,0,0.4)] static-racing-gradient" style={{ WebkitTextStroke: '0.5px rgba(220,0,0,0.2)' }}>
|
||||
{heading}
|
||||
</Heading>
|
||||
<div className="text-sm md:text-base lg:text-lg text-slate-400 font-light leading-relaxed md:leading-loose space-y-3 md:space-y-5">
|
||||
{description}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mockup - Right (fade right) */}
|
||||
<div className="relative">
|
||||
<div
|
||||
className="w-full h-[400px] lg:h-[500px]"
|
||||
style={{
|
||||
maskImage: 'linear-gradient(to right, white 50%, rgba(255,255,255,0.8) 70%, rgba(255,255,255,0.4) 85%, transparent 100%)',
|
||||
WebkitMaskImage: 'linear-gradient(to right, white 50%, rgba(255,255,255,0.8) 70%, rgba(255,255,255,0.4) 85%, transparent 100%)'
|
||||
}}
|
||||
>
|
||||
{mockup}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{/* Mockup - Left (fade left) */}
|
||||
<div className="relative order-2 lg:order-1">
|
||||
<div
|
||||
className="w-full h-[400px] lg:h-[500px]"
|
||||
style={{
|
||||
maskImage: 'linear-gradient(to left, white 50%, rgba(255,255,255,0.8) 70%, rgba(255,255,255,0.4) 85%, transparent 100%)',
|
||||
WebkitMaskImage: 'linear-gradient(to left, white 50%, rgba(255,255,255,0.8) 70%, rgba(255,255,255,0.4) 85%, transparent 100%)'
|
||||
}}
|
||||
>
|
||||
{mockup}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Text Content - Right */}
|
||||
<div className="space-y-6 order-1 lg:order-2">
|
||||
<h2 className="text-3xl sm:text-4xl font-semibold text-white leading-tight">
|
||||
{heading}
|
||||
</h2>
|
||||
<div className="text-lg text-slate-400 font-light leading-relaxed space-y-4">
|
||||
{description}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{/* Mockup - Always second on mobile, respects layout on desktop */}
|
||||
<div
|
||||
className={`relative group ${layout === 'text-right' ? 'lg:order-1' : ''}`}
|
||||
style={{
|
||||
opacity: 1,
|
||||
transform: 'translateX(0) scale(1)'
|
||||
}}
|
||||
>
|
||||
<div className={`w-full min-h-[240px] md:h-[380px] lg:h-[440px] transition-transform duration-speed group-hover:scale-[1.02] ${layout === 'text-left' ? 'md:[mask-image:linear-gradient(to_right,white_50%,rgba(255,255,255,0.8)_70%,rgba(255,255,255,0.4)_85%,transparent_100%)] md:[-webkit-mask-image:linear-gradient(to_right,white_50%,rgba(255,255,255,0.8)_70%,rgba(255,255,255,0.4)_85%,transparent_100%)]' : 'md:[mask-image:linear-gradient(to_left,white_50%,rgba(255,255,255,0.8)_70%,rgba(255,255,255,0.4)_85%,transparent_100%)] md:[-webkit-mask-image:linear-gradient(to_left,white_50%,rgba(255,255,255,0.8)_70%,rgba(255,255,255,0.4)_85%,transparent_100%)]'}`}>
|
||||
{mockup}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</section>
|
||||
|
||||
165
apps/website/components/landing/DiscordCTA.tsx
Normal file
165
apps/website/components/landing/DiscordCTA.tsx
Normal file
@@ -0,0 +1,165 @@
|
||||
'use client';
|
||||
|
||||
import { useRef } from 'react';
|
||||
import Button from '@/components/ui/Button';
|
||||
import { useScrollProgress } from '@/hooks/useScrollProgress';
|
||||
|
||||
export default function DiscordCTA() {
|
||||
const discordUrl = process.env.NEXT_PUBLIC_DISCORD_URL || '#';
|
||||
|
||||
return (
|
||||
<section id="community" className="relative py-4 md:py-12 lg:py-16 bg-gradient-to-b from-deep-graphite to-iron-gray">
|
||||
<div className="max-w-4xl mx-auto px-2 md:px-3 lg:px-4">
|
||||
<div
|
||||
className="relative rounded-xl bg-gradient-to-br from-iron-gray via-deep-graphite to-iron-gray p-3 md:p-6 lg:p-10 border border-charcoal-outline shadow-[0_0_80px_rgba(88,101,242,0.15)]"
|
||||
style={{
|
||||
opacity: 1,
|
||||
transform: 'scale(1)'
|
||||
}}
|
||||
>
|
||||
{/* Discord brand accent */}
|
||||
<div className="absolute top-0 left-0 right-0 h-1 bg-gradient-to-r from-transparent via-[#5865F2]/60 to-transparent" />
|
||||
|
||||
<div className="text-center space-y-2 md:space-y-4 lg:space-y-6">
|
||||
{/* Header */}
|
||||
<div className="space-y-1.5 md:space-y-3 lg:space-y-4">
|
||||
<div
|
||||
className="inline-flex items-center justify-center w-10 h-10 md:w-14 md:h-14 lg:w-18 lg:h-18 rounded-full bg-[#5865F2]/20 border border-[#5865F2]/30 mb-1.5 md:mb-3"
|
||||
style={{
|
||||
opacity: 1,
|
||||
transform: 'scale(1) rotate(0deg)'
|
||||
}}
|
||||
>
|
||||
<svg className="w-6 h-6 md:w-8 md:h-8 text-[#5865F2]" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z"/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
opacity: 1,
|
||||
transform: 'translateY(0)'
|
||||
}}
|
||||
>
|
||||
<h2 className="text-lg md:text-2xl lg:text-3xl font-semibold text-white">
|
||||
Join us on Discord
|
||||
</h2>
|
||||
<div className="w-16 md:w-24 lg:w-32 h-0.5 md:h-1 bg-gradient-to-r from-[#5865F2] to-[#7289DA] mx-auto rounded-full" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Personal message */}
|
||||
<div
|
||||
className="max-w-2xl mx-auto space-y-1.5 md:space-y-3 lg:space-y-4"
|
||||
style={{
|
||||
opacity: 1,
|
||||
transform: 'translateY(0)'
|
||||
}}
|
||||
>
|
||||
<p className="text-xs md:text-sm lg:text-base text-gray-300 font-light leading-relaxed">
|
||||
GridPilot is a <strong className="text-white">solo developer project</strong>, and I'm building it because I got tired of the chaos in league racing.
|
||||
</p>
|
||||
<p className="text-xs md:text-sm text-gray-400 font-light leading-relaxed">
|
||||
This is <strong className="text-gray-300">early days</strong>, and I need your help. Join the Discord to:
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Benefits grid */}
|
||||
<div
|
||||
className="grid md:grid-cols-2 gap-1.5 md:gap-2 lg:gap-3 max-w-2xl mx-auto mt-2 md:mt-4 lg:mt-6"
|
||||
style={{
|
||||
opacity: 1,
|
||||
transform: 'scale(1)'
|
||||
}}
|
||||
>
|
||||
<div className="flex items-start gap-1.5 md:gap-2.5 text-left p-2 md:p-3.5 rounded-lg bg-iron-gray/50 border border-charcoal-outline hover:border-[#5865F2]/30 transition-colors">
|
||||
<div className="flex-shrink-0 w-5 h-5 md:w-6 md:h-6 rounded-lg bg-[#5865F2]/20 border border-[#5865F2]/30 flex items-center justify-center mt-0.5">
|
||||
<svg className="w-3 h-3 md:w-4 md:h-4 text-[#5865F2]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-[10px] md:text-xs text-white font-medium mb-0.5">Share your pain points</h3>
|
||||
<p className="text-[9px] md:text-xs text-gray-400 leading-relaxed">Tell me what frustrates you about league racing today</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-1.5 md:gap-2.5 text-left p-2 md:p-3.5 rounded-lg bg-iron-gray/50 border border-charcoal-outline hover:border-[#5865F2]/30 transition-colors">
|
||||
<div className="flex-shrink-0 w-5 h-5 md:w-6 md:h-6 rounded-lg bg-[#5865F2]/20 border border-[#5865F2]/30 flex items-center justify-center mt-0.5">
|
||||
<svg className="w-3 h-3 md:w-4 md:h-4 text-[#5865F2]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-[10px] md:text-xs text-white font-medium mb-0.5">Shape the product</h3>
|
||||
<p className="text-[9px] md:text-xs text-gray-400 leading-relaxed">Your ideas will directly influence what gets built</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-1.5 md:gap-2.5 text-left p-2 md:p-3.5 rounded-lg bg-iron-gray/50 border border-charcoal-outline hover:border-[#5865F2]/30 transition-colors">
|
||||
<div className="flex-shrink-0 w-5 h-5 md:w-6 md:h-6 rounded-lg bg-[#5865F2]/20 border border-[#5865F2]/30 flex items-center justify-center mt-0.5">
|
||||
<svg className="w-3 h-3 md:w-4 md:h-4 text-[#5865F2]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-[10px] md:text-xs text-white font-medium mb-0.5">Be part of the community</h3>
|
||||
<p className="text-[9px] md:text-xs text-gray-400 leading-relaxed">Connect with other league racers who get it</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-1.5 md:gap-2.5 text-left p-2 md:p-3.5 rounded-lg bg-iron-gray/50 border border-charcoal-outline hover:border-[#5865F2]/30 transition-colors">
|
||||
<div className="flex-shrink-0 w-5 h-5 md:w-6 md:h-6 rounded-lg bg-[#5865F2]/20 border border-[#5865F2]/30 flex items-center justify-center mt-0.5">
|
||||
<svg className="w-3 h-3 md:w-4 md:h-4 text-[#5865F2]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-[10px] md:text-xs text-white font-medium mb-0.5">Get early access</h3>
|
||||
<p className="text-[9px] md:text-xs text-gray-400 leading-relaxed">Test features first and help iron out the rough edges</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* CTA Button */}
|
||||
<div
|
||||
className="pt-2 md:pt-4 lg:pt-6"
|
||||
style={{
|
||||
opacity: 1,
|
||||
transform: 'translateY(0) scale(1)'
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
as="a"
|
||||
href={discordUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1.5 md:gap-2.5 px-3 py-2 md:px-5 md:py-3 text-sm md:text-base bg-[#5865F2] hover:bg-[#4752C4] shadow-[0_0_40px_rgba(88,101,242,0.3)] hover:shadow-[0_0_60px_rgba(88,101,242,0.5)] transition-all duration-300 hover:scale-105"
|
||||
>
|
||||
<svg className="w-4 h-4 md:w-5 md:h-5" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z"/>
|
||||
</svg>
|
||||
Join the Discord
|
||||
</Button>
|
||||
{!process.env.NEXT_PUBLIC_DISCORD_URL && (
|
||||
<p className="mt-4 text-xs text-gray-500">
|
||||
Note: Configure NEXT_PUBLIC_DISCORD_URL in your environment variables
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer note */}
|
||||
<p
|
||||
className="text-[9px] md:text-xs text-gray-500 font-light leading-relaxed max-w-xl mx-auto pt-2 md:pt-4"
|
||||
style={{
|
||||
opacity: 1
|
||||
}}
|
||||
>
|
||||
This is a community effort. Every voice matters. Let's build something that actually works for league racing.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -44,16 +44,16 @@ function FAQItem({ faq, index }: { faq: typeof faqs[0]; index: number }) {
|
||||
<div className="rounded-lg bg-iron-gray border border-charcoal-outline transition-all duration-150 hover:-translate-y-1 hover:shadow-lg hover:border-primary-blue/50">
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="w-full p-6 text-left focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-blue rounded-lg"
|
||||
className="w-full p-2 md:p-3 lg:p-4 text-left focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-blue rounded-lg min-h-[44px]"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<h3 className="text-lg font-semibold text-white group-hover:text-primary-blue transition-colors duration-150">
|
||||
<div className="flex items-center justify-between gap-1.5 md:gap-2">
|
||||
<h3 className="text-xs md:text-sm font-semibold text-white group-hover:text-primary-blue transition-colors duration-150">
|
||||
{faq.question}
|
||||
</h3>
|
||||
<motion.svg
|
||||
animate={{ rotate: isOpen ? 180 : 0 }}
|
||||
transition={{ duration: 0.15, ease: 'easeInOut' }}
|
||||
className="w-5 h-5 text-neon-aqua flex-shrink-0"
|
||||
className="w-3.5 h-3.5 md:w-4 md:h-4 text-neon-aqua flex-shrink-0"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
@@ -74,8 +74,8 @@ function FAQItem({ faq, index }: { faq: typeof faqs[0]; index: number }) {
|
||||
}}
|
||||
className="overflow-hidden"
|
||||
>
|
||||
<div className="px-6 pb-6">
|
||||
<p className="text-base text-gray-300 font-light">
|
||||
<div className="px-2 pb-2 pt-1 md:px-3 md:pb-3">
|
||||
<p className="text-[10px] md:text-xs text-gray-300 font-light leading-relaxed">
|
||||
{faq.answer}
|
||||
</p>
|
||||
</div>
|
||||
@@ -87,15 +87,27 @@ function FAQItem({ faq, index }: { faq: typeof faqs[0]; index: number }) {
|
||||
|
||||
export default function FAQ() {
|
||||
return (
|
||||
<section className="py-24 bg-deep-graphite">
|
||||
<div className="max-w-3xl mx-auto px-6">
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="text-4xl md:text-5xl font-semibold text-white mb-3">
|
||||
<section className="relative py-3 md:py-12 lg:py-16 bg-deep-graphite overflow-hidden">
|
||||
{/* Background image with mask */}
|
||||
<div
|
||||
className="absolute inset-0 bg-cover bg-center"
|
||||
style={{
|
||||
backgroundImage: 'url(/images/porsche.jpeg)',
|
||||
maskImage: 'radial-gradient(ellipse at center, rgba(0,0,0,0.18) 0%, rgba(0,0,0,0.1) 40%, transparent 70%)',
|
||||
WebkitMaskImage: 'radial-gradient(ellipse at center, rgba(0,0,0,0.18) 0%, rgba(0,0,0,0.1) 40%, transparent 70%)'
|
||||
}}
|
||||
/>
|
||||
{/* Racing red accent */}
|
||||
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-red-500/30 to-transparent" />
|
||||
|
||||
<div className="max-w-3xl mx-auto px-4 md:px-6 relative z-10">
|
||||
<div className="text-center mb-4 md:mb-8">
|
||||
<h2 className="text-base md:text-xl lg:text-2xl font-semibold text-white mb-1">
|
||||
Frequently Asked Questions
|
||||
</h2>
|
||||
<div className="w-32 h-1 bg-gradient-to-r from-primary-blue to-neon-aqua mx-auto rounded-full" />
|
||||
<div className="w-24 md:w-32 h-1 bg-gradient-to-r from-primary-blue to-neon-aqua mx-auto rounded-full" />
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1.5 md:space-y-2">
|
||||
{faqs.map((faq, index) => (
|
||||
<FAQItem key={faq.question} faq={faq} index={index} />
|
||||
))}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useRef, useState, useEffect } from 'react';
|
||||
import Section from '@/components/ui/Section';
|
||||
import Container from '@/components/ui/Container';
|
||||
import Heading from '@/components/ui/Heading';
|
||||
@@ -8,6 +11,7 @@ import TeamCompetitionMockup from '@/components/mockups/TeamCompetitionMockup';
|
||||
import ProtestWorkflowMockup from '@/components/mockups/ProtestWorkflowMockup';
|
||||
import LeagueDiscoveryMockup from '@/components/mockups/LeagueDiscoveryMockup';
|
||||
import DriverProfileMockup from '@/components/mockups/DriverProfileMockup';
|
||||
import { useScrollProgress } from '@/hooks/useScrollProgress';
|
||||
|
||||
const features = [
|
||||
{
|
||||
@@ -42,35 +46,59 @@ const features = [
|
||||
}
|
||||
];
|
||||
|
||||
function FeatureCard({ feature, index }: { feature: typeof features[0], index: number }) {
|
||||
return (
|
||||
<div
|
||||
className="flex flex-col gap-6 sm:gap-6 group"
|
||||
style={{
|
||||
opacity: 1,
|
||||
transform: 'translateY(0) scale(1)'
|
||||
}}
|
||||
>
|
||||
<div className="aspect-video w-full relative">
|
||||
<div className="absolute -inset-0.5 bg-gradient-to-r from-racing-red/20 via-primary-blue/20 to-racing-red/20 rounded-lg opacity-0 group-hover:opacity-100 transition-opacity duration-500 blur-sm" />
|
||||
<div className="relative">
|
||||
<MockupStack index={index}>
|
||||
<feature.MockupComponent />
|
||||
</MockupStack>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Heading level={3} className="bg-gradient-to-r from-red-600 via-white to-blue-600 bg-clip-text text-transparent font-medium drop-shadow-[0_0_15px_rgba(220,0,0,0.4)] static-racing-gradient" style={{ WebkitTextStroke: '0.5px rgba(220,0,0,0.2)' }}>
|
||||
{feature.title}
|
||||
</Heading>
|
||||
</div>
|
||||
<p className="text-sm sm:text-base leading-7 sm:leading-7 text-gray-400 font-light">
|
||||
{feature.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function FeatureGrid() {
|
||||
return (
|
||||
<Section variant="default">
|
||||
<Container>
|
||||
<Container className="relative z-10">
|
||||
<Container size="sm" center>
|
||||
<Heading level={2} className="text-white">
|
||||
Built for League Racing
|
||||
</Heading>
|
||||
<p className="mt-4 text-lg text-gray-400">
|
||||
Everything you need to run a professional iRacing league, nothing you don't
|
||||
</p>
|
||||
<div
|
||||
style={{
|
||||
opacity: 1,
|
||||
transform: 'translateY(0)'
|
||||
}}
|
||||
>
|
||||
<Heading level={2} className="bg-gradient-to-r from-red-600 via-white to-blue-600 bg-clip-text text-transparent font-semibold drop-shadow-[0_0_20px_rgba(220,0,0,0.5)] static-racing-gradient" style={{ WebkitTextStroke: '1px rgba(220,0,0,0.2)' }}>
|
||||
Building for League Racing
|
||||
</Heading>
|
||||
<p className="mt-4 sm:mt-6 text-base sm:text-lg text-gray-400">
|
||||
These features are in development. Join the community to help shape what gets built first
|
||||
</p>
|
||||
</div>
|
||||
</Container>
|
||||
<div className="mx-auto mt-16 grid max-w-2xl grid-cols-1 gap-16 sm:mt-20 lg:max-w-none lg:grid-cols-2 xl:grid-cols-3">
|
||||
<div className="mx-auto mt-8 sm:mt-12 md:mt-16 grid max-w-2xl grid-cols-1 gap-10 sm:gap-12 md:gap-16 lg:max-w-none lg:grid-cols-2 xl:grid-cols-3">
|
||||
{features.map((feature, index) => (
|
||||
<div key={feature.title} className="flex flex-col gap-6">
|
||||
<div className="aspect-video w-full">
|
||||
<MockupStack index={index}>
|
||||
<feature.MockupComponent />
|
||||
</MockupStack>
|
||||
</div>
|
||||
<div>
|
||||
<Heading level={3} className="text-white">
|
||||
{feature.title}
|
||||
</Heading>
|
||||
<p className="mt-2 text-base leading-7 text-gray-400">
|
||||
{feature.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<FeatureCard key={feature.title} feature={feature} index={index} />
|
||||
))}
|
||||
</div>
|
||||
</Container>
|
||||
|
||||
@@ -1,57 +1,73 @@
|
||||
'use client';
|
||||
|
||||
import { useRef } from 'react';
|
||||
import { useScrollProgress } from '@/hooks/useScrollProgress';
|
||||
|
||||
export default function Footer() {
|
||||
return (
|
||||
<footer className="relative bg-deep-graphite">
|
||||
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-primary-blue to-transparent" />
|
||||
|
||||
<div className="max-w-6xl mx-auto px-6 py-16">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-12 mb-12">
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold text-white mb-2">GridPilot</h3>
|
||||
<div className="w-16 h-0.5 bg-gradient-to-r from-primary-blue to-neon-aqua mb-4 rounded-full" />
|
||||
<p className="text-sm text-gray-400 font-light">
|
||||
Making league racing less chaotic
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-white mb-4">Product</h4>
|
||||
<ul className="space-y-3">
|
||||
<li>
|
||||
<span className="text-sm text-gray-400 font-light cursor-not-allowed opacity-50">
|
||||
Roadmap <span className="text-xs">(coming soon)</span>
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<span className="text-sm text-gray-400 font-light cursor-not-allowed opacity-50">
|
||||
Docs <span className="text-xs">(coming soon)</span>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-white mb-4">Legal</h4>
|
||||
<ul className="space-y-3">
|
||||
<li>
|
||||
<span className="text-sm text-gray-400 font-light cursor-not-allowed opacity-50">
|
||||
Privacy <span className="text-xs">(coming soon)</span>
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<span className="text-sm text-gray-400 font-light cursor-not-allowed opacity-50">
|
||||
Terms <span className="text-xs">(coming soon)</span>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="max-w-4xl mx-auto px-[calc(1.5rem+var(--sal))] pr-[calc(1.5rem+var(--sar))] py-2 md:py-8 lg:py-12 pb-[calc(0.5rem+var(--sab))] md:pb-[calc(1.5rem+var(--sab))]">
|
||||
{/* Racing stripe accent */}
|
||||
<div
|
||||
className="flex gap-1 mb-2 md:mb-4 lg:mb-6 justify-center"
|
||||
style={{
|
||||
opacity: 1,
|
||||
transform: 'scaleX(1)'
|
||||
}}
|
||||
>
|
||||
<div className="w-12 md:w-20 lg:w-28 h-[2px] md:h-0.5 lg:h-1 bg-white rounded-full" />
|
||||
<div className="w-12 md:w-20 lg:w-28 h-[2px] md:h-0.5 lg:h-1 bg-primary-blue rounded-full" />
|
||||
<div className="w-12 md:w-20 lg:w-28 h-[2px] md:h-0.5 lg:h-1 bg-white rounded-full" />
|
||||
</div>
|
||||
|
||||
<div className="pt-8 border-t border-charcoal-outline">
|
||||
<div className="flex flex-col md:flex-row justify-between items-center gap-4">
|
||||
<p className="text-sm text-gray-500 font-light">
|
||||
© 2025 GridPilot. All rights reserved.
|
||||
</p>
|
||||
</div>
|
||||
{/* Personal message */}
|
||||
<div
|
||||
className="text-center mb-3 md:mb-6 lg:mb-8"
|
||||
style={{
|
||||
opacity: 1,
|
||||
transform: 'translateY(0)'
|
||||
}}
|
||||
>
|
||||
<p className="text-[9px] md:text-xs lg:text-sm text-gray-300 mb-1 md:mb-2">
|
||||
🏁 Built by a sim racer, for sim racers
|
||||
</p>
|
||||
<p className="text-[9px] md:text-xs text-gray-400 font-light max-w-2xl mx-auto">
|
||||
Just a fellow racer tired of spreadsheets and chaos. GridPilot is my passion project to make league racing actually fun again.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Community links */}
|
||||
<div
|
||||
className="flex justify-center gap-4 md:gap-6 lg:gap-8 mb-3 md:mb-6 lg:mb-8"
|
||||
style={{
|
||||
opacity: 1,
|
||||
transform: 'scale(1)'
|
||||
}}
|
||||
>
|
||||
<a
|
||||
href="https://discord.gg/gridpilot"
|
||||
className="text-[9px] md:text-xs text-primary-blue hover:text-neon-aqua transition-colors font-medium inline-flex items-center justify-center min-h-[44px] min-w-[44px] px-3 py-2 active:scale-95 transition-transform"
|
||||
>
|
||||
💬 Join Discord
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Development status */}
|
||||
<div
|
||||
className="text-center pt-2 md:pt-4 lg:pt-6 border-t border-charcoal-outline"
|
||||
style={{
|
||||
opacity: 1,
|
||||
transform: 'translateY(0)'
|
||||
}}
|
||||
>
|
||||
<p className="text-[9px] md:text-xs lg:text-sm text-gray-500 mb-1 md:mb-2">
|
||||
⚡ Early development • Feedback welcome
|
||||
</p>
|
||||
<p className="text-[9px] md:text-xs text-gray-600">
|
||||
Questions? Find me on Discord
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -1,67 +1,139 @@
|
||||
'use client';
|
||||
|
||||
import { useRef } from 'react';
|
||||
import Button from '@/components/ui/Button';
|
||||
import Container from '@/components/ui/Container';
|
||||
import Heading from '@/components/ui/Heading';
|
||||
import { useScrollProgress, useParallax } from '@/hooks/useScrollProgress';
|
||||
|
||||
const discordUrl = process.env.NEXT_PUBLIC_DISCORD_URL || '#';
|
||||
|
||||
if (!process.env.NEXT_PUBLIC_DISCORD_URL) {
|
||||
console.warn('NEXT_PUBLIC_DISCORD_URL is not set. Discord button will use "#" as fallback.');
|
||||
}
|
||||
|
||||
export default function Hero() {
|
||||
const sectionRef = useRef<HTMLElement>(null);
|
||||
|
||||
const bgParallax = useParallax(sectionRef, 0.3);
|
||||
|
||||
return (
|
||||
<section className="relative overflow-hidden bg-deep-graphite px-6 py-24 sm:py-32 lg:px-8">
|
||||
{/* Background image layer */}
|
||||
<section ref={sectionRef} className="relative overflow-hidden bg-deep-graphite px-[calc(1.5rem+var(--sal))] pr-[calc(1.5rem+var(--sar))] pt-[calc(3rem+var(--sat))] pb-16 sm:pt-[calc(4rem+var(--sat))] sm:pb-24 md:py-32 lg:px-8">
|
||||
{/* Background image layer with parallax */}
|
||||
<div
|
||||
className="absolute inset-0 bg-cover bg-center"
|
||||
style={{
|
||||
backgroundImage: 'url(/images/porsche.jpeg)',
|
||||
maskImage: 'radial-gradient(ellipse at center, rgba(0,0,0,0.15) 0%, rgba(0,0,0,0.08) 40%, transparent 70%)',
|
||||
WebkitMaskImage: 'radial-gradient(ellipse at center, rgba(0,0,0,0.15) 0%, rgba(0,0,0,0.08) 40%, transparent 70%)'
|
||||
backgroundImage: 'url(/images/header.jpeg)',
|
||||
maskImage: 'radial-gradient(ellipse at center, rgba(0,0,0,0.5) 0%, rgba(0,0,0,0.35) 40%, transparent 70%)',
|
||||
WebkitMaskImage: 'radial-gradient(ellipse at center, rgba(0,0,0,0.5) 0%, rgba(0,0,0,0.35) 40%, transparent 70%)',
|
||||
transform: `translateY(${bgParallax * 0.5}px)`
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Subtle radial gradient overlay */}
|
||||
<div className="absolute inset-0 bg-gradient-radial from-primary-blue/10 via-transparent to-transparent opacity-40 pointer-events-none" />
|
||||
{/* Racing red accent gradient */}
|
||||
<div className="absolute top-0 left-0 right-0 h-1 bg-gradient-to-r from-transparent via-red-600/40 to-transparent" />
|
||||
|
||||
{/* Optional motorsport grid pattern */}
|
||||
<div className="absolute inset-0 opacity-[0.015]" style={{
|
||||
backgroundImage: `linear-gradient(rgba(255,255,255,0.5) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(255,255,255,0.5) 1px, transparent 1px)`,
|
||||
backgroundSize: '40px 40px'
|
||||
}} />
|
||||
{/* Racing stripes background */}
|
||||
<div className="absolute inset-0 racing-stripes opacity-30" />
|
||||
|
||||
<Container size="sm" center className="relative z-10">
|
||||
<Heading level={1} className="text-white leading-tight tracking-tight">
|
||||
{/* Checkered pattern overlay */}
|
||||
<div className="absolute inset-0 checkered-pattern opacity-20" />
|
||||
|
||||
{/* Speed lines - left side */}
|
||||
<div className="absolute left-0 top-1/4 w-32 h-px bg-gradient-to-r from-transparent to-primary-blue/30 animate-speed-lines" style={{ animationDelay: '0s' }} />
|
||||
<div className="absolute left-0 top-1/3 w-24 h-px bg-gradient-to-r from-transparent to-primary-blue/20 animate-speed-lines" style={{ animationDelay: '0.3s' }} />
|
||||
<div className="absolute left-0 top-2/5 w-28 h-px bg-gradient-to-r from-transparent to-primary-blue/25 animate-speed-lines" style={{ animationDelay: '0.6s' }} />
|
||||
|
||||
{/* Carbon fiber accent - bottom */}
|
||||
<div className="absolute bottom-0 left-0 right-0 h-32 carbon-fiber opacity-50" />
|
||||
|
||||
{/* Radial gradient overlay with racing red accent */}
|
||||
<div className="absolute inset-0 bg-gradient-radial from-red-600/5 via-primary-blue/5 to-transparent opacity-60 pointer-events-none" />
|
||||
|
||||
<Container size="sm" center className="relative z-10 space-y-6 sm:space-y-8 md:space-y-12">
|
||||
<Heading
|
||||
level={1}
|
||||
className="text-2xl sm:text-4xl md:text-5xl lg:text-6xl leading-tight tracking-tight font-semibold bg-gradient-to-r from-red-600 via-white to-blue-600 bg-clip-text text-transparent drop-shadow-[0_0_15px_rgba(220,0,0,0.4)] static-racing-gradient"
|
||||
style={{
|
||||
WebkitTextStroke: '0.5px rgba(220,0,0,0.2)',
|
||||
opacity: 1,
|
||||
transform: 'translateY(0)',
|
||||
filter: 'blur(0)'
|
||||
}}
|
||||
>
|
||||
League racing is incredible. What's missing is everything around it.
|
||||
</Heading>
|
||||
<div className="mt-8 text-lg leading-relaxed text-slate-400 font-light">
|
||||
<p>
|
||||
<div
|
||||
className="text-sm sm:text-lg md:text-xl lg:text-2xl leading-relaxed text-slate-200 font-light space-y-4 sm:space-y-6"
|
||||
style={{
|
||||
opacity: 1,
|
||||
transform: 'translateY(0)'
|
||||
}}
|
||||
>
|
||||
<p className="text-left">
|
||||
If you've been in any league, you know the feeling:
|
||||
</p>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 mt-6 max-w-2xl mx-auto">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-primary-blue text-xl">•</span>
|
||||
<span>Results scattered across Discord</span>
|
||||
{/* Problem badges - mobile optimized */}
|
||||
<div className="flex flex-col sm:flex-row sm:flex-wrap gap-2 sm:gap-3 items-stretch sm:justify-center sm:items-center max-w-2xl mx-auto">
|
||||
<div className="flex items-center gap-2.5 px-3 py-2.5 sm:gap-2.5 sm:px-5 sm:py-2.5 bg-gradient-to-br from-slate-800/80 to-slate-900/80 border border-red-500/20 rounded-lg backdrop-blur-md active:scale-95 sm:hover:border-red-500/40 sm:hover:shadow-[0_0_15px_rgba(239,68,68,0.15)] transition-all duration-300">
|
||||
<span className="text-red-500 text-sm sm:text-sm font-semibold flex-shrink-0">×</span>
|
||||
<span className="text-slate-100 text-sm sm:text-base font-medium text-left">Results scattered across Discord</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-primary-blue text-xl">•</span>
|
||||
<span>No long-term identity</span>
|
||||
<div className="flex items-center gap-2.5 px-3 py-2.5 sm:gap-2.5 sm:px-5 sm:py-2.5 bg-gradient-to-br from-slate-800/80 to-slate-900/80 border border-red-500/20 rounded-lg backdrop-blur-md active:scale-95 sm:hover:border-red-500/40 sm:hover:shadow-[0_0_15px_rgba(239,68,68,0.15)] transition-all duration-300">
|
||||
<span className="text-red-500 text-sm sm:text-sm font-semibold flex-shrink-0">×</span>
|
||||
<span className="text-slate-100 text-sm sm:text-base font-medium text-left">No long-term identity</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-primary-blue text-xl">•</span>
|
||||
<span>No career progression</span>
|
||||
<div className="flex items-center gap-2.5 px-3 py-2.5 sm:gap-2.5 sm:px-5 sm:py-2.5 bg-gradient-to-br from-slate-800/80 to-slate-900/80 border border-red-500/20 rounded-lg backdrop-blur-md active:scale-95 sm:hover:border-red-500/40 sm:hover:shadow-[0_0_15px_rgba(239,68,68,0.15)] transition-all duration-300">
|
||||
<span className="text-red-500 text-sm sm:text-sm font-semibold flex-shrink-0">×</span>
|
||||
<span className="text-slate-100 text-sm sm:text-base font-medium text-left">No career progression</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-primary-blue text-xl">•</span>
|
||||
<span>Forgotten after each season</span>
|
||||
<div className="flex items-center gap-2.5 px-3 py-2.5 sm:gap-2.5 sm:px-5 sm:py-2.5 bg-gradient-to-br from-slate-800/80 to-slate-900/80 border border-red-500/20 rounded-lg backdrop-blur-md active:scale-95 sm:hover:border-red-500/40 sm:hover:shadow-[0_0_15px_rgba(239,68,68,0.15)] transition-all duration-300">
|
||||
<span className="text-red-500 text-sm sm:text-sm font-semibold flex-shrink-0">×</span>
|
||||
<span className="text-slate-100 text-sm sm:text-base font-medium text-left">Forgotten after each season</span>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-8">
|
||||
<p className="text-left">
|
||||
The ecosystem isn't built for this.
|
||||
</p>
|
||||
<p className="mt-4">
|
||||
<strong className="text-white">GridPilot gives your league racing a real home.</strong>
|
||||
<p className="text-left">
|
||||
<strong className="text-white font-semibold">GridPilot gives your league racing a real home.</strong>
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-12 flex items-center justify-center">
|
||||
<Button as="a" href="#early-access" className="shadow-glow hover:shadow-glow-strong transition-shadow duration-300">
|
||||
Get Early Access
|
||||
</Button>
|
||||
<div
|
||||
className="flex items-center justify-center"
|
||||
style={{
|
||||
opacity: 1,
|
||||
transform: 'translateY(0) scale(1)'
|
||||
}}
|
||||
>
|
||||
<a
|
||||
href={discordUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="group relative inline-flex items-center justify-center gap-3 px-8 py-4 min-h-[44px] min-w-[44px] bg-[#5865F2] hover:bg-[#4752C4] text-white font-semibold text-base sm:text-lg rounded-lg transition-all duration-300 hover:scale-105 hover:-translate-y-0.5 shadow-[0_0_20px_rgba(88,101,242,0.3)] hover:shadow-[0_0_30px_rgba(88,101,242,0.6)] active:scale-95"
|
||||
aria-label="Join our Discord community"
|
||||
>
|
||||
{/* Discord Logo SVG */}
|
||||
<svg
|
||||
className="w-7 h-7 transition-transform duration-300 group-hover:scale-110"
|
||||
viewBox="0 0 71 55"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clipPath="url(#clip0)">
|
||||
<path
|
||||
d="M60.1045 4.8978C55.5792 2.8214 50.7265 1.2916 45.6527 0.41542C45.5603 0.39851 45.468 0.440769 45.4204 0.525289C44.7963 1.6353 44.105 3.0834 43.6209 4.2216C38.1637 3.4046 32.7345 3.4046 27.3892 4.2216C26.905 3.0581 26.1886 1.6353 25.5617 0.525289C25.5141 0.443589 25.4218 0.40133 25.3294 0.41542C20.2584 1.2888 15.4057 2.8186 10.8776 4.8978C10.8384 4.9147 10.8048 4.9429 10.7825 4.9795C1.57795 18.7309 -0.943561 32.1443 0.293408 45.3914C0.299005 45.4562 0.335386 45.5182 0.385761 45.5576C6.45866 50.0174 12.3413 52.7249 18.1147 54.5195C18.2071 54.5477 18.305 54.5139 18.3638 54.4378C19.7295 52.5728 20.9469 50.6063 21.9907 48.5383C22.0523 48.4172 21.9935 48.2735 21.8676 48.2256C19.9366 47.4931 18.0979 46.6 16.3292 45.5858C16.1893 45.5041 16.1781 45.304 16.3068 45.2082C16.679 44.9293 17.0513 44.6391 17.4067 44.3461C17.471 44.2926 17.5606 44.2813 17.6362 44.3151C29.2558 49.6202 41.8354 49.6202 53.3179 44.3151C53.3935 44.2785 53.4831 44.2898 53.5502 44.3433C53.9057 44.6363 54.2779 44.9293 54.6529 45.2082C54.7816 45.304 54.7732 45.5041 54.6333 45.5858C52.8646 46.6197 51.0259 47.4931 49.0921 48.2228C48.9662 48.2707 48.9102 48.4172 48.9718 48.5383C50.038 50.6034 51.2554 52.5699 52.5959 54.435C52.6519 54.5139 52.7526 54.5477 52.845 54.5195C58.6464 52.7249 64.529 50.0174 70.6019 45.5576C70.6551 45.5182 70.6887 45.459 70.6943 45.3942C72.1747 30.0791 68.2147 16.7757 60.1968 4.9823C60.1772 4.9429 60.1437 4.9147 60.1045 4.8978ZM23.7259 37.3253C20.2276 37.3253 17.3451 34.1136 17.3451 30.1693C17.3451 26.225 20.1717 23.0133 23.7259 23.0133C27.308 23.0133 30.1626 26.2532 30.1066 30.1693C30.1066 34.1136 27.28 37.3253 23.7259 37.3253ZM47.3178 37.3253C43.8196 37.3253 40.9371 34.1136 40.9371 30.1693C40.9371 26.225 43.7636 23.0133 47.3178 23.0133C50.9 23.0133 53.7545 26.2532 53.6986 30.1693C53.6986 34.1136 50.9 37.3253 47.3178 37.3253Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0">
|
||||
<rect width="71" height="55" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
<span>Join us on Discord</span>
|
||||
</a>
|
||||
</div>
|
||||
</Container>
|
||||
</section>
|
||||
|
||||
@@ -1,59 +1,109 @@
|
||||
'use client';
|
||||
|
||||
import { motion, useReducedMotion } from 'framer-motion';
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
export default function CareerProgressionMockup() {
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
const [isMobile, setIsMobile] = useState(false);
|
||||
|
||||
const containerVariants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: { staggerChildren: shouldReduceMotion ? 0 : 0.08 }
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
const checkMobile = () => setIsMobile(window.innerWidth < 768);
|
||||
checkMobile();
|
||||
window.addEventListener('resize', checkMobile);
|
||||
return () => window.removeEventListener('resize', checkMobile);
|
||||
}, []);
|
||||
|
||||
const itemVariants = {
|
||||
hidden: { opacity: 0, x: shouldReduceMotion ? 0 : -20 },
|
||||
visible: { opacity: 1, x: 0 }
|
||||
hidden: { opacity: 0, y: shouldReduceMotion ? 0 : 8 },
|
||||
visible: { opacity: 1, y: 0 }
|
||||
};
|
||||
|
||||
// Simple mobile version - just the essence
|
||||
if (isMobile) {
|
||||
return (
|
||||
<div className="relative w-full h-full bg-gradient-to-br from-deep-graphite to-iron-gray rounded-lg p-4 overflow-hidden flex items-center justify-center">
|
||||
<motion.div
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
variants={{
|
||||
visible: { transition: { staggerChildren: 0.1 } }
|
||||
}}
|
||||
className="space-y-4 w-full"
|
||||
>
|
||||
{/* Clean stat cards */}
|
||||
<motion.div variants={itemVariants} className="grid grid-cols-3 gap-3">
|
||||
{[
|
||||
{ value: '24', label: 'Wins' },
|
||||
{ value: '48', label: 'Podiums' },
|
||||
{ value: '156', label: 'Races' }
|
||||
].map((stat, i) => (
|
||||
<div key={i} className="bg-iron-gray/60 rounded-lg p-3 border border-primary-blue/20 text-center">
|
||||
<div className="text-2xl font-bold text-primary-blue font-mono">{stat.value}</div>
|
||||
<div className="text-[11px] text-white/50 mt-1">{stat.label}</div>
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
|
||||
{/* Single elegant season card */}
|
||||
<motion.div variants={itemVariants}>
|
||||
<div className="bg-iron-gray/40 rounded-lg p-3 border border-charcoal-outline">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-white/70">GT3 Championship</span>
|
||||
<span className="px-2.5 py-1 bg-performance-green/20 rounded text-xs text-performance-green font-semibold">P2</span>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Desktop version - more detailed
|
||||
return (
|
||||
<div className="relative w-full h-full bg-gradient-to-br from-deep-graphite via-iron-gray to-deep-graphite rounded-lg p-8 overflow-hidden">
|
||||
<div className="relative w-full h-full bg-gradient-to-br from-deep-graphite via-iron-gray to-deep-graphite rounded-lg p-1.5 sm:p-3 md:p-5 lg:p-8 overflow-hidden">
|
||||
<motion.div
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
className="space-y-6"
|
||||
variants={{
|
||||
visible: { transition: { staggerChildren: shouldReduceMotion ? 0 : 0.08 } }
|
||||
}}
|
||||
className="space-y-1.5 sm:space-y-3 md:space-y-4 lg:space-y-6"
|
||||
>
|
||||
{/* Driver Header */}
|
||||
<motion.div variants={itemVariants} className="flex items-center gap-4 pb-6 border-b border-charcoal-outline">
|
||||
<div className="h-16 w-16 bg-charcoal-outline rounded-full border-2 border-primary-blue/30"></div>
|
||||
<motion.div variants={itemVariants} className="flex items-center gap-1.5 sm:gap-2 md:gap-3 lg:gap-4 pb-1.5 sm:pb-3 md:pb-4 lg:pb-6 border-b border-charcoal-outline">
|
||||
<div className="h-8 w-8 sm:h-10 sm:w-10 md:h-12 md:w-12 lg:h-16 lg:w-16 bg-charcoal-outline rounded-full border-2 border-primary-blue/30 flex items-center justify-center text-base sm:text-xl md:text-2xl lg:text-3xl overflow-hidden">
|
||||
🏎️
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="h-4 w-40 bg-white/10 rounded mb-2"></div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="h-3 w-24 bg-primary-blue/20 rounded"></div>
|
||||
<div className="h-3 w-20 bg-performance-green/20 rounded"></div>
|
||||
<div className="text-[9px] sm:text-xs md:text-sm font-semibold text-white/90 mb-1 sm:mb-1.5 md:mb-2">Your Racing Identity</div>
|
||||
<div className="flex items-center gap-1 sm:gap-2 md:gap-3">
|
||||
<span className="text-[8px] sm:text-[10px] md:text-xs text-primary-blue/70">Multi-league profile</span>
|
||||
<span className="text-[8px] sm:text-[10px] md:text-xs text-performance-green/70">Career tracking</span>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Career Stats */}
|
||||
<motion.div variants={itemVariants}>
|
||||
<div className="text-sm font-semibold text-white/60 mb-3">Career Overview</div>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<div className="text-[9px] sm:text-xs md:text-sm font-semibold text-white/60 mb-1 sm:mb-2 md:mb-3">Career Overview</div>
|
||||
<div className="grid grid-cols-3 gap-1 sm:gap-2 md:gap-3">
|
||||
{[
|
||||
{ label: 'Wins', value: '24' },
|
||||
{ label: 'Podiums', value: '48' },
|
||||
{ label: 'Races', value: '156' }
|
||||
].map((stat, i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
className="bg-iron-gray rounded-lg p-3 border border-charcoal-outline"
|
||||
className="bg-iron-gray rounded-lg p-1 sm:p-2 md:p-3 border border-charcoal-outline"
|
||||
whileHover={shouldReduceMotion ? {} : {
|
||||
y: -2,
|
||||
boxShadow: '0 4px 16px rgba(0,0,0,0.3)',
|
||||
transition: { duration: 0.15 }
|
||||
}}
|
||||
>
|
||||
<div className="h-6 w-12 bg-primary-blue/30 rounded mb-2 font-mono"></div>
|
||||
<div className="h-2 w-16 bg-white/5 rounded"></div>
|
||||
<div className="text-sm sm:text-base md:text-lg font-bold text-primary-blue font-mono mb-0.5">{stat.value}</div>
|
||||
<div className="text-[8px] sm:text-[10px] md:text-xs text-white/40">{stat.label}</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
@@ -61,26 +111,32 @@ export default function CareerProgressionMockup() {
|
||||
|
||||
{/* Season Timeline */}
|
||||
<motion.div variants={itemVariants}>
|
||||
<div className="text-sm font-semibold text-white/60 mb-3">Season History</div>
|
||||
<div className="space-y-2">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<div className="text-[9px] sm:text-xs md:text-sm font-semibold text-white/60 mb-1 sm:mb-2 md:mb-3">Season History</div>
|
||||
<div className="space-y-1 sm:space-y-1.5 md:space-y-2">
|
||||
{[
|
||||
{ league: 'GT3 Championship', season: 'S3', position: 'P2', points: '248' },
|
||||
{ league: 'Endurance Series', season: 'S2', position: 'P1', points: '312' },
|
||||
{ league: 'Formula Sprint', season: 'S1', position: 'P5', points: '186' }
|
||||
].map((season, i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
className="flex items-center gap-3 bg-iron-gray rounded-lg p-3 border border-charcoal-outline"
|
||||
className="flex items-center gap-1.5 sm:gap-2 md:gap-3 bg-iron-gray rounded-lg p-1.5 sm:p-2 md:p-3 border border-charcoal-outline"
|
||||
whileHover={shouldReduceMotion ? {} : {
|
||||
x: 4,
|
||||
boxShadow: '0 2px 12px rgba(25,140,255,0.2)',
|
||||
transition: { duration: 0.15 }
|
||||
}}
|
||||
>
|
||||
<div className="h-8 w-8 bg-charcoal-outline rounded border border-primary-blue/20"></div>
|
||||
<div className="flex-1">
|
||||
<div className="h-2.5 w-32 bg-white/10 rounded mb-1.5"></div>
|
||||
<div className="h-2 w-24 bg-white/5 rounded"></div>
|
||||
<div className="h-5 w-5 sm:h-6 sm:w-6 md:h-8 md:w-8 bg-charcoal-outline rounded border border-primary-blue/20 flex items-center justify-center text-xs sm:text-sm md:text-base">
|
||||
🏁
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<div className="h-6 w-8 bg-performance-green/20 rounded text-center font-mono"></div>
|
||||
<div className="h-6 w-8 bg-primary-blue/20 rounded text-center font-mono"></div>
|
||||
<div className="flex-1">
|
||||
<div className="text-[9px] sm:text-xs text-white/80 mb-0.5 sm:mb-1">{season.league}</div>
|
||||
<div className="text-[8px] sm:text-xs text-white/40">Season complete</div>
|
||||
</div>
|
||||
<div className="flex gap-1 sm:gap-1.5 md:gap-2">
|
||||
<div className="px-1 sm:px-1.5 md:px-2 py-0.5 bg-performance-green/20 rounded text-[8px] sm:text-[10px] md:text-xs text-performance-green font-semibold">{season.position}</div>
|
||||
<div className="px-1 sm:px-1.5 md:px-2 py-0.5 bg-primary-blue/20 rounded text-[8px] sm:text-[10px] md:text-xs text-primary-blue font-mono">{season.points}</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
@@ -89,13 +145,15 @@ export default function CareerProgressionMockup() {
|
||||
|
||||
{/* Multi-League Badge */}
|
||||
<motion.div variants={itemVariants}>
|
||||
<div className="flex items-center gap-2 bg-charcoal-outline rounded-lg p-3 border border-primary-blue/30">
|
||||
<div className="flex items-center gap-1 sm:gap-1.5 md:gap-2 bg-charcoal-outline rounded-lg p-1.5 sm:p-2 md:p-3 border border-primary-blue/30">
|
||||
<div className="flex -space-x-2">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<div key={i} className="h-6 w-6 bg-iron-gray rounded-full border-2 border-charcoal-outline"></div>
|
||||
<div key={i} className="h-4 w-4 sm:h-5 sm:w-5 md:h-6 md:w-6 bg-iron-gray rounded-full border-2 border-charcoal-outline flex items-center justify-center text-[8px] sm:text-[10px] md:text-xs">
|
||||
🏆
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="h-2 w-32 bg-white/5 rounded"></div>
|
||||
<span className="text-[8px] sm:text-[10px] md:text-xs text-white/60">Active in 3 leagues across seasons</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
|
||||
@@ -1,110 +1,171 @@
|
||||
'use client';
|
||||
|
||||
import { motion, useReducedMotion } from 'framer-motion';
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
export default function CompanionAutomationMockup() {
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
const [isMobile, setIsMobile] = useState(false);
|
||||
|
||||
const containerVariants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: { staggerChildren: shouldReduceMotion ? 0 : 0.12 }
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
const checkMobile = () => setIsMobile(window.innerWidth < 768);
|
||||
checkMobile();
|
||||
window.addEventListener('resize', checkMobile);
|
||||
return () => window.removeEventListener('resize', checkMobile);
|
||||
}, []);
|
||||
|
||||
const itemVariants = {
|
||||
hidden: { opacity: 0, y: shouldReduceMotion ? 0 : 10 },
|
||||
visible: { opacity: 1, y: 0 }
|
||||
};
|
||||
// Simple mobile version - just the essence of automation
|
||||
if (isMobile) {
|
||||
return (
|
||||
<div className="relative w-full h-full bg-gradient-to-br from-deep-graphite to-iron-gray rounded-lg p-4 overflow-hidden flex items-center justify-center">
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
className="space-y-4 w-full"
|
||||
>
|
||||
{/* Simple progress indicator */}
|
||||
<div className="bg-iron-gray/60 rounded-xl p-4 border border-primary-blue/40">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<motion.div
|
||||
className="h-8 w-8 bg-performance-green/40 rounded-full flex items-center justify-center border-2 border-performance-green/60 flex-shrink-0"
|
||||
animate={shouldReduceMotion ? {} : {
|
||||
scale: [1, 1.1, 1],
|
||||
opacity: [0.6, 1, 0.6]
|
||||
}}
|
||||
transition={{ duration: 1.5, repeat: Infinity }}
|
||||
>
|
||||
<div className="h-3 w-3 bg-performance-green rounded-full"></div>
|
||||
</motion.div>
|
||||
<div>
|
||||
<div className="text-sm text-white font-medium">Creating Session</div>
|
||||
<div className="text-xs text-white/50">Automated</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-2.5 w-full bg-white/5 rounded-full overflow-hidden">
|
||||
<motion.div
|
||||
className="h-full bg-primary-blue/60"
|
||||
initial={{ width: '0%' }}
|
||||
animate={{ width: '75%' }}
|
||||
transition={{ duration: 2, ease: 'easeInOut' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Simple CTA */}
|
||||
<div className="flex justify-center">
|
||||
<div className="bg-primary-blue/20 text-primary-blue px-6 py-2.5 rounded-lg border border-primary-blue/40 text-sm font-semibold">
|
||||
One Click
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Desktop version - richer with more automation steps
|
||||
return (
|
||||
<div className="relative w-full h-full bg-gradient-to-br from-deep-graphite via-iron-gray to-deep-graphite rounded-lg p-8 overflow-hidden">
|
||||
<div className="relative w-full h-full bg-gradient-to-br from-deep-graphite via-iron-gray to-deep-graphite rounded-lg p-3 md:p-4 lg:p-6 overflow-hidden">
|
||||
<motion.div
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
className="space-y-6"
|
||||
variants={{
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: { staggerChildren: shouldReduceMotion ? 0 : 0.12 }
|
||||
}
|
||||
}}
|
||||
className="space-y-3 md:space-y-4 lg:space-y-5"
|
||||
>
|
||||
{/* Companion App Header */}
|
||||
<motion.div variants={itemVariants}>
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="h-10 w-10 bg-primary-blue/20 rounded-lg border border-primary-blue/40 flex items-center justify-center">
|
||||
<div className="h-5 w-5 bg-primary-blue/60 rounded"></div>
|
||||
{/* Companion App Header - Enhanced */}
|
||||
<motion.div variants={{ hidden: { opacity: 0, y: -10 }, visible: { opacity: 1, y: 0 } }}>
|
||||
<div className="flex items-center gap-2.5 md:gap-3 lg:gap-4 mb-3 md:mb-4 lg:mb-5">
|
||||
<div className="h-10 w-10 md:h-12 md:w-12 lg:h-14 lg:w-14 bg-primary-blue/20 rounded-lg border-2 border-primary-blue/40 flex items-center justify-center shadow-lg">
|
||||
<div className="h-6 w-6 md:h-7 md:w-7 lg:h-8 lg:w-8 bg-primary-blue/60 rounded"></div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-semibold text-white">GridPilot Companion</div>
|
||||
<div className="text-xs text-white/50">Session Creator</div>
|
||||
<div className="text-base md:text-lg lg:text-xl font-semibold text-white">GridPilot Companion</div>
|
||||
<div className="text-xs md:text-sm lg:text-base text-white/50">Automated Session Creator</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Configuration Card */}
|
||||
<motion.div
|
||||
variants={itemVariants}
|
||||
className="bg-iron-gray rounded-lg p-4 border border-charcoal-outline"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="text-xs text-white/60">Session Template</div>
|
||||
<div className="h-5 w-20 bg-performance-green/30 rounded-full flex items-center justify-center">
|
||||
<div className="text-xs text-performance-green">Ready</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-2 w-2 bg-primary-blue rounded-full"></div>
|
||||
<div className="h-2 w-32 bg-white/10 rounded"></div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-2 w-2 bg-primary-blue rounded-full"></div>
|
||||
<div className="h-2 w-28 bg-white/10 rounded"></div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-2 w-2 bg-primary-blue rounded-full"></div>
|
||||
<div className="h-2 w-36 bg-white/10 rounded"></div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Browser Automation Visual */}
|
||||
<motion.div
|
||||
variants={itemVariants}
|
||||
className="relative bg-charcoal-outline rounded-lg p-4 border border-primary-blue/30 overflow-hidden"
|
||||
{/* Browser Automation Visual - Full workflow */}
|
||||
<motion.div
|
||||
variants={{ hidden: { opacity: 0, y: 10 }, visible: { opacity: 1, y: 0 } }}
|
||||
className="relative bg-charcoal-outline rounded-lg p-3 md:p-4 lg:p-5 border-2 border-primary-blue/40 overflow-hidden"
|
||||
>
|
||||
{/* Browser Window Mockup */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2 pb-3 border-b border-white/10">
|
||||
<div className="h-2 w-2 bg-white/20 rounded-full"></div>
|
||||
<div className="h-2 w-2 bg-white/20 rounded-full"></div>
|
||||
<div className="h-2 w-2 bg-white/20 rounded-full"></div>
|
||||
<div className="flex-1 h-2 bg-white/5 rounded ml-2"></div>
|
||||
<div className="space-y-3 md:space-y-4">
|
||||
<div className="flex items-center gap-2 md:gap-2.5 pb-3 md:pb-4 border-b border-white/10">
|
||||
<div className="h-2.5 md:h-3 w-2.5 md:w-3 bg-red-500/60 rounded-full"></div>
|
||||
<div className="h-2.5 md:h-3 w-2.5 md:w-3 bg-warning-amber/60 rounded-full"></div>
|
||||
<div className="h-2.5 md:h-3 w-2.5 md:w-3 bg-performance-green/60 rounded-full"></div>
|
||||
<div className="flex-1 h-2.5 md:h-3 bg-white/5 rounded ml-2 px-2 flex items-center">
|
||||
<div className="text-[8px] md:text-[9px] text-white/30 font-mono">members.iracing.com</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Automation Steps */}
|
||||
<div className="space-y-2">
|
||||
{[1, 2, 3, 4].map((step, index) => (
|
||||
{/* Automation Steps - More detailed */}
|
||||
<div className="space-y-3 md:space-y-4">
|
||||
{[
|
||||
{ label: 'Open iRacing', status: 'Complete', detail: 'Browser ready' },
|
||||
{ label: 'Navigate to Hosted', status: 'Complete', detail: 'Page loaded' },
|
||||
{ label: 'Create Session', status: 'Running', detail: 'Filling form...' },
|
||||
{ label: 'Configure Settings', status: 'Pending', detail: 'Waiting...' }
|
||||
].map((step, index) => (
|
||||
<motion.div
|
||||
key={step}
|
||||
className="flex items-center gap-2"
|
||||
key={index}
|
||||
className="space-y-2"
|
||||
initial={{ opacity: 0, x: -10 }}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
animate={{
|
||||
opacity: 1,
|
||||
x: 0,
|
||||
transition: { delay: shouldReduceMotion ? 0 : 0.5 + (index * 0.15) }
|
||||
transition: { delay: shouldReduceMotion ? 0 : 0.4 + (index * 0.15) }
|
||||
}}
|
||||
>
|
||||
<motion.div
|
||||
className="h-3 w-3 bg-performance-green/40 rounded-full flex items-center justify-center"
|
||||
animate={shouldReduceMotion ? {} : {
|
||||
scale: index === 3 ? [1, 1.2, 1] : 1,
|
||||
opacity: index === 3 ? [0.4, 1, 0.4] : 1
|
||||
}}
|
||||
transition={{ duration: 1.5, repeat: Infinity }}
|
||||
>
|
||||
<div className="h-1.5 w-1.5 bg-performance-green rounded-full"></div>
|
||||
</motion.div>
|
||||
<div className="h-2 w-full bg-white/5 rounded"></div>
|
||||
<div className="flex items-center gap-2.5 md:gap-3">
|
||||
<motion.div
|
||||
className={`h-7 w-7 md:h-8 md:w-8 lg:h-9 lg:w-9 rounded-full flex items-center justify-center flex-shrink-0 border-2 ${
|
||||
step.status === 'Complete'
|
||||
? 'bg-performance-green/40 border-performance-green/60'
|
||||
: step.status === 'Running'
|
||||
? 'bg-primary-blue/40 border-primary-blue/60'
|
||||
: 'bg-charcoal-outline border-white/20'
|
||||
}`}
|
||||
animate={shouldReduceMotion ? {} : step.status === 'Running' ? {
|
||||
scale: [1, 1.15, 1],
|
||||
opacity: [0.4, 1, 0.4]
|
||||
} : {}}
|
||||
transition={{ duration: 1.5, repeat: Infinity }}
|
||||
>
|
||||
{step.status === 'Complete' && (
|
||||
<svg className="h-4 w-4 md:h-5 md:w-5 text-performance-green" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
)}
|
||||
{step.status === 'Running' && (
|
||||
<div className="h-3 w-3 md:h-4 md:w-4 bg-primary-blue rounded-full"></div>
|
||||
)}
|
||||
{step.status === 'Pending' && (
|
||||
<div className="h-2 w-2 md:h-2.5 md:w-2.5 bg-white/30 rounded-full"></div>
|
||||
)}
|
||||
</motion.div>
|
||||
<div className="flex-1">
|
||||
<div className="text-sm md:text-base lg:text-lg text-white font-medium">{step.label}</div>
|
||||
<div className="text-[10px] md:text-xs lg:text-sm text-white/50">{step.detail}</div>
|
||||
</div>
|
||||
</div>
|
||||
{step.status !== 'Pending' && (
|
||||
<div className="h-2.5 md:h-3 w-full bg-white/5 rounded-full overflow-hidden ml-9 md:ml-10 lg:ml-11">
|
||||
<motion.div
|
||||
className={`h-full ${step.status === 'Complete' ? 'bg-performance-green/60' : 'bg-primary-blue/60'}`}
|
||||
initial={{ width: '0%' }}
|
||||
animate={{ width: step.status === 'Complete' ? '100%' : '65%' }}
|
||||
transition={{ duration: 2, ease: 'easeInOut' }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
@@ -112,34 +173,34 @@ export default function CompanionAutomationMockup() {
|
||||
|
||||
{/* Automation Running Indicator */}
|
||||
<motion.div
|
||||
className="absolute top-2 right-2 flex items-center gap-2 bg-deep-graphite/80 backdrop-blur-sm px-3 py-1.5 rounded-full border border-primary-blue/30"
|
||||
className="absolute top-3 right-3 flex items-center gap-2 bg-deep-graphite/90 backdrop-blur-sm px-3 py-2 rounded-full border-2 border-primary-blue/40"
|
||||
animate={shouldReduceMotion ? {} : {
|
||||
boxShadow: [
|
||||
'0 0 8px rgba(25,140,255,0.2)',
|
||||
'0 0 16px rgba(25,140,255,0.4)',
|
||||
'0 0 8px rgba(25,140,255,0.2)'
|
||||
'0 0 12px rgba(25,140,255,0.3)',
|
||||
'0 0 20px rgba(25,140,255,0.5)',
|
||||
'0 0 12px rgba(25,140,255,0.3)'
|
||||
]
|
||||
}}
|
||||
transition={{ duration: 2, repeat: Infinity }}
|
||||
>
|
||||
<motion.div
|
||||
className="h-2 w-2 bg-primary-blue rounded-full"
|
||||
className="h-2.5 w-2.5 md:h-3 md:w-3 bg-primary-blue rounded-full"
|
||||
animate={shouldReduceMotion ? {} : {
|
||||
opacity: [1, 0.5, 1]
|
||||
}}
|
||||
transition={{ duration: 1.5, repeat: Infinity }}
|
||||
/>
|
||||
<div className="text-xs text-primary-blue">Running</div>
|
||||
<div className="text-xs md:text-sm text-primary-blue font-medium">Running</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
|
||||
{/* One-Click Action */}
|
||||
<motion.div
|
||||
variants={itemVariants}
|
||||
className="flex items-center justify-center"
|
||||
{/* One-Click Action - Enhanced */}
|
||||
<motion.div
|
||||
variants={{ hidden: { opacity: 0, y: 10 }, visible: { opacity: 1, y: 0 } }}
|
||||
className="flex flex-col items-center gap-2 md:gap-3"
|
||||
>
|
||||
<motion.div
|
||||
className="bg-primary-blue/20 text-primary-blue px-6 py-2.5 rounded-lg border border-primary-blue/40 text-sm font-semibold"
|
||||
className="bg-primary-blue/20 text-primary-blue px-8 py-4 rounded-lg border-2 border-primary-blue/40 text-base md:text-lg font-semibold cursor-pointer"
|
||||
whileHover={shouldReduceMotion ? {} : {
|
||||
scale: 1.03,
|
||||
boxShadow: '0 4px 24px rgba(25,140,255,0.3)',
|
||||
@@ -148,6 +209,7 @@ export default function CompanionAutomationMockup() {
|
||||
>
|
||||
Create Session
|
||||
</motion.div>
|
||||
<div className="text-[10px] md:text-xs text-white/40">One click. All fields automated.</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
'use client';
|
||||
|
||||
import { motion, useReducedMotion, useMotionValue, useSpring, useTransform } from 'framer-motion';
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export default function DriverProfileMockup() {
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
const [isMobile, setIsMobile] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setIsMobile(window.innerWidth < 768);
|
||||
}, []);
|
||||
|
||||
const stats = [
|
||||
{ label: 'Wins', value: 24 },
|
||||
@@ -16,6 +21,69 @@ export default function DriverProfileMockup() {
|
||||
|
||||
const formData = [85, 72, 68, 91, 88, 95, 88, 79, 82, 91];
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<div className="relative w-full h-full bg-gradient-to-br from-deep-graphite via-iron-gray to-deep-graphite rounded-lg p-3 overflow-hidden">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative h-12 w-12 rounded-full border-2 border-primary-blue/50 overflow-hidden bg-charcoal-outline">
|
||||
<div className="absolute inset-0 flex items-center justify-center text-2xl">🏎️</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-base font-bold text-white">Driver Profile</div>
|
||||
<div className="text-xs text-white/50">Cross-league</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-charcoal-outline">#33</div>
|
||||
</div>
|
||||
|
||||
<div className="relative h-2 bg-charcoal-outline rounded-full overflow-hidden mb-1">
|
||||
<div
|
||||
className="absolute inset-y-0 left-0 bg-gradient-to-r from-primary-blue to-neon-aqua rounded-full"
|
||||
style={{ width: '86%' }}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<span className="text-xs text-gray-400">2150 GP Rating</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-sm font-semibold text-white mb-2">Career Stats</div>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{stats.slice(0, 3).map((stat) => (
|
||||
<div
|
||||
key={stat.label}
|
||||
className="bg-iron-gray/50 border border-charcoal-outline rounded-lg p-2 text-center"
|
||||
>
|
||||
<div className="text-base font-bold text-white font-mono">
|
||||
{stat.value}{stat.suffix}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400 mt-0.5">{stat.label}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-sm font-semibold text-white mb-2">Recent Form</div>
|
||||
<div className="h-16 bg-iron-gray/30 border border-charcoal-outline rounded-lg p-2 flex items-end gap-1">
|
||||
{formData.slice(-6).map((value, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex-1 bg-gradient-to-t from-performance-green to-primary-blue rounded-sm"
|
||||
style={{ height: `${value}%` }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const containerVariants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
@@ -34,28 +102,33 @@ export default function DriverProfileMockup() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative w-full h-full bg-gradient-to-br from-deep-graphite via-iron-gray to-deep-graphite rounded-lg p-6 md:p-8 overflow-hidden">
|
||||
<div className="relative w-full h-full bg-gradient-to-br from-deep-graphite via-iron-gray to-deep-graphite rounded-lg p-1.5 sm:p-3 md:p-5 lg:p-8 overflow-hidden">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: shouldReduceMotion ? 0 : -10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="mb-6"
|
||||
className="mb-1.5 sm:mb-3 md:mb-4 lg:mb-6"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<div className="h-6 w-48 bg-white/10 rounded mb-2"></div>
|
||||
<div className="h-4 w-24 bg-white/5 rounded"></div>
|
||||
<div className="flex items-center justify-between mb-1.5 sm:mb-2 md:mb-3 lg:mb-4">
|
||||
<div className="flex items-center gap-1.5 sm:gap-2 md:gap-3 lg:gap-4">
|
||||
<div className="relative h-8 w-8 sm:h-10 sm:w-10 md:h-12 md:w-12 lg:h-16 lg:w-16 rounded-full border-2 border-primary-blue/50 overflow-hidden bg-charcoal-outline">
|
||||
<div className="absolute inset-0 flex items-center justify-center text-base sm:text-xl md:text-2xl lg:text-3xl">🏎️</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm sm:text-base md:text-lg lg:text-xl font-bold text-white mb-1 sm:mb-1.5 md:mb-2">Driver Profile</div>
|
||||
<div className="text-[8px] sm:text-[10px] md:text-xs text-white/50">Cross-league racing identity</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-4xl font-bold text-charcoal-outline">#33</div>
|
||||
<div className="text-xl sm:text-2xl md:text-3xl lg:text-4xl font-bold text-charcoal-outline">#33</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 mb-2">
|
||||
<div className="text-xs text-gray-400">GridPilot Rating:</div>
|
||||
<div className="flex flex-wrap items-center gap-1 sm:gap-2 md:gap-3 lg:gap-4 mb-1 sm:mb-1.5 md:mb-2">
|
||||
<div className="text-[8px] sm:text-[10px] md:text-xs text-gray-400">GridPilot Rating:</div>
|
||||
<AnimatedRating shouldReduceMotion={shouldReduceMotion ?? false} value={2150} />
|
||||
<div className="text-xs text-gray-400 ml-4">iRating:</div>
|
||||
<div className="text-[8px] sm:text-[10px] md:text-xs text-gray-400">iRating:</div>
|
||||
<AnimatedRating shouldReduceMotion={shouldReduceMotion ?? false} value={3200} />
|
||||
</div>
|
||||
|
||||
<div className="relative h-3 bg-charcoal-outline rounded-full overflow-hidden">
|
||||
<div className="relative h-1.5 sm:h-2 md:h-2.5 lg:h-3 bg-charcoal-outline rounded-full overflow-hidden">
|
||||
<motion.div
|
||||
className="absolute inset-y-0 left-0 bg-gradient-to-r from-primary-blue to-neon-aqua rounded-full"
|
||||
initial={{ width: '0%' }}
|
||||
@@ -64,7 +137,7 @@ export default function DriverProfileMockup() {
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-end mt-1">
|
||||
<span className="text-xs text-gray-400">86%</span>
|
||||
<span className="text-[8px] sm:text-[10px] md:text-xs text-gray-400">86%</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
@@ -72,16 +145,17 @@ export default function DriverProfileMockup() {
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
className="mb-6"
|
||||
className="mb-1.5 sm:mb-3 md:mb-4 lg:mb-6"
|
||||
>
|
||||
<div className="h-4 w-32 bg-white/10 rounded mb-3"></div>
|
||||
<div className="text-[9px] sm:text-xs md:text-sm font-semibold text-white mb-1">Career Statistics</div>
|
||||
<div className="text-[8px] sm:text-[10px] md:text-xs text-white/50 mb-1 sm:mb-2 md:mb-3">Aggregated across all leagues</div>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-3">
|
||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-1.5 sm:gap-2 md:gap-3">
|
||||
{stats.map((stat, index) => (
|
||||
<motion.div
|
||||
key={stat.label}
|
||||
variants={itemVariants}
|
||||
className="bg-iron-gray/50 border border-charcoal-outline rounded-lg p-3 text-center"
|
||||
className="bg-iron-gray/50 border border-charcoal-outline rounded-lg p-1.5 sm:p-2 md:p-3 text-center"
|
||||
>
|
||||
<AnimatedCounter
|
||||
value={stat.value}
|
||||
@@ -89,7 +163,7 @@ export default function DriverProfileMockup() {
|
||||
delay={index * 0.1}
|
||||
suffix={stat.suffix}
|
||||
/>
|
||||
<div className="text-xs text-gray-400 mt-1">{stat.label}</div>
|
||||
<div className="text-[8px] sm:text-[10px] md:text-xs text-gray-400 mt-0.5">{stat.label}</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
@@ -99,11 +173,12 @@ export default function DriverProfileMockup() {
|
||||
initial={{ opacity: 0, y: shouldReduceMotion ? 0 : 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: shouldReduceMotion ? 0 : 0.6 }}
|
||||
className="mb-6"
|
||||
className="mb-1.5 sm:mb-3 md:mb-4 lg:mb-6"
|
||||
>
|
||||
<div className="h-4 w-28 bg-white/10 rounded mb-3"></div>
|
||||
<div className="text-[9px] sm:text-xs md:text-sm font-semibold text-white mb-1">Recent Form</div>
|
||||
<div className="text-[8px] sm:text-[10px] md:text-xs text-white/50 mb-1 sm:mb-2 md:mb-3">Performance trend over last 10 races</div>
|
||||
|
||||
<div className="h-20 bg-iron-gray/30 border border-charcoal-outline rounded-lg p-3 flex items-end gap-1">
|
||||
<div className="h-12 sm:h-16 md:h-20 bg-iron-gray/30 border border-charcoal-outline rounded-lg p-1.5 sm:p-2 md:p-3 flex items-end gap-0.5">
|
||||
{formData.map((value, i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
@@ -118,7 +193,7 @@ export default function DriverProfileMockup() {
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex justify-between mt-1 text-xs text-gray-500">
|
||||
<div className="flex justify-between mt-0.5 text-[8px] sm:text-[10px] md:text-xs text-gray-500">
|
||||
<span>Last 10 races</span>
|
||||
<span>Recent</span>
|
||||
</div>
|
||||
@@ -129,9 +204,10 @@ export default function DriverProfileMockup() {
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: shouldReduceMotion ? 0 : 0.8 }}
|
||||
>
|
||||
<div className="h-4 w-20 bg-white/10 rounded mb-3"></div>
|
||||
<div className="text-[9px] sm:text-xs md:text-sm font-semibold text-white mb-1">Teams</div>
|
||||
<div className="text-[8px] sm:text-[10px] md:text-xs text-white/50 mb-1 sm:mb-2 md:mb-3">Current and past team memberships</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-1 sm:space-y-1.5 md:space-y-2">
|
||||
{[
|
||||
{ team: 'Red Bull Racing', status: 'Current', color: 'primary-blue' },
|
||||
{ team: 'Mercedes AMG', status: '2023', color: 'charcoal-outline' }
|
||||
@@ -141,12 +217,17 @@ export default function DriverProfileMockup() {
|
||||
initial={{ opacity: 0, x: shouldReduceMotion ? 0 : -10 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: shouldReduceMotion ? 0 : 0.9 + i * 0.1 }}
|
||||
className="flex items-center justify-between bg-iron-gray/30 border border-charcoal-outline rounded-lg p-2 text-sm"
|
||||
className="flex items-center justify-between bg-iron-gray/30 border border-charcoal-outline rounded-lg p-1 sm:p-1.5 md:p-2 text-[10px] sm:text-xs md:text-sm"
|
||||
>
|
||||
<div className="h-3 w-32 bg-white/10 rounded"></div>
|
||||
<span className={`text-xs px-2 py-0.5 rounded ${
|
||||
team.status === 'Current'
|
||||
? 'bg-primary-blue/20 text-primary-blue'
|
||||
<div className="flex items-center gap-2 sm:gap-3">
|
||||
<div className="h-5 w-5 sm:h-6 sm:w-6 md:h-8 md:w-8 rounded border border-primary-blue/30 bg-charcoal-outline flex items-center justify-center text-sm sm:text-base md:text-lg">
|
||||
🏁
|
||||
</div>
|
||||
<div className="h-1.5 sm:h-2 md:h-3 w-16 sm:w-20 md:w-32 bg-white/10 rounded"></div>
|
||||
</div>
|
||||
<span className={`text-[8px] sm:text-[10px] md:text-xs px-1 sm:px-1.5 md:px-2 py-0.5 rounded ${
|
||||
team.status === 'Current'
|
||||
? 'bg-primary-blue/20 text-primary-blue'
|
||||
: 'bg-charcoal-outline text-gray-400'
|
||||
}`}>
|
||||
{team.status}
|
||||
@@ -159,7 +240,7 @@ export default function DriverProfileMockup() {
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: shouldReduceMotion ? 0 : 1.2 }}
|
||||
className="mt-4 text-center text-xs text-gray-400"
|
||||
className="mt-2 sm:mt-3 md:mt-4 text-center text-[8px] sm:text-[10px] md:text-xs text-gray-400"
|
||||
>
|
||||
Active in 3 leagues
|
||||
</motion.div>
|
||||
@@ -183,7 +264,7 @@ function AnimatedRating({ shouldReduceMotion, value }: { shouldReduceMotion: boo
|
||||
}, [shouldReduceMotion, count, value]);
|
||||
|
||||
return (
|
||||
<motion.span className="text-lg font-bold text-primary-blue font-mono">
|
||||
<motion.span className="text-sm sm:text-base md:text-lg font-bold text-primary-blue font-mono">
|
||||
{shouldReduceMotion ? value : <motion.span>{rounded}</motion.span>}
|
||||
</motion.span>
|
||||
);
|
||||
@@ -213,7 +294,7 @@ function AnimatedCounter({
|
||||
}, [shouldReduceMotion, count, value, delay]);
|
||||
|
||||
return (
|
||||
<div className="text-xl font-bold text-white font-mono">
|
||||
<div className="text-sm sm:text-base md:text-lg lg:text-xl font-bold text-white font-mono">
|
||||
{shouldReduceMotion ? value : <motion.span>{rounded}</motion.span>}{suffix}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
'use client';
|
||||
|
||||
import { motion, useReducedMotion } from 'framer-motion';
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
export default function LeagueDiscoveryMockup() {
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
|
||||
const [isMobile, setIsMobile] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setIsMobile(window.innerWidth < 768);
|
||||
}, []);
|
||||
|
||||
const leagues = [
|
||||
{
|
||||
@@ -30,6 +35,81 @@ export default function LeagueDiscoveryMockup() {
|
||||
}
|
||||
];
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<div className="relative w-full h-full bg-gradient-to-br from-deep-graphite via-iron-gray to-deep-graphite rounded-lg p-3 overflow-hidden">
|
||||
<div className="mb-3">
|
||||
<div className="h-4 w-40 bg-white/10 rounded mb-3"></div>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{['Game', 'Region'].map((filter) => (
|
||||
<div
|
||||
key={filter}
|
||||
className="h-6 px-3 bg-charcoal-outline border border-primary-blue/30 rounded-full flex items-center"
|
||||
>
|
||||
<div className="h-1.5 w-10 bg-white/10 rounded"></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{leagues.map((league) => (
|
||||
<div
|
||||
key={league.name}
|
||||
className="bg-iron-gray/80 border border-charcoal-outline rounded-lg p-3"
|
||||
>
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-10 w-10 rounded-lg border-2 border-primary-blue/30 bg-charcoal-outline flex items-center justify-center text-xl">
|
||||
{league.icon}
|
||||
</div>
|
||||
<div>
|
||||
<div className="h-3 w-32 bg-white/10 rounded mb-1"></div>
|
||||
<div className="flex gap-1 text-xs">
|
||||
<span className="px-1.5 py-0.5 bg-primary-blue/20 text-primary-blue rounded">
|
||||
{league.carClass}
|
||||
</span>
|
||||
<span className="px-1.5 py-0.5 bg-neon-aqua/20 text-neon-aqua rounded">
|
||||
{league.region}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between text-xs text-gray-400 mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{league.drivers} drivers</span>
|
||||
<span>•</span>
|
||||
<span>{league.schedule}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-1">
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<svg
|
||||
key={i}
|
||||
className={`w-3 h-3 ${i < Math.floor(league.rating) ? 'text-warning-amber' : 'text-charcoal-outline'}`}
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
|
||||
</svg>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<button className="px-2 py-1 bg-primary-blue text-white text-xs rounded">
|
||||
Join
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const containerVariants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
@@ -48,24 +128,24 @@ export default function LeagueDiscoveryMockup() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative w-full h-full bg-gradient-to-br from-deep-graphite via-iron-gray to-deep-graphite rounded-lg p-6 md:p-8 overflow-hidden">
|
||||
<div className="relative w-full h-full bg-gradient-to-br from-deep-graphite via-iron-gray to-deep-graphite rounded-lg p-1.5 sm:p-3 md:p-5 lg:p-8 overflow-hidden">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: shouldReduceMotion ? 0 : -10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="mb-6"
|
||||
className="mb-1.5 sm:mb-3 md:mb-4 lg:mb-6"
|
||||
>
|
||||
<div className="h-6 w-52 bg-white/10 rounded mb-4"></div>
|
||||
<div className="h-3 sm:h-4 md:h-5 lg:h-6 w-32 sm:w-40 md:w-52 bg-white/10 rounded mb-1.5 sm:mb-2 md:mb-3 lg:mb-4"></div>
|
||||
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<div className="flex gap-1 sm:gap-1.5 md:gap-2 flex-wrap">
|
||||
{['Game', 'Region', 'Skill'].map((filter, i) => (
|
||||
<motion.div
|
||||
key={filter}
|
||||
initial={{ opacity: 0, scale: shouldReduceMotion ? 1 : 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ delay: shouldReduceMotion ? 0 : i * 0.1 }}
|
||||
className="h-8 px-4 bg-charcoal-outline border border-primary-blue/30 rounded-full flex items-center"
|
||||
className="h-5 sm:h-6 md:h-7 lg:h-8 px-2 sm:px-3 md:px-4 bg-charcoal-outline border border-primary-blue/30 rounded-full flex items-center"
|
||||
>
|
||||
<div className="h-2 w-12 bg-white/10 rounded"></div>
|
||||
<div className="h-1 sm:h-1.5 md:h-2 w-8 sm:w-10 md:w-12 bg-white/10 rounded"></div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
@@ -75,7 +155,7 @@ export default function LeagueDiscoveryMockup() {
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
className="space-y-4"
|
||||
className="space-y-1.5 sm:space-y-2 md:space-y-3 lg:space-y-4"
|
||||
>
|
||||
{leagues.map((league, index) => (
|
||||
<motion.div
|
||||
@@ -88,21 +168,23 @@ export default function LeagueDiscoveryMockup() {
|
||||
y: -4,
|
||||
transition: { duration: 0.2 }
|
||||
}}
|
||||
className="bg-iron-gray/80 border border-charcoal-outline rounded-lg p-4 backdrop-blur-sm"
|
||||
className="bg-iron-gray/80 border border-charcoal-outline rounded-lg p-1.5 sm:p-2 md:p-3 lg:p-4 backdrop-blur-sm"
|
||||
>
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="text-2xl">{league.icon}</div>
|
||||
<div className="flex items-start justify-between mb-1.5 sm:mb-2 md:mb-3">
|
||||
<div className="flex items-center gap-1.5 sm:gap-2 md:gap-3">
|
||||
<div className="h-7 w-7 sm:h-8 sm:w-8 md:h-10 md:w-10 lg:h-12 lg:w-12 rounded-lg border-2 border-primary-blue/30 bg-charcoal-outline flex items-center justify-center text-base sm:text-lg md:text-xl lg:text-2xl shadow-[0_0_12px_rgba(25,140,255,0.2)]">
|
||||
{league.icon}
|
||||
</div>
|
||||
<div>
|
||||
<div className="h-4 w-40 bg-white/10 rounded mb-2"></div>
|
||||
<div className="flex gap-2 text-xs">
|
||||
<span className="px-2 py-0.5 bg-primary-blue/20 text-primary-blue rounded">
|
||||
<div className="h-2.5 sm:h-3 md:h-4 w-28 sm:w-32 md:w-40 bg-white/10 rounded mb-1 sm:mb-1.5 md:mb-2"></div>
|
||||
<div className="flex gap-1 sm:gap-1.5 md:gap-2 text-[8px] sm:text-[10px] md:text-xs">
|
||||
<span className="px-1 sm:px-1.5 md:px-2 py-0.5 bg-primary-blue/20 text-primary-blue rounded">
|
||||
{league.carClass}
|
||||
</span>
|
||||
<span className="px-2 py-0.5 bg-neon-aqua/20 text-neon-aqua rounded">
|
||||
<span className="px-1 sm:px-1.5 md:px-2 py-0.5 bg-neon-aqua/20 text-neon-aqua rounded">
|
||||
{league.region}
|
||||
</span>
|
||||
<span className="px-2 py-0.5 bg-charcoal-outline text-gray-400 rounded">
|
||||
<span className="px-1 sm:px-1.5 md:px-2 py-0.5 bg-charcoal-outline text-gray-400 rounded">
|
||||
{league.skill}
|
||||
</span>
|
||||
</div>
|
||||
@@ -110,16 +192,16 @@ export default function LeagueDiscoveryMockup() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between text-xs text-gray-400 mb-3">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center justify-between text-[8px] sm:text-[10px] md:text-xs text-gray-400 mb-1.5 sm:mb-2 md:mb-3">
|
||||
<div className="flex items-center gap-1.5 sm:gap-2 md:gap-3 lg:gap-4">
|
||||
<div className="flex items-center gap-1">
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<svg className="w-2.5 h-2.5 sm:w-3 sm:h-3 md:w-4 md:h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
|
||||
</svg>
|
||||
<span>{league.drivers} drivers</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<svg className="w-2.5 h-2.5 sm:w-3 sm:h-3 md:w-4 md:h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span>{league.schedule}</span>
|
||||
@@ -132,7 +214,7 @@ export default function LeagueDiscoveryMockup() {
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<motion.svg
|
||||
key={i}
|
||||
className={`w-4 h-4 ${i < Math.floor(league.rating) ? 'text-warning-amber' : 'text-charcoal-outline'}`}
|
||||
className={`w-2.5 h-2.5 sm:w-3 sm:h-3 md:w-4 md:h-4 ${i < Math.floor(league.rating) ? 'text-warning-amber' : 'text-charcoal-outline'}`}
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
animate={hoveredIndex === index && !shouldReduceMotion ? {
|
||||
@@ -143,21 +225,21 @@ export default function LeagueDiscoveryMockup() {
|
||||
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
|
||||
</motion.svg>
|
||||
))}
|
||||
<span className="text-xs text-gray-400 ml-1">{league.rating}</span>
|
||||
<span className="text-[8px] sm:text-[10px] md:text-xs text-gray-400 ml-1">{league.rating}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<div className="flex gap-1 sm:gap-1.5 md:gap-2">
|
||||
<motion.button
|
||||
whileHover={shouldReduceMotion ? {} : { scale: 1.05 }}
|
||||
whileTap={shouldReduceMotion ? {} : { scale: 0.95 }}
|
||||
className="px-3 py-1 bg-primary-blue text-white text-xs rounded hover:bg-primary-blue/80 transition-colors"
|
||||
className="px-1.5 sm:px-2 md:px-3 py-0.5 bg-primary-blue text-white text-[8px] sm:text-[10px] md:text-xs rounded hover:bg-primary-blue/80 transition-colors"
|
||||
>
|
||||
Join
|
||||
</motion.button>
|
||||
<motion.button
|
||||
whileHover={shouldReduceMotion ? {} : { scale: 1.05 }}
|
||||
whileTap={shouldReduceMotion ? {} : { scale: 0.95 }}
|
||||
className="px-3 py-1 bg-charcoal-outline text-gray-300 text-xs rounded hover:bg-charcoal-outline/80 transition-colors"
|
||||
className="px-1.5 sm:px-2 md:px-3 py-0.5 bg-charcoal-outline text-gray-300 text-[8px] sm:text-[10px] md:text-xs rounded hover:bg-charcoal-outline/80 transition-colors"
|
||||
>
|
||||
View
|
||||
</motion.button>
|
||||
|
||||
@@ -1,9 +1,63 @@
|
||||
'use client';
|
||||
|
||||
import { motion, useReducedMotion } from 'framer-motion';
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
export default function LeagueHomeMockup() {
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
const [isMobile, setIsMobile] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setIsMobile(window.innerWidth < 768);
|
||||
}, []);
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<div className="relative w-full h-full bg-gradient-to-br from-deep-graphite via-iron-gray to-deep-graphite rounded-lg p-3 overflow-hidden">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="h-12 w-12 rounded-lg border-2 border-primary-blue/50 bg-charcoal-outline flex items-center justify-center text-2xl shadow-[0_0_20px_rgba(25,140,255,0.3)]">
|
||||
🏆
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-base font-bold text-white">Super GT</div>
|
||||
<div className="text-xs text-gray-400">Round 8/12</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-sm font-semibold text-white mb-3">Next Race</div>
|
||||
<div className="relative flex items-center gap-3 bg-iron-gray rounded-lg p-3 border border-charcoal-outline">
|
||||
<div className="h-8 w-8 bg-charcoal-outline rounded border border-primary-blue/20 flex items-center justify-center text-base">
|
||||
🏁
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="h-2.5 w-28 bg-white/10 rounded mb-2"></div>
|
||||
<div className="h-2 w-20 bg-white/5 rounded"></div>
|
||||
</div>
|
||||
<div className="w-2 h-2 bg-primary-blue rounded-full shadow-glow"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-sm font-semibold text-white mb-3">Latest Result</div>
|
||||
<div className="bg-iron-gray rounded-lg p-3 border border-charcoal-outline">
|
||||
<div className="flex items-center gap-3 py-2 border-b border-charcoal-outline">
|
||||
<div className="h-2.5 w-6 bg-white/10 rounded"></div>
|
||||
<div className="h-2.5 flex-1 bg-white/10 rounded"></div>
|
||||
<div className="h-2.5 w-10 bg-white/10 rounded"></div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 py-2">
|
||||
<div className="h-2 w-6 bg-white/5 rounded"></div>
|
||||
<div className="h-2 flex-1 bg-white/5 rounded"></div>
|
||||
<div className="h-2 w-10 bg-performance-green/20 rounded"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const containerVariants = {
|
||||
hidden: { opacity: 0 },
|
||||
@@ -19,25 +73,34 @@ export default function LeagueHomeMockup() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative w-full h-full bg-gradient-to-br from-deep-graphite via-iron-gray to-deep-graphite rounded-lg p-8 overflow-hidden">
|
||||
<div className="relative w-full h-full bg-gradient-to-br from-deep-graphite via-iron-gray to-deep-graphite rounded-lg p-1.5 sm:p-3 md:p-5 lg:p-8 overflow-hidden">
|
||||
<motion.div
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
className="space-y-6"
|
||||
className="space-y-1.5 sm:space-y-3 md:space-y-4 lg:space-y-6"
|
||||
>
|
||||
<motion.div variants={itemVariants}>
|
||||
<div className="text-xl font-bold text-white mb-2">Super GT Championship</div>
|
||||
<div className="text-sm text-gray-400">Season 3 • Round 8/12</div>
|
||||
<div className="flex items-center gap-1.5 sm:gap-2 md:gap-3 lg:gap-4 mb-1 sm:mb-1.5 md:mb-2">
|
||||
<div className="h-8 w-8 sm:h-10 sm:w-10 md:h-12 md:w-12 lg:h-16 lg:w-16 rounded-lg border-2 border-primary-blue/50 bg-charcoal-outline flex items-center justify-center text-base sm:text-xl md:text-2xl lg:text-3xl shadow-[0_0_20px_rgba(25,140,255,0.3)]">
|
||||
🏆
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm sm:text-base md:text-lg lg:text-xl font-bold text-white mb-0.5">Super GT Championship</div>
|
||||
<div className="text-[9px] sm:text-xs md:text-sm text-gray-400">Season 3 • Round 8/12</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-[8px] sm:text-[10px] md:text-xs text-white/50 mt-1 sm:mt-1.5 md:mt-2">Your league's dedicated home page</div>
|
||||
</motion.div>
|
||||
|
||||
<motion.div variants={itemVariants}>
|
||||
<div className="text-base font-semibold text-white mb-4">Upcoming Races</div>
|
||||
<div className="space-y-3">
|
||||
<div className="text-[10px] sm:text-sm md:text-base font-semibold text-white mb-1.5 sm:mb-2 md:mb-3 lg:mb-4">Upcoming Races</div>
|
||||
<div className="text-[8px] sm:text-[10px] md:text-xs text-white/50 mb-1.5 sm:mb-2 md:mb-3">Calendar automatically synced from iRacing</div>
|
||||
<div className="space-y-1.5 sm:space-y-2 md:space-y-3">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
className="relative flex items-center gap-4 bg-iron-gray rounded-lg p-4 border border-charcoal-outline shadow-[inset_0_1px_2px_rgba(0,0,0,0.2)]"
|
||||
className="relative flex items-center gap-1.5 sm:gap-2 md:gap-3 lg:gap-4 bg-iron-gray rounded-lg p-1.5 sm:p-2 md:p-3 lg:p-4 border border-charcoal-outline shadow-[inset_0_1px_2px_rgba(0,0,0,0.2)]"
|
||||
whileHover={shouldReduceMotion ? {} : {
|
||||
y: -2,
|
||||
boxShadow: '0 4px 24px rgba(0,0,0,0.4), 0 0 20px rgba(25,140,255,0.3)',
|
||||
@@ -45,10 +108,12 @@ export default function LeagueHomeMockup() {
|
||||
}}
|
||||
transition={{ type: 'spring', stiffness: 200, damping: 20 }}
|
||||
>
|
||||
<div className="h-10 w-10 bg-charcoal-outline rounded border border-primary-blue/20"></div>
|
||||
<div className="h-6 w-6 sm:h-7 sm:w-7 md:h-8 md:w-8 lg:h-10 lg:w-10 bg-charcoal-outline rounded border border-primary-blue/20 flex items-center justify-center text-sm sm:text-base md:text-lg lg:text-xl">
|
||||
🏁
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="h-3 w-32 bg-white/10 rounded mb-2"></div>
|
||||
<div className="h-2.5 w-24 bg-white/5 rounded font-mono"></div>
|
||||
<div className="h-1.5 sm:h-2 md:h-2.5 lg:h-3 w-20 sm:w-24 md:w-28 lg:w-32 bg-white/10 rounded mb-1 sm:mb-1.5 md:mb-2"></div>
|
||||
<div className="h-1 sm:h-1.5 md:h-2 lg:h-2.5 w-12 sm:w-16 md:w-20 lg:w-24 bg-white/5 rounded font-mono"></div>
|
||||
</div>
|
||||
{i === 1 && (
|
||||
<motion.div
|
||||
@@ -63,7 +128,7 @@ export default function LeagueHomeMockup() {
|
||||
}}
|
||||
transition={{ duration: 2, repeat: Infinity }}
|
||||
>
|
||||
<div className="w-3 h-3 bg-primary-blue rounded-full shadow-glow"></div>
|
||||
<div className="w-1.5 h-1.5 sm:w-2 sm:h-2 md:w-2.5 md:h-2.5 lg:w-3 lg:h-3 bg-primary-blue rounded-full shadow-glow"></div>
|
||||
</motion.div>
|
||||
)}
|
||||
</motion.div>
|
||||
@@ -72,18 +137,19 @@ export default function LeagueHomeMockup() {
|
||||
</motion.div>
|
||||
|
||||
<motion.div variants={itemVariants}>
|
||||
<div className="text-base font-semibold text-white mb-4">Recent Results</div>
|
||||
<div className="bg-iron-gray rounded-lg p-4 border border-charcoal-outline shadow-[0_4px_24px_rgba(0,0,0,0.4)]">
|
||||
<div className="flex items-center gap-3 mb-3 pb-3 border-b border-charcoal-outline">
|
||||
<div className="h-2.5 w-8 bg-white/10 rounded font-mono"></div>
|
||||
<div className="h-2.5 flex-1 bg-white/10 rounded"></div>
|
||||
<div className="h-2.5 w-12 bg-white/10 rounded font-mono"></div>
|
||||
<div className="text-[10px] sm:text-sm md:text-base font-semibold text-white mb-1.5 sm:mb-2 md:mb-3 lg:mb-4">Recent Results</div>
|
||||
<div className="text-[8px] sm:text-[10px] md:text-xs text-white/50 mb-1.5 sm:mb-2 md:mb-3">Results appear instantly after each race</div>
|
||||
<div className="bg-iron-gray rounded-lg p-1.5 sm:p-2 md:p-3 lg:p-4 border border-charcoal-outline shadow-[0_4px_24px_rgba(0,0,0,0.4)]">
|
||||
<div className="flex items-center gap-1.5 sm:gap-2 md:gap-3 mb-1.5 sm:mb-2 md:mb-3 pb-1.5 sm:pb-2 md:pb-3 border-b border-charcoal-outline">
|
||||
<div className="h-1.5 sm:h-2 md:h-2.5 w-5 sm:w-6 md:w-8 bg-white/10 rounded font-mono"></div>
|
||||
<div className="h-1.5 sm:h-2 md:h-2.5 flex-1 bg-white/10 rounded"></div>
|
||||
<div className="h-1.5 sm:h-2 md:h-2.5 w-8 sm:w-10 md:w-12 bg-white/10 rounded font-mono"></div>
|
||||
</div>
|
||||
{[1, 2].map((i) => (
|
||||
<div key={i} className="flex items-center gap-3 py-2">
|
||||
<div className="h-2.5 w-8 bg-white/5 rounded font-mono"></div>
|
||||
<div className="h-2.5 flex-1 bg-white/5 rounded"></div>
|
||||
<div className="h-2.5 w-12 bg-performance-green/20 rounded text-center font-mono text-performance-green"></div>
|
||||
<div key={i} className="flex items-center gap-1.5 sm:gap-2 md:gap-3 py-1 sm:py-1.5 md:py-2">
|
||||
<div className="h-1.5 sm:h-2 md:h-2.5 w-5 sm:w-6 md:w-8 bg-white/5 rounded font-mono"></div>
|
||||
<div className="h-1.5 sm:h-2 md:h-2.5 flex-1 bg-white/5 rounded"></div>
|
||||
<div className="h-1.5 sm:h-2 md:h-2.5 w-8 sm:w-10 md:w-12 bg-performance-green/20 rounded text-center font-mono text-performance-green"></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
'use client';
|
||||
|
||||
import { motion, useReducedMotion } from 'framer-motion';
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
export default function ProtestWorkflowMockup() {
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
const [activeStep, setActiveStep] = useState<number>(1);
|
||||
const [isMobile, setIsMobile] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setIsMobile(window.innerWidth < 768);
|
||||
}, []);
|
||||
|
||||
const steps = [
|
||||
{
|
||||
@@ -28,6 +33,50 @@ export default function ProtestWorkflowMockup() {
|
||||
},
|
||||
];
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'pending': return 'bg-charcoal-outline border-charcoal-outline text-gray-500';
|
||||
case 'active': return 'bg-warning-amber/20 border-warning-amber text-warning-amber';
|
||||
case 'resolved': return 'bg-performance-green/20 border-performance-green text-performance-green';
|
||||
default: return 'bg-charcoal-outline border-charcoal-outline text-gray-500';
|
||||
}
|
||||
};
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<div className="relative w-full h-full bg-gradient-to-br from-deep-graphite via-iron-gray to-deep-graphite rounded-lg overflow-hidden p-3 flex flex-col justify-center gap-4">
|
||||
<div className="flex items-center justify-center gap-3">
|
||||
{steps.map((step, i) => (
|
||||
<div key={step.name} className="flex items-center">
|
||||
<div className="flex flex-col items-center">
|
||||
<div
|
||||
className={`w-10 h-10 rounded-lg flex items-center justify-center mb-1 border-2 ${getStatusColor(step.status)}`}
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={step.icon} />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="text-xs text-white/70 text-center">{step.name}</div>
|
||||
</div>
|
||||
{i < steps.length - 1 && (
|
||||
<svg className="w-4 h-4 mx-1" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M5 12h14m-7-7l7 7-7 7" stroke="#43C9E6" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="relative h-1 bg-charcoal-outline rounded-full overflow-hidden">
|
||||
<div
|
||||
className="absolute inset-y-0 left-0 bg-gradient-to-r from-neon-aqua to-primary-blue rounded-full"
|
||||
style={{ width: `${((activeStep + 1) / steps.length) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const stepVariants = {
|
||||
hidden: { opacity: 0, scale: shouldReduceMotion ? 1 : 0.8 },
|
||||
visible: (i: number) => ({
|
||||
@@ -55,18 +104,9 @@ export default function ProtestWorkflowMockup() {
|
||||
})
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'pending': return 'bg-charcoal-outline border-charcoal-outline text-gray-500';
|
||||
case 'active': return 'bg-warning-amber/20 border-warning-amber text-warning-amber';
|
||||
case 'resolved': return 'bg-performance-green/20 border-performance-green text-performance-green';
|
||||
default: return 'bg-charcoal-outline border-charcoal-outline text-gray-500';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative w-full h-full bg-gradient-to-br from-deep-graphite via-iron-gray to-deep-graphite rounded-lg overflow-hidden p-6 flex flex-col justify-center">
|
||||
<div className="flex flex-col md:flex-row items-center justify-center gap-4 mb-4">
|
||||
<div className="relative w-full h-full bg-gradient-to-br from-deep-graphite via-iron-gray to-deep-graphite rounded-lg overflow-hidden p-1.5 sm:p-3 md:p-5 lg:p-8 flex flex-col justify-center gap-2 sm:gap-4 md:gap-6 lg:gap-8">
|
||||
<div className="flex flex-col md:flex-row items-center justify-center gap-2 sm:gap-3 md:gap-4">
|
||||
{steps.map((step, i) => (
|
||||
<div key={step.name} className="flex items-center flex-shrink-0">
|
||||
<motion.div
|
||||
@@ -78,7 +118,7 @@ export default function ProtestWorkflowMockup() {
|
||||
onHoverStart={() => !shouldReduceMotion && setActiveStep(i)}
|
||||
>
|
||||
<motion.div
|
||||
className={`relative w-12 h-12 md:w-14 md:h-14 rounded-lg flex items-center justify-center mb-2 border-2 ${getStatusColor(step.status)}`}
|
||||
className={`relative w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 lg:w-14 lg:h-14 rounded-lg flex items-center justify-center mb-1 sm:mb-1.5 md:mb-2 border-2 ${getStatusColor(step.status)}`}
|
||||
whileHover={shouldReduceMotion ? {} : {
|
||||
scale: 1.1,
|
||||
boxShadow: step.status === 'active'
|
||||
@@ -90,7 +130,7 @@ export default function ProtestWorkflowMockup() {
|
||||
}}
|
||||
transition={{ type: 'spring', stiffness: 300, damping: 15 }}
|
||||
>
|
||||
<svg className="w-6 h-6 md:w-7 md:h-7" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<svg className="w-4 h-4 sm:w-5 sm:h-5 md:w-6 md:h-6 lg:w-7 lg:h-7" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={step.icon} />
|
||||
</svg>
|
||||
{step.status === 'active' && (
|
||||
@@ -107,9 +147,9 @@ export default function ProtestWorkflowMockup() {
|
||||
/>
|
||||
)}
|
||||
</motion.div>
|
||||
<div className="text-xs text-white/70 text-center mb-1">{step.name}</div>
|
||||
<div className="text-[8px] sm:text-[10px] md:text-xs text-white/70 text-center mb-0.5">{step.name}</div>
|
||||
<motion.div
|
||||
className={`h-1.5 w-10 md:w-12 rounded ${
|
||||
className={`h-0.5 sm:h-1 md:h-1.5 w-6 sm:w-8 md:w-10 lg:w-12 rounded ${
|
||||
step.status === 'pending' ? 'bg-charcoal-outline' :
|
||||
step.status === 'active' ? 'bg-warning-amber/30' :
|
||||
'bg-performance-green/30'
|
||||
@@ -122,8 +162,8 @@ export default function ProtestWorkflowMockup() {
|
||||
</motion.div>
|
||||
|
||||
{i < steps.length - 1 && (
|
||||
<div className="hidden md:block relative ml-1.5">
|
||||
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none">
|
||||
<div className="hidden md:block relative ml-1">
|
||||
<svg className="w-3 h-3 sm:w-4 sm:h-4 md:w-5 md:h-5" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M5 12h14m-7-7l7 7-7 7" stroke="#43C9E6" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
@@ -136,7 +176,7 @@ export default function ProtestWorkflowMockup() {
|
||||
initial={{ opacity: 0, scaleX: 0 }}
|
||||
animate={{ opacity: 1, scaleX: 1 }}
|
||||
transition={{ delay: shouldReduceMotion ? 0 : 0.8, duration: 0.6 }}
|
||||
className="relative h-1 bg-charcoal-outline rounded-full overflow-hidden"
|
||||
className="relative h-0.5 sm:h-0.5 md:h-1 bg-charcoal-outline rounded-full overflow-hidden"
|
||||
>
|
||||
<motion.div
|
||||
className="absolute inset-y-0 left-0 bg-gradient-to-r from-neon-aqua to-primary-blue rounded-full"
|
||||
|
||||
@@ -1,130 +1,204 @@
|
||||
'use client';
|
||||
|
||||
import { motion, useReducedMotion } from 'framer-motion';
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
export default function RaceHistoryMockup() {
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
const [isMobile, setIsMobile] = useState(false);
|
||||
|
||||
const containerVariants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: { staggerChildren: shouldReduceMotion ? 0 : 0.15 }
|
||||
}
|
||||
useEffect(() => {
|
||||
const checkMobile = () => setIsMobile(window.innerWidth < 768);
|
||||
checkMobile();
|
||||
window.addEventListener('resize', checkMobile);
|
||||
return () => window.removeEventListener('resize', checkMobile);
|
||||
}, []);
|
||||
|
||||
const itemVariants = {
|
||||
hidden: { opacity: 0, scale: shouldReduceMotion ? 1 : 0.95 },
|
||||
visible: { opacity: 1, scale: 1 }
|
||||
};
|
||||
|
||||
const raceCardVariants = {
|
||||
hidden: { opacity: 0, x: shouldReduceMotion ? 0 : -20 },
|
||||
visible: { opacity: 1, x: 0 }
|
||||
};
|
||||
// Simple, elegant mobile version - just the core story
|
||||
if (isMobile) {
|
||||
return (
|
||||
<div className="relative w-full h-full bg-gradient-to-br from-deep-graphite to-iron-gray rounded-lg p-4 overflow-hidden flex items-center justify-center">
|
||||
<motion.div
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
variants={{
|
||||
visible: { transition: { staggerChildren: 0.12 } }
|
||||
}}
|
||||
className="space-y-4 w-full max-w-xs"
|
||||
>
|
||||
{/* Race result - clean and simple */}
|
||||
<motion.div variants={itemVariants}>
|
||||
<div className="bg-iron-gray/60 rounded-xl p-4 border-2 border-primary-blue/40">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="h-14 w-14 bg-primary-blue/20 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||
<span className="text-2xl font-bold text-primary-blue">P3</span>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-base font-semibold text-white">Watkins Glen</div>
|
||||
<div className="text-xs text-white/60">GT3 Sprint</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
const profileSectionVariants = {
|
||||
hidden: { opacity: 0, x: shouldReduceMotion ? 0 : 20 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
x: 0,
|
||||
transition: { delay: shouldReduceMotion ? 0 : 0.2 }
|
||||
}
|
||||
};
|
||||
{/* Simple arrow */}
|
||||
<motion.div variants={itemVariants} className="flex justify-center">
|
||||
<div className="text-primary-blue text-2xl">↓</div>
|
||||
</motion.div>
|
||||
|
||||
const connectionLineVariants = {
|
||||
hidden: { scaleX: 0, opacity: 0 },
|
||||
visible: {
|
||||
scaleX: 1,
|
||||
opacity: 0.3,
|
||||
transition: { duration: 0.6 }
|
||||
}
|
||||
};
|
||||
{/* Updates - minimal */}
|
||||
<motion.div variants={itemVariants}>
|
||||
<div className="bg-iron-gray/40 rounded-xl p-3 border border-charcoal-outline space-y-2">
|
||||
<div className="text-xs text-white/70 text-center mb-2">Profile Updated</div>
|
||||
<div className="flex gap-2">
|
||||
<div className="flex-1 bg-deep-graphite/50 rounded py-2 text-center border border-primary-blue/30">
|
||||
<div className="text-xs text-primary-blue font-semibold">Stats ↑</div>
|
||||
</div>
|
||||
<div className="flex-1 bg-deep-graphite/50 rounded py-2 text-center border border-performance-green/30">
|
||||
<div className="text-xs text-performance-green font-semibold">+12</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Desktop version - richer with more updates
|
||||
return (
|
||||
<div className="relative w-full h-full bg-gradient-to-br from-deep-graphite via-iron-gray to-deep-graphite rounded-lg p-8 overflow-hidden flex items-center justify-center">
|
||||
<div className="relative w-full h-full bg-gradient-to-br from-deep-graphite via-iron-gray to-deep-graphite rounded-lg p-1.5 sm:p-3 md:p-5 lg:p-8 overflow-hidden">
|
||||
<motion.div
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
className="relative w-full max-w-4xl"
|
||||
variants={{
|
||||
visible: { transition: { staggerChildren: shouldReduceMotion ? 0 : 0.12 } }
|
||||
}}
|
||||
className="space-y-2 sm:space-y-3 md:space-y-4 lg:space-y-5"
|
||||
>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 items-center">
|
||||
{/* Left: Race Result Card */}
|
||||
<motion.div variants={raceCardVariants} className="relative">
|
||||
<div className="bg-iron-gray rounded-lg p-6 border border-primary-blue/40 shadow-[0_0_24px_rgba(25,140,255,0.2)]">
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
<div className="h-16 w-16 bg-charcoal-outline rounded-lg border border-primary-blue/30 flex items-center justify-center">
|
||||
<div className="text-white text-2xl font-bold">P3</div>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="text-lg font-semibold text-white mb-1">Watkins Glen</div>
|
||||
<div className="text-sm text-white/50">GT3 Sprint • Race 8</div>
|
||||
</div>
|
||||
{/* Race Result Card - Enhanced */}
|
||||
<motion.div variants={itemVariants}>
|
||||
<div className="bg-iron-gray rounded-lg p-2 sm:p-3 md:p-4 lg:p-5 border-2 border-primary-blue/40">
|
||||
<div className="flex items-center gap-2 sm:gap-3 md:gap-4">
|
||||
<div className="relative h-12 w-12 sm:h-14 sm:w-14 md:h-16 md:w-16 lg:h-20 lg:w-20 bg-charcoal-outline rounded-lg border-2 border-primary-blue/30 overflow-hidden flex-shrink-0">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-primary-blue/20 to-performance-green/20"></div>
|
||||
<div className="absolute inset-0 flex items-center justify-center text-white text-xl sm:text-2xl md:text-3xl lg:text-4xl font-bold">P3</div>
|
||||
</div>
|
||||
<div className="space-y-2 pt-4 border-t border-charcoal-outline">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-white/50">Finish</span>
|
||||
<span className="text-white font-medium">3rd of 24</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-1 sm:gap-1.5 md:gap-2 mb-1">
|
||||
<div className="text-sm sm:text-base md:text-lg">🏁</div>
|
||||
<div className="text-sm sm:text-base md:text-lg lg:text-xl font-semibold text-white truncate">Watkins Glen</div>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-white/50">Incidents</span>
|
||||
<span className="text-performance-green font-medium">0x</span>
|
||||
<div className="text-xs sm:text-sm md:text-base text-white/60 mb-1">GT3 Sprint Race</div>
|
||||
<div className="flex items-center gap-2 sm:gap-3 md:gap-4 text-[10px] sm:text-xs md:text-sm text-white/50">
|
||||
<span>24 drivers</span>
|
||||
<span>•</span>
|
||||
<span>45 min</span>
|
||||
<span>•</span>
|
||||
<span>Just finished</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Connection Arrow */}
|
||||
<motion.div
|
||||
variants={connectionLineVariants}
|
||||
className="hidden lg:block absolute left-1/2 top-1/2 -translate-y-1/2 w-16 h-0.5 bg-gradient-to-r from-primary-blue to-performance-green origin-left"
|
||||
style={{ transformOrigin: 'left center' }}
|
||||
{/* Connection Flow with Animation */}
|
||||
<motion.div variants={itemVariants} className="flex justify-center">
|
||||
<motion.div
|
||||
className="flex flex-col items-center gap-1"
|
||||
animate={shouldReduceMotion ? {} : {
|
||||
y: [0, 5, 0]
|
||||
}}
|
||||
transition={{ duration: 2, repeat: Infinity }}
|
||||
>
|
||||
<div className="absolute right-0 top-1/2 -translate-y-1/2">
|
||||
<div className="w-2 h-2 bg-performance-green rotate-45 transform translate-x-1"></div>
|
||||
</div>
|
||||
<div className="text-primary-blue text-2xl sm:text-3xl md:text-4xl">↓</div>
|
||||
<div className="text-[10px] sm:text-xs text-primary-blue/70">Auto-sync</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
|
||||
{/* Right: Unified Profile Preview */}
|
||||
<motion.div variants={profileSectionVariants} className="relative">
|
||||
<div className="bg-iron-gray/80 backdrop-blur-sm rounded-lg p-6 border border-charcoal-outline space-y-4">
|
||||
<div className="text-sm font-semibold text-white/70 mb-3">Updates Your Profile</div>
|
||||
|
||||
{/* Stats Section */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between py-2 px-3 bg-deep-graphite/50 rounded border border-primary-blue/20">
|
||||
<span className="text-xs text-white/50">Career Stats</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-1.5 w-16 bg-primary-blue/40 rounded"></div>
|
||||
<span className="text-xs text-performance-green">↑</span>
|
||||
</div>
|
||||
{/* Profile Updates Grid - More detailed */}
|
||||
<motion.div variants={itemVariants}>
|
||||
<div className="bg-iron-gray/80 rounded-lg p-2 sm:p-3 md:p-4 lg:p-5 border border-charcoal-outline">
|
||||
<div className="text-xs sm:text-sm md:text-base font-semibold text-white/80 text-center mb-2 sm:mb-3 md:mb-4">
|
||||
Profile Updates
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2 sm:gap-3 md:gap-4">
|
||||
{/* Career Stats Update */}
|
||||
<div className="bg-deep-graphite/50 rounded-lg p-2 sm:p-3 md:p-4 border border-primary-blue/30">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-[10px] sm:text-xs md:text-sm text-white/70">Career Stats</span>
|
||||
<motion.span
|
||||
className="text-[10px] sm:text-xs md:text-sm text-performance-green font-semibold"
|
||||
animate={shouldReduceMotion ? {} : { scale: [1, 1.2, 1] }}
|
||||
transition={{ duration: 0.5, delay: 0.5 }}
|
||||
>
|
||||
↑
|
||||
</motion.span>
|
||||
</div>
|
||||
|
||||
{/* Team Points */}
|
||||
<div className="flex items-center justify-between py-2 px-3 bg-deep-graphite/50 rounded border border-performance-green/20">
|
||||
<span className="text-xs text-white/50">Team Points</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-1.5 w-12 bg-performance-green/40 rounded"></div>
|
||||
<span className="text-xs text-performance-green">+18</span>
|
||||
</div>
|
||||
<div className="text-[9px] sm:text-[10px] md:text-xs text-white/50">
|
||||
Wins: 24 → 25
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Rating */}
|
||||
<div className="flex items-center justify-between py-2 px-3 bg-deep-graphite/50 rounded border border-subtle-neon-aqua/20">
|
||||
<span className="text-xs text-white/50">Rating</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-1.5 w-10 bg-subtle-neon-aqua/40 rounded"></div>
|
||||
<span className="text-xs text-performance-green">+12</span>
|
||||
</div>
|
||||
{/* Rating Update */}
|
||||
<div className="bg-deep-graphite/50 rounded-lg p-2 sm:p-3 md:p-4 border border-performance-green/30">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-[10px] sm:text-xs md:text-sm text-white/70">Rating</span>
|
||||
<motion.span
|
||||
className="text-[10px] sm:text-xs md:text-sm text-performance-green font-semibold font-mono"
|
||||
animate={shouldReduceMotion ? {} : { scale: [1, 1.2, 1] }}
|
||||
transition={{ duration: 0.5, delay: 0.6 }}
|
||||
>
|
||||
+12
|
||||
</motion.span>
|
||||
</div>
|
||||
<div className="text-[9px] sm:text-[10px] md:text-xs text-white/50">
|
||||
1342 → 1354
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Season History */}
|
||||
<div className="flex items-center justify-between py-2 px-3 bg-deep-graphite/50 rounded border border-white/10">
|
||||
<span className="text-xs text-white/50">Season Record</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-1.5 w-14 bg-white/20 rounded"></div>
|
||||
</div>
|
||||
{/* Season Points Update */}
|
||||
<div className="bg-deep-graphite/50 rounded-lg p-2 sm:p-3 md:p-4 border border-warning-amber/30">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-[10px] sm:text-xs md:text-sm text-white/70">Season</span>
|
||||
<motion.span
|
||||
className="text-[10px] sm:text-xs md:text-sm text-warning-amber font-semibold font-mono"
|
||||
animate={shouldReduceMotion ? {} : { scale: [1, 1.2, 1] }}
|
||||
transition={{ duration: 0.5, delay: 0.7 }}
|
||||
>
|
||||
+18
|
||||
</motion.span>
|
||||
</div>
|
||||
<div className="text-[9px] sm:text-[10px] md:text-xs text-white/50">
|
||||
248 → 266 pts
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Team Points Update */}
|
||||
<div className="bg-deep-graphite/50 rounded-lg p-2 sm:p-3 md:p-4 border border-neon-aqua/30">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-[10px] sm:text-xs md:text-sm text-white/70">Team</span>
|
||||
<motion.span
|
||||
className="text-[10px] sm:text-xs md:text-sm text-neon-aqua font-semibold font-mono"
|
||||
animate={shouldReduceMotion ? {} : { scale: [1, 1.2, 1] }}
|
||||
transition={{ duration: 0.5, delay: 0.8 }}
|
||||
>
|
||||
+18
|
||||
</motion.span>
|
||||
</div>
|
||||
<div className="text-[9px] sm:text-[10px] md:text-xs text-white/50">
|
||||
Contributing
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -6,6 +6,11 @@ import { useEffect, useState } from 'react';
|
||||
export default function RatingFactorsMockup() {
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
const [isHovered, setIsHovered] = useState<number | null>(null);
|
||||
const [isMobile, setIsMobile] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setIsMobile(window.innerWidth < 768);
|
||||
}, []);
|
||||
|
||||
const factors = [
|
||||
{ name: 'Position', value: 85, color: 'text-primary-blue', bgColor: 'bg-primary-blue' },
|
||||
@@ -16,6 +21,43 @@ export default function RatingFactorsMockup() {
|
||||
{ name: 'Team Points', value: 79, color: 'text-performance-green', bgColor: 'bg-performance-green' },
|
||||
];
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<div className="relative w-full h-full bg-gradient-to-br from-deep-graphite via-iron-gray to-deep-graphite rounded-lg p-3 overflow-hidden">
|
||||
<div className="text-center mb-4">
|
||||
<div className="h-5 w-40 bg-white/10 rounded mx-auto mb-2"></div>
|
||||
<div className="h-3 w-32 bg-white/5 rounded mx-auto"></div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3 mb-4">
|
||||
{factors.slice(0, 4).map((factor) => (
|
||||
<div key={factor.name} className="w-full">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="text-xs text-gray-400">{factor.name}</div>
|
||||
<span className={`text-sm font-semibold font-mono ${factor.color}`}>
|
||||
{factor.value}
|
||||
</span>
|
||||
</div>
|
||||
<div className="relative h-2.5 bg-charcoal-outline rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`absolute inset-y-0 left-0 ${factor.bgColor} rounded-full`}
|
||||
style={{ width: `${factor.value}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center bg-iron-gray/50 rounded-lg p-3 border border-charcoal-outline">
|
||||
<div className="text-center">
|
||||
<div className="h-2.5 w-20 bg-white/10 rounded mb-2 mx-auto"></div>
|
||||
<div className="text-3xl font-bold text-primary-blue font-mono">1342</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const containerVariants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
@@ -34,21 +76,21 @@ export default function RatingFactorsMockup() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative w-full h-full bg-gradient-to-br from-deep-graphite via-iron-gray to-deep-graphite rounded-lg p-8 overflow-hidden">
|
||||
<div className="relative w-full h-full bg-gradient-to-br from-deep-graphite via-iron-gray to-deep-graphite rounded-lg p-3 md:p-4 lg:p-6 overflow-hidden">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: shouldReduceMotion ? 0 : -10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="text-center mb-6"
|
||||
className="text-center mb-3 md:mb-4 lg:mb-6"
|
||||
>
|
||||
<div className="h-7 w-56 bg-white/10 rounded mx-auto mb-3"></div>
|
||||
<div className="h-4 w-40 bg-white/5 rounded mx-auto"></div>
|
||||
<div className="h-6 md:h-6 lg:h-7 w-44 md:w-56 bg-white/10 rounded mx-auto mb-2.5 md:mb-3"></div>
|
||||
<div className="h-4 md:h-4 w-32 md:w-40 bg-white/5 rounded mx-auto"></div>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
className="grid grid-cols-3 gap-4 mb-6 max-w-4xl mx-auto"
|
||||
className="grid grid-cols-2 md:grid-cols-3 gap-3 md:gap-4 mb-3 md:mb-4 lg:mb-6 max-w-4xl mx-auto"
|
||||
>
|
||||
{factors.map((factor, index) => (
|
||||
<motion.div
|
||||
@@ -74,11 +116,11 @@ export default function RatingFactorsMockup() {
|
||||
initial={{ opacity: 0, y: shouldReduceMotion ? 0 : 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: shouldReduceMotion ? 0 : 0.6 }}
|
||||
className="flex items-center justify-center gap-3 bg-iron-gray/50 rounded-lg p-5 border border-charcoal-outline shadow-[0_4px_24px_rgba(0,0,0,0.4)] backdrop-blur-sm max-w-md mx-auto"
|
||||
className="flex items-center justify-center gap-3 md:gap-3 bg-iron-gray/50 rounded-lg p-4 md:p-4 lg:p-5 border border-charcoal-outline shadow-[0_4px_24px_rgba(0,0,0,0.4)] backdrop-blur-sm max-w-md mx-auto"
|
||||
>
|
||||
<div className="text-center">
|
||||
<div className="h-3 w-28 bg-white/10 rounded mb-2 mx-auto"></div>
|
||||
<div className="h-12 w-24 bg-charcoal-outline rounded flex items-center justify-center border border-primary-blue/30">
|
||||
<div className="h-3 md:h-3 w-24 md:w-28 bg-white/10 rounded mb-2 md:mb-2 mx-auto"></div>
|
||||
<div className="h-12 md:h-12 w-24 md:w-24 bg-charcoal-outline rounded flex items-center justify-center border border-primary-blue/30">
|
||||
<AnimatedRating shouldReduceMotion={shouldReduceMotion ?? false} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -121,16 +163,16 @@ function RatingFactor({
|
||||
whileHover={shouldReduceMotion ? {} : { scale: 1.05 }}
|
||||
transition={{ type: 'spring', stiffness: 300, damping: 20 }}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="text-xs font-light text-gray-400 tracking-wide">{name}</div>
|
||||
<div className="flex items-center justify-between mb-2.5 md:mb-3">
|
||||
<div className="text-sm md:text-base lg:text-lg font-light text-gray-400 tracking-wide">{name}</div>
|
||||
<motion.span
|
||||
className={`text-sm font-semibold font-mono ${color}`}
|
||||
className={`text-sm md:text-base font-semibold font-mono ${color}`}
|
||||
animate={isHovered && !shouldReduceMotion ? { scale: 1.1 } : { scale: 1 }}
|
||||
>
|
||||
{value}
|
||||
</motion.span>
|
||||
</div>
|
||||
<div className="relative h-2 bg-charcoal-outline rounded-full overflow-hidden">
|
||||
<div className="relative h-3 md:h-3.5 lg:h-4 bg-charcoal-outline rounded-full overflow-hidden">
|
||||
<motion.div
|
||||
className={`absolute inset-y-0 left-0 ${bgColor} rounded-full`}
|
||||
style={{ width }}
|
||||
@@ -158,7 +200,7 @@ function AnimatedRating({ shouldReduceMotion }: { shouldReduceMotion: boolean })
|
||||
}, [shouldReduceMotion, count]);
|
||||
|
||||
return (
|
||||
<motion.span className="text-3xl font-bold text-primary-blue font-mono">
|
||||
<motion.span className="text-3xl md:text-4xl lg:text-5xl font-bold text-primary-blue font-mono">
|
||||
{shouldReduceMotion ? 1342 : <motion.span>{rounded}</motion.span>}
|
||||
</motion.span>
|
||||
);
|
||||
|
||||
@@ -1,84 +1,128 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
export default function SimPlatformMockup() {
|
||||
return (
|
||||
<div className="relative w-full max-w-3xl mx-auto">
|
||||
<div className="bg-iron-gray border border-charcoal-outline rounded-lg p-6 shadow-2xl">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between pb-4 border-b border-charcoal-outline">
|
||||
<div className="text-sm font-semibold text-slate-300">Platform Support</div>
|
||||
<div className="text-xs text-slate-500">Active: 1 | Planned: 3</div>
|
||||
const [isMobile, setIsMobile] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const checkMobile = () => setIsMobile(window.innerWidth < 768);
|
||||
checkMobile();
|
||||
window.addEventListener('resize', checkMobile);
|
||||
return () => window.removeEventListener('resize', checkMobile);
|
||||
}, []);
|
||||
|
||||
// Simple mobile version - just the essence of cross-platform
|
||||
if (isMobile) {
|
||||
return (
|
||||
<div className="relative w-full h-full bg-gradient-to-br from-deep-graphite to-iron-gray rounded-lg p-4 overflow-hidden flex items-center justify-center">
|
||||
<div className="space-y-3 w-full">
|
||||
{/* Active Platform - Clean */}
|
||||
<div className="bg-iron-gray/60 border-2 border-primary-blue rounded-xl p-4 relative">
|
||||
<div className="absolute top-3 right-3">
|
||||
<div className="w-2 h-2 rounded-full bg-performance-green animate-pulse" />
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 rounded-lg bg-primary-blue/20 flex items-center justify-center flex-shrink-0">
|
||||
<span className="text-2xl font-bold text-primary-blue">iR</span>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-base font-semibold text-white">iRacing</div>
|
||||
<div className="text-xs text-performance-green">Active</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{/* Simple "more coming" indicator */}
|
||||
<div className="bg-iron-gray/30 rounded-xl p-3 border border-charcoal-outline/50">
|
||||
<div className="text-xs text-center text-slate-500">More platforms coming</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Desktop version
|
||||
return (
|
||||
<div className="relative w-full max-w-3xl mx-auto">
|
||||
<div className="bg-iron-gray border border-charcoal-outline rounded-lg p-1.5 sm:p-3 md:p-4 lg:p-6 shadow-2xl">
|
||||
<div className="space-y-1.5 sm:space-y-2 md:space-y-3 lg:space-y-4">
|
||||
<div className="flex items-center justify-between pb-1.5 sm:pb-2 md:pb-3 lg:pb-4 border-b border-charcoal-outline">
|
||||
<div className="text-[9px] sm:text-xs md:text-sm font-semibold text-slate-300">Platform Support</div>
|
||||
<div className="text-[8px] sm:text-[10px] md:text-xs text-slate-500">Active: 1 | Planned: 3</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-1.5 sm:gap-2 md:gap-3">
|
||||
{/* iRacing - Active */}
|
||||
<div className="bg-deep-graphite border-2 border-primary-blue rounded-lg p-4 relative overflow-hidden">
|
||||
<div className="absolute top-2 right-2">
|
||||
<div className="w-2 h-2 rounded-full bg-performance-green animate-pulse" />
|
||||
<div className="bg-deep-graphite border-2 border-primary-blue rounded-lg p-1.5 sm:p-2 md:p-3 lg:p-4 relative overflow-hidden">
|
||||
<div className="absolute top-1 sm:top-1.5 md:top-2 right-1 sm:right-1.5 md:right-2">
|
||||
<div className="w-1 sm:w-1.5 md:w-2 h-1 sm:h-1.5 md:h-2 rounded-full bg-performance-green animate-pulse" />
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 rounded bg-primary-blue/10 flex items-center justify-center">
|
||||
<span className="text-2xl font-bold text-primary-blue">iR</span>
|
||||
<div className="flex items-center gap-1.5 sm:gap-2 md:gap-3">
|
||||
<div className="w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 rounded bg-primary-blue/10 flex items-center justify-center">
|
||||
<span className="text-base sm:text-xl md:text-2xl font-bold text-primary-blue">iR</span>
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-semibold text-white">iRacing</div>
|
||||
<div className="text-xs text-performance-green">Active</div>
|
||||
<div className="text-xs sm:text-sm md:text-base font-semibold text-white">iRacing</div>
|
||||
<div className="text-[8px] sm:text-[10px] md:text-xs text-performance-green">Active</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 text-xs text-slate-400">
|
||||
<div className="mt-1.5 sm:mt-2 md:mt-3 text-[8px] sm:text-[10px] md:text-xs text-slate-400">
|
||||
Full integration
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ACC - Future */}
|
||||
<div className="bg-deep-graphite border border-charcoal-outline rounded-lg p-4 opacity-40">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 rounded bg-slate-700/20 flex items-center justify-center">
|
||||
<span className="text-2xl font-bold text-slate-600">AC</span>
|
||||
<div className="bg-deep-graphite border border-charcoal-outline rounded-lg p-1.5 sm:p-2 md:p-3 lg:p-4 opacity-40">
|
||||
<div className="flex items-center gap-1.5 sm:gap-2 md:gap-3">
|
||||
<div className="w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 rounded bg-slate-700/20 flex items-center justify-center">
|
||||
<span className="text-base sm:text-xl md:text-2xl font-bold text-slate-600">AC</span>
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-semibold text-slate-500">ACC</div>
|
||||
<div className="text-xs text-slate-600">Planned</div>
|
||||
<div className="text-xs sm:text-sm font-semibold text-slate-500">ACC</div>
|
||||
<div className="text-[9px] sm:text-xs text-slate-600">Planned</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 text-xs text-slate-600">
|
||||
<div className="mt-1.5 sm:mt-2 md:mt-3 text-[8px] sm:text-[10px] md:text-xs text-slate-600">
|
||||
Coming later
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* rFactor 2 - Future */}
|
||||
<div className="bg-deep-graphite border border-charcoal-outline rounded-lg p-4 opacity-40">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 rounded bg-slate-700/20 flex items-center justify-center">
|
||||
<span className="text-2xl font-bold text-slate-600">rF</span>
|
||||
<div className="bg-deep-graphite border border-charcoal-outline rounded-lg p-1.5 sm:p-2 md:p-3 lg:p-4 opacity-40">
|
||||
<div className="flex items-center gap-1.5 sm:gap-2 md:gap-3">
|
||||
<div className="w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 rounded bg-slate-700/20 flex items-center justify-center">
|
||||
<span className="text-base sm:text-xl md:text-2xl font-bold text-slate-600">rF</span>
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-semibold text-slate-500">rFactor 2</div>
|
||||
<div className="text-xs text-slate-600">Planned</div>
|
||||
<div className="text-xs sm:text-sm md:text-base font-semibold text-slate-500">rFactor 2</div>
|
||||
<div className="text-[8px] sm:text-[10px] md:text-xs text-slate-600">Planned</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 text-xs text-slate-600">
|
||||
<div className="mt-1.5 sm:mt-2 md:mt-3 text-[8px] sm:text-[10px] md:text-xs text-slate-600">
|
||||
Coming later
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* LMU - Future */}
|
||||
<div className="bg-deep-graphite border border-charcoal-outline rounded-lg p-4 opacity-40">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 rounded bg-slate-700/20 flex items-center justify-center">
|
||||
<span className="text-2xl font-bold text-slate-600">LM</span>
|
||||
<div className="bg-deep-graphite border border-charcoal-outline rounded-lg p-1.5 sm:p-2 md:p-3 lg:p-4 opacity-40">
|
||||
<div className="flex items-center gap-1.5 sm:gap-2 md:gap-3">
|
||||
<div className="w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 rounded bg-slate-700/20 flex items-center justify-center">
|
||||
<span className="text-base sm:text-xl md:text-2xl font-bold text-slate-600">LM</span>
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-semibold text-slate-500">Le Mans Ult.</div>
|
||||
<div className="text-xs text-slate-600">Planned</div>
|
||||
<div className="text-xs sm:text-sm md:text-base font-semibold text-slate-500">Le Mans Ult.</div>
|
||||
<div className="text-[8px] sm:text-[10px] md:text-xs text-slate-600">Planned</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 text-xs text-slate-600">
|
||||
<div className="mt-1.5 sm:mt-2 md:mt-3 text-[8px] sm:text-[10px] md:text-xs text-slate-600">
|
||||
Coming later
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-4 border-t border-charcoal-outline">
|
||||
<div className="text-xs text-slate-500 text-center">
|
||||
<div className="pt-1.5 sm:pt-2 md:pt-3 lg:pt-4 border-t border-charcoal-outline">
|
||||
<div className="text-[8px] sm:text-[10px] md:text-xs text-slate-500 text-center">
|
||||
Your identity stays with you across platforms
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,69 @@ import { useEffect, useState } from 'react';
|
||||
export default function StandingsTableMockup() {
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
const [hoveredRow, setHoveredRow] = useState<number | null>(null);
|
||||
const [isMobile, setIsMobile] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setIsMobile(window.innerWidth < 768);
|
||||
}, []);
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<div className="relative w-full h-full bg-gradient-to-br from-deep-graphite via-iron-gray to-deep-graphite rounded-lg p-3 overflow-hidden">
|
||||
<div className="mb-3">
|
||||
<div className="flex items-center gap-2 pb-2 border-b border-charcoal-outline">
|
||||
<div className="text-xs font-mono text-gray-400">#</div>
|
||||
<div className="text-xs flex-1 font-semibold text-white">Driver</div>
|
||||
<div className="text-xs font-mono text-gray-400">Pts</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{[1, 2, 3, 4, 5].map((i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`flex items-center gap-2 py-2 px-2 rounded-lg border ${
|
||||
i <= 3
|
||||
? 'bg-gradient-to-r from-performance-green/10 to-iron-gray border-performance-green/20'
|
||||
: 'bg-iron-gray border-charcoal-outline'
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`h-7 w-7 rounded-full flex items-center justify-center font-semibold text-xs ${
|
||||
i <= 3
|
||||
? 'bg-primary-blue text-white shadow-glow'
|
||||
: 'bg-charcoal-outline text-gray-400'
|
||||
}`}
|
||||
>
|
||||
{i}
|
||||
</div>
|
||||
<div className="flex-1 flex items-center gap-2">
|
||||
<div className="h-5 w-5 rounded-full bg-charcoal-outline border border-primary-blue/20 flex items-center justify-center text-xs">
|
||||
🏎️
|
||||
</div>
|
||||
<div className="h-2.5 w-full max-w-[100px] bg-white/10 rounded"></div>
|
||||
</div>
|
||||
<div className="relative w-16 h-5 bg-charcoal-outline rounded border border-primary-blue/20 overflow-hidden">
|
||||
<div
|
||||
className={`absolute inset-y-0 left-0 ${
|
||||
i <= 3
|
||||
? 'bg-gradient-to-r from-performance-green/40 to-performance-green/20'
|
||||
: 'bg-gradient-to-r from-iron-gray to-charcoal-outline'
|
||||
}`}
|
||||
style={{ width: `${100 - (i - 1) * 15}%` }}
|
||||
/>
|
||||
<div className="relative h-full flex items-center justify-center">
|
||||
<span className="text-xs font-mono font-semibold text-white">
|
||||
{300 - i * 20}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const getRowAnimation = (i: number) => ({
|
||||
hidden: { opacity: 0, y: shouldReduceMotion ? 0 : 10 },
|
||||
@@ -22,24 +85,28 @@ export default function StandingsTableMockup() {
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="relative w-full h-full bg-gradient-to-br from-deep-graphite via-iron-gray to-deep-graphite rounded-lg p-6 overflow-hidden">
|
||||
<div className="mb-4">
|
||||
<div className="flex items-center gap-4 pb-3 border-b border-charcoal-outline">
|
||||
<div className="text-xs font-mono text-gray-400">#</div>
|
||||
<div className="text-xs flex-1 font-semibold text-white">Driver</div>
|
||||
<div className="text-xs font-mono text-gray-400">Wins</div>
|
||||
<div className="text-xs font-mono text-gray-400">Points</div>
|
||||
<div className="relative w-full h-full bg-gradient-to-br from-deep-graphite via-iron-gray to-deep-graphite rounded-lg p-1.5 sm:p-3 md:p-4 lg:p-6 overflow-hidden">
|
||||
<div className="mb-1.5 sm:mb-2 md:mb-3 lg:mb-4">
|
||||
<div className="text-[8px] sm:text-[10px] md:text-xs text-white/50 mb-1.5 sm:mb-2 md:mb-3">Real-time standings updated after every race</div>
|
||||
<div className="flex items-center gap-1.5 sm:gap-2 md:gap-3 lg:gap-4 pb-1.5 sm:pb-2 md:pb-3 border-b border-charcoal-outline">
|
||||
<div className="text-[8px] sm:text-[10px] md:text-xs font-mono text-gray-400">#</div>
|
||||
<div className="text-[8px] sm:text-[10px] md:text-xs flex-1 font-semibold text-white">Driver</div>
|
||||
<div className="text-[8px] sm:text-[10px] md:text-xs font-mono text-gray-400 hidden md:block">Wins</div>
|
||||
<div className="text-[8px] sm:text-[10px] md:text-xs font-mono text-gray-400 flex items-center gap-1">
|
||||
<span>Points</span>
|
||||
<span className="text-performance-green text-[8px]">●</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="space-y-0.5">
|
||||
{[1, 2, 3, 4, 5, 6, 7, 8].map((i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
variants={getRowAnimation(i)}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
className={`relative flex items-center gap-4 py-3 px-3 rounded-lg border transition-all duration-150 ${
|
||||
className={`relative flex items-center gap-1.5 sm:gap-2 md:gap-3 lg:gap-4 py-1.5 sm:py-2 md:py-2.5 lg:py-3 px-1.5 sm:px-2 md:px-3 rounded-lg border transition-all duration-150 ${
|
||||
i <= 3
|
||||
? 'bg-gradient-to-r from-performance-green/10 to-iron-gray border-performance-green/20'
|
||||
: 'bg-iron-gray border-charcoal-outline'
|
||||
@@ -53,7 +120,7 @@ export default function StandingsTableMockup() {
|
||||
}}
|
||||
>
|
||||
<motion.div
|
||||
className={`h-7 w-7 rounded-full flex items-center justify-center font-semibold text-xs ${
|
||||
className={`h-6 w-6 sm:h-7 sm:w-7 rounded-full flex items-center justify-center font-semibold text-[10px] sm:text-xs ${
|
||||
i <= 3
|
||||
? 'bg-primary-blue text-white shadow-glow'
|
||||
: 'bg-charcoal-outline text-gray-400'
|
||||
@@ -66,10 +133,13 @@ export default function StandingsTableMockup() {
|
||||
>
|
||||
{i}
|
||||
</motion.div>
|
||||
<div className="flex-1">
|
||||
<div className="h-3 w-full max-w-[140px] bg-white/10 rounded"></div>
|
||||
<div className="flex-1 flex items-center gap-1 sm:gap-1.5 md:gap-2">
|
||||
<div className="h-4 w-4 sm:h-5 sm:w-5 md:h-6 md:w-6 rounded-full bg-charcoal-outline border border-primary-blue/20 flex items-center justify-center text-[8px] sm:text-[10px] md:text-xs aspect-square">
|
||||
🏎️
|
||||
</div>
|
||||
<div className="h-1.5 sm:h-2 md:h-2.5 lg:h-3 w-full max-w-[80px] sm:max-w-[100px] md:max-w-[140px] bg-white/10 rounded"></div>
|
||||
</div>
|
||||
<div className="h-3 w-16 bg-white/5 rounded font-mono"></div>
|
||||
<div className="h-1.5 sm:h-2 md:h-2.5 lg:h-3 w-10 sm:w-12 md:w-16 bg-white/5 rounded font-mono hidden md:block"></div>
|
||||
<div className="relative">
|
||||
<AnimatedPoints
|
||||
points={300 - i * 20}
|
||||
@@ -107,7 +177,7 @@ function AnimatedPoints({
|
||||
const percentage = (points / 300) * 100;
|
||||
|
||||
return (
|
||||
<div className="relative w-24 h-7 bg-charcoal-outline rounded border border-primary-blue/20 overflow-hidden">
|
||||
<div className="relative w-12 sm:w-16 md:w-20 lg:w-24 h-4 sm:h-5 md:h-6 lg:h-7 bg-charcoal-outline rounded border border-primary-blue/20 overflow-hidden">
|
||||
<motion.div
|
||||
className={`absolute inset-y-0 left-0 ${
|
||||
position <= 3
|
||||
@@ -119,7 +189,7 @@ function AnimatedPoints({
|
||||
transition={{ duration: shouldReduceMotion ? 0 : 0.8, ease: 'easeOut', delay: 0.1 + position * 0.05 }}
|
||||
/>
|
||||
<div className="relative h-full flex items-center justify-center">
|
||||
<motion.span className="text-xs font-mono font-semibold text-white">
|
||||
<motion.span className="text-[8px] sm:text-[10px] md:text-xs font-mono font-semibold text-white">
|
||||
{shouldReduceMotion ? points : <motion.span>{spring}</motion.span>}
|
||||
</motion.span>
|
||||
</div>
|
||||
|
||||
@@ -1,15 +1,98 @@
|
||||
'use client';
|
||||
|
||||
import { motion, useReducedMotion } from 'framer-motion';
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
export default function TeamCompetitionMockup() {
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
const [hoveredDriver, setHoveredDriver] = useState<number | null>(null);
|
||||
const [hoveredTeam, setHoveredTeam] = useState<number | null>(null);
|
||||
const [isMobile, setIsMobile] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setIsMobile(window.innerWidth < 768);
|
||||
}, []);
|
||||
|
||||
const teamColors = ['#198CFF', '#6FE37A', '#FFC556', '#43C9E6', '#9333EA'];
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<div className="relative w-full h-full bg-gradient-to-br from-deep-graphite via-iron-gray to-deep-graphite rounded-lg p-3 overflow-hidden">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<div className="text-sm font-semibold text-white mb-3">Drivers</div>
|
||||
<div className="space-y-2">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="relative flex items-center gap-2 bg-iron-gray rounded-lg p-2 border border-charcoal-outline overflow-hidden"
|
||||
>
|
||||
<div
|
||||
className="absolute left-0 top-0 bottom-0 w-1"
|
||||
style={{ backgroundColor: teamColors[i-1] }}
|
||||
/>
|
||||
<div
|
||||
className="h-5 w-5 rounded-full flex items-center justify-center font-semibold text-xs border-2"
|
||||
style={{
|
||||
borderColor: teamColors[i-1],
|
||||
backgroundColor: `${teamColors[i-1]}20`
|
||||
}}
|
||||
>
|
||||
<span className="text-white">{i}</span>
|
||||
</div>
|
||||
<div className="h-6 w-6 rounded-full flex items-center justify-center text-sm" style={{ backgroundColor: `${teamColors[i-1]}20`, borderWidth: '1px', borderColor: teamColors[i-1] }}>
|
||||
🏎️
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="h-2.5 w-full bg-white/10 rounded"></div>
|
||||
</div>
|
||||
<div className="h-3 w-10 bg-charcoal-outline rounded text-xs flex items-center justify-center text-white/70"></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-px bg-gradient-to-r from-transparent via-charcoal-outline to-transparent" />
|
||||
|
||||
<div>
|
||||
<div className="text-sm font-semibold text-white mb-3">Constructors</div>
|
||||
<div className="space-y-2">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="relative flex items-center gap-2 bg-iron-gray rounded-lg p-2 border border-charcoal-outline overflow-hidden"
|
||||
>
|
||||
<div
|
||||
className="absolute left-0 top-0 bottom-0 w-1"
|
||||
style={{ backgroundColor: teamColors[i-1] }}
|
||||
/>
|
||||
<div
|
||||
className="h-6 w-6 rounded flex items-center justify-center text-sm border-2"
|
||||
style={{
|
||||
borderColor: teamColors[i-1],
|
||||
backgroundColor: `${teamColors[i-1]}20`
|
||||
}}
|
||||
>
|
||||
🏁
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="h-2.5 w-full bg-white/10 rounded mb-1"></div>
|
||||
<div className="relative h-1.5 bg-charcoal-outline rounded-full overflow-hidden">
|
||||
<div
|
||||
className="absolute inset-y-0 left-0 rounded-full"
|
||||
style={{ backgroundColor: teamColors[i-1], width: `${100 - (i-1) * 15}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const leftColumnVariants = {
|
||||
hidden: { opacity: 0, x: shouldReduceMotion ? 0 : -20 },
|
||||
visible: {
|
||||
@@ -51,18 +134,18 @@ export default function TeamCompetitionMockup() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative w-full h-full bg-gradient-to-br from-deep-graphite via-iron-gray to-deep-graphite rounded-lg p-6 overflow-hidden">
|
||||
<div className="grid grid-cols-2 gap-6 h-full">
|
||||
<div className="relative w-full h-full bg-gradient-to-br from-deep-graphite via-iron-gray to-deep-graphite rounded-lg p-1.5 sm:p-3 md:p-4 lg:p-6 overflow-hidden">
|
||||
<div className="grid grid-cols-2 gap-2 sm:gap-3 md:gap-4 lg:gap-6 h-full">
|
||||
<motion.div
|
||||
variants={leftColumnVariants}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
className="relative"
|
||||
>
|
||||
<div className="h-5 w-24 bg-white/10 rounded mb-4 text-xs flex items-center justify-center text-white font-semibold">
|
||||
<div className="h-3 sm:h-4 md:h-5 w-16 sm:w-20 md:w-24 bg-white/10 rounded mb-1.5 sm:mb-2 md:mb-3 lg:mb-4 text-[8px] sm:text-[10px] md:text-xs flex items-center justify-center text-white font-semibold">
|
||||
Drivers
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-1 sm:space-y-1.5 md:space-y-2">
|
||||
{[1, 2, 3, 4, 5].map((i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
@@ -70,7 +153,7 @@ export default function TeamCompetitionMockup() {
|
||||
variants={rowVariants}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
className="relative flex items-center gap-3 bg-iron-gray rounded-lg p-2.5 border border-charcoal-outline overflow-hidden"
|
||||
className="relative flex items-center gap-1.5 sm:gap-2 md:gap-2.5 lg:gap-3 bg-iron-gray rounded-lg p-1 sm:p-1.5 md:p-2 lg:p-2.5 border border-charcoal-outline overflow-hidden"
|
||||
onHoverStart={() => !shouldReduceMotion && setHoveredDriver(i)}
|
||||
onHoverEnd={() => setHoveredDriver(null)}
|
||||
whileHover={shouldReduceMotion ? {} : {
|
||||
@@ -84,7 +167,7 @@ export default function TeamCompetitionMockup() {
|
||||
style={{ backgroundColor: teamColors[i-1] }}
|
||||
/>
|
||||
<div
|
||||
className="h-5 w-5 rounded-full flex items-center justify-center font-semibold text-[10px] border-2"
|
||||
className="h-3.5 w-3.5 sm:h-4 sm:w-4 md:h-5 md:w-5 rounded-full flex items-center justify-center font-semibold text-[8px] sm:text-[9px] md:text-[10px] border-2"
|
||||
style={{
|
||||
borderColor: teamColors[i-1],
|
||||
backgroundColor: `${teamColors[i-1]}20`
|
||||
@@ -92,10 +175,13 @@ export default function TeamCompetitionMockup() {
|
||||
>
|
||||
<span className="text-white">{i}</span>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="h-2.5 w-full bg-white/10 rounded"></div>
|
||||
<div className="h-5 w-5 sm:h-6 sm:w-6 md:h-7 md:w-7 rounded-full flex items-center justify-center text-[10px] sm:text-xs" style={{ backgroundColor: `${teamColors[i-1]}20`, borderWidth: '1px', borderColor: teamColors[i-1] }}>
|
||||
🏎️
|
||||
</div>
|
||||
<div className="h-3 w-12 bg-charcoal-outline rounded font-mono text-[10px] flex items-center justify-center text-white/70"></div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="h-1.5 sm:h-2 md:h-2.5 w-full bg-white/10 rounded"></div>
|
||||
</div>
|
||||
<div className="h-2 sm:h-2.5 md:h-3 w-8 sm:w-10 md:w-12 bg-charcoal-outline rounded font-mono text-[8px] sm:text-[9px] md:text-[10px] flex items-center justify-center text-white/70"></div>
|
||||
{hoveredDriver === i && (
|
||||
<motion.div
|
||||
className="absolute inset-0 pointer-events-none"
|
||||
@@ -119,10 +205,10 @@ export default function TeamCompetitionMockup() {
|
||||
animate="visible"
|
||||
className="relative"
|
||||
>
|
||||
<div className="h-5 w-32 bg-white/10 rounded mb-4 text-xs flex items-center justify-center text-white font-semibold">
|
||||
<div className="h-3 sm:h-4 md:h-5 w-20 sm:w-24 md:w-32 bg-white/10 rounded mb-1.5 sm:mb-2 md:mb-3 lg:mb-4 text-[8px] sm:text-[10px] md:text-xs flex items-center justify-center text-white font-semibold">
|
||||
Constructors
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-1 sm:space-y-1.5 md:space-y-2">
|
||||
{[1, 2, 3, 4, 5].map((i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
@@ -130,7 +216,7 @@ export default function TeamCompetitionMockup() {
|
||||
variants={rowVariants}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
className="relative flex items-center gap-3 bg-iron-gray rounded-lg p-2.5 border border-charcoal-outline overflow-hidden"
|
||||
className="relative flex items-center gap-1.5 sm:gap-2 md:gap-2.5 lg:gap-3 bg-iron-gray rounded-lg p-1 sm:p-1.5 md:p-2 lg:p-2.5 border border-charcoal-outline overflow-hidden"
|
||||
onHoverStart={() => !shouldReduceMotion && setHoveredTeam(i)}
|
||||
onHoverEnd={() => setHoveredTeam(null)}
|
||||
whileHover={shouldReduceMotion ? {} : {
|
||||
@@ -144,17 +230,17 @@ export default function TeamCompetitionMockup() {
|
||||
style={{ backgroundColor: teamColors[i-1] }}
|
||||
/>
|
||||
<div
|
||||
className="h-5 w-5 rounded flex items-center justify-center font-semibold text-[10px] border-2"
|
||||
className="h-5 w-5 sm:h-6 sm:w-6 md:h-7 md:w-7 rounded flex items-center justify-center text-[10px] sm:text-xs md:text-sm border-2"
|
||||
style={{
|
||||
borderColor: teamColors[i-1],
|
||||
backgroundColor: `${teamColors[i-1]}20`
|
||||
}}
|
||||
>
|
||||
<span className="text-white">{i}</span>
|
||||
🏁
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="h-2.5 w-full bg-white/10 rounded mb-1.5"></div>
|
||||
<div className="relative h-1.5 bg-charcoal-outline rounded-full overflow-hidden">
|
||||
<div className="h-1.5 sm:h-2 md:h-2.5 w-full bg-white/10 rounded mb-0.5 sm:mb-1 md:mb-1.5"></div>
|
||||
<div className="relative h-0.5 sm:h-1 md:h-1.5 bg-charcoal-outline rounded-full overflow-hidden">
|
||||
<motion.div
|
||||
className="absolute inset-y-0 left-0 rounded-full"
|
||||
style={{ backgroundColor: teamColors[i-1] }}
|
||||
@@ -165,7 +251,7 @@ export default function TeamCompetitionMockup() {
|
||||
</div>
|
||||
</div>
|
||||
{i === 3 && (
|
||||
<div className="h-4 px-1.5 bg-warning-amber/20 rounded text-[9px] flex items-center justify-center text-warning-amber font-semibold border border-warning-amber/30">
|
||||
<div className="h-3 sm:h-3.5 md:h-4 px-0.5 sm:px-1 md:px-1.5 bg-warning-amber/20 rounded text-[7px] sm:text-[8px] md:text-[9px] flex items-center justify-center text-warning-amber font-semibold border border-warning-amber/30">
|
||||
=
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -22,11 +22,11 @@ export default function Button({
|
||||
as = 'button',
|
||||
...props
|
||||
}: ButtonProps) {
|
||||
const baseStyles = 'rounded-full px-6 py-3 text-sm font-semibold transition-all duration-75 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 hover:scale-[1.03] active:scale-[0.98]';
|
||||
const baseStyles = 'min-h-[44px] rounded-full px-6 py-3 text-sm font-semibold transition-all duration-75 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 hover:scale-[1.03] active:scale-95';
|
||||
|
||||
const variantStyles = {
|
||||
primary: 'bg-primary-blue text-white hover:shadow-glow active:ring-2 active:ring-primary-blue focus-visible:outline-primary-blue',
|
||||
secondary: 'bg-iron-gray text-white border border-charcoal-outline hover:shadow-glow-strong hover:border-primary-blue focus-visible:outline-primary-blue'
|
||||
primary: 'bg-primary-blue text-white shadow-[0_0_15px_rgba(25,140,255,0.4)] hover:shadow-[0_0_25px_rgba(25,140,255,0.6)] active:ring-2 active:ring-primary-blue focus-visible:outline-primary-blue',
|
||||
secondary: 'bg-iron-gray text-white border border-charcoal-outline shadow-[0_0_10px_rgba(25,140,255,0.2)] hover:shadow-[0_0_20px_rgba(25,140,255,0.4)] hover:border-primary-blue focus-visible:outline-primary-blue'
|
||||
};
|
||||
|
||||
const classes = `${baseStyles} ${variantStyles[variant]} ${className}`;
|
||||
|
||||
@@ -4,9 +4,10 @@ interface HeadingProps {
|
||||
level: 1 | 2 | 3;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
export default function Heading({ level, children, className = '' }: HeadingProps) {
|
||||
export default function Heading({ level, children, className = '', style }: HeadingProps) {
|
||||
const baseStyles = 'font-bold tracking-tight';
|
||||
|
||||
const levelStyles = {
|
||||
@@ -18,7 +19,7 @@ export default function Heading({ level, children, className = '' }: HeadingProp
|
||||
const Tag = `h${level}` as keyof JSX.IntrinsicElements;
|
||||
|
||||
return (
|
||||
<Tag className={`${baseStyles} ${levelStyles[level]} ${className}`}>
|
||||
<Tag className={`${baseStyles} ${levelStyles[level]} ${className}`} style={style}>
|
||||
{children}
|
||||
</Tag>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { motion, useReducedMotion } from 'framer-motion';
|
||||
import { ReactNode } from 'react';
|
||||
import { ReactNode, useEffect, useState } from 'react';
|
||||
|
||||
interface MockupStackProps {
|
||||
children: ReactNode;
|
||||
@@ -10,13 +10,68 @@ interface MockupStackProps {
|
||||
|
||||
export default function MockupStack({ children, index = 0 }: MockupStackProps) {
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
const [isMobile, setIsMobile] = useState(true); // Default to mobile (no animations)
|
||||
|
||||
useEffect(() => {
|
||||
setIsMounted(true);
|
||||
const checkMobile = () => setIsMobile(window.innerWidth < 768);
|
||||
checkMobile();
|
||||
window.addEventListener('resize', checkMobile);
|
||||
return () => window.removeEventListener('resize', checkMobile);
|
||||
}, []);
|
||||
|
||||
const seed = index * 1337;
|
||||
const rotation1 = ((seed * 17) % 80 - 40) / 20;
|
||||
const rotation2 = ((seed * 23) % 80 - 40) / 20;
|
||||
|
||||
// On mobile or before mount, render without animations
|
||||
if (!isMounted || isMobile) {
|
||||
return (
|
||||
<div className="relative w-full h-full scale-60 sm:scale-70 md:scale-85 lg:scale-95 max-w-[85vw] mx-auto my-4 sm:my-0" style={{ perspective: '1200px' }}>
|
||||
<div
|
||||
className="absolute rounded-lg bg-iron-gray/80 border border-charcoal-outline"
|
||||
style={{
|
||||
rotate: rotation1,
|
||||
zIndex: 1,
|
||||
top: '-8px',
|
||||
left: '-8px',
|
||||
right: '-8px',
|
||||
bottom: '-8px',
|
||||
boxShadow: '0 12px 40px rgba(0,0,0,0.3)',
|
||||
opacity: 0.5,
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
className="absolute rounded-lg bg-iron-gray/90 border border-charcoal-outline"
|
||||
style={{
|
||||
rotate: rotation2,
|
||||
zIndex: 2,
|
||||
top: '-4px',
|
||||
left: '-4px',
|
||||
right: '-4px',
|
||||
bottom: '-4px',
|
||||
boxShadow: '0 16px 48px rgba(0,0,0,0.35)',
|
||||
opacity: 0.7,
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
className="relative z-10 w-full h-full rounded-lg overflow-hidden"
|
||||
style={{
|
||||
boxShadow: '0 20px 60px rgba(0,0,0,0.45)',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Desktop: render with animations
|
||||
return (
|
||||
<div className="relative w-full h-full" style={{ perspective: '1200px' }}>
|
||||
<div className="relative w-full h-full scale-60 sm:scale-70 md:scale-85 lg:scale-95 max-w-[85vw] mx-auto my-4 sm:my-0" style={{ perspective: '1200px' }}>
|
||||
<motion.div
|
||||
className="absolute rounded-lg bg-iron-gray/80 border border-charcoal-outline"
|
||||
style={{
|
||||
|
||||
@@ -22,7 +22,7 @@ export default function Section({
|
||||
return (
|
||||
<section
|
||||
id={id}
|
||||
className={`${variantStyles[variant]} px-6 py-32 sm:py-40 lg:px-8 ${className}`}
|
||||
className={`${variantStyles[variant]} px-[calc(1rem+var(--sal))] pr-[calc(1rem+var(--sar))] py-16 sm:py-20 md:py-32 md:px-[calc(2rem+var(--sal))] md:pr-[calc(2rem+var(--sar))] lg:px-8 ${className}`}
|
||||
>
|
||||
{children}
|
||||
</section>
|
||||
|
||||
125
apps/website/hooks/useScrollProgress.ts
Normal file
125
apps/website/hooks/useScrollProgress.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState, RefObject } from 'react';
|
||||
|
||||
/**
|
||||
* Calculate scroll progress (0-1) based on element's position in viewport
|
||||
* @param ref - Reference to the element to track
|
||||
* @param offset - Offset from viewport edges (0-1, default 0.1)
|
||||
* @returns progress value between 0 and 1
|
||||
*/
|
||||
export function useScrollProgress(ref: RefObject<HTMLElement>, offset: number = 0.1): number {
|
||||
const [progress, setProgress] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref.current) return;
|
||||
|
||||
let rafId: number;
|
||||
|
||||
const calculateProgress = () => {
|
||||
if (!ref.current) return;
|
||||
|
||||
const rect = ref.current.getBoundingClientRect();
|
||||
const viewportHeight = window.innerHeight;
|
||||
const scrollY = window.scrollY;
|
||||
const documentHeight = document.documentElement.scrollHeight;
|
||||
|
||||
// Element enters viewport from bottom
|
||||
const enterPoint = viewportHeight * (1 - offset);
|
||||
// Element reaches top of viewport
|
||||
const exitPoint = viewportHeight * offset;
|
||||
|
||||
// Calculate progress: 0 when entering, 1 at 30% viewport (accelerated)
|
||||
const elementCenter = rect.top + rect.height / 2;
|
||||
const totalDistance = enterPoint - exitPoint;
|
||||
const currentDistance = enterPoint - elementCenter;
|
||||
|
||||
// Accelerate progress to reach 1.0 at 30% viewport height
|
||||
// Scale factor: 1.67 makes progress reach 1.0 at ~30% instead of 50%
|
||||
const rawProgress = (currentDistance / totalDistance) * 1.67;
|
||||
let clampedProgress = Math.max(0, Math.min(1, rawProgress));
|
||||
|
||||
// At bottom of page - ensure elements near bottom can reach 100%
|
||||
// Only apply if we're at the very bottom AND this element is below the fold
|
||||
if (scrollY + viewportHeight >= documentHeight - 50 && rect.top < viewportHeight) {
|
||||
clampedProgress = Math.max(clampedProgress, 1);
|
||||
}
|
||||
|
||||
setProgress(clampedProgress);
|
||||
};
|
||||
|
||||
const handleScroll = () => {
|
||||
if (rafId) {
|
||||
cancelAnimationFrame(rafId);
|
||||
}
|
||||
rafId = requestAnimationFrame(calculateProgress);
|
||||
};
|
||||
|
||||
// Initial calculation
|
||||
calculateProgress();
|
||||
|
||||
window.addEventListener('scroll', handleScroll, { passive: true });
|
||||
window.addEventListener('resize', handleScroll, { passive: true });
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('scroll', handleScroll);
|
||||
window.removeEventListener('resize', handleScroll);
|
||||
if (rafId) {
|
||||
cancelAnimationFrame(rafId);
|
||||
}
|
||||
};
|
||||
}, [ref, offset]);
|
||||
|
||||
return progress;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate parallax offset based on scroll position
|
||||
* @param ref - Reference to the element to track
|
||||
* @param speed - Parallax speed multiplier (default 0.5)
|
||||
* @returns offset in pixels
|
||||
*/
|
||||
export function useParallax(ref: RefObject<HTMLElement>, speed: number = 0.5): number {
|
||||
const [offset, setOffset] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref.current) return;
|
||||
|
||||
let rafId: number;
|
||||
|
||||
const calculateOffset = () => {
|
||||
if (!ref.current) return;
|
||||
|
||||
const rect = ref.current.getBoundingClientRect();
|
||||
const viewportHeight = window.innerHeight;
|
||||
|
||||
// Calculate offset based on element position relative to viewport
|
||||
const scrolled = viewportHeight - rect.top;
|
||||
const parallaxOffset = scrolled * speed;
|
||||
|
||||
setOffset(parallaxOffset);
|
||||
};
|
||||
|
||||
const handleScroll = () => {
|
||||
if (rafId) {
|
||||
cancelAnimationFrame(rafId);
|
||||
}
|
||||
rafId = requestAnimationFrame(calculateOffset);
|
||||
};
|
||||
|
||||
calculateOffset();
|
||||
|
||||
window.addEventListener('scroll', handleScroll, { passive: true });
|
||||
window.addEventListener('resize', handleScroll, { passive: true });
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('scroll', handleScroll);
|
||||
window.removeEventListener('resize', handleScroll);
|
||||
if (rafId) {
|
||||
cancelAnimationFrame(rafId);
|
||||
}
|
||||
};
|
||||
}, [ref, speed]);
|
||||
|
||||
return offset;
|
||||
}
|
||||
@@ -46,6 +46,6 @@ export const config = {
|
||||
* - favicon.ico (favicon file)
|
||||
* - public folder files
|
||||
*/
|
||||
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
|
||||
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp|mp4|webm|mov|avi)$).*)',
|
||||
],
|
||||
};
|
||||
@@ -7,6 +7,13 @@ const nextConfig = {
|
||||
eslint: {
|
||||
ignoreDuringBuilds: false,
|
||||
},
|
||||
webpack: (config) => {
|
||||
config.module.rules.push({
|
||||
test: /\.(mp4|webm)$/,
|
||||
type: 'asset/resource',
|
||||
});
|
||||
return config;
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
BIN
apps/website/public/gameplay.mp4
Normal file
BIN
apps/website/public/gameplay.mp4
Normal file
Binary file not shown.
BIN
apps/website/public/images/header.jpeg
Normal file
BIN
apps/website/public/images/header.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.6 MiB |
@@ -14,6 +14,9 @@ module.exports = {
|
||||
'performance-green': '#6FE37A',
|
||||
'warning-amber': '#FFC556',
|
||||
'neon-aqua': '#43C9E6',
|
||||
'racing-red': '#E31E24',
|
||||
'carbon-black': '#0A0A0A',
|
||||
'metallic-silver': '#C0C0C8',
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['Inter', 'system-ui', 'sans-serif'],
|
||||
@@ -22,9 +25,20 @@ module.exports = {
|
||||
'glow': '0 0 20px rgba(25, 140, 255, 0.3)',
|
||||
'glow-strong': '0 0 28px rgba(25, 140, 255, 0.5)',
|
||||
'card': '0 8px 24px rgba(0, 0, 0, 0.12)',
|
||||
'racing': '0 4px 16px rgba(227, 30, 36, 0.15)',
|
||||
},
|
||||
transitionTimingFunction: {
|
||||
'spring': 'cubic-bezier(0.34, 1.56, 0.64, 1)',
|
||||
'speed': 'cubic-bezier(0.22, 1, 0.36, 1)',
|
||||
},
|
||||
animation: {
|
||||
'speed-pulse': 'speed-pulse 2s ease-in-out infinite',
|
||||
},
|
||||
keyframes: {
|
||||
'speed-pulse': {
|
||||
'0%, 100%': { opacity: '0.5' },
|
||||
'50%': { opacity: '1' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user