wip milano-cortina 2026

This commit is contained in:
Fabrice Lamant
2025-12-05 21:30:27 +01:00
parent 14c99c6252
commit 5074fd5155
20 changed files with 7628 additions and 0 deletions

43
ui/.gitignore vendored Normal file
View File

@ -0,0 +1,43 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
/public/data/

BIN
ui/app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

267
ui/app/flag.tsx Normal file
View File

@ -0,0 +1,267 @@
export default function Flag({ iso3, name }: { iso3: string; name: string }) {
const iso3to2 = {
AFG: "AF",
ALA: "AX",
ALB: "AL",
DZA: "DZ",
ASM: "AS",
AND: "AD",
AGO: "AO",
AIA: "AI",
ATA: "AQ",
ATG: "AG",
ARG: "AR",
ARM: "AM",
ABW: "AW",
AUS: "AU",
AUT: "AT",
AZE: "AZ",
BHS: "BS",
BHR: "BH",
BGD: "BD",
BRB: "BB",
BLR: "BY",
BEL: "BE",
BLZ: "BZ",
BEN: "BJ",
BMU: "BM",
BTN: "BT",
BOL: "BO",
BES: "BQ",
BIH: "BA",
BWA: "BW",
BVT: "BV",
BRA: "BR",
VGB: "VG",
IOT: "IO",
BRN: "BN",
BGR: "BG",
BFA: "BF",
BDI: "BI",
KHM: "KH",
CMR: "CM",
CAN: "CA",
CPV: "CV",
CYM: "KY",
CAF: "CF",
TCD: "TD",
CHL: "CL",
CHN: "CN",
HKG: "HK",
MAC: "MO",
CXR: "CX",
CCK: "CC",
COL: "CO",
COM: "KM",
COG: "CG",
COD: "CD",
COK: "CK",
CRI: "CR",
CIV: "CI",
HRV: "HR",
CUB: "CU",
CUW: "CW",
CYP: "CY",
CZE: "CZ",
DNK: "DK",
DJI: "DJ",
DMA: "DM",
DOM: "DO",
ECU: "EC",
EGY: "EG",
SLV: "SV",
GNQ: "GQ",
ERI: "ER",
EST: "EE",
ETH: "ET",
FLK: "FK",
FRO: "FO",
FJI: "FJ",
FIN: "FI",
FRA: "FR",
GUF: "GF",
PYF: "PF",
ATF: "TF",
GAB: "GA",
GMB: "GM",
GEO: "GE",
DEU: "DE",
GHA: "GH",
GIB: "GI",
GRC: "GR",
GRL: "GL",
GRD: "GD",
GLP: "GP",
GUM: "GU",
GTM: "GT",
GGY: "GG",
GIN: "GN",
GNB: "GW",
GUY: "GY",
HTI: "HT",
HMD: "HM",
VAT: "VA",
HND: "HN",
HUN: "HU",
ISL: "IS",
IND: "IN",
IDN: "ID",
IRN: "IR",
IRQ: "IQ",
IRL: "IE",
IMN: "IM",
ISR: "IL",
ITA: "IT",
JAM: "JM",
JPN: "JP",
JEY: "JE",
JOR: "JO",
KAZ: "KZ",
KEN: "KE",
KIR: "KI",
PRK: "KP",
KOR: "KR",
KWT: "KW",
KGZ: "KG",
LAO: "LA",
LVA: "LV",
LBN: "LB",
LSO: "LS",
LBR: "LR",
LBY: "LY",
LIE: "LI",
LTU: "LT",
LUX: "LU",
MKD: "MK",
MDG: "MG",
MWI: "MW",
MYS: "MY",
MDV: "MV",
MLI: "ML",
MLT: "MT",
MHL: "MH",
MTQ: "MQ",
MRT: "MR",
MUS: "MU",
MYT: "YT",
MEX: "MX",
FSM: "FM",
MDA: "MD",
MCO: "MC",
MNG: "MN",
MNE: "ME",
MSR: "MS",
MAR: "MA",
MOZ: "MZ",
MMR: "MM",
NAM: "NA",
NRU: "NR",
NPL: "NP",
NLD: "NL",
ANT: "AN",
NCL: "NC",
NZL: "NZ",
NIC: "NI",
NER: "NE",
NGA: "NG",
NIU: "NU",
NFK: "NF",
MNP: "MP",
NOR: "NO",
OMN: "OM",
PAK: "PK",
PLW: "PW",
PSE: "PS",
PAN: "PA",
PNG: "PG",
PRY: "PY",
PER: "PE",
PHL: "PH",
PCN: "PN",
POL: "PL",
PRT: "PT",
PRI: "PR",
QAT: "QA",
REU: "RE",
ROU: "RO",
RUS: "RU",
RWA: "RW",
BLM: "BL",
SHN: "SH",
KNA: "KN",
LCA: "LC",
MAF: "MF",
SPM: "PM",
VCT: "VC",
WSM: "WS",
SMR: "SM",
STP: "ST",
SAU: "SA",
SEN: "SN",
SRB: "RS",
SYC: "SC",
SLE: "SL",
SGP: "SG",
SXM: "SX",
SVK: "SK",
SVN: "SI",
SLB: "SB",
SOM: "SO",
ZAF: "ZA",
SGS: "GS",
SSD: "SS",
ESP: "ES",
LKA: "LK",
SDN: "SD",
SUR: "SR",
SJM: "SJ",
SWZ: "SZ",
SWE: "SE",
CHE: "CH",
SYR: "SY",
TWN: "TW",
TJK: "TJ",
TZA: "TZ",
THA: "TH",
TLS: "TL",
TGO: "TG",
TKL: "TK",
TON: "TO",
TTO: "TT",
TUN: "TN",
TUR: "TR",
TKM: "TM",
TCA: "TC",
TUV: "TV",
UGA: "UG",
UKR: "UA",
ARE: "AE",
GBR: "GB",
USA: "US",
UMI: "UM",
URY: "UY",
UZB: "UZ",
VUT: "VU",
VEN: "VE",
VNM: "VN",
VIR: "VI",
WLF: "WF",
ESH: "EH",
YEM: "YE",
ZMB: "ZM",
ZWE: "ZW",
XKX: "XK",
SUI: "CH",
GER: "DE",
};
const iso2 = (iso3to2[iso3.toUpperCase()] || "").toLowerCase();
return <img
src={`https://gstatic.olympics.com/s3/noc/oly/3x2/${iso3.toUpperCase()}.png`}
height="24"
alt={`${iso3} - ${iso2}`}
className="inline-block mx-2 h-5 border-1 border-gray-300" />
}

