wip
4
.gitignore
vendored
@@ -41,4 +41,6 @@ logs/
|
||||
|
||||
# Temporary files
|
||||
tmp/
|
||||
temp/
|
||||
temp/
|
||||
.vercel
|
||||
.env*.local
|
||||
|
||||
111
apps/website/app/api/signup/route.ts
Normal 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,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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[]>([]);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -226,7 +226,7 @@ export default function LeagueAdmin({ league, onLeagueUpdate }: LeagueAdminProps
|
||||
if (!activeRejectRequest) {
|
||||
setRejectReason('');
|
||||
}
|
||||
}, [activeRejectRequest?.id]);
|
||||
}, [activeRejectRequest, setRejectReason]);
|
||||
|
||||
const isRejectModalOpen = modal === 'reject-request' && !!activeRejectRequest;
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -433,7 +433,7 @@ function YearCalendarPreview({
|
||||
}
|
||||
|
||||
return view;
|
||||
}, [raceDates, seasonStart, seasonEnd]);
|
||||
}, [raceDates, seasonStart, seasonEnd, months, isSeasonStartDate, isSeasonEndDate]);
|
||||
|
||||
// Calculate season stats
|
||||
const firstRace = raceDates[0];
|
||||
|
||||
3
apps/website/env.d.ts
vendored
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
|
||||
14
apps/website/public/favicon.svg
Normal 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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
BIN
apps/website/public/images/avatars/female-default-avatar.jpeg
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
apps/website/public/images/avatars/male-default-avatar.jpg
Normal file
|
After Width: | Height: | Size: 250 KiB |
BIN
apps/website/public/images/avatars/neutral-default-avatar.jpeg
Normal file
|
After Width: | Height: | Size: 66 KiB |
1
apps/website/public/images/logos/icon-square-dark.svg
Normal 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 |
47
apps/website/public/images/logos/wordmark-rectangle-dark.svg
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
apps/website/public/images/tracks/daytona.jpg
Normal file
|
After Width: | Height: | Size: 560 KiB |
BIN
apps/website/public/images/tracks/spa.jpg
Normal file
|
After Width: | Height: | Size: 560 KiB |
134
package-lock.json
generated
@@ -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",
|
||||
|
||||
73
package.json
@@ -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",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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';
|
||||
@@ -11,5 +11,10 @@
|
||||
"./application/*": "./application/*",
|
||||
"./infrastructure/*": "./infrastructure/*"
|
||||
},
|
||||
"dependencies": {}
|
||||
"dependencies": {
|
||||
"uuid": "^11.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/uuid": "^10.0.0"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 8.0 KiB After Width: | Height: | Size: 13 KiB |
@@ -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 |
@@ -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 |
|
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 8.0 KiB After Width: | Height: | Size: 13 KiB |
47
resources/logo/svg/Wordmark Dark.svg
Normal file
|
After Width: | Height: | Size: 15 KiB |
49
resources/logo/svg/Wordmark Light.svg
Normal file
|
After Width: | Height: | Size: 16 KiB |
72
scripts/merge-website-env.js
Normal 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();
|
||||
40
tests/unit/infrastructure/DemoImageServiceAdapter.test.ts
Normal 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,
|
||||
);
|
||||
});
|
||||
});
|
||||
63
tests/unit/website/getAppMode.test.ts
Normal 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/);
|
||||
});
|
||||
});
|
||||
135
tests/unit/website/signupRoute.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
@@ -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/*'),
|
||||
},
|
||||
|
||||