This commit is contained in:
2025-12-06 00:17:24 +01:00
parent 78c85a429c
commit 70d5f5689e
54 changed files with 826 additions and 210 deletions

4
.gitignore vendored
View File

@@ -41,4 +41,6 @@ logs/
# Temporary files
tmp/
temp/
temp/
.vercel
.env*.local

View File

@@ -0,0 +1,111 @@
import { NextResponse } from 'next/server';
import { validateEmail, isDisposableEmail } from '@gridpilot/identity/domain/value-objects/EmailAddress';
import { checkRateLimit, getClientIp } from '@/lib/rate-limit';
const SIGNUP_DEV_STORE = new Map<string, { email: string; createdAt: number; ip: string }>();
const SIGNUP_KV_HASH_KEY = 'signups:emails';
const isDev = !process.env.KV_REST_API_URL;
function jsonError(status: number, message: string, extra: Record<string, unknown> = {}) {
return NextResponse.json(
{
error: message,
...extra,
},
{ status },
);
}
export async function POST(request: Request) {
let body: unknown;
try {
body = await request.json();
} catch {
return jsonError(400, 'Invalid request body');
}
const email = (body as any)?.email;
if (typeof email !== 'string' || !email.trim()) {
return jsonError(400, 'Invalid email address');
}
const validation = validateEmail(email);
if (!validation.success) {
return jsonError(400, validation.error || 'Invalid email address');
}
const normalizedEmail = validation.email;
if (isDisposableEmail(normalizedEmail)) {
return jsonError(400, 'Disposable email addresses are not allowed');
}
const ip = getClientIp(request);
try {
const rateResult = await checkRateLimit(ip);
if (!rateResult.allowed) {
const retryAfterSeconds = Math.max(0, Math.round((rateResult.resetAt - Date.now()) / 1000));
return jsonError(429, 'Too many signups, please try again later.', {
retryAfter: retryAfterSeconds,
});
}
} catch {
return jsonError(503, 'Temporarily unable to accept signups.');
}
try {
if (isDev) {
const existing = SIGNUP_DEV_STORE.get(normalizedEmail);
if (existing) {
return jsonError(409, 'You are already on the list.');
}
SIGNUP_DEV_STORE.set(normalizedEmail, {
email: normalizedEmail,
createdAt: Date.now(),
ip,
});
} else {
const { kv } = await import('@vercel/kv');
const existing = await kv.hget<{ email: string; createdAt: number; ip: string }>(
SIGNUP_KV_HASH_KEY,
normalizedEmail,
);
if (existing) {
return jsonError(409, 'You are already on the list.');
}
await kv.hset(SIGNUP_KV_HASH_KEY, {
[normalizedEmail]: {
email: normalizedEmail,
createdAt: Date.now(),
ip,
},
});
}
} catch (error) {
console.error('Signup storage error:', error);
return jsonError(503, 'Temporarily unable to accept signups.');
}
return NextResponse.json(
{
ok: true,
message: 'You are on the grid! We will be in touch soon.',
},
{
status: 201,
},
);
}

View File