69
ui/app/globals.css Normal file
View File

@ -0,0 +1,69 @@
@import "tailwindcss";
@plugin "daisyui" {
themes: winter;
}
html {
background-color: #eeeeee;
}
.fg-azzurro {
color: #b0cde9;
}
.fg-rosso {
color: #fe5a59;
}
.fg-viola {
color: #c9badc;
}
.fg-giallo {
color: #f2e95d;
}
.fg-rosa {
color: #e9bdd2;
}
.fg-verde {
color: #769d91;
}
.fg-main {
color: #01647c;
}
.bg-main {
background-color: #01647c;
color : #ffffff;
}
.fg-gold {
color: #FFD700;
}
.bg-gold {
background-color: #FFD700;
color : #000000;
}
.fg-silver {
color: #C0C0C0;
}
.bg-silver {
background-color: #C0C0C0;
color : #000000;
}
.fg-bronze {
color: #CD7F32;
}
.bg-bronze {
background-color: #CD7F32;
color : #000000;
}

38
ui/app/layout.tsx Normal file
View File

@ -0,0 +1,38 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import Head from "next/head";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Milano Cortina 2026 Winter Olympics Calendar",
description: "Made with ❤️ by Fabrice Lamant",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html data-theme="winter">
<Head>
<link rel="icon" href="/favicon.ico" sizes="any" />
</Head>
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
</body>
</html>
);
}

359
ui/app/page.tsx Normal file
View File

