diff --git a/frontend/README.md b/frontend/README.md index 4620fb955e7d..4ecf8547e73a 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -16,25 +16,41 @@ We welcome contributions to our documentation! Whether you're fixing a typo or a To preview your changes locally: -1. **Navigate to the frontend directory:** - ```bash - cd frontend - ``` -2. **Install dependencies:** - ```bash - npm install - ``` -3. **Start the development server:** - ```bash - npm start - ``` - The site will be available at `http://localhost:3000`. Most changes to Markdown files will live-reload in the browser. +1. **Navigate to the frontend directory:** + + ```bash + cd frontend + ``` + +2. **Install dependencies:** + + ```bash + npm install + ``` + +3. **Start the development server:** + + ```bash + npm start + ``` + + The site will be available at `http://localhost:3000`. Most changes to Markdown files will live-reload in the browser. + + #### Choosing a locale + + ```bash + npm start -- --locale pt-BR # http://localhost:3000/pt-BR/ + npm start -- --locale zh-CN # http://localhost:3000/zh-CN/ + ``` + + `start` accepts a **single** locale — a second `--locale` is silently ignored (the last one wins) — so run one dev server per language on different ports. ### Adding or Editing Pages - To **edit** an existing page, find the corresponding `.md` file in `docs/` and make your changes. - To **add** a new page, create a new `.md` file in the appropriate subdirectory of `docs/`. - Remember to update the **metadata** (front matter) at the top of the file: + ```markdown --- title: Page Title @@ -58,29 +74,38 @@ npm i ### Local Development ```bash -npm start +npm start # default locale +npm start -- --locale pt-BR # specific locale ``` ### Build ```bash -npm run build +npm run build # all locales +npm run build -- --locale pt-BR # one locale +npm run build -- --locale en --locale pt-BR --locale zh-CN # several locales ``` This command generates static content into the `build` directory. +### Local Preview + +```bash +npm run serve +``` + ### Deployment Using SSH: ``` -$ USE_SSH=true yarn deploy +$ USE_SSH=true npm run deploy ``` Not using SSH: ``` -$ GIT_USER= yarn deploy +$ GIT_USER= npm run deploy ``` If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. diff --git a/frontend/docusaurus.config.ts b/frontend/docusaurus.config.ts index f94699fd26c7..e44240a8e7d2 100644 --- a/frontend/docusaurus.config.ts +++ b/frontend/docusaurus.config.ts @@ -105,28 +105,6 @@ const config: Config = { staticDirectories: ["public"], - plugins: [ - async function serverRoutesPlugin(context, options) { - return { - name: "server-routes-plugin", - async contentLoaded({ actions }) { - const { addRoute } = actions; - const { i18n: { locales, defaultLocale } } = context; - - // Add the dynamic server detail route. - locales.forEach((locale) => { - const prefix = locale === defaultLocale ? '' : `/${locale}`; - addRoute({ - path: `${prefix}/servers/`, - component: "@site/src/pages/servers/index", - exact: false, - }); - }) - }, - }; - }, - ], - presets: [ [ "classic", @@ -244,7 +222,34 @@ const config: Config = { var locales = ["en","es","ru","ar","bs","de","fil","fr","fa","hu","id","it","nl","pl","pt-BR","ro","sl","sr","ta","th","tr","uk","vi","zh-CN","zh-TW"]; var path = window.location.pathname; var firstSegment = path.split("/")[1]; - + + var pageLocale = document.documentElement.getAttribute("lang") || "en"; + var lastSegment = path.split("/").pop(); + var is404Page = lastSegment === "404" || lastSegment === "404.html"; + + var localePrefix = locales.indexOf(firstSegment) !== -1 ? "/" + firstSegment : ""; + var rest = path.slice(localePrefix.length); + var serversPrefix = "/servers/"; + + if(rest.indexOf(serversPrefix) === 0 && rest.length > serversPrefix.length) { + var address = rest.slice(serversPrefix.length); + if(address.charAt(address.length - 1) === "/") address = address.slice(0, -1); + try { address = decodeURIComponent(address); } catch (e) {} + window.location.replace(localePrefix + "/servers?ip=" + encodeURIComponent(address)); + return; + } + + function redirectLocale404() { + if(is404Page || firstSegment === pageLocale || locales.indexOf(firstSegment) === -1) return; + if(!document.querySelector("meta[name=omp-not-found]")) return; + + localStorage.setItem("omp-locale", firstSegment); + window.location.replace("/" + firstSegment + "/404"); + } + + redirectLocale404(); + document.addEventListener("DOMContentLoaded", redirectLocale404); + if(locales.indexOf(firstSegment) !== -1) { localStorage.setItem("omp-locale", firstSegment); return; @@ -272,7 +277,7 @@ const config: Config = { if(locales.indexOf(target) === -1 && navLang.indexOf("-") !== -1) { target = navLang.split("-")[0]; } - + if(locales.indexOf(target) !== -1 && target !== "en") { localStorage.setItem("omp-locale", target); window.location.href = "/" + target + path + window.location.search + window.location.hash; diff --git a/frontend/i18n/fa/code.json b/frontend/i18n/fa/code.json index 919a7f5528ff..1b2eb6f7969e 100644 --- a/frontend/i18n/fa/code.json +++ b/frontend/i18n/fa/code.json @@ -259,6 +259,9 @@ "serverInfo.backToServers": { "message": "→ بازگشت به سرورها" }, + "serverInfo.backToPartners": { + "message": "← بازگشت به سرورهای شریک" + }, "serverInfo.quickJoin": { "message": "ورود سریع" }, @@ -283,6 +286,9 @@ "serverInfo.lastUpdated": { "message": "{time} پیش به‌روزرسانی شده" }, + "serverInfo.notFound": { + "message": "این سرور پیدا نشد. ممکن است آفلاین باشد یا دیگر در فهرست نباشد." + }, "theme.footer.title.Documentations": { "message": "مستندات" }, diff --git a/frontend/i18n/id/code.json b/frontend/i18n/id/code.json index df87aeb41514..9acda2ec2802 100644 --- a/frontend/i18n/id/code.json +++ b/frontend/i18n/id/code.json @@ -265,6 +265,9 @@ "serverInfo.backToServers": { "message": "← Kembali ke Server" }, + "serverInfo.backToPartners": { + "message": "← Kembali ke Server Mitra" + }, "serverInfo.quickJoin": { "message": "Bergabung Cepat" }, @@ -289,6 +292,9 @@ "serverInfo.lastUpdated": { "message": "Diperbarui {time} yang lalu" }, + "serverInfo.notFound": { + "message": "Server ini tidak ditemukan. Mungkin server sedang offline atau tidak lagi terdaftar." + }, "downloads.server.pageTitle": { "message": "Server open.mp" }, diff --git a/frontend/i18n/pt-BR/code.json b/frontend/i18n/pt-BR/code.json index 10ecf08397a2..17aab3ff007e 100644 --- a/frontend/i18n/pt-BR/code.json +++ b/frontend/i18n/pt-BR/code.json @@ -272,6 +272,9 @@ "serverInfo.backToServers": { "message": "← Voltar aos Servidores" }, + "serverInfo.backToPartners": { + "message": "← Voltar aos Servidores Parceiros" + }, "serverInfo.quickJoin": { "message": "Entrar Rapidamente" }, @@ -296,6 +299,9 @@ "serverInfo.lastUpdated": { "message": "Atualizado há {time}" }, + "serverInfo.notFound": { + "message": "Este servidor não foi encontrado. Ele pode estar offline ou não estar mais na lista." + }, "downloads.server.pageTitle": { "message": "Servidor open.mp" }, diff --git a/frontend/i18n/th/code.json b/frontend/i18n/th/code.json index 3188e886cb7b..71e31469735b 100644 --- a/frontend/i18n/th/code.json +++ b/frontend/i18n/th/code.json @@ -265,6 +265,9 @@ "serverInfo.backToServers": { "message": "← กลับไปที่เซิร์ฟเวอร์" }, + "serverInfo.backToPartners": { + "message": "← กลับไปที่เซิร์ฟเวอร์พันธมิตร" + }, "serverInfo.quickJoin": { "message": "เข้าร่วมอย่างรวดเร็ว" }, @@ -289,6 +292,9 @@ "serverInfo.lastUpdated": { "message": "อัปเดตเมื่อ {time} ที่แล้ว" }, + "serverInfo.notFound": { + "message": "ไม่พบเซิร์ฟเวอร์นี้ อาจออฟไลน์อยู่หรือถูกนำออกจากรายการแล้ว" + }, "downloads.server.pageTitle": { "message": "เซิร์ฟเวอร์ open.mp" }, diff --git a/frontend/i18n/tr/code.json b/frontend/i18n/tr/code.json index 939018bf5a48..b1c7f481eaf5 100644 --- a/frontend/i18n/tr/code.json +++ b/frontend/i18n/tr/code.json @@ -259,6 +259,9 @@ "serverInfo.backToServers": { "message": "← Sunuculara Geri Dön" }, + "serverInfo.backToPartners": { + "message": "← İş Ortağı Sunuculara Dön" + }, "serverInfo.quickJoin": { "message": "Hızlı Katıl" }, @@ -283,6 +286,9 @@ "serverInfo.lastUpdated": { "message": "{time} önce güncellendi" }, + "serverInfo.notFound": { + "message": "Bu sunucu bulunamadı. Çevrimdışı olabilir veya artık listelenmiyor olabilir." + }, "theme.footer.title.Documentations": { "message": "Dokümantasyon" }, diff --git a/frontend/i18n/uk/code.json b/frontend/i18n/uk/code.json index dd55c7b6d6e8..1d7f1da5a859 100644 --- a/frontend/i18n/uk/code.json +++ b/frontend/i18n/uk/code.json @@ -259,6 +259,9 @@ "serverInfo.backToServers": { "message": "← Назад до серверів" }, + "serverInfo.backToPartners": { + "message": "← Назад до серверів-партнерів" + }, "serverInfo.quickJoin": { "message": "Швидкий вхід" }, @@ -283,6 +286,9 @@ "serverInfo.lastUpdated": { "message": "Оновлено {time} тому" }, + "serverInfo.notFound": { + "message": "Сервер не знайдено. Можливо, він офлайн або його більше немає у списку." + }, "theme.footer.title.Documentations": { "message": "Документація" }, diff --git a/frontend/i18n/zh-CN/code.json b/frontend/i18n/zh-CN/code.json index 023c80757907..31f1d6a57f43 100644 --- a/frontend/i18n/zh-CN/code.json +++ b/frontend/i18n/zh-CN/code.json @@ -272,6 +272,9 @@ "serverInfo.backToServers": { "message": "← 返回服务器列表" }, + "serverInfo.backToPartners": { + "message": "← 返回合作伙伴服务器" + }, "serverInfo.quickJoin": { "message": "快速加入" }, @@ -296,6 +299,9 @@ "serverInfo.lastUpdated": { "message": "{time}前更新" }, + "serverInfo.notFound": { + "message": "找不到该服务器,它可能已离线或不再列出。" + }, "downloads.server.pageTitle": { "message": "open.mp 服务器" }, diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 244be1fac96a..97d845b20214 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -26,6 +26,7 @@ "@docusaurus/module-type-aliases": "^3.10.1", "@docusaurus/tsconfig": "^3.10.1", "@docusaurus/types": "^3.10.1", + "@types/nprogress": "^0.2.3", "typescript": "~5.6.2" }, "engines": { @@ -5173,9 +5174,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5189,9 +5187,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -5205,9 +5200,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5221,9 +5213,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -5712,9 +5701,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -5731,9 +5717,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -5750,9 +5733,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -5769,9 +5749,6 @@ "cpu": [ "s390x" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -5788,9 +5765,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -5807,9 +5781,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -5954,9 +5925,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -5973,9 +5941,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -5992,9 +5957,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -6011,9 +5973,6 @@ "cpu": [ "s390x" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -6030,9 +5989,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -6049,9 +6005,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -6340,6 +6293,13 @@ "undici-types": ">=7.24.0 <7.24.7" } }, + "node_modules/@types/nprogress": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@types/nprogress/-/nprogress-0.2.3.tgz", + "integrity": "sha512-k7kRA033QNtC+gLc4VPlfnue58CM1iQLgn1IMAU8VPHGOj7oIHPp9UlhedEnD/Gl8evoCjwkZjlBORtZ3JByUA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/prismjs": { "version": "1.26.6", "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.6.tgz", @@ -11446,9 +11406,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -11469,9 +11426,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -11492,9 +11446,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -11515,9 +11466,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ diff --git a/frontend/package.json b/frontend/package.json index dec2e2a9f6e4..8e74bc91cb51 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -33,6 +33,7 @@ "@docusaurus/module-type-aliases": "^3.10.1", "@docusaurus/tsconfig": "^3.10.1", "@docusaurus/types": "^3.10.1", + "@types/nprogress": "^0.2.3", "typescript": "~5.6.2" }, "browserslist": { diff --git a/frontend/src/components/ServerInfoPage.tsx b/frontend/src/components/ServerInfoPage.tsx index 897feb9b7074..850038bd6da3 100644 --- a/frontend/src/components/ServerInfoPage.tsx +++ b/frontend/src/components/ServerInfoPage.tsx @@ -1,11 +1,12 @@ import Link from "@docusaurus/Link"; +import Translate from "@docusaurus/Translate"; +import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; import LoadingBanner from "@site/src/components/LoadingBanner"; import { API_ADDRESS } from "@site/src/constants"; +import { formatElapsed } from "@site/src/lib/time"; import { ServerAllData } from "@site/src/types"; import Layout from "@theme/Layout"; -import Translate from "@docusaurus/Translate"; import { useEffect, useState } from "react"; -import BrowserOnly from "@docusaurus/BrowserOnly"; const API_SERVER = (ip: string) => `${API_ADDRESS}/servers/${ip}`; @@ -20,9 +21,15 @@ const getServer = async (ip: string): Promise => { }; const BackLink: React.FC<{ to: string }> = ({ to }) => { + const isPartners = to === "/partners"; + return ( - ← Back to Servers + {isPartners ? ( + ← Back to Partner Servers + ) : ( + ← Back to Servers + )} ); }; @@ -72,6 +79,45 @@ const ServerAddressCopy = ({ address }: { address: string }) => { ); }; +const useNow = (intervalMs = 60_000): number | null => { + const [now, setNow] = useState(null); + + useEffect(() => { + const tick = () => setNow(Date.now()); + tick(); + const id = window.setInterval(tick, intervalMs); + return () => window.clearInterval(id); + }, [intervalMs]); + + return now; +}; + +const UpdatedFooter = ({ lastUpdated }: { lastUpdated: string }) => { + const { + i18n: { currentLocale }, + } = useDocusaurusContext(); + const now = useNow(); + const timestamp = new Date(lastUpdated).getTime(); + + if (now === null || Number.isNaN(timestamp)) { + return null; + } + + return ( +
+ +
+ ); +}; + const Info = ({ data }: { data: ServerAllData }) => (
@@ -159,26 +205,7 @@ const Info = ({ data }: { data: ServerAllData }) => (
- - {() => ( -
- -
- )} -
+
@@ -189,21 +216,43 @@ const Content = ({ ip }: { ip: string }) => { const [data, setData] = useState(undefined); useEffect(() => { + let cancelled = false; + setLoading(true); getServer(ip).then((server) => { - setLoading(false); + if (cancelled) { + return; + } setData(server); + setLoading(false); }); + + return () => { + cancelled = true; + }; }, [ip]); - if (!data || loading) { + if (loading) { return ; } + if (!data) { + return ( +

+ + This server could not be found. It may be offline or no longer listed. + +

+ ); + } + return ; }; -const ServerInfoPage = ({ ip }: { ip: string }) => { +const ServerInfoPage = ({ ip, from }: { ip: string; from?: string }) => { if (!ip) { return ; } @@ -212,7 +261,7 @@ const ServerInfoPage = ({ ip }: { ip: string }) => {
- +
@@ -221,35 +270,3 @@ const ServerInfoPage = ({ ip }: { ip: string }) => { }; export default ServerInfoPage; - -const formatDistance = ( - date: Date | string | number, - baseDate: Date | string | number = new Date() -): string => { - const compareDate = date instanceof Date ? date : new Date(date); - const baseDateObj = baseDate instanceof Date ? baseDate : new Date(baseDate); - - const diff = baseDateObj.getTime() - compareDate.getTime(); - const absDiff = Math.abs(diff); - - const seconds = Math.floor(absDiff / 1000); - const minutes = Math.floor(seconds / 60); - const hours = Math.floor(minutes / 60); - const days = Math.floor(hours / 24); - const months = Math.floor(days / 30); - const years = Math.floor(days / 365); - - if (years > 0) { - return years === 1 ? "1 year" : `${years} years`; - } else if (months > 0) { - return months === 1 ? "1 month" : `${months} months`; - } else if (days > 0) { - return days === 1 ? "1 day" : `${days} days`; - } else if (hours > 0) { - return hours === 1 ? "1 hour" : `${hours} hours`; - } else if (minutes > 0) { - return minutes === 1 ? "1 minute" : `${minutes} minutes`; - } else { - return "less than a minute"; - } -}; diff --git a/frontend/src/components/ServerRow.tsx b/frontend/src/components/ServerRow.tsx index 3188d352297d..11b46801b8d2 100644 --- a/frontend/src/components/ServerRow.tsx +++ b/frontend/src/components/ServerRow.tsx @@ -36,12 +36,12 @@ const CopyBadge = ({ text }: { text: string }) => { ); }; -const ServerRow = ({ server }: { server: CoreServerData }) => { +const ServerRow = ({ server, from }: { server: CoreServerData; from?: string }) => { return (

{server.hn}

diff --git a/frontend/src/lib/time.ts b/frontend/src/lib/time.ts new file mode 100644 index 000000000000..78f8e1b0679a --- /dev/null +++ b/frontend/src/lib/time.ts @@ -0,0 +1,123 @@ +const UNIT_STEPS = [ + ["year", 365 * 24 * 60 * 60], + ["month", 30 * 24 * 60 * 60], + ["day", 24 * 60 * 60], + ["hour", 60 * 60], + ["minute", 60], +] as const; + +type TimeUnit = (typeof UNIT_STEPS)[number][0]; + +const ENGLISH_UNITS: Record = { + year: ["year", "years"], + month: ["month", "months"], + day: ["day", "days"], + hour: ["hour", "hours"], + minute: ["minute", "minutes"], +}; + +const formatters = new Map(); + +let unitStyleSupport: boolean | undefined; + +function supportsUnitStyle(): boolean { + if (unitStyleSupport === undefined) { + try { + unitStyleSupport = + new Intl.NumberFormat("en", { + style: "unit", + unit: "hour", + unitDisplay: "long", + }).format(1) === "1 hour"; + } catch { + unitStyleSupport = false; + } + } + + return unitStyleSupport; +} + +function resolveLocale(locale: string): string { + try { + const [supported] = Intl.NumberFormat.supportedLocalesOf([locale]); + return supported ?? "en"; + } catch { + return "en"; + } +} + +function getUnitFormatter( + locale: string, + unit: TimeUnit, +): Intl.NumberFormat | undefined { + if (!supportsUnitStyle()) { + return undefined; + } + + const resolved = resolveLocale(locale); + const cacheKey = `${resolved}|${unit}`; + const cached = formatters.get(cacheKey); + + if (cached) { + return cached; + } + + const formatter = new Intl.NumberFormat(resolved, { + style: "unit", + unit, + unitDisplay: "long", + maximumFractionDigits: 0, + }); + + formatters.set(cacheKey, formatter); + return formatter; +} + +function formatEnglishUnit(value: number, unit: TimeUnit): string { + const [singular, plural] = ENGLISH_UNITS[unit]; + return `${value} ${value === 1 ? singular : plural}`; +} + +export function formatElapsed( + locale: string, + timestamp: number, + now: number, +): string { + const seconds = Math.max(0, Math.round((now - timestamp) / 1000)); + const parts: string[] = []; + let remaining = seconds; + + for (const [unit, size] of UNIT_STEPS) { + const value = Math.floor(remaining / size); + remaining -= value * size; + + if (value === 0) { + continue; + } + + parts.push( + getUnitFormatter(locale, unit)?.format(value) ?? + formatEnglishUnit(value, unit), + ); + + if (parts.length === 3) { + break; + } + } + + if (parts.length === 0) { + return ( + getUnitFormatter(locale, "minute")?.format(1) ?? + formatEnglishUnit(1, "minute") + ); + } + + try { + return new Intl.ListFormat(resolveLocale(locale), { + style: "long", + type: "unit", + }).format(parts); + } catch { + return parts.join(" "); + } +} diff --git a/frontend/src/pages/partners.tsx b/frontend/src/pages/partners.tsx index 5f4608ce9850..4445f013b47f 100644 --- a/frontend/src/pages/partners.tsx +++ b/frontend/src/pages/partners.tsx @@ -2,7 +2,11 @@ import Layout from "@theme/Layout"; import { ReactNode, useEffect, useMemo, useState } from "react"; import clsx from "clsx"; import { default as Translate, translate } from "@docusaurus/Translate"; -import { List as FixedSizeList, RowComponentProps } from "react-window"; +import { + List as FixedSizeList, + RowComponentProps, + useListRef, +} from "react-window"; import LoadingBanner from "../components/LoadingBanner"; import Admonition from "../components/Admonition"; import ServerRow from "../components/ServerRow"; @@ -199,23 +203,25 @@ const StatsComponent = ({ stats: { players, servers } }: { stats: Stats }) => { ); }; +const initialQuery: Query = { + search: "", + showEmpty: true, + showPassworded: true, + language: "", + sort: "relevance", +}; + // List Component const List = ({ data }: { data: CoreServerData[] }) => { - const [search, setSearch] = useState(""); - const [showEmpty, setShowEmpty] = useState(true); - const [showPassworded, setShowPassworded] = useState(true); - const [language, setLanguage] = useState(""); - const [sort, setSort] = useState("relevance"); - - const filteredData = useMemo(() => { - return filterServers(data, { - search, - showEmpty, - showPassworded, - language, - sort, - }); - }, [data, search, showEmpty, showPassworded, language, sort]); + const [query, setQuery] = useState(initialQuery); + + const filteredData = useMemo(() => filterServers(data, query), [data, query]); + + const listRef = useListRef(null); + + useEffect(() => { + listRef.current?.element?.scrollTo({ top: 0, behavior: "smooth" }); + }, [query]); const rowHeight = 134; @@ -223,7 +229,7 @@ const List = ({ data }: { data: CoreServerData[] }) => { const server = filteredData[index]; return (
- +
); }; @@ -233,8 +239,10 @@ const List = ({ data }: { data: CoreServerData[] }) => {
setShowEmpty(e.target.checked)} + checked={query.showEmpty} + onChange={(e) => + setQuery((q) => ({ ...q, showEmpty: e.target.checked })) + } className="servers-checkbox" /> @@ -323,8 +337,10 @@ const List = ({ data }: { data: CoreServerData[] }) => {