@@ -8,8 +8,8 @@ import DriverProfile from '@/components/drivers/DriverProfile';
import Button from '@/components/ui/Button';
import Card from '@/components/ui/Card';
import Breadcrumbs from '@/components/layout/Breadcrumbs';
import { EntityMappers } from '@gridpilot/racing/application/mappers/EntityMappers';
import type { DriverDTO } from '@gridpilot/racing/application/dto/DriverDTO';
import { EntityMappers } from '@gridpilot/racing';
import type { DriverDTO } from '@gridpilot/racing';
export default function DriverDetailPage({
searchParams,

View File

@@ -1,5 +1,7 @@
import React from 'react';
import type { Metadata } from 'next';
import Image from 'next/image';
import Link from 'next/link';
import './globals.css';
import { getAppMode } from '@/lib/mode';
import { getAuthService } from '@/lib/auth';
@@ -35,6 +37,9 @@ export const metadata: Metadata = {
title: 'GridPilot - iRacing League Racing Platform',
description: 'Structure over chaos. The professional platform for iRacing league racing.',
},
icons: {
icon: '/favicon.svg',
},
};
export default async function RootLayout({
@@ -75,9 +80,22 @@ export default async function RootLayout({
<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">
<h1 className="text-2xl font-semibold text-white">GridPilot</h1>
<p className="text-sm text-gray-400 font-light">Making league racing less chaotic</p>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<Link href="/" className="inline-flex items-center">
<Image
src="/images/logos/wordmark-rectangle-dark.svg"
alt="GridPilot"
width={160}
height={30}
className="h-6 w-auto md:h-8"
priority
/>
</Link>
<p className="hidden sm:block text-sm text-gray-400 font-light">
Making league racing less chaotic
</p>
</div>
</div>
</div>
</header>

View File

@@ -12,12 +12,14 @@ import StandingsTable from '@/components/leagues/StandingsTable';
import LeagueScoringTab from '@/components/leagues/LeagueScoringTab';
import DriverIdentity from '@/components/drivers/DriverIdentity';
import DriverSummaryPill from '@/components/profile/DriverSummaryPill';
import { League } from '@gridpilot/racing/domain/entities/League';
import { Driver } from '@gridpilot/racing/domain/entities/Driver';
import type { DriverDTO } from '@gridpilot/racing/application/dto/DriverDTO';
import type { LeagueDriverSeasonStatsDTO } from '@gridpilot/racing/application/dto/LeagueDriverSeasonStatsDTO';
import type { LeagueScoringConfigDTO } from '@gridpilot/racing/application/dto/LeagueScoringConfigDTO';
import { EntityMappers } from '@gridpilot/racing/application/mappers/EntityMappers';
import {
League,
Driver,
EntityMappers,
type DriverDTO,
type LeagueDriverSeasonStatsDTO,
type LeagueScoringConfigDTO,
} from '@gridpilot/racing';
import {
getLeagueRepository,
getRaceRepository,

View File

@@ -3,12 +3,14 @@
import { useState, useEffect } from 'react';
import Card from '@/components/ui/Card';
import StandingsTable from '@/components/leagues/StandingsTable';
import type { DriverDTO } from '@gridpilot/racing/application/dto/DriverDTO';
import type { LeagueDriverSeasonStatsDTO } from '@gridpilot/racing/application/dto/LeagueDriverSeasonStatsDTO';
import { EntityMappers } from '@gridpilot/racing/application/mappers/EntityMappers';
import {
EntityMappers,
type DriverDTO,
type LeagueDriverSeasonStatsDTO,
} from '@gridpilot/racing';
import { getGetLeagueDriverSeasonStatsQuery, getDriverRepository } from '@/lib/di-container';
export default function LeagueStandingsPage({ params }: { params: { id: string } }) {
export default function LeagueStandingsPage({ params }: any) {
const leagueId = params.id;
const [standings, setStandings] = useState<LeagueDriverSeasonStatsDTO[]>([]);

View File

@@ -40,7 +40,7 @@ export default function LeaguesPage() {
.filter((league) => {
const matchesSearch =
league.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
league.description.toLowerCase().includes(searchQuery.toLowerCase());
(league.description ?? '').toLowerCase().includes(searchQuery.toLowerCase());
return matchesSearch;
})
.sort((a, b) => {
@@ -137,7 +137,7 @@ export default function LeaguesPage() {
</p>
<Button
variant="primary"
onClick={() => setShowCreateForm(true)}
onClick={() => router.push('/leagues/create')}
>
Create Your First League
</Button>

View File

@@ -2,9 +2,7 @@
import { useState, useEffect } from 'react';
import { getDriverRepository } from '@/lib/di-container';
import { Driver } from '@gridpilot/racing/domain/entities/Driver';
import { EntityMappers } from '@gridpilot/racing/application/mappers/EntityMappers';
import type { DriverDTO } from '@gridpilot/racing/application/dto/DriverDTO';
import { Driver, EntityMappers, type DriverDTO } from '@gridpilot/racing';
import CreateDriverForm from '@/components/drivers/CreateDriverForm';
import Card from '@/components/ui/Card';
import DriverProfile from '@/components/drivers/DriverProfile';

View File

@@ -4,7 +4,7 @@ import { useState, FormEvent } from 'react';
import { useRouter } from 'next/navigation';
import Input from '../ui/Input';
import Button from '../ui/Button';
import { Driver } from '@gridpilot/racing/domain/entities/Driver';
import { Driver } from '@gridpilot/racing';
import { getDriverRepository } from '../../lib/di-container';
interface FormErrors {

View File

@@ -1,8 +1,12 @@
'use client';
import Image from 'next/image';
import { useRef } from 'react';
import { useScrollProgress } from '@/hooks/useScrollProgress';
const discordUrl = process.env.NEXT_PUBLIC_DISCORD_URL || 'https://discord.gg/gridpilot';
const xUrl = process.env.NEXT_PUBLIC_X_URL || '#';
export default function Footer() {
return (
<footer className="relative bg-deep-graphite">
@@ -30,6 +34,15 @@ export default function Footer() {
transform: 'translateY(0)'
}}
>
<div className="mb-2 flex justify-center">
<Image
src="/images/logos/icon-square-dark.svg"
alt="GridPilot"
width={40}
height={40}
className="h-8 w-auto md:h-10"
/>
</div>
<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>
@@ -47,11 +60,17 @@ export default function Footer() {
}}
>
<a
href="https://discord.gg/gridpilot"
href={discordUrl}
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>
<a
href={xUrl}
className="text-[9px] md:text-xs text-gray-300 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"
>
𝕏 Follow on X
</a>
</div>
{/* Development status */}

View File

@@ -226,7 +226,7 @@ export default function LeagueAdmin({ league, onLeagueUpdate }: LeagueAdminProps
if (!activeRejectRequest) {
setRejectReason('');
}
}, [activeRejectRequest?.id]);
}, [activeRejectRequest, setRejectReason]);
const isRejectModalOpen = modal === 'reject-request' && !!activeRejectRequest;

View File

@@ -104,7 +104,7 @@ function InfoFlyout({ isOpen, onClose, title, children, anchorRef }: InfoFlyoutP
);
}
function InfoButton({ onClick, buttonRef }: { onClick: () => void; buttonRef: React.RefObject<HTMLButtonElement | null> }) {
function InfoButton({ onClick, buttonRef }: { onClick: () => void; buttonRef: React.RefObject<HTMLButtonElement> }) {
return (
<button
ref={buttonRef}

View File

@@ -1,6 +1,6 @@
'use client';
import { useState, useEffect } from 'react';
import { useState, useEffect, useCallback } from 'react';
import { useRouter } from 'next/navigation';
import { Race } from '@gridpilot/racing/domain/entities/Race';
import {
@@ -25,11 +25,7 @@ export default function LeagueSchedule({ leagueId }: LeagueScheduleProps) {
const currentDriverId = useEffectiveDriverId();
useEffect(() => {
loadRaces();
}, [leagueId]);
const loadRaces = async () => {
const loadRaces = useCallback(async () => {
setLoading(true);
try {
const raceRepo = getRaceRepository();
@@ -59,7 +55,12 @@ export default function LeagueSchedule({ leagueId }: LeagueScheduleProps) {
} finally {
setLoading(false);
}
};
}, [leagueId, currentDriverId]);
useEffect(() => {
loadRaces();
}, [loadRaces]);
const handleRegister = async (race: Race, e: React.MouseEvent) => {
e.stopPropagation();

View File

@@ -117,7 +117,7 @@ function InfoFlyout({ isOpen, onClose, title, children, anchorRef }: InfoFlyoutP
);
}
function InfoButton({ onClick, buttonRef }: { onClick: () => void; buttonRef: React.RefObject<HTMLButtonElement | null> }) {
function InfoButton({ onClick, buttonRef }: { onClick: () => void; buttonRef: React.RefObject<HTMLButtonElement> }) {
return (
<button
ref={buttonRef}

View File

@@ -433,7 +433,7 @@ function YearCalendarPreview({
}
return view;
}, [raceDates, seasonStart, seasonEnd]);
}, [raceDates, seasonStart, seasonEnd, months, isSeasonStartDate, isSeasonEndDate]);
// Calculate season stats
const firstRace = raceDates[0];

View File

@@ -3,6 +3,7 @@
declare namespace NodeJS {
interface ProcessEnv {
NEXT_PUBLIC_GRIDPILOT_MODE?: 'prelaunch' | 'alpha';
NEXT_PUBLIC_GRIDPILOT_MODE?: 'pre-launch' | 'alpha';
NEXT_PUBLIC_X_URL?: string;
}
}

View File

@@ -1,4 +1,5 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
images: {
@@ -19,11 +20,18 @@ const nextConfig = {
eslint: {
ignoreDuringBuilds: false,
},
transpilePackages: [
'@gridpilot/racing',
'@gridpilot/identity',
'@gridpilot/social',
'@gridpilot/testing-support',
],
webpack: (config) => {
config.module.rules.push({
test: /\.(mp4|webm)$/,
type: 'asset/resource',
});
return config;
},
};

View File

@@ -12,17 +12,18 @@
},
"dependencies": {
"@faker-js/faker": "^9.2.0",
"@gridpilot/identity": "0.1.0",
"@gridpilot/racing": "0.1.0",
"@gridpilot/social": "0.1.0",
"@gridpilot/testing-support": "0.1.0",
"@gridpilot/identity": "file:../../packages/identity",
"@gridpilot/racing": "file:../../packages/racing",
"@gridpilot/social": "file:../../packages/social",
"@gridpilot/testing-support": "file:../../packages/testing-support",
"@vercel/kv": "^3.0.0",
"framer-motion": "^12.23.25",
"lucide-react": "^0.555.0",
"next": "^15.0.0",
"next": "15.5.7",
"react": "^18.3.0",
"react-dom": "^18.3.0",
"zod": "^3.25.76"
"zod": "^3.25.76",
"uuid": "^11.0.5"
},
"devDependencies": {
"@types/node": "^20.0.0",
@@ -30,7 +31,7 @@
"@types/react-dom": "^18.3.0",
"autoprefixer": "^10.4.22",
"eslint": "^8.57.0",
"eslint-config-next": "^15.0.0",
"eslint-config-next": "15.5.7",
"postcss": "^8.5.6",
"tailwindcss": "^3.4.18",
"typescript": "^5.6.0"

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 1025 1025" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<rect id="Square-Icon-Dark" serif:id="Square Icon Dark" x="0.521" y="0.228" width="1024" height="1024"/>
<g transform="matrix(2.83814,0,0,2.83814,-723.612,-329.472)">
<path d="M493,227.168c15.577,-0 28.925,5.503 40.031,16.537c11.132,11.06 16.697,24.433 16.697,40.119c-0,15.686 -5.565,29.059 -16.697,40.118c-11.106,11.035 -24.454,16.538 -40.031,16.538l-6.92,-0l-0,25.488l-95.32,-0c-12.58,-0 -24.193,-3.137 -34.852,-9.36c-10.602,-6.19 -18.998,-14.586 -25.188,-25.188c-6.224,-10.66 -9.36,-22.273 -9.36,-34.852c-0,-9.433 1.862,-18.466 5.582,-27.101c3.683,-8.551 8.625,-15.857 14.743,-21.974c6.117,-6.117 13.423,-11.06 21.974,-14.743c8.635,-3.72 17.668,-5.582 27.101,-5.582l102.24,-0Zm-0,19l-53.424,-0l-0,27.792l53.424,-0c2.784,-0 5.136,0.96 7.056,2.88c1.92,1.92 2.88,4.248 2.88,6.984c-0,2.736 -0.96,5.088 -2.88,7.056c-1.92,1.968 -4.272,2.952 -7.056,2.952l-39.888,-0c-3.84,-0 -7.104,1.344 -9.792,4.032c-2.688,2.688 -4.032,5.952 -4.032,9.792l-0,39.312l27.792,-0l-0,-25.488l25.92,-0c10.368,-0 19.248,-3.672 26.64,-11.016c7.392,-7.344 11.088,-16.224 11.088,-26.64c-0,-10.416 -3.696,-19.296 -11.088,-26.64c-7.392,-7.344 -16.272,-11.016 -26.64,-11.016Zm-87.12,37.008l-0,36l-15.12,-0c-6.24,-0 -11.568,-2.208 -15.984,-6.624c-4.416,-4.416 -6.624,-9.744 -6.624,-15.984c-0,-6.24 2.208,-11.568 6.624,-15.984c4.416,-4.416 9.744,-6.624 15.984,-6.624l41.472,-0l-0,-27.792l-41.472,-0c-6.816,-0 -13.344,1.344 -19.584,4.032c-6.24,2.688 -11.592,6.264 -16.056,10.728c-4.464,4.464 -8.04,9.816 -10.728,16.056c-2.688,6.24 -4.032,12.768 -4.032,19.584c-0,9.12 2.256,17.544 6.768,25.272c4.512,7.728 10.632,13.848 18.36,18.36c7.728,4.512 16.152,6.768 25.272,6.768l42.912,-0l-0,-63.792l-27.792,-0Z" style="fill:#fff;"/>
<g transform="matrix(144,0,0,144,340.072,346.968)">
<path d="M0.457,-0.443l0.193,0l0,0.443l-0.298,-0c-0.063,-0 -0.122,-0.016 -0.176,-0.047c-0.053,-0.031 -0.096,-0.074 -0.127,-0.128c-0.031,-0.053 -0.047,-0.112 -0.047,-0.175c-0,-0.047 0.009,-0.093 0.028,-0.136c0.019,-0.043 0.043,-0.081 0.074,-0.112c0.031,-0.03 0.069,-0.055 0.112,-0.074c0.043,-0.019 0.089,-0.028 0.136,-0.028l0.288,-0l-0,0.193l-0.288,-0c-0.043,-0 -0.08,0.015 -0.111,0.046c-0.031,0.031 -0.046,0.068 -0.046,0.111c-0,0.043 0.015,0.08 0.046,0.111c0.031,0.031 0.068,0.046 0.111,0.046l0.105,-0l-0,-0.25Z" style="fill-rule:nonzero;"/>
</g>
<g transform="matrix(144,0,0,144,436.696,346.968)">
<path d="M0.391,-0.7c0.072,0 0.134,0.025 0.185,0.077c0.051,0.05 0.077,0.112 0.077,0.185c0,0.072 -0.026,0.134 -0.077,0.185c-0.051,0.051 -0.113,0.076 -0.185,0.076l-0.18,0l-0,0.177l-0.193,0l-0,-0.273c-0,-0.027 0.009,-0.049 0.028,-0.068c0.019,-0.019 0.041,-0.028 0.068,-0.028l0.277,0c0.019,0 0.036,-0.007 0.049,-0.02c0.013,-0.014 0.02,-0.03 0.02,-0.049c-0,-0.019 -0.007,-0.036 -0.02,-0.049c-0.013,-0.013 -0.03,-0.02 -0.049,-0.02l-0.371,0l-0,-0.193l0.371,0Z" style="fill-rule:nonzero;"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -1,11 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<defs>
<linearGradient id="gpAvatar1" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#1f5fff"/>
<stop offset="1" stop-color="#23c4ff"/>
</linearGradient>
</defs>
<rect width="64" height="64" rx="16" fill="#050814"/>
<circle cx="32" cy="24" r="12" fill="url(#gpAvatar1)"/>
<path d="M16 52c3.5-9 9-14 16-14s12.5 5 16 14" fill="#111827"/>
</svg>

Before

Width:  |  Height:  |  Size: 444 B

View File

@@ -1,11 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<defs>
<linearGradient id="gpAvatar2" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#7c3aed"/>
<stop offset="1" stop-color="#ec4899"/>
</linearGradient>
</defs>
<rect width="64" height="64" rx="16" fill="#050814"/>
<circle cx="32" cy="24" r="12" fill="url(#gpAvatar2)"/>
<path d="M16 52c3.5-9 9-14 16-14s12.5 5 16 14" fill="#111827"/>
</svg>

Before

Width:  |  Height:  |  Size: 444 B

View File

@@ -1,11 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<defs>
<linearGradient id="gpAvatar3" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#22c55e"/>
<stop offset="1" stop-color="#16a34a"/>
</linearGradient>
</defs>
<rect width="64" height="64" rx="16" fill="#050814"/>
<circle cx="32" cy="24" r="12" fill="url(#gpAvatar3)"/>
<path d="M16 52c3.5-9 9-14 16-14s12.5 5 16 14" fill="#111827"/>
</svg>

Before

Width:  |  Height:  |  Size: 444 B

View File

@@ -1,11 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<defs>
<linearGradient id="gpAvatar4" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#f97316"/>
<stop offset="1" stop-color="#ea580c"/>
</linearGradient>
</defs>
<rect width="64" height="64" rx="16" fill="#050814"/>
<circle cx="32" cy="24" r="12" fill="url(#gpAvatar4)"/>
<path d="M16 52c3.5-9 9-14 16-14s12.5 5 16 14" fill="#111827"/>
</svg>

Before

Width:  |  Height:  |  Size: 444 B

View File

@@ -1,11 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<defs>
<linearGradient id="gpAvatar5" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#eab308"/>
<stop offset="1" stop-color="#a16207"/>
</linearGradient>
</defs>
<rect width="64" height="64" rx="16" fill="#050814"/>
<circle cx="32" cy="24" r="12" fill="url(#gpAvatar5)"/>
<path d="M16 52c3.5-9 9-14 16-14s12.5 5 16 14" fill="#111827"/>
</svg>

Before

Width:  |  Height:  |  Size: 444 B

View File

@@ -1,11 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<defs>
<linearGradient id="gpAvatar6" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#06b6d4"/>
<stop offset="1" stop-color="#0ea5e9"/>
</linearGradient>
</defs>
<rect width="64" height="64" rx="16" fill="#050814"/>
<circle cx="32" cy="24" r="12" fill="url(#gpAvatar6)"/>
<path d="M16 52c3.5-9 9-14 16-14s12.5 5 16 14" fill="#111827"/>
</svg>

Before

Width:  |  Height:  |  Size: 444 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 1025 1025" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><rect id="Square-Icon-Dark" serif:id="Square Icon Dark" x="0.521" y="0.228" width="1024" height="1024"/><g transform="matrix(2.83814,0,0,2.83814,-723.612,-329.472)"><path d="M493,227.168c15.577,-0 28.925,5.503 40.031,16.537c11.132,11.06 16.697,24.433 16.697,40.119c-0,15.686 -5.565,29.059 -16.697,40.118c-11.106,11.035 -24.454,16.538 -40.031,16.538l-6.92,-0l-0,25.488l-95.32,-0c-12.58,-0 -24.193,-3.137 -34.852,-9.36c-10.602,-6.19 -18.998,-14.586 -25.188,-25.188c-6.224,-10.66 -9.36,-22.273 -9.36,-34.852c-0,-9.433 1.862,-18.466 5.582,-27.101c3.683,-8.551 8.625,-15.857 14.743,-21.974c6.117,-6.117 13.423,-11.06 21.974,-14.743c8.635,-3.72 17.668,-5.582 27.101,-5.582l102.24,-0Zm-0,19l-53.424,-0l-0,27.792l53.424,-0c2.784,-0 5.136,0.96 7.056,2.88c1.92,1.92 2.88,4.248 2.88,6.984c-0,2.736 -0.96,5.088 -2.88,7.056c-1.92,1.968 -4.272,2.952 -7.056,2.952l-39.888,-0c-3.84,-0 -7.104,1.344 -9.792,4.032c-2.688,2.688 -4.032,5.952 -4.032,9.792l-0,39.312l27.792,-0l-0,-25.488l25.92,-0c10.368,-0 19.248,-3.672 26.64,-11.016c7.392,-7.344 11.088,-16.224 11.088,-26.64c-0,-10.416 -3.696,-19.296 -11.088,-26.64c-7.392,-7.344 -16.272,-11.016 -26.64,-11.016Zm-87.12,37.008l-0,36l-15.12,-0c-6.24,-0 -11.568,-2.208 -15.984,-6.624c-4.416,-4.416 -6.624,-9.744 -6.624,-15.984c-0,-6.24 2.208,-11.568 6.624,-15.984c4.416,-4.416 9.744,-6.624 15.984,-6.624l41.472,-0l-0,-27.792l-41.472,-0c-6.816,-0 -13.344,1.344 -19.584,4.032c-6.24,2.688 -11.592,6.264 -16.056,10.728c-4.464,4.464 -8.04,9.816 -10.728,16.056c-2.688,6.24 -4.032,12.768 -4.032,19.584c-0,9.12 2.256,17.544 6.768,25.272c4.512,7.728 10.632,13.848 18.36,18.36c7.728,4.512 16.152,6.768 25.272,6.768l42.912,-0l-0,-63.792l-27.792,-0Z" style="fill:#fff;"/><g transform="matrix(144,0,0,144,340.072,346.968)"><path d="M0.457,-0.443l0.193,0l0,0.443l-0.298,-0c-0.063,-0 -0.122,-0.016 -0.176,-0.047c-0.053,-0.031 -0.096,-0.074 -0.127,-0.128c-0.031,-0.053 -0.047,-0.112 -0.047,-0.175c-0,-0.047 0.009,-0.093 0.028,-0.136c0.019,-0.043 0.043,-0.081 0.074,-0.112c0.031,-0.03 0.069,-0.055 0.112,-0.074c0.043,-0.019 0.089,-0.028 0.136,-0.028l0.288,-0l-0,0.193l-0.288,-0c-0.043,-0 -0.08,0.015 -0.111,0.046c-0.031,0.031 -0.046,0.068 -0.046,0.111c-0,0.043 0.015,0.08 0.046,0.111c0.031,0.031 0.068,0.046 0.111,0.046l0.105,-0l-0,-0.25Z" style="fill-rule:nonzero;"/></g><g transform="matrix(144,0,0,144,436.696,346.968)"><path d="M0.391,-0.7c0.072,0 0.134,0.025 0.185,0.077c0.051,0.05 0.077,0.112 0.077,0.185c0,0.072 -0.026,0.134 -0.077,0.185c-0.051,0.051 -0.113,0.076 -0.185,0.076l-0.18,0l-0,0.177l-0.193,0l-0,-0.273c-0,-0.027 0.009,-0.049 0.028,-0.068c0.019,-0.019 0.041,-0.028 0.068,-0.028l0.277,0c0.019,0 0.036,-0.007 0.049,-0.02c0.013,-0.014 0.02,-0.03 0.02,-0.049c-0,-0.019 -0.007,-0.036 -0.02,-0.049c-0.013,-0.013 -0.03,-0.02 -0.049,-0.02l-0.371,0l-0,-0.193l0.371,0Z" style="fill-rule:nonzero;"/></g></g></svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 560 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 560 KiB

134
package-lock.json generated
View File

@@ -140,16 +140,17 @@
"version": "0.1.0",
"dependencies": {
"@faker-js/faker": "^9.2.0",
"@gridpilot/identity": "0.1.0",
"@gridpilot/racing": "0.1.0",
"@gridpilot/social": "0.1.0",
"@gridpilot/testing-support": "0.1.0",
"@gridpilot/identity": "file:../../packages/identity",
"@gridpilot/racing": "file:../../packages/racing",
"@gridpilot/social": "file:../../packages/social",
"@gridpilot/testing-support": "file:../../packages/testing-support",
"@vercel/kv": "^3.0.0",
"framer-motion": "^12.23.25",
"lucide-react": "^0.555.0",
"next": "^15.0.0",
"next": "15.5.7",
"react": "^18.3.0",
"react-dom": "^18.3.0",
"uuid": "^11.0.5",
"zod": "^3.25.76"
},
"devDependencies": {
@@ -158,7 +159,7 @@
"@types/react-dom": "^18.3.0",
"autoprefixer": "^10.4.22",
"eslint": "^8.57.0",
"eslint-config-next": "^15.0.0",
"eslint-config-next": "15.5.7",
"postcss": "^8.5.6",
"tailwindcss": "^3.4.18",
"typescript": "^5.6.0"
@@ -1534,10 +1535,18 @@
"resolved": "apps/companion",
"link": true
},
"node_modules/@gridpilot/demo-infrastructure": {
"resolved": "packages/demo-infrastructure",
"link": true
},
"node_modules/@gridpilot/identity": {
"resolved": "packages/identity",
"link": true
},
"node_modules/@gridpilot/media": {
"resolved": "packages/media",
"link": true
},
"node_modules/@gridpilot/racing": {
"resolved": "packages/racing",
"link": true
@@ -2189,15 +2198,15 @@
}
},
"node_modules/@next/env": {
"version": "15.5.6",
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.6.tgz",
"integrity": "sha512-3qBGRW+sCGzgbpc5TS1a0p7eNxnOarGVQhZxfvTdnV0gFI61lX7QNtQ4V1TSREctXzYn5NetbUsLvyqwLFJM6Q==",
"version": "15.5.7",
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.7.tgz",
"integrity": "sha512-4h6Y2NyEkIEN7Z8YxkA27pq6zTkS09bUSYC0xjd0NpwFxjnIKeZEeH591o5WECSmjpUhLn3H2QLJcDye3Uzcvg==",
"license": "MIT"
},
"node_modules/@next/eslint-plugin-next": {
"version": "15.5.6",
"resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.5.6.tgz",
"integrity": "sha512-YxDvsT2fwy1j5gMqk3ppXlsgDopHnkM4BoxSVASbvvgh5zgsK8lvWerDzPip8k3WVzsTZ1O7A7si1KNfN4OZfQ==",
"version": "15.5.7",
"resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.5.7.tgz",
"integrity": "sha512-DtRU2N7BkGr8r+pExfuWHwMEPX5SD57FeA6pxdgCHODo+b/UgIgjE+rgWKtJAbEbGhVZ2jtHn4g3wNhWFoNBQQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2205,9 +2214,9 @@
}
},
"node_modules/@next/swc-darwin-arm64": {
"version": "15.5.6",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.6.tgz",
"integrity": "sha512-ES3nRz7N+L5Umz4KoGfZ4XX6gwHplwPhioVRc25+QNsDa7RtUF/z8wJcbuQ2Tffm5RZwuN2A063eapoJ1u4nPg==",
"version": "15.5.7",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.7.tgz",
"integrity": "sha512-IZwtxCEpI91HVU/rAUOOobWSZv4P2DeTtNaCdHqLcTJU4wdNXgAySvKa/qJCgR5m6KI8UsKDXtO2B31jcaw1Yw==",
"cpu": [
"arm64"
],
@@ -2221,9 +2230,9 @@
}
},
"node_modules/@next/swc-darwin-x64": {
"version": "15.5.6",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.6.tgz",
"integrity": "sha512-JIGcytAyk9LQp2/nuVZPAtj8uaJ/zZhsKOASTjxDug0SPU9LAM3wy6nPU735M1OqacR4U20LHVF5v5Wnl9ptTA==",
"version": "15.5.7",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.7.tgz",
"integrity": "sha512-UP6CaDBcqaCBuiq/gfCEJw7sPEoX1aIjZHnBWN9v9qYHQdMKvCKcAVs4OX1vIjeE+tC5EIuwDTVIoXpUes29lg==",
"cpu": [
"x64"
],
@@ -2237,9 +2246,9 @@
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
"version": "15.5.6",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.6.tgz",
"integrity": "sha512-qvz4SVKQ0P3/Im9zcS2RmfFL/UCQnsJKJwQSkissbngnB/12c6bZTCB0gHTexz1s6d/mD0+egPKXAIRFVS7hQg==",
"version": "15.5.7",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.7.tgz",
"integrity": "sha512-NCslw3GrNIw7OgmRBxHtdWFQYhexoUCq+0oS2ccjyYLtcn1SzGzeM54jpTFonIMUjNbHmpKpziXnpxhSWLcmBA==",
"cpu": [
"arm64"
],
@@ -2253,9 +2262,9 @@
}
},
"node_modules/@next/swc-linux-arm64-musl": {
"version": "15.5.6",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.6.tgz",
"integrity": "sha512-FsbGVw3SJz1hZlvnWD+T6GFgV9/NYDeLTNQB2MXoPN5u9VA9OEDy6fJEfePfsUKAhJufFbZLgp0cPxMuV6SV0w==",
"version": "15.5.7",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.7.tgz",
"integrity": "sha512-nfymt+SE5cvtTrG9u1wdoxBr9bVB7mtKTcj0ltRn6gkP/2Nu1zM5ei8rwP9qKQP0Y//umK+TtkKgNtfboBxRrw==",
"cpu": [
"arm64"
],
@@ -2269,9 +2278,9 @@
}
},
"node_modules/@next/swc-linux-x64-gnu": {
"version": "15.5.6",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.6.tgz",
"integrity": "sha512-3QnHGFWlnvAgyxFxt2Ny8PTpXtQD7kVEeaFat5oPAHHI192WKYB+VIKZijtHLGdBBvc16tiAkPTDmQNOQ0dyrA==",
"version": "15.5.7",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.7.tgz",
"integrity": "sha512-hvXcZvCaaEbCZcVzcY7E1uXN9xWZfFvkNHwbe/n4OkRhFWrs1J1QV+4U1BN06tXLdaS4DazEGXwgqnu/VMcmqw==",
"cpu": [
"x64"
],
@@ -2285,9 +2294,9 @@
}
},
"node_modules/@next/swc-linux-x64-musl": {
"version": "15.5.6",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.6.tgz",
"integrity": "sha512-OsGX148sL+TqMK9YFaPFPoIaJKbFJJxFzkXZljIgA9hjMjdruKht6xDCEv1HLtlLNfkx3c5w2GLKhj7veBQizQ==",
"version": "15.5.7",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.7.tgz",
"integrity": "sha512-4IUO539b8FmF0odY6/SqANJdgwn1xs1GkPO5doZugwZ3ETF6JUdckk7RGmsfSf7ws8Qb2YB5It33mvNL/0acqA==",
"cpu": [
"x64"
],
@@ -2301,9 +2310,9 @@
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
"version": "15.5.6",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.6.tgz",
"integrity": "sha512-ONOMrqWxdzXDJNh2n60H6gGyKed42Ieu6UTVPZteXpuKbLZTH4G4eBMsr5qWgOBA+s7F+uB4OJbZnrkEDnZ5Fg==",
"version": "15.5.7",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.7.tgz",
"integrity": "sha512-CpJVTkYI3ZajQkC5vajM7/ApKJUOlm6uP4BknM3XKvJ7VXAvCqSjSLmM0LKdYzn6nBJVSjdclx8nYJSa3xlTgQ==",
"cpu": [
"arm64"
],
@@ -2317,9 +2326,9 @@
}
},
"node_modules/@next/swc-win32-x64-msvc": {
"version": "15.5.6",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.6.tgz",
"integrity": "sha512-pxK4VIjFRx1MY92UycLOOw7dTdvccWsNETQ0kDHkBlcFH1GrTLUjSiHU1ohrznnux6TqRHgv5oflhfIWZwVROQ==",
"version": "15.5.7",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.7.tgz",
"integrity": "sha512-gMzgBX164I6DN+9/PGA+9dQiwmTkE4TloBNx8Kv9UiGARsr9Nba7IpcBRA1iTV9vwlYnrE3Uy6I7Aj6qLjQuqw==",
"cpu": [
"x64"
],
@@ -5980,13 +5989,13 @@
}
},
"node_modules/eslint-config-next": {
"version": "15.5.6",
"resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.5.6.tgz",
"integrity": "sha512-cGr3VQlPsZBEv8rtYp4BpG1KNXDqGvPo9VC1iaCgIA11OfziC/vczng+TnAS3WpRIR3Q5ye/6yl+CRUuZ1fPGg==",
"version": "15.5.7",
"resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.5.7.tgz",
"integrity": "sha512-nU/TRGHHeG81NeLW5DeQT5t6BDUqbpsNQTvef1ld/tqHT+/zTx60/TIhKnmPISTTe++DVo+DLxDmk4rnwHaZVw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@next/eslint-plugin-next": "15.5.6",
"@next/eslint-plugin-next": "15.5.7",
"@rushstack/eslint-patch": "^1.10.3",
"@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0",
"@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0",
@@ -8978,12 +8987,12 @@
}
},
"node_modules/next": {
"version": "15.5.6",
"resolved": "https://registry.npmjs.org/next/-/next-15.5.6.tgz",
"integrity": "sha512-zTxsnI3LQo3c9HSdSf91O1jMNsEzIXDShXd4wVdg9y5shwLqBXi4ZtUUJyB86KGVSJLZx0PFONvO54aheGX8QQ==",
"version": "15.5.7",
"resolved": "https://registry.npmjs.org/next/-/next-15.5.7.tgz",
"integrity": "sha512-+t2/0jIJ48kUpGKkdlhgkv+zPTEOoXyr60qXe68eB/pl3CMJaLeIGjzp5D6Oqt25hCBiBTt8wEeeAzfJvUKnPQ==",
"license": "MIT",
"dependencies": {
"@next/env": "15.5.6",
"@next/env": "15.5.7",
"@swc/helpers": "0.5.15",
"caniuse-lite": "^1.0.30001579",
"postcss": "8.4.31",
@@ -8996,14 +9005,14 @@
"node": "^18.18.0 || ^19.8.0 || >= 20.0.0"
},
"optionalDependencies": {
"@next/swc-darwin-arm64": "15.5.6",
"@next/swc-darwin-x64": "15.5.6",
"@next/swc-linux-arm64-gnu": "15.5.6",
"@next/swc-linux-arm64-musl": "15.5.6",
"@next/swc-linux-x64-gnu": "15.5.6",
"@next/swc-linux-x64-musl": "15.5.6",
"@next/swc-win32-arm64-msvc": "15.5.6",
"@next/swc-win32-x64-msvc": "15.5.6",
"@next/swc-darwin-arm64": "15.5.7",
"@next/swc-darwin-x64": "15.5.7",
"@next/swc-linux-arm64-gnu": "15.5.7",
"@next/swc-linux-arm64-musl": "15.5.7",
"@next/swc-linux-x64-gnu": "15.5.7",
"@next/swc-linux-x64-musl": "15.5.7",
"@next/swc-win32-arm64-msvc": "15.5.7",
"@next/swc-win32-x64-msvc": "15.5.7",
"sharp": "^0.34.3"
},
"peerDependencies": {
@@ -12739,7 +12748,6 @@
"version": "11.0.5",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.5.tgz",
"integrity": "sha512-508e6IcKLrhxKdBbcA2b4KQZlLVp2+J5UwQ6F7Drckkc5N9ZJwFa4TgWtsww9UG8fGHbm6gbV19TdM5pQ4GaIA==",
"dev": true,
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
@@ -13434,6 +13442,14 @@
"@gridpilot/automation": "*"
}
},
"packages/demo-infrastructure": {
"name": "@gridpilot/demo-infrastructure",
"version": "0.1.0",
"dependencies": {
"@faker-js/faker": "^9.0.0",
"@gridpilot/media": "file:../media"
}
},
"packages/demo-support": {
"name": "@gridpilot/testing-support",
"version": "0.1.0",
@@ -13454,9 +13470,19 @@
"zod": "^3.25.76"
}
},
"packages/media": {
"name": "@gridpilot/media",
"version": "0.1.0"
},
"packages/racing": {
"name": "@gridpilot/racing",
"version": "0.1.0"
"version": "0.1.0",
"dependencies": {
"uuid": "^11.0.5"
},
"devDependencies": {
"@types/uuid": "^10.0.0"
}
},
"packages/racing-application": {
"name": "@gridpilot/racing-application",

View File

@@ -11,41 +11,44 @@
"apps/*"
],
"scripts": {
"dev": "echo 'Development server placeholder - to be configured'",
"build": "echo 'Build all packages placeholder - to be configured'",
"test": "vitest run && vitest run --config vitest.e2e.config.ts && npm run smoke:website",
"test:unit": "vitest run tests/unit",
"test:integration": "vitest run tests/integration",
"test:e2e": "vitest run --config vitest.e2e.config.ts",
"test:e2e:docker": "vitest run --config vitest.e2e.config.ts tests/e2e/docker/",
"test:watch": "vitest watch",
"test:smoke": "vitest run --config vitest.smoke.config.ts",
"test:smoke:watch": "vitest watch --config vitest.smoke.config.ts",
"test:smoke:electron": "playwright test --config=playwright.smoke.config.ts",
"smoke:website": "npm run website:build && npx playwright test -c playwright.website.config.ts",
"test:hosted-real": "vitest run --config vitest.e2e.config.ts tests/e2e/hosted-real/",
"test:companion-hosted": "vitest run --config vitest.e2e.config.ts tests/e2e/companion/companion-ui-full-workflow.e2e.test.ts",
"typecheck": "tsc --noEmit",
"test:types": "tsc --noEmit -p tsconfig.tests.json",
"companion": "npm run companion:build --workspace=@gridpilot/companion && npm run start --workspace=@gridpilot/companion",
"companion:dev": "npm run dev --workspace=@gridpilot/companion",
"companion:build": "npm run build --workspace=@gridpilot/companion",
"companion:start": "npm run start --workspace=@gridpilot/companion",
"website:dev": "npm run dev --workspace=@gridpilot/website",
"website:build": "npm run build --workspace=@gridpilot/website",
"website:start": "npm run start --workspace=@gridpilot/website",
"website:lint": "npm run lint --workspace=@gridpilot/website",
"website:type-check": "npm run type-check --workspace=@gridpilot/website",
"website:clean": "npm run clean --workspace=@gridpilot/website",
"chrome:debug": "open -a 'Google Chrome' --args --remote-debugging-port=9222 --user-data-dir=/tmp/chrome-debug",
"docker:e2e:up": "docker-compose -f docker/docker-compose.e2e.yml up -d",
"docker:e2e:down": "docker-compose -f docker/docker-compose.e2e.yml down",
"generate-templates": "npx tsx scripts/generate-templates/index.ts",
"minify-fixtures": "npx tsx scripts/minify-fixtures.ts",
"minify-fixtures:force": "npx tsx scripts/minify-fixtures.ts --force",
"dom:process": "npx tsx scripts/dom-export/processWorkflows.ts",
"prepare": "husky"
},
"dev": "echo 'Development server placeholder - to be configured'",
"build": "echo 'Build all packages placeholder - to be configured'",
"test": "vitest run && vitest run --config vitest.e2e.config.ts && npm run smoke:website",
"test:unit": "vitest run tests/unit",
"test:integration": "vitest run tests/integration",
"test:e2e": "vitest run --config vitest.e2e.config.ts",
"test:e2e:docker": "vitest run --config vitest.e2e.config.ts tests/e2e/docker/",
"test:watch": "vitest watch",
"test:smoke": "vitest run --config vitest.smoke.config.ts",
"test:smoke:watch": "vitest watch --config vitest.smoke.config.ts",
"test:smoke:electron": "playwright test --config=playwright.smoke.config.ts",
"smoke:website": "npm run website:build && npx playwright test -c playwright.website.config.ts",
"test:hosted-real": "vitest run --config vitest.e2e.config.ts tests/e2e/hosted-real/",
"test:companion-hosted": "vitest run --config vitest.e2e.config.ts tests/e2e/companion/companion-ui-full-workflow.e2e.test.ts",
"typecheck": "tsc --noEmit",
"test:types": "tsc --noEmit -p tsconfig.tests.json",
"companion:dev": "npm run dev --workspace=@gridpilot/companion",
"companion:build": "npm run build --workspace=@gridpilot/companion",
"companion:start": "npm run start --workspace=@gridpilot/companion",
"env:website:merge": "node scripts/merge-website-env.js",
"website:dev": "npm run env:website:merge && npm run dev --workspace=@gridpilot/website",
"website:build": "npm run env:website:merge && npm run build --workspace=@gridpilot/website",
"website:start": "npm run start --workspace=@gridpilot/website",
"website:lint": "npm run lint --workspace=@gridpilot/website",
"website:type-check": "npm run type-check --workspace=@gridpilot/website",
"website:clean": "npm run clean --workspace=@gridpilot/website",
"deploy:website:preview": "npx vercel deploy --cwd apps/website",
"deploy:website:prod": "npx vercel deploy --prod",
"deploy:website": "npm run deploy:website:prod",
"chrome:debug": "open -a 'Google Chrome' --args --remote-debugging-port=9222 --user-data-dir=/tmp/chrome-debug",
"docker:e2e:up": "docker-compose -f docker/docker-compose.e2e.yml up -d",
"docker:e2e:down": "docker-compose -f docker/docker-compose.e2e.yml down",
"generate-templates": "npx tsx scripts/generate-templates/index.ts",
"minify-fixtures": "npx tsx scripts/minify-fixtures.ts",
"minify-fixtures:force": "npx tsx scripts/minify-fixtures.ts --force",
"dom:process": "npx tsx scripts/dom-export/processWorkflows.ts",
"prepare": "husky install || true"
},
"devDependencies": {
"@cucumber/cucumber": "^11.0.1",
"@playwright/test": "^1.57.0",

View File

@@ -1,9 +1,19 @@
import type { ImageServicePort } from '@gridpilot/media';
const MALE_DEFAULT_AVATAR = '/images/avatars/male-default-avatar.jpg';
const FEMALE_DEFAULT_AVATAR = '/images/avatars/female-default-avatar.jpeg';
export class DemoImageServiceAdapter implements ImageServicePort {
getDriverAvatar(driverId: string): string {
const numericSuffixMatch = driverId.match(/(\d+)$/
);
if (numericSuffixMatch) {
const numericSuffix = Number.parseInt(numericSuffixMatch[1], 10);
return numericSuffix % 2 === 0 ? FEMALE_DEFAULT_AVATAR : MALE_DEFAULT_AVATAR;
}
const seed = stableHash(driverId);
return `https://picsum.photos/seed/driver-${seed}/128/128`;
return seed % 2 === 0 ? FEMALE_DEFAULT_AVATAR : MALE_DEFAULT_AVATAR;
}
getTeamLogo(teamId: string): string {

View File

@@ -102,7 +102,7 @@ export class GetLeagueScoringConfigQuery {
}
private buildPointsPreview(
tables: Record<string, PointsTable>,
tables: Record<string, any>,
): Array<{ sessionType: string; position: number; points: number }> {
const preview: Array<{
sessionType: string;

View File

@@ -93,7 +93,7 @@ export class EventScoringService {
sessionType: SessionType,
): BonusRule[] {
const all = championship.bonusRulesBySessionType ?? {};
return all[sessionType] ?? [];
return (all as Record<SessionType, BonusRule[]>)[sessionType] ?? [];
}
private applyFastestLapBonus(

View File

@@ -15,4 +15,9 @@ export * from './domain/repositories/IStandingRepository';
export * from './domain/repositories/ILeagueMembershipRepository';
export * from './domain/repositories/IRaceRegistrationRepository';
export * from './domain/repositories/ITeamRepository';
export * from './domain/repositories/ITeamMembershipRepository';
export * from './domain/repositories/ITeamMembershipRepository';
export * from './application/mappers/EntityMappers';
export * from './application/dto/DriverDTO';
export * from './application/dto/LeagueDriverSeasonStatsDTO';
export * from './application/dto/LeagueScoringConfigDTO';

View File

@@ -11,5 +11,10 @@
"./application/*": "./application/*",
"./infrastructure/*": "./infrastructure/*"
},
"dependencies": {}
"dependencies": {
"uuid": "^11.0.5"
},
"devDependencies": {
"@types/uuid": "^10.0.0"
}
}

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 13 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -1 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 1025 1025" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><rect id="Square-Icon-Dark" serif:id="Square Icon Dark" x="0.521" y="0.228" width="1024" height="1024"/><g transform="matrix(2.83814,0,0,2.83814,-723.612,-329.472)"><path d="M493,227.168c15.577,-0 28.925,5.503 40.031,16.537c11.132,11.06 16.697,24.433 16.697,40.119c-0,15.686 -5.565,29.059 -16.697,40.118c-11.106,11.035 -24.454,16.538 -40.031,16.538l-6.92,-0l-0,25.488l-95.32,-0c-12.58,-0 -24.193,-3.137 -34.852,-9.36c-10.602,-6.19 -18.998,-14.586 -25.188,-25.188c-6.224,-10.66 -9.36,-22.273 -9.36,-34.852c-0,-9.433 1.862,-18.466 5.582,-27.101c3.683,-8.551 8.625,-15.857 14.743,-21.974c6.117,-6.117 13.423,-11.06 21.974,-14.743c8.635,-3.72 17.668,-5.582 27.101,-5.582l102.24,-0Zm-87.12,56.008l-0,36l-15.12,-0c-6.24,-0 -11.568,-2.208 -15.984,-6.624c-4.416,-4.416 -6.624,-9.744 -6.624,-15.984c-0,-6.24 2.208,-11.568 6.624,-15.984c4.416,-4.416 9.744,-6.624 15.984,-6.624l41.472,-0l-0,-27.792l-41.472,-0c-6.816,-0 -13.344,1.344 -19.584,4.032c-6.24,2.688 -11.592,6.264 -16.056,10.728c-4.464,4.464 -8.04,9.816 -10.728,16.056c-2.688,6.24 -4.032,12.768 -4.032,19.584c-0,9.12 2.256,17.544 6.768,25.272c4.512,7.728 10.632,13.848 18.36,18.36c7.728,4.512 16.152,6.768 25.272,6.768l42.912,-0l-0,-63.792l-27.792,-0Zm87.12,-37.008l-53.424,-0l-0,27.792l53.424,-0c2.784,-0 5.136,0.96 7.056,2.88c1.92,1.92 2.88,4.248 2.88,6.984c-0,2.736 -0.96,5.088 -2.88,7.056c-1.92,1.968 -4.272,2.952 -7.056,2.952l-39.888,-0c-3.84,-0 -7.104,1.344 -9.792,4.032c-2.688,2.688 -4.032,5.952 -4.032,9.792l-0,39.312l27.792,-0l-0,-25.488l25.92,-0c10.368,-0 19.248,-3.672 26.64,-11.016c7.392,-7.344 11.088,-16.224 11.088,-26.64c-0,-10.416 -3.696,-19.296 -11.088,-26.64c-7.392,-7.344 -16.272,-11.016 -26.64,-11.016Z" style="fill:#fff;"/><g transform="matrix(144,0,0,144,530.584,346.968)"></g><text x="340.072px" y="346.968px" style="font-family:'ElectricFormulaRegular', 'Electric Formula';font-size:144px;">GP</text></g></svg>
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 1025 1025" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><rect id="Square-Icon-Dark" serif:id="Square Icon Dark" x="0.521" y="0.228" width="1024" height="1024"/><g transform="matrix(2.83814,0,0,2.83814,-723.612,-329.472)"><path d="M493,227.168c15.577,-0 28.925,5.503 40.031,16.537c11.132,11.06 16.697,24.433 16.697,40.119c-0,15.686 -5.565,29.059 -16.697,40.118c-11.106,11.035 -24.454,16.538 -40.031,16.538l-6.92,-0l-0,25.488l-95.32,-0c-12.58,-0 -24.193,-3.137 -34.852,-9.36c-10.602,-6.19 -18.998,-14.586 -25.188,-25.188c-6.224,-10.66 -9.36,-22.273 -9.36,-34.852c-0,-9.433 1.862,-18.466 5.582,-27.101c3.683,-8.551 8.625,-15.857 14.743,-21.974c6.117,-6.117 13.423,-11.06 21.974,-14.743c8.635,-3.72 17.668,-5.582 27.101,-5.582l102.24,-0Zm-0,19l-53.424,-0l-0,27.792l53.424,-0c2.784,-0 5.136,0.96 7.056,2.88c1.92,1.92 2.88,4.248 2.88,6.984c-0,2.736 -0.96,5.088 -2.88,7.056c-1.92,1.968 -4.272,2.952 -7.056,2.952l-39.888,-0c-3.84,-0 -7.104,1.344 -9.792,4.032c-2.688,2.688 -4.032,5.952 -4.032,9.792l-0,39.312l27.792,-0l-0,-25.488l25.92,-0c10.368,-0 19.248,-3.672 26.64,-11.016c7.392,-7.344 11.088,-16.224 11.088,-26.64c-0,-10.416 -3.696,-19.296 -11.088,-26.64c-7.392,-7.344 -16.272,-11.016 -26.64,-11.016Zm-87.12,37.008l-0,36l-15.12,-0c-6.24,-0 -11.568,-2.208 -15.984,-6.624c-4.416,-4.416 -6.624,-9.744 -6.624,-15.984c-0,-6.24 2.208,-11.568 6.624,-15.984c4.416,-4.416 9.744,-6.624 15.984,-6.624l41.472,-0l-0,-27.792l-41.472,-0c-6.816,-0 -13.344,1.344 -19.584,4.032c-6.24,2.688 -11.592,6.264 -16.056,10.728c-4.464,4.464 -8.04,9.816 -10.728,16.056c-2.688,6.24 -4.032,12.768 -4.032,19.584c-0,9.12 2.256,17.544 6.768,25.272c4.512,7.728 10.632,13.848 18.36,18.36c7.728,4.512 16.152,6.768 25.272,6.768l42.912,-0l-0,-63.792l-27.792,-0Z" style="fill:#fff;"/><g transform="matrix(144,0,0,144,340.072,346.968)"><path d="M0.457,-0.443l0.193,0l0,0.443l-0.298,-0c-0.063,-0 -0.122,-0.016 -0.176,-0.047c-0.053,-0.031 -0.096,-0.074 -0.127,-0.128c-0.031,-0.053 -0.047,-0.112 -0.047,-0.175c-0,-0.047 0.009,-0.093 0.028,-0.136c0.019,-0.043 0.043,-0.081 0.074,-0.112c0.031,-0.03 0.069,-0.055 0.112,-0.074c0.043,-0.019 0.089,-0.028 0.136,-0.028l0.288,-0l-0,0.193l-0.288,-0c-0.043,-0 -0.08,0.015 -0.111,0.046c-0.031,0.031 -0.046,0.068 -0.046,0.111c-0,0.043 0.015,0.08 0.046,0.111c0.031,0.031 0.068,0.046 0.111,0.046l0.105,-0l-0,-0.25Z" style="fill-rule:nonzero;"/></g><g transform="matrix(144,0,0,144,436.696,346.968)"><path d="M0.391,-0.7c0.072,0 0.134,0.025 0.185,0.077c0.051,0.05 0.077,0.112 0.077,0.185c0,0.072 -0.026,0.134 -0.077,0.185c-0.051,0.051 -0.113,0.076 -0.185,0.076l-0.18,0l-0,0.177l-0.193,0l-0,-0.273c-0,-0.027 0.009,-0.049 0.028,-0.068c0.019,-0.019 0.041,-0.028 0.068,-0.028l0.277,0c0.019,0 0.036,-0.007 0.049,-0.02c0.013,-0.014 0.02,-0.03 0.02,-0.049c-0,-0.019 -0.007,-0.036 -0.02,-0.049c-0.013,-0.013 -0.03,-0.02 -0.049,-0.02l-0.371,0l-0,-0.193l0.371,0Z" style="fill-rule:nonzero;"/></g></g></svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -1 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 1025 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><rect id="Square-Icon" serif:id="Square Icon" x="0.521" y="0" width="1024" height="1024" style="fill:none;"/><g transform="matrix(2.83814,0,0,2.83814,-723.612,-329.7)"><path d="M493,227.168c15.577,-0 28.925,5.503 40.031,16.537c11.132,11.06 16.697,24.433 16.697,40.119c-0,15.686 -5.565,29.059 -16.697,40.118c-11.106,11.035 -24.454,16.538 -40.031,16.538l-6.92,-0l-0,25.488l-95.32,-0c-12.58,-0 -24.193,-3.137 -34.852,-9.36c-10.602,-6.19 -18.998,-14.586 -25.188,-25.188c-6.224,-10.66 -9.36,-22.273 -9.36,-34.852c-0,-9.433 1.862,-18.466 5.582,-27.101c3.683,-8.551 8.625,-15.857 14.743,-21.974c6.117,-6.117 13.423,-11.06 21.974,-14.743c8.635,-3.72 17.668,-5.582 27.101,-5.582l102.24,-0Zm-87.12,56.008l-0,36l-15.12,-0c-6.24,-0 -11.568,-2.208 -15.984,-6.624c-4.416,-4.416 -6.624,-9.744 -6.624,-15.984c-0,-6.24 2.208,-11.568 6.624,-15.984c4.416,-4.416 9.744,-6.624 15.984,-6.624l41.472,-0l-0,-27.792l-41.472,-0c-6.816,-0 -13.344,1.344 -19.584,4.032c-6.24,2.688 -11.592,6.264 -16.056,10.728c-4.464,4.464 -8.04,9.816 -10.728,16.056c-2.688,6.24 -4.032,12.768 -4.032,19.584c-0,9.12 2.256,17.544 6.768,25.272c4.512,7.728 10.632,13.848 18.36,18.36c7.728,4.512 16.152,6.768 25.272,6.768l42.912,-0l-0,-63.792l-27.792,-0Zm87.12,-37.008l-53.424,-0l-0,27.792l53.424,-0c2.784,-0 5.136,0.96 7.056,2.88c1.92,1.92 2.88,4.248 2.88,6.984c-0,2.736 -0.96,5.088 -2.88,7.056c-1.92,1.968 -4.272,2.952 -7.056,2.952l-39.888,-0c-3.84,-0 -7.104,1.344 -9.792,4.032c-2.688,2.688 -4.032,5.952 -4.032,9.792l-0,39.312l27.792,-0l-0,-25.488l25.92,-0c10.368,-0 19.248,-3.672 26.64,-11.016c7.392,-7.344 11.088,-16.224 11.088,-26.64c-0,-10.416 -3.696,-19.296 -11.088,-26.64c-7.392,-7.344 -16.272,-11.016 -26.64,-11.016Z"/><g transform="matrix(144,0,0,144,530.584,346.968)"></g><text x="340.072px" y="346.968px" style="font-family:'ElectricFormulaRegular', 'Electric Formula';font-size:144px;fill:#fff;">GP</text></g></svg>
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 1025 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><rect id="Square-Icon" serif:id="Square Icon" x="0.521" y="0" width="1024" height="1024" style="fill:none;"/><g transform="matrix(2.83814,0,0,2.83814,-723.612,-329.7)"><path d="M493,227.168c15.577,-0 28.925,5.503 40.031,16.537c11.132,11.06 16.697,24.433 16.697,40.119c-0,15.686 -5.565,29.059 -16.697,40.118c-11.106,11.035 -24.454,16.538 -40.031,16.538l-6.92,-0l-0,25.488l-95.32,-0c-12.58,-0 -24.193,-3.137 -34.852,-9.36c-10.602,-6.19 -18.998,-14.586 -25.188,-25.188c-6.224,-10.66 -9.36,-22.273 -9.36,-34.852c-0,-9.433 1.862,-18.466 5.582,-27.101c3.683,-8.551 8.625,-15.857 14.743,-21.974c6.117,-6.117 13.423,-11.06 21.974,-14.743c8.635,-3.72 17.668,-5.582 27.101,-5.582l102.24,-0Zm-0,19l-53.424,-0l-0,27.792l53.424,-0c2.784,-0 5.136,0.96 7.056,2.88c1.92,1.92 2.88,4.248 2.88,6.984c-0,2.736 -0.96,5.088 -2.88,7.056c-1.92,1.968 -4.272,2.952 -7.056,2.952l-39.888,-0c-3.84,-0 -7.104,1.344 -9.792,4.032c-2.688,2.688 -4.032,5.952 -4.032,9.792l-0,39.312l27.792,-0l-0,-25.488l25.92,-0c10.368,-0 19.248,-3.672 26.64,-11.016c7.392,-7.344 11.088,-16.224 11.088,-26.64c-0,-10.416 -3.696,-19.296 -11.088,-26.64c-7.392,-7.344 -16.272,-11.016 -26.64,-11.016Zm-87.12,37.008l-0,36l-15.12,-0c-6.24,-0 -11.568,-2.208 -15.984,-6.624c-4.416,-4.416 -6.624,-9.744 -6.624,-15.984c-0,-6.24 2.208,-11.568 6.624,-15.984c4.416,-4.416 9.744,-6.624 15.984,-6.624l41.472,-0l-0,-27.792l-41.472,-0c-6.816,-0 -13.344,1.344 -19.584,4.032c-6.24,2.688 -11.592,6.264 -16.056,10.728c-4.464,4.464 -8.04,9.816 -10.728,16.056c-2.688,6.24 -4.032,12.768 -4.032,19.584c-0,9.12 2.256,17.544 6.768,25.272c4.512,7.728 10.632,13.848 18.36,18.36c7.728,4.512 16.152,6.768 25.272,6.768l42.912,-0l-0,-63.792l-27.792,-0Z"/><g transform="matrix(144,0,0,144,340.072,346.968)"><path d="M0.457,-0.443l0.193,0l0,0.443l-0.298,-0c-0.063,-0 -0.122,-0.016 -0.176,-0.047c-0.053,-0.031 -0.096,-0.074 -0.127,-0.128c-0.031,-0.053 -0.047,-0.112 -0.047,-0.175c-0,-0.047 0.009,-0.093 0.028,-0.136c0.019,-0.043 0.043,-0.081 0.074,-0.112c0.031,-0.03 0.069,-0.055 0.112,-0.074c0.043,-0.019 0.089,-0.028 0.136,-0.028l0.288,-0l-0,0.193l-0.288,-0c-0.043,-0 -0.08,0.015 -0.111,0.046c-0.031,0.031 -0.046,0.068 -0.046,0.111c-0,0.043 0.015,0.08 0.046,0.111c0.031,0.031 0.068,0.046 0.111,0.046l0.105,-0l-0,-0.25Z" style="fill:#fff;fill-rule:nonzero;"/></g><g transform="matrix(144,0,0,144,436.696,346.968)"><path d="M0.391,-0.7c0.072,0 0.134,0.025 0.185,0.077c0.051,0.05 0.077,0.112 0.077,0.185c0,0.072 -0.026,0.134 -0.077,0.185c-0.051,0.051 -0.113,0.076 -0.185,0.076l-0.18,0l-0,0.177l-0.193,0l-0,-0.273c-0,-0.027 0.009,-0.049 0.028,-0.068c0.019,-0.019 0.041,-0.028 0.068,-0.028l0.277,0c0.019,0 0.036,-0.007 0.049,-0.02c0.013,-0.014 0.02,-0.03 0.02,-0.049c-0,-0.019 -0.007,-0.036 -0.02,-0.049c-0.013,-0.013 -0.03,-0.02 -0.049,-0.02l-0.371,0l-0,-0.193l0.371,0Z" style="fill:#fff;fill-rule:nonzero;"/></g></g></svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 9.8 KiB

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 9.8 KiB

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 13 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 13 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +1,72 @@
const fs = require("fs");
const path = require("path");
const rootEnvPath = path.resolve(__dirname, "..", ".env.local");
const websiteEnvPath = path.resolve(__dirname, "..", "apps/website/.env.local");
function parseEnvFile(filePath) {
let content;
try {
content = fs.readFileSync(filePath, "utf8");
} catch (err) {
if (err && err.code === "ENOENT") {
return {};
}
console.error(`Error reading env file at ${filePath}:`, err.message || err);
process.exitCode = 1;
return {};
}
const result = {};
const lines = content.split(/\r?\n/);
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#")) {
continue;
}
const eqIndex = trimmed.indexOf("=");
if (eqIndex === -1) {
continue;
}
const key = trimmed.slice(0, eqIndex).trim();
const value = trimmed.slice(eqIndex + 1);
if (!key) {
continue;
}
result[key] = value;
}
return result;
}
function mergeEnv() {
const rootExists = fs.existsSync(rootEnvPath);
const websiteExists = fs.existsSync(websiteEnvPath);
if (!rootExists && !websiteExists) {
return;
}
const rootEnv = rootExists ? parseEnvFile(rootEnvPath) : {};
const websiteEnv = websiteExists ? parseEnvFile(websiteEnvPath) : {};
const merged = { ...rootEnv, ...websiteEnv };
const lines = Object.entries(merged).map(([key, value]) => `${key}=${value}`);
const output = lines.join("\n") + "\n";
try {
fs.writeFileSync(websiteEnvPath, output, "utf8");
} catch (err) {
console.error(`Error writing merged env file to ${websiteEnvPath}:`, err.message || err);
process.exit(1);
}
}
mergeEnv();

View File

@@ -0,0 +1,40 @@
import { describe, it, expect } from 'vitest';
import { DemoImageServiceAdapter } from '../../../packages/demo-infrastructure/media/DemoImageServiceAdapter';
describe('DemoImageServiceAdapter - driver avatars', () => {
it('returns male default avatar for a demo driver treated as male (odd id suffix)', () => {
// Given a demo driver id that maps to a male profile
const adapter = new DemoImageServiceAdapter();
// When resolving the driver avatar
const src = adapter.getDriverAvatar('driver-1');
// Then it should use the male default avatar asset
expect(src).toBe('/images/avatars/male-default-avatar.jpg');
});
it('returns female default avatar for a demo driver treated as female (even id suffix)', () => {
// Given a demo driver id that maps to a female profile
const adapter = new DemoImageServiceAdapter();
// When resolving the driver avatar
const src = adapter.getDriverAvatar('driver-2');
// Then it should use the female default avatar asset
expect(src).toBe('/images/avatars/female-default-avatar.jpeg');
});
it('falls back to a sensible default avatar when driver id has no numeric suffix', () => {
// Given a demo driver id without a numeric suffix
const adapter = new DemoImageServiceAdapter();
// When resolving the driver avatar
const src = adapter.getDriverAvatar('demo-driver');
// Then it should still resolve to one of the default avatar assets
expect(['/images/avatars/male-default-avatar.jpg', '/images/avatars/female-default-avatar.jpeg']).toContain(
src,
);
});
});

View File

@@ -0,0 +1,63 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { getAppMode, AppMode } from '../../../apps/website/lib/mode';
const ORIGINAL_NODE_ENV = process.env.NODE_ENV;
describe('getAppMode', () => {
const originalEnv = process.env;
beforeEach(() => {
process.env = { ...originalEnv };
process.env.NODE_ENV = 'production';
});
afterEach(() => {
process.env = originalEnv;
process.env.NODE_ENV = ORIGINAL_NODE_ENV;
});
it('returns "pre-launch" when NEXT_PUBLIC_GRIDPILOT_MODE is undefined', () => {
delete process.env.NEXT_PUBLIC_GRIDPILOT_MODE;
const mode = getAppMode();
expect(mode).toBe<AppMode>('pre-launch');
});
it('returns "pre-launch" when NEXT_PUBLIC_GRIDPILOT_MODE is explicitly set to "pre-launch"', () => {
process.env.NEXT_PUBLIC_GRIDPILOT_MODE = 'pre-launch';
const mode = getAppMode();
expect(mode).toBe<AppMode>('pre-launch');
});
it('returns "alpha" when NEXT_PUBLIC_GRIDPILOT_MODE is set to "alpha"', () => {
process.env.NEXT_PUBLIC_GRIDPILOT_MODE = 'alpha';
const mode = getAppMode();
expect(mode).toBe<AppMode>('alpha');
});
it('falls back to "pre-launch" and logs when NEXT_PUBLIC_GRIDPILOT_MODE is invalid in production', () => {
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {});
process.env.NEXT_PUBLIC_GRIDPILOT_MODE = 'invalid-mode';
const mode = getAppMode();
expect(mode).toBe<AppMode>('pre-launch');
expect(consoleError).toHaveBeenCalled();
consoleError.mockRestore();
});
it('throws in development when NEXT_PUBLIC_GRIDPILOT_MODE is invalid', () => {
process.env.NODE_ENV = 'development';
process.env.NEXT_PUBLIC_GRIDPILOT_MODE = 'invalid-mode';
expect(() => getAppMode()).toThrowError(/Invalid NEXT_PUBLIC_GRIDPILOT_MODE/);
});
});

View File

@@ -0,0 +1,135 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
type RateLimitResult = {
allowed: boolean;
remaining: number;
resetAt: number;
};
const mockCheckRateLimit = vi.fn<[], Promise<RateLimitResult>>();
const mockGetClientIp = vi.fn<[], string>();
vi.mock('../../../apps/website/lib/rate-limit', () => ({
checkRateLimit: (...args: any[]) => mockCheckRateLimit(...(args as [])),
getClientIp: (..._args: any[]) => mockGetClientIp(),
}));
async function getPostHandler() {
const routeModule: any = await import('../../../apps/website/app/api/signup/route');
return routeModule.POST as (request: Request) => Promise<Response>;
}
function createJsonRequest(body: unknown): Request {
return new Request('http://localhost/api/signup', {
method: 'POST',
headers: {
'content-type': 'application/json',
},
body: JSON.stringify(body),
});
}
describe('/api/signup POST', () => {
beforeEach(() => {
vi.resetModules();
mockCheckRateLimit.mockReset();
mockGetClientIp.mockReset();
mockGetClientIp.mockReturnValue('127.0.0.1');
mockCheckRateLimit.mockResolvedValue({
allowed: true,
remaining: 4,
resetAt: Date.now() + 60 * 60 * 1000,
});
});
it('accepts a valid email within rate limits and returns success payload', async () => {
const POST = await getPostHandler();
const response = await POST(
createJsonRequest({
email: 'user@example.com',
}),
);
expect(response.status).toBeGreaterThanOrEqual(200);
expect(response.status).toBeLessThan(300);
const data = (await response.json()) as any;
expect(data).toHaveProperty('message');
expect(typeof data.message).toBe('string');
expect(data).toHaveProperty('ok', true);
});
it('rejects an invalid email with 400 and error message', async () => {
const POST = await getPostHandler();
const response = await POST(
createJsonRequest({
email: 'not-an-email',
}),
);
expect(response.status).toBe(400);
const data = (await response.json()) as any;
expect(typeof data.error).toBe('string');
expect(data.error.toLowerCase()).toContain('email');
});
it('rejects disposable email domains with 400 and error message', async () => {
const POST = await getPostHandler();
const response = await POST(
createJsonRequest({
email: 'foo@mailinator.com',
}),
);
expect(response.status).toBe(400);
const data = (await response.json()) as any;
expect(typeof data.error).toBe('string');
});
it('returns 409 and friendly message when email is already subscribed', async () => {
const POST = await getPostHandler();
const email = 'duplicate@example.com';
const first = await POST(createJsonRequest({ email }));
expect(first.status).toBeGreaterThanOrEqual(200);
expect(first.status).toBeLessThan(300);
const second = await POST(createJsonRequest({ email }));
expect(second.status).toBe(409);
const data = (await second.json()) as any;
expect(typeof data.error).toBe('string');
expect(data.error.toLowerCase()).toContain('already');
});
it('returns 429 with retryAfter when rate limit is exceeded', async () => {
mockCheckRateLimit.mockResolvedValueOnce({
allowed: false,
remaining: 0,
resetAt: Date.now() + 30_000,
});
const POST = await getPostHandler();
const response = await POST(
createJsonRequest({
email: 'limited@example.com',
}),
);
expect(response.status).toBe(429);
const data = (await response.json()) as any;
expect(typeof data.error).toBe('string');
expect(data).toHaveProperty('retryAfter');
});
});

View File

@@ -13,6 +13,8 @@ export default defineConfig({
'@gridpilot/automation': path.resolve(__dirname, 'packages/automation'),
'@gridpilot/automation/*': path.resolve(__dirname, 'packages/automation/*'),
'@gridpilot/testing-support': path.resolve(__dirname, 'packages/testing-support'),
'@gridpilot/demo-infrastructure': path.resolve(__dirname, 'packages/demo-infrastructure'),
'@gridpilot/media': path.resolve(__dirname, 'packages/media'),
'@': path.resolve(__dirname, 'apps/website'),
'@/*': path.resolve(__dirname, 'apps/website/*'),
},