@ -0,0 +1,359 @@
"use client";
import { loadSchedule } from "../lib/data";
import { useEffect, useState } from "react";
import Flag from "./flag";
import { useSearchParams } from "next/navigation";
import { COPY, COPY_SUCCESS, FILTER_BY_COUNTRY, FILTER_BY_SPORT } from "../lib/text";
import useLocalStorage from "@/lib/local-storage";
interface MultilingualString {
[key: string]: string;
}
interface Language {
code: string;
name: string;
}
interface Sport {
key: string;
name: MultilingualString
}
interface Team {
key: string;
name: MultilingualString;
}
interface Match {
team1: Team;
team2: Team;
}
interface Event {
key: string;
start: string;
end: string;
sport: string;
isTraining: boolean;
medal: '0' | '1' | '3';
name: MultilingualString;
match?: Match;
}
interface Calendar {
languages: Language[];
sports: Sport[];
events: Event[];
nocs: Team[];
}
const COLORS = ['azzurro', 'giallo', 'rosa', 'rosso', 'verde', 'viola'];
export default function Home() {
const qs = useSearchParams();
const [data, setData] = useState<Calendar | null>(null);
const [language, setLanguage] = useLocalStorage('lang', (navigator.language || 'en').split('-')[0]);
const translate = (text: MultilingualString) => {
return text[`${language}`] || text['en'] || Object.values(text)[0] || '';
};
const generateLink = ({ noc, sport, lang }: { noc?: string; sport?: string, lang?: string }) => {
const currentParams = new URLSearchParams(qs.toString());
if (noc !== undefined) {
if (noc === "") {
currentParams.delete('noc');
} else {
currentParams.set('noc', noc);
}
}
if (sport !== undefined) {
if (sport === "") {
currentParams.delete('sport');
} else {
currentParams.set('sport', sport);
}
}
if (lang !== undefined) {
if (lang === "") {
currentParams.delete('lang');
} else {
currentParams.set('lang', lang);
}
}
const paramString = currentParams.toString();
return paramString ? `./?${paramString}` : '.';
}
const generateCalendarLink = () => {
const host = typeof window !== 'undefined' ? window.location.host : '';
const noc = qs.get('noc') || 'calendar';
const sport = qs.get('sport') || 'all-sports';
return `http://${host}/data/${language}/${sport}/${noc}.ics`;
};
const getColor = (i: number) => COLORS[i % COLORS.length];
useEffect(() => {
if (data == null) {
loadSchedule()
.then(setData)
.catch(console.log);
}
}, [data]);
const filter = (event: Event) => {
let visible = true;
if (event.end < new Date().toISOString()) {
return false;
}
const sport = qs.get('sport');
if (sport && event.sport !== sport) {
visible = false;
}
const noc = qs.get('noc');
if (noc) {
if (event.match) {
if (event.match.team1.key !== noc && event.match.team2.key !== noc) {
visible = false;
}
} else {
visible = false;
}
}
return visible;
}
const copyToClipboard = (text: string) => {
if (typeof navigator !== 'undefined' && navigator.clipboard) {
const button = document.getElementById('copy_button')!;
navigator.clipboard.writeText(text).then(() => {
button.textContent = translate(COPY_SUCCESS);
button.classList.add('text-success');
button.classList.add('font-bold');
setTimeout(() => {
// document.getElementById('copy_toast')?.classList.remove('toast-open');
button.textContent = translate(COPY);
button.classList.remove('text-success');
button.classList.remove('font-bold');
}, 2000);
});
}
}
const calendarLink = generateCalendarLink();
if (data) {
let lastDay = "";
if (data.languages.find(lang => lang.code === language) === undefined) {
setLanguage('en')
}
return (
<div>
<div className="navbar bg-main">
<div className="navbar-start">
<a href="." className="text-xl">Milano Cortina 2026 Winter Olympics Calendar</a>
</div>
<div className="navbar-end">
<ul className="menu menu-horizontal px-2">
<li className="px-2">
<div className="dropdown">
<div tabIndex={0} role="button" className="select bg-transparent">
{qs.get('sport') ? (
<>{translate(data.sports.find((sport) => sport.key === qs.get('sport'))!.name)}</>
) : (
<>{translate(FILTER_BY_SPORT)}</>
)}
</div>
<ul tabIndex={-1} className="dropdown-content menu bg-base-100 text-black rounded-box z-1 w-52 p-2 shadow-sm">
{data.sports.sort((a, b) => translate(a.name).localeCompare(translate(b.name))).map(sport => {
if (sport.key === qs.get('sport')) {
return (
<li key={sport.key}>
<a href={generateLink({ sport: "" })}><div aria-label="success" className="status status-success"></div> {translate(sport.name)}</a>
</li>
)
}
return (
<li key={sport.key}>
<a href={generateLink({ sport: sport.key })}>{translate(sport.name)}</a>
</li>
)
})}
</ul>
</div>
</li>
<li className="px-2">
<div className="dropdown">
<div tabIndex={0} role="button" className="select bg-transparent">
{qs.get('noc') ? (
<>{translate(data.nocs.find((noc) => noc.key === qs.get('noc'))!.name)}</>
) : (
<>{translate(FILTER_BY_COUNTRY)}</>
)}
</div>
<ul tabIndex={-1} className="dropdown-content menu bg-base-100 text-black rounded-box z-1 w-52 p-2 shadow-sm">
{data.nocs.sort((a, b) => translate(a.name).localeCompare(translate(b.name))).map(noc => {
if (noc.key === qs.get('noc')) {
return (
<li key={noc.key}>
<a href={generateLink({ noc: "" })}><div aria-label="success" className="status status-success"></div> {translate(noc.name)}</a>
</li>
)
}
return (
<li key={noc.key}>
<a href={generateLink({ noc: noc.key })}>{translate(noc.name)}</a>
</li>
)
})}
</ul>
</div>
</li>
<li className="px-2">
<div className="dropdown dropdown-end">
<div tabIndex={0} role="button" className="btn btn-ghost">
<svg className="size-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path strokeLinejoin="round" strokeLinecap="round" strokeWidth="2" fill="none" stroke="currentColor" d="M12 21a9 9 0 1 0 0-18m0 18a9 9 0 1 1 0-18m0 18c2.761 0 3.941-5.163 3.941-9S14.761 3 12 3m0 18c-2.761 0-3.941-5.163-3.941-9S9.239 3 12 3M3.5 9h17m-17 6h17"></path>
</svg>
<svg className="mt-px hidden size-2 fill-current opacity-60 sm:inline-block" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2048 2048"><path d="M1799 349l242 241-1017 1017L7 590l242-241 775 775 775-775z"></path></svg>
</div>
<ul tabIndex={-1} className="menu menu-sm dropdown-content bg-base-100 text-black rounded-box z-1 mt-3 w-52 p-2 shadow">
{data.languages.map(lang => (
<li key={lang.code}>
<a onClick={() => setLanguage(lang.code)}>{lang.code.toUpperCase()} - {lang.name}</a>
</li>
))}
</ul>
</div>
</li>
</ul>
</div>
</div>
<div>
<div className="text-center pt-6">
<span className="input w-1/3">
<input type="text" placeholder={calendarLink} readOnly={true} />
<button id="copy_button" className="label cursor-pointer" onClick={() => copyToClipboard(calendarLink)}>{translate(COPY)}</button>
</span>
<a className="inline-block" href={calendarLink.replace("https://", "webcal://")} target="_blank">
<img src="/img/icon-apple.svg" alt="Apple Calendar" className="inline-block size-6 ml-4 mr-2" />
</a>
<a className="inline-block" href={`https://calendar.google.com/calendar/u/0/r?cid=${encodeURIComponent(calendarLink)}`} target="_blank">
<img src="/img/icon-google.svg" alt="Google Calendar" className="inline-block size-5 ml-4 mr-2" />
</a>
<a className="inline-block" href={`https://outlook.office.com/calendar/0/deeplink/subscribe?url=${encodeURIComponent(calendarLink)}`} target="_blank">
<img src="/img/icon-office365.svg" alt="Office 365 Calendar" className="inline-block size-5 ml-4 mr-2" />
</a>
<a className="inline-block" href={`https://outlook.live.com/calendar/0/deeplink/subscribe?url=${encodeURIComponent(calendarLink)}`} target="_blank">
<img src="/img/icon-outlookcom.svg" alt="Outlook Calendar" className="inline-block size-5 ml-4 mr-2" />
</a>
<a className="inline-block" href={`https://calendar.yahoo.com/?ics=${encodeURIComponent(calendarLink)}`} target="_blank">
<img src="/img/icon-yahoo.svg" alt="Yahoo Calendar" className="inline-block size-5 ml-4 mr-2" />
</a>
</div>
{
data.events
.filter(event => filter(event))
.sort((a, b) => a.start.localeCompare(b.start))
.map((event, i) => {
const startDate = new Date(event.start);
const endDate = new Date(event.end);
const startHours = startDate.getHours().toString().padStart(2, '0');
const startMinutes = startDate.getMinutes().toString().padStart(2, '0');
const endHours = endDate.getHours().toString().padStart(2, '0');
const endMinutes = endDate.getMinutes().toString().padStart(2, '0');
const participants = [];
let titleColor = "fg-main";
if (event.medal === '1') {
titleColor = "bg-gold";
} else if (event.medal === '3') {
titleColor = "bg-bronze";
}
if (event.match) {
participants.push(event.match.team1.key);
participants.push(event.match.team2.key);
}
const day = event.start.split('T')[0];
let dayHeader = <></>;
if (lastDay !== day) {
dayHeader = (
<div className="day-header text-center my-8">
<h2 className="text-3xl font-light fg-main">
{new Date(day).toLocaleDateString(language, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })}
</h2>
</div>
);
}
lastDay = day;
return (
<div key={event.key}>
{dayHeader}
<div className="py-4 mx-auto my-4 bg-white w-3/4 rounded-lg">
<div className={`fg-${getColor(i)} w-1/4 align-top text-right inline-block text-5xl tabular-nums pr-2 border-r border-slate-900/10`}>
<span className="time-start">{startHours}:{startMinutes}</span>
<div className="time-end text-xs">{endHours}:{endMinutes}</div>
</div>
<div className="w-3/5 align-top inline-block text-black pl-2">
<div className="px-2">
{translate(data.sports.find(sport => sport.key === event.sport)?.name || {}).toUpperCase()}
</div>
<div className={`font-bold inline-block px-2 ${titleColor}`}>{translate(event.name)}</div>
{event.match?.team1?.key && event.match?.team2.key && (
<div className="competitors min-w-md max-w-md px-2 font-light">
<div className="w-1/3 inline-block">
{translate(event.match.team1.name)}
</div>
<div className="w-1/9 inline-block">
<Flag iso3={event.match.team1.key} name={translate(event.match.team1.name)} />
</div>
<div className="w-1/9 inline-block text-center">-</div>
<div className="w-1/9 inline-block text-right">
<Flag iso3={event.match.team2.key} name={translate(event.match.team2.name)} />
</div>
<div className="w-1/3 inline-block text-right">
{translate(event.match.team2.name)}
</div>
</div>
)}
</div>
</div>
</div>
)
})
}
</div>
</div >
);
}
return (
<div>Loading</div>
);
}

