responsive

This commit is contained in:
Fabrice LAMANT
2026-02-04 23:02:06 +01:00
parent e909a1fa23
commit f77a37ca02
4 changed files with 189 additions and 121 deletions

View File

@ -260,9 +260,9 @@ export default function Flag({ iso3, name }: { iso3: string; name: string }) {
const iso2 = (iso3to2[iso3.toUpperCase()] || "").toLowerCase();
return <img
src={`https://gstatic.olympics.com/s3/noc/oly/3x2/${iso3.toUpperCase()}.png`}
height="24"
alt={name}
className="inline-block mr-1 h-5 border-1 border-gray-300"
src={`https://gstatic.olympics.com/s3/noc/oly/3x2/${iso3.toUpperCase()}.png`}
title={name}
className="inline-block mx-2 h-5 border-1 border-gray-300" />
/>
}

View File

@ -1,5 +1,5 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import { Geist, Geist_Mono, DM_Sans } from "next/font/google";
import "./globals.css";
import Head from "next/head";
@ -13,6 +13,11 @@ const geistMono = Geist_Mono({
subsets: ["latin"],
});
const dmSans = DM_Sans({
variable: "--font-dm-sans",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Milano Cortina 2026 Winter Olympics Calendar",
description: "Made with ❤️ by Fabrice Lamant",
@ -29,7 +34,7 @@ export default function RootLayout({
<link rel="icon" href="/favicon.ico" sizes="any" />
</Head>
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
className={`${geistSans.variable} ${dmSans.className} antialiased`}
>
{children}
</body>

View File

@ -3,10 +3,22 @@
import { loadSchedule } from "../lib/data";
import { useEffect, useState } from "react";
import Flag from "./flag";
import { COPY, COPY_SUCCESS, FILTER_BY_COUNTRY, FILTER_BY_SPORT, MADE_BY_FABRICE, NOT_AFFILIATED, NO_EVENT_FOR_FILTERS } from "../lib/text";
import { COPY, COPY_SUCCESS, FILTER_BY_COUNTRY, FILTER_BY_SPORT, LANGUAGE, MADE_BY_FABRICE, NOT_AFFILIATED, NO_EVENT_FOR_FILTERS } from "../lib/text";
import useLocalStorage from "@/lib/local-storage";
import { GoogleAnalytics } from "@next/third-parties/google";
import { Google_Sans, SUSE_Mono } from "next/font/google";
const googleSans = Google_Sans({
variable: "--font-google-sans",
subsets: ["latin"],
});
const suseMono = SUSE_Mono({
variable: "--font-suse-mono",
subsets: ["latin"],
});
export interface MultilingualString {
[key: string]: string;
}
@ -176,31 +188,33 @@ export default function Home() {
main = (
<div>
<div className="text-center pt-6">
<span className="input w-1/3">
<span className="input md: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>
<div className="pt-2 md:pt-0 lg:inline-block">
<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://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.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://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>
<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>
</div>
{
events
@ -272,16 +286,16 @@ export default function Home() {
return (
<div key={event.key}>
{dayHeader}
<div className="p-4 mx-2 my-4 bg-white rounded-md md:w-3/4 md:mx-auto">
<div className={`fg-${getColor(i)} align-top inline-block tabular-nums pr-2`}>
<span className="text-3xl">{startHours}:{startMinutes}</span>
<div className="p-2 m-2 my-4 bg-white rounded-md md:w-3/4 md:mx-auto">
<div className={`fg-${getColor(i)} ${suseMono.className} text-right font-bold align-top inline-block tabular-nums pr-2`}>
<span className="text-xl">{startHours}:{startMinutes}</span>
<div className="text-xs">{endHours}:{endMinutes}</div>
</div>
<div className="align-top inline-block text-black pl-2 border-l border-slate-900/10">
<div className="px-2">
<div className="font-bold">
{translate(data.sports.find(sport => sport.key === event.sport)?.name || {})}
</div>
<div className={`inline-block font-bold text-sm px-2 nowrap ${titleColor}`}>{translate(event.name)}</div>
<div className={`inline-block font-bold text-sm ${titleColor}`}>{translate(event.name)}</div>
{competitors}
</div>
</div>
@ -293,52 +307,90 @@ export default function Home() {
)
}
const header = (
<div className="navbar bg-main">
<div className={`flex-1 ${googleSans.className}`}>
<a href="." className="text-xl font-bold">Olympics Calendar</a>
</div>
<div className="flex">
<label htmlFor="my-drawer-5" className="drawer-button btn btn-main btn-ghost">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" className="inline-block h-5 w-5 stroke-current"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 6h16M4 12h16M4 18h16"></path> </svg>
</label>
{/* */}
</div>
</div>
);
const footer = (
<footer className="footer footer-horizontal footer-center bg-gray-800 text-primary-content p-10">
<aside>
<p className="font-bold">
{translate(MADE_BY_FABRICE)}
</p>
<p>{translate(NOT_AFFILIATED)}</p>
</aside>
<nav>
<div className="grid grid-flow-col gap-4">
<a href="https://github.com/fabrice404/olympics-calendar" target="_blank">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
className="fill-current">
<path d="M5.315 2.1c.791 -.113 1.9 .145 3.333 .966l.272 .161l.16 .1l.397 -.083a13.3 13.3 0 0 1 4.59 -.08l.456 .08l.396 .083l.161 -.1c1.385 -.84 2.487 -1.17 3.322 -1.148l.164 .008l.147 .017l.076 .014l.05 .011l.144 .047a1 1 0 0 1 .53 .514a5.2 5.2 0 0 1 .397 2.91l-.047 .267l-.046 .196l.123 .163c.574 .795 .93 1.728 1.03 2.707l.023 .295l.007 .272c0 3.855 -1.659 5.883 -4.644 6.68l-.245 .061l-.132 .029l.014 .161l.008 .157l.004 .365l-.002 .213l-.003 3.834a1 1 0 0 1 -.883 .993l-.117 .007h-6a1 1 0 0 1 -.993 -.883l-.007 -.117v-.734c-1.818 .26 -3.03 -.424 -4.11 -1.878l-.535 -.766c-.28 -.396 -.455 -.579 -.589 -.644l-.048 -.019a1 1 0 0 1 .564 -1.918c.642 .188 1.074 .568 1.57 1.239l.538 .769c.76 1.079 1.36 1.459 2.609 1.191l.001 -.678l-.018 -.168a5.03 5.03 0 0 1 -.021 -.824l.017 -.185l.019 -.12l-.108 -.024c-2.976 -.71 -4.703 -2.573 -4.875 -6.139l-.01 -.31l-.004 -.292a5.6 5.6 0 0 1 .908 -3.051l.152 -.222l.122 -.163l-.045 -.196a5.2 5.2 0 0 1 .145 -2.642l.1 -.282l.106 -.253a1 1 0 0 1 .529 -.514l.144 -.047l.154 -.03z" />
</svg>
</a>
</div>
</nav>
</footer>
);
let cookie = <></>;
if (cookieConsent === 'true') {
cookie = (
<GoogleAnalytics gaId="G-SLBLJRE0CM" />
);
} else if (cookieConsent === 'null') {
cookie = (
<div className="sticky bottom-0 bg-gray-800 text-white text-center p-8">
<p className="p-4">This website uses cookies for statistics purposes and to enhance the user experience.</p>
<button className="btn btn-sm mx-2" onClick={() => setCookieConsent('true')}>Accept</button>
<button className="btn btn-sm btn-outline" onClick={() => setCookieConsent('false')}>Decline</button>
</div>
);
}
return (
<div>
<div className="navbar bg-main">
<div className="navbar-start">
<a href="." className="text-xl">Milano Cortina 2026 Winter Olympics Calendar</a>
<div className="drawer drawer-end">
<input id="my-drawer-5" type="checkbox" className="drawer-toggle" />
<div className="drawer-content">
{header}
{main}
{footer}
{cookie}
</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 className="drawer-side">
<label htmlFor="my-drawer-5" aria-label="close sidebar" className="drawer-overlay"></label>
<div className="menu bg-base-200 min-h-full w-80 p-4">
<div>
<span className="font-bold">{translate(FILTER_BY_COUNTRY)}</span>
{qs.get('noc') && (
<div className="my-1">
<a href={generateLink({ noc: "" })} className="btn bg-white btn-sm">
<span className="font-bold text-red-400">X</span> {translate(data.nocs.find(noc => noc.key === qs.get('noc'))!.name)}
</a>
</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 => {
)}
<div className="bg-white h-[200px] max-h-[200px] overflow-y-scroll">
<ul>
{data.nocs.toSorted((a, b) => translate(a.name).localeCompare(translate(b.name))).map(noc => {
if (noc.key === qs.get('noc')) {
return (
<li key={noc.key}>
@ -354,62 +406,61 @@ export default function Home() {
})}
</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>
<div className="mt-2 pt-2 border-t-1 border-slate-300">
<span className="font-bold">{translate(FILTER_BY_SPORT)}</span>
{qs.get('sport') && (
<div className="my-1">
<a href={generateLink({ sport: "" })} className="btn bg-white btn-sm">
<span className="font-bold text-red-400">X</span> {translate(data.sports.find(sport => sport.key === qs.get('sport'))!.name)}
</a>
</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>
))}
)}
<div className="bg-white h-[200px] max-h-[200px] overflow-y-scroll">
<ul>
{data.sports.toSorted((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>
</ul>
</div>
<div className="mt-2 pt-2 border-t-1 border-slate-300">
<span className="font-bold">{translate(LANGUAGE)}</span>
<div className="bg-white h-[200px] max-h-[200px] overflow-y-scroll">
<ul>
{data.languages.map(lang => {
if (lang.code === language) {
return (
<li key={lang.code}>
<a onClick={() => setLanguage(lang.code)}><div aria-label="success" className="status status-success"></div> {lang.code.toUpperCase()} - {lang.name}</a>
</li>
);
}
return (
<li key={lang.code}>
<a onClick={() => setLanguage(lang.code)}>{lang.code.toUpperCase()} - {lang.name}</a>
</li>
);
})}
</ul>
</div>
</div>
</div>
</div>
</div>
{main}
<footer className="footer footer-horizontal footer-center bg-gray-800 text-primary-content p-10">
<aside>
<p className="font-bold">
{translate(MADE_BY_FABRICE)}
</p>
<p>{translate(NOT_AFFILIATED)}</p>
</aside>
<nav>
<div className="grid grid-flow-col gap-4">
<a href="https://github.com/fabrice404/olympics-calendar" target="_blank">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
className="fill-current">
<path d="M5.315 2.1c.791 -.113 1.9 .145 3.333 .966l.272 .161l.16 .1l.397 -.083a13.3 13.3 0 0 1 4.59 -.08l.456 .08l.396 .083l.161 -.1c1.385 -.84 2.487 -1.17 3.322 -1.148l.164 .008l.147 .017l.076 .014l.05 .011l.144 .047a1 1 0 0 1 .53 .514a5.2 5.2 0 0 1 .397 2.91l-.047 .267l-.046 .196l.123 .163c.574 .795 .93 1.728 1.03 2.707l.023 .295l.007 .272c0 3.855 -1.659 5.883 -4.644 6.68l-.245 .061l-.132 .029l.014 .161l.008 .157l.004 .365l-.002 .213l-.003 3.834a1 1 0 0 1 -.883 .993l-.117 .007h-6a1 1 0 0 1 -.993 -.883l-.007 -.117v-.734c-1.818 .26 -3.03 -.424 -4.11 -1.878l-.535 -.766c-.28 -.396 -.455 -.579 -.589 -.644l-.048 -.019a1 1 0 0 1 .564 -1.918c.642 .188 1.074 .568 1.57 1.239l.538 .769c.76 1.079 1.36 1.459 2.609 1.191l.001 -.678l-.018 -.168a5.03 5.03 0 0 1 -.021 -.824l.017 -.185l.019 -.12l-.108 -.024c-2.976 -.71 -4.703 -2.573 -4.875 -6.139l-.01 -.31l-.004 -.292a5.6 5.6 0 0 1 .908 -3.051l.152 -.222l.122 -.163l-.045 -.196a5.2 5.2 0 0 1 .145 -2.642l.1 -.282l.106 -.253a1 1 0 0 1 .529 -.514l.144 -.047l.154 -.03z" />
</svg>
</a>
</div>
</nav>
</footer>
{cookieConsent === 'true' &&
<GoogleAnalytics gaId="G-SLBLJRE0CM" />
}
{cookieConsent === 'null' &&
<div className="sticky bottom-0 bg-gray-800 text-white text-center p-8">
<p className="p-4">This website uses cookies for statistics purposes and to enhance the user experience.</p>
<button className="btn btn-sm mx-2" onClick={() => setCookieConsent('true')}>Accept</button>
<button className="btn btn-sm btn-outline" onClick={() => setCookieConsent('false')}>Decline</button>
</div>
}
</div>
);