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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,12 @@
'use client';
import Image from 'next/image';
import { useRef } from 'react';
import { useScrollProgress } from '@/hooks/useScrollProgress';
const discordUrl = process.env.NEXT_PUBLIC_DISCORD_URL || 'https://discord.gg/gridpilot';
const xUrl = process.env.NEXT_PUBLIC_X_URL || '#';
export default function Footer() {
return (
<footer className="relative bg-deep-graphite">
@@ -30,6 +34,15 @@ export default function Footer() {
transform: 'translateY(0)'
}}
>
<div className="mb-2 flex justify-center">
<Image
src="/images/logos/icon-square-dark.svg"
alt="GridPilot"
width={40}
height={40}
className="h-8 w-auto md:h-10"
/>
</div>
<p className="text-[9px] md:text-xs lg:text-sm text-gray-300 mb-1 md:mb-2">
🏁 Built by a sim racer, for sim racers
</p>
@@ -47,11 +60,17 @@ export default function Footer() {
}}
>
<a
href="https://discord.gg/gridpilot"
href={discordUrl}
className="text-[9px] md:text-xs text-primary-blue hover:text-neon-aqua transition-colors font-medium inline-flex items-center justify-center min-h-[44px] min-w-[44px] px-3 py-2 active:scale-95 transition-transform"
>
💬 Join Discord
</a>
<a
href={xUrl}
className="text-[9px] md:text-xs text-gray-300 hover:text-neon-aqua transition-colors font-medium inline-flex items-center justify-center min-h-[44px] min-w-[44px] px-3 py-2 active:scale-95 transition-transform"
>
𝕏 Follow on X
</a>
</div>
{/* Development status */}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

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

Before

Width:  |  Height:  |  Size: 444 B

View File

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

Before

Width:  |  Height:  |  Size: 444 B

View File

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

Before

Width:  |  Height:  |  Size: 444 B

View File

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

Before

Width:  |  Height:  |  Size: 444 B

View File

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

Before

Width:  |  Height:  |  Size: 444 B

View File

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

Before

Width:  |  Height:  |  Size: 444 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View File

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

After

Width:  |  Height:  |  Size: 3.2 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 560 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 560 KiB