18
ui/eslint.config.mjs Normal file
View File

@ -0,0 +1,18 @@
import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";
const eslintConfig = defineConfig([
...nextVitals,
...nextTs,
// Override default ignores of eslint-config-next.
globalIgnores([
// Default ignores of eslint-config-next:
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
]),
]);
export default eslintConfig;

5
ui/lib/data.ts Normal file
View File

@ -0,0 +1,5 @@
export const loadSchedule = async () => {
const response = await fetch('/data/calendar.json');
const data = await response.json();
return data;
};

30
ui/lib/local-storage.ts Normal file
View File

@ -0,0 +1,30 @@
import { useCallback, useEffect, useState } from "react"
export const useLocalStorage = (key: string, initialValue: string) => {
const [state, setState] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
useEffect(() => {
try {
if (state !== undefined) {
window.localStorage.setItem(key, JSON.stringify(state));
} else {
window.localStorage.removeItem(key);
}
} catch { }
}, [key, state]);
const setValue = useCallback((value: string) => {
setState(value);
}, [])
return [state, setValue];
};
export default useLocalStorage;

60
ui/lib/text.ts Normal file
View File

@ -0,0 +1,60 @@
export const FILTER_BY_COUNTRY = {
en: "Filter by country",
fr: "Filtrer par pays",
es: "Filtrar por país",
de: "Nach Land filtern",
it: "Filtra per paese",
pt: "Filtrar por país",
zh: "按国家筛选",
ja: "国でフィルタリング",
ru: "Фильтр по стране"
}
export const FILTER_BY_SPORT = {
en: "Filter by sport",
fr: "Filtrer par sport",
es: "Filtrar por deporte",
de: "Nach Sport filtern",
it: "Filtra per sport",
pt: "Filtrar por esporte",
zh: "按运动筛选",
ja: "スポーツでフィルタリング",
ru: "Фильтр по виду спорта"
};
export const GET_CALENDAR = {
en: "Get Calendar",
fr: "Obtenir le calendrier",
es: "Obtener calendario",
de: "Kalender abrufen",
it: "Ottieni calendario",
pt: "Obter calendário",
zh: "获取日历",
ja: "カレンダーを取得",
ru: "Получить календарь"
}
export const COPY = {
en: "Copy",
fr: "Copier",
es: "Copiar",
de: "Kopieren",
it: "Copia",
pt: "Copiar",
zh: "复制",
ja: "コピー",
ru: "Копировать"
}
export const COPY_SUCCESS = {
en: "Link copied to clipboard!",
fr: "Lien copié dans le presse-papiers !",
es: "¡Enlace copiado al portapapeles!",
de: "Link in die Zwischenablage kopiert!",
it: "Link copiato negli appunti!",
pt: "Link copiado para a área de transferência!",
zh: "链接已复制到剪贴板!",
ja: "リンクがクリップボードにコピーされました!",
ru: "Ссылка скопирована в буфер обмена!"
}

