wip
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 |