mirror of
https://github.com/fabrice404/olympics-calendar.git
synced 2025-12-13 14:49:46 +00:00
wip milano-cortina 2026
This commit is contained in:
43
ui/.gitignore
vendored
Normal file
43
ui/.gitignore
vendored
Normal 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
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
267
ui/app/flag.tsx
Normal 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
69
ui/app/globals.css
Normal 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
38
ui/app/layout.tsx
Normal 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
359
ui/app/page.tsx
Normal 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
18
ui/eslint.config.mjs
Normal 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
5
ui/lib/data.ts
Normal 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
30
ui/lib/local-storage.ts
Normal 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
60
ui/lib/text.ts
Normal 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
7
ui/next.config.ts
Normal 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
6569
ui/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
27
ui/package.json
Normal file
27
ui/package.json
Normal 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
7
ui/postcss.config.mjs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
const config = {
|
||||||
|
plugins: {
|
||||||
|
"@tailwindcss/postcss": {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
21
ui/public/img/icon-apple.svg
Normal file
21
ui/public/img/icon-apple.svg
Normal 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 |
23
ui/public/img/icon-google.svg
Normal file
23
ui/public/img/icon-google.svg
Normal 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 |
1
ui/public/img/icon-office365.svg
Normal file
1
ui/public/img/icon-office365.svg
Normal 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 |
29
ui/public/img/icon-outlookcom.svg
Normal file
29
ui/public/img/icon-outlookcom.svg
Normal 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 |
21
ui/public/img/icon-yahoo.svg
Normal file
21
ui/public/img/icon-yahoo.svg
Normal 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
34
ui/tsconfig.json
Normal 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"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user