diff options
| author | Maksymilian Jopek <maks@jopek.eu> | 2023-11-15 19:29:24 +0100 |
|---|---|---|
| committer | Maksymilian Jopek <maks@jopek.eu> | 2023-11-15 19:34:12 +0100 |
| commit | 857c528369473a66518407dee6f6cac89bbc538c (patch) | |
| tree | cccf02a369ee6cafb2a89750ee222ebba69d2269 /src | |
| download | listeya-xelata-nobele-857c528369473a66518407dee6f6cac89bbc538c.tar.gz listeya-xelata-nobele-857c528369473a66518407dee6f6cac89bbc538c.tar.zst listeya-xelata-nobele-857c528369473a66518407dee6f6cac89bbc538c.zip | |
Diffstat (limited to 'src')
| -rw-r--r-- | src/App.tsx | 25 | ||||
| -rw-r--r-- | src/Router.tsx | 30 | ||||
| -rw-r--r-- | src/Search.tsx | 54 | ||||
| -rw-r--r-- | src/Table.tsx | 124 | ||||
| -rw-r--r-- | src/data.ts | 16 | ||||
| -rw-r--r-- | src/index.css | 91 | ||||
| -rw-r--r-- | src/main.tsx | 10 | ||||
| -rw-r--r-- | src/vite-env.d.ts | 1 |
8 files changed, 351 insertions, 0 deletions
diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..8845354 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,25 @@ +import { useEffect, useState } from "react"; +import Router from "./Router"; +import { setNobelPrizes } from "./data"; + +let loadOnce = false; +function App() { + const [loading, setLoading] = useState(true); + useEffect(() => { + (async () => { + if (loadOnce === false) { + loadOnce = true; + await setNobelPrizes(); + setLoading(false); + } + })(); + }, []); + + if (loading) { + return <>Loading nobel prizes</>; + } else { + return <Router />; + } +} + +export default App; diff --git a/src/Router.tsx b/src/Router.tsx new file mode 100644 index 0000000..eb4cf21 --- /dev/null +++ b/src/Router.tsx @@ -0,0 +1,30 @@ +import { useEffect, useState } from "react"; +import Search from "./Search"; +import Table from "./Table"; + +const startPath = + location.pathname.at(-1) === "/" + ? location.pathname + : location.pathname + "/"; +export default function Router() { + const [path, setPath] = useState(""); + useEffect(() => { + setPath(location.pathname); + }, []); + window.addEventListener("popstate", () => { + setPath(location.pathname); + }); + + function updatePath(newPath: string) { + console.log({ newPath, startPath }); + const path = startPath + newPath; + history.pushState({}, "", path); + setPath(path); + } + + if (path.includes("/nagrody/")) { + return <Table />; + } else { + return <Search setPath={updatePath} />; + } +} diff --git a/src/Search.tsx b/src/Search.tsx new file mode 100644 index 0000000..cca6e42 --- /dev/null +++ b/src/Search.tsx @@ -0,0 +1,54 @@ +import { useEffect, useState } from "react"; +import { nobelPrizes } from "./data"; + +export default function Search({ setPath }: { setPath: (a: string) => void }) { + const languages = { Angielski: "en", Norweski: "no", Szwedzki: "se" }; + const languagesFlags = { Angielski: "🇬🇧", Norweski: "🇳🇴", Szwedzki: "🇸🇪" }; + const langs = Object.keys(languages) as (keyof typeof languages)[]; + + const [language, setLanguage] = useState(langs[0]); + const [years, setYears] = useState<number[]>([]); + const [year, setYear] = useState<null | number>(null); + + function changeLanguage() { + let i = langs.findIndex((l) => language === l) + 1; + if (i >= langs.length) i = 0; + setLanguage(langs[i]); + } + + useEffect(() => { + setYears([...new Set(nobelPrizes.map((np) => parseInt(np.awardYear)))]); + }, []); + + return ( + <> + <h2>Wybierz z którego roku nagrody Nobla chcesz zobaczyć</h2> + <select + className="year-select" + onChange={(e) => + setYear(e.target.value ? parseInt(e.target.value) : null) + } + > + <option value="">Wybierz rok</option> + {years.map((y) => ( + <option value={y} key={y}> + {y} + </option> + ))} + </select> + <br /> + <br /> + <button + disabled={year === null} + onClick={() => setPath(`nagrody/${languages[language]}/${year}`)} + > + Wyszukaj nagrody + </button> + <div style={{ position: "absolute", top: 10, right: 10 }}> + <span onClick={changeLanguage}> + Język: {language} {languagesFlags[language]} + </span> + </div> + </> + ); +} diff --git a/src/Table.tsx b/src/Table.tsx new file mode 100644 index 0000000..26c08c7 --- /dev/null +++ b/src/Table.tsx @@ -0,0 +1,124 @@ +import { ReactNode, useEffect, useState } from "react"; +import { nobelPrizes } from "./data"; + +export default function Table() { + const year = location.pathname.split("/").pop(); + //@ts-expect-error Split exists on this type + const lang = location.pathname.match(/\/nagrody\/.*\//g)[0].split("/")[2] as + | "en" + | "no" + | "se"; + const cols = { + awardYear: "Rok przyznania", + category: "Kategoria", + dateAwarded: "Data przyznania nagrody", + prizeAmount: "Kwota nagrody", + }; + const colsKeys = Object.keys(cols) as (keyof typeof cols)[]; + const [filterText, setFilterText] = useState(""); + const [sortCol, setSortCol] = useState("awardYear"); + //eslint-disable-next-line + let [sortDir, setSortDir] = useState(true); + const [trs, setTrs] = useState<ReactNode[]>([]); + + function updateTrs(col: string, inverseDir = true) { + if (inverseDir && col === sortCol) sortDir = !sortDir; + if (col !== sortCol) setSortCol(col); + + const npz = nobelPrizes.filter((np) => { + if (np.awardYear !== year) return false; + if (filterText === "") return true; + + const ft = filterText.toLowerCase(); + + return ( + np.awardYear.toLowerCase().includes(ft) || + np.category[lang].toLowerCase().includes(ft) || + formatDate(np.dateAwarded)?.toLowerCase().includes(ft) || + np.prizeAmount.toString().toLowerCase().includes(ft) + ); + }); + npz.sort((npA, npB) => { + switch (col) { + case "awardYear": + return ( + (parseInt(npA.awardYear) - parseInt(npB.awardYear)) * + (sortDir ? 1 : -1) + ); + case "category": + return sortDir + ? npA.category[lang].localeCompare(npB.category[lang]) + : npB.category[lang].localeCompare(npA.category[lang]); + case "dateAwarded": + return ( + // @ts-expect-error Subtracting dates return the difference in their unix timestamps + (new Date(npA.dateAwarded) - new Date(npB.dateAwarded)) * + (sortDir ? 1 : -1) + ); + case "prizeAmount": + return (npA.prizeAmount - npB.prizeAmount) * (sortDir ? 1 : -1); + default: + return 1; + } + }); + let key = 0; + setTrs( + npz.map((np) => ( + <tr key={key++}> + <td>{np.awardYear}</td> + <td>{np.category[lang]}</td> + <td>{formatDate(np.dateAwarded)}</td> + <td> + {np.prizeAmount + .toString() + .match(/.{1,3}/g) + ?.join(" ")} + kr + </td> + </tr> + )), + ); + if (inverseDir && col === sortCol) setSortDir(sortDir); + } + function formatDate(date: string) { + return date ? new Date(date).toLocaleDateString("pl-PL") : "-"; + } + useEffect(() => updateTrs(sortCol, false), [filterText]); + + return ( + <> + <h1>Nagrody nobla w roku {year}</h1> + <table> + <thead> + <tr> + {colsKeys.map((col) => ( + <th key={col} onClick={() => updateTrs(col)}> + {cols[col]} + {sortCol === col && (sortDir ? <>🡅</> : <>🡇</>)} + </th> + ))} + </tr> + </thead> + <tbody>{trs}</tbody> + </table> + <br /> + <br /> + <label> + Filtruj + <input onChange={(e) => setFilterText(e.target.value)} /> + </label> + <br /> + <br /> + <label> + Sortuj po: + <select onChange={(e) => updateTrs(e.target.value)}> + {colsKeys.map((col) => ( + <option key={col} value={col}> + {cols[col]} + </option> + ))} + </select> + </label> + </> + ); +} diff --git a/src/data.ts b/src/data.ts new file mode 100644 index 0000000..116308d --- /dev/null +++ b/src/data.ts @@ -0,0 +1,16 @@ +export let nobelPrizes: NobelPrize[] = []; +export async function setNobelPrizes() { + nobelPrizes = ( + await fetch("https://api.nobelprize.org/2.1/nobelPrizes").then((res) => + res.json(), + ) + ).nobelPrizes; +} + +export interface NobelPrize { + awardYear: string; + category: { en: string; no: string; se: string }; + dateAwarded: string; + prizeAmount: number; + prizeAmountAdjusted: number; +} diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..503e8af --- /dev/null +++ b/src/index.css @@ -0,0 +1,91 @@ +:root { + font-family: monospace; + line-height: 1.5; + font-weight: 400; + font-size: 20px; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 1.8rem; + line-height: 1.1; + font-weight: 550; +} +h2 { + font-size: 1.5rem; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + button { + background-color: #f9f9f9; + } +} + +#root { + margin: 0 auto; + text-align: center; +} +select, +input { + padding: 0.2rem; +} +.year-select { + padding: 1rem; +} +span, +th { + user-select: none; +} + +th, +td { + padding-inline: 1rem; + padding-block: 0.5rem; +} + +table { + border-collapse: collapse; +} +td { + border-top: 1px solid white; +} diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..3d7150d --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,10 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App.tsx' +import './index.css' + +ReactDOM.createRoot(document.getElementById('root')!).render( + <React.StrictMode> + <App /> + </React.StrictMode>, +) diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1 @@ +/// <reference types="vite/client" /> |