7
ui/next.config.ts Normal file
View File

@ -0,0 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
};
export default nextConfig;

6569
ui/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

27
ui/package.json Normal file
View File

@ -0,0 +1,27 @@
{
"name": "ui",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint"
},
"dependencies": {
"next": "16.0.6",
"react": "19.2.0",
"react-dom": "19.2.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"daisyui": "^5.5.5",
"eslint": "^9.39.1",
"eslint-config-next": "16.0.6",
"tailwindcss": "^4",
"typescript": "^5"
}
}

7
ui/postcss.config.mjs Normal file
View File

@ -0,0 +1,7 @@
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 56.7 56.7" style="enable-background:new 0 0 56.7 56.7;" xml:space="preserve">
<style type="text/css">
.st0{fill:none;}
.st1{fill:#606060;}
</style>
<g>
<title>background</title>
<rect x="-1" y="-1" class="st0" width="582" height="402"/>
</g>
<g>
<title>Layer 1</title>
<path class="st1" d="M41.8,30.5c-0.1-6.2,5.1-9.2,5.3-9.4c-2.9-4.2-7.4-4.8-9-4.9c-3.8-0.4-7.5,2.3-9.4,2.3c-1.9,0-4.9-2.2-8.1-2.1
c-4.2,0.1-8,2.4-10.2,6.2C6,30.1,9.3,41.2,13.5,47.3c2.1,3,4.5,6.3,7.8,6.2c3.1-0.1,4.3-2,8.1-2s4.8,2,8.1,2c3.4-0.1,5.5-3,7.5-6
c2.4-3.5,3.3-6.8,3.4-7C48.3,40.4,41.8,38,41.8,30.5z"/>
<path class="st1" d="M35.6,12.2c1.7-2.1,2.9-5,2.6-7.9c-2.5,0.1-5.5,1.6-7.2,3.7c-1.6,1.8-3,4.8-2.6,7.6
C31,15.9,33.9,14.3,35.6,12.2z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 128 128" style="enable-background:new 0 0 128 128;" xml:space="preserve">
<style type="text/css">
.st0{fill:none;}
.st1{fill:#FBBC05;}
.st2{fill:#EA4335;}
.st3{fill:#34A853;}
.st4{fill:#4285F4;}
</style>
<g>
<rect class="st0" width="128" height="128"/>
<path class="st1" d="M27.6,64c0-4.2,0.7-8.1,1.9-11.9L7.9,35.6C3.7,44.2,1.4,53.8,1.4,64c0,10.2,2.4,19.8,6.6,28.3l21.6-16.5
C28.3,72.1,27.6,68.1,27.6,64"/>
<path class="st2" d="M65.5,26.2c9,0,17.2,3.2,23.6,8.4L107.7,16C96.3,6.1,81.8,0,65.5,0C40.1,0,18.4,14.5,7.9,35.6l21.6,16.5
C34.5,37,48.6,26.2,65.5,26.2"/>
<path class="st3" d="M65.5,101.8c-16.8,0-31-10.9-35.9-25.9L7.9,92.4C18.4,113.5,40.1,128,65.5,128c15.6,0,30.6-5.6,41.8-16
L86.7,96.2C81,99.9,73.7,101.8,65.5,101.8"/>
<path class="st4" d="M126.6,64c0-3.8-0.6-7.9-1.5-11.6H65.5v24.7h34.4c-1.7,8.4-6.4,14.9-13.1,19.1l20.5,15.8
C119,101.1,126.6,84.9,126.6,64"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1 @@
<svg enable-background="new 0 0 2075 2499.8" viewBox="0 0 2075 2499.8" xmlns="http://www.w3.org/2000/svg"><path d="m0 2016.6v-1519.8l1344.4-496.8 730.6 233.7v2045.9l-730.6 220.3-1344.4-483.3 1344.4 161.8v-1769.2l-876.8 204.6v1198.3z" fill="#eb3c00"/></svg>

After

Width:  |  Height:  |  Size: 256 B

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 1024 1024" style="enable-background:new 0 0 1024 1024;" xml:space="preserve">
<style type="text/css">
.st0{fill:none;}
.st1{fill:#0072C6;}
</style>
<g>
<title>background</title>
<rect x="-1" y="-1" class="st0" width="582" height="402"/>
</g>
<g>
<title>Layer 1</title>
<path class="st1" d="M644.9,764.4V492.7c19.3,13.3,37.4,25.6,55.4,38c16.2,11.1,18.9,11,35.6-0.1c92.5-61.5,185-122.9,277.5-184.4
c2.8-1.9,5.7-3.6,9.8-6.1c0.3,4.2,0.8,7.4,0.8,10.5c0,118.2,0.1,236.4,0,354.7c0,39.3-19.9,59.1-59,59.1c-102.8,0-205.6,0-308.4,0
C652.9,764.4,649.2,764.4,644.9,764.4L644.9,764.4z"/>
<path class="st1" d="M645.6,216.9h10.5c108.6,0,217.1,0,325.7,0c19.4,0,34.4,11.9,40.5,30.5c2.7,8.2,0,12.2-6.7,16.6
c-84.1,56.3-168,112.9-252,169.4c-12,8.1-24.1,15.9-35.9,24.3c-6.4,4.6-12.2,4.8-18.8,0.5c-19.2-12.4-38.7-24.2-57.8-36.7
c-3.2-2.1-6.5-6.9-6.5-10.4c-0.3-63.5-0.2-127-0.1-190.5C644.5,219.8,644.9,219.1,645.6,216.9L645.6,216.9z"/>
<path class="st1" d="M596.7,1024C397.6,982.3,199.3,940.8,0.3,899.2v-13.1c0-252.5,0.1-505-0.3-757.5c0-10.5,2.8-14.1,13-16
c159.1-30.4,318.2-61.2,477.3-92C525.4,13.7,560.5,7,596.7,0L596.7,1024L596.7,1024z M438,511c-0.4-41.6-6.1-80-26.7-114.9
c-16.6-28.1-39-49.4-71.3-58.6c-62.8-17.9-122.8,11.5-152.4,75c-18.9,40.7-23,83.9-19.3,128.1c3.2,38.3,14.3,73.7,39,104
c49.1,60.2,138.4,60.6,188.2,0.8C428.4,605.8,437.4,558.7,438,511L438,511z"/>
<path class="st1" d="M232.9,511.9c-0.1-29.7,3.9-58.4,20.7-83.8c15.5-23.5,39-33.1,64.9-26.6c20.1,5,32.3,19.3,40.4,37.2
c13.8,30.7,16.2,63.3,12.1,95.9c-2.3,18.1-7.5,36.5-14.8,53.2c-9.5,21.6-27.8,34-52.2,34.5c-24.6,0.5-41.7-12.2-53.6-32.6
C236.3,565.8,232.9,539.2,232.9,511.9L232.9,511.9z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 1792 1792" style="enable-background:new 0 0 1792 1792;" xml:space="preserve">
<style type="text/css">
.st0{fill:none;}
.st1{fill:#4A089F;}
</style>
<g>
<title>background</title>
<rect x="-1" y="-1" class="st0" width="582" height="402"/>
</g>
<g>
<title>Layer 1</title>
<path class="st1" d="M987,957l13,707c-41.3-7.3-76.3-11-105-11c-27.3,0-62.3,3.7-105,11l13-707c-26.7-46-82.8-144.5-168.5-295.5
S476.7,385.7,418,287S299,92.7,237,0c38.7,10,74.7,15,108,15c28.7,0,65.7-5,111-15c42,74,86.5,150.5,133.5,229.5
s102.7,171.2,167,276.5S867,687,895,733c24.7-40.7,61.2-99.8,109.5-177.5c48.3-77.7,87.5-141,117.5-190s65-107.7,105-176
S1302.7,58,1334,0c36,9.3,71.7,14,107,14c37.3,0,75.3-4.7,114-14c-18.7,26-38.7,55.5-60,88.5s-37.8,59.2-49.5,78.5
s-30.5,51.3-56.5,96s-42.3,72.7-49,84C1242.7,512.3,1125,715.7,987,957z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

34
ui/tsconfig.json Normal file
View File

@ -0,0 +1,34 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts",
"**/*.mts"
],
"exclude": ["node_modules"]
}