Skip to content

Commit f987562

Browse files
committed
add partners page
1 parent e19d1dd commit f987562

File tree

3 files changed

+178
-7
lines changed

3 files changed

+178
-7
lines changed

frontend/docusaurus.config.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -177,11 +177,8 @@ const config: Config = {
177177
label: "Docs",
178178
},
179179
{ to: "/blog", label: "Blog", position: "right" },
180-
{
181-
to: "/servers",
182-
label: "Servers",
183-
position: "right",
184-
},
180+
{ to: "/servers", label: "Servers", position: "right" },
181+
{ to: "/partners", label: "Partners", position: "right" },
185182
{
186183
type: "localeDropdown", // This adds the language switcher
187184
position: "right",

frontend/src/pages/partners.tsx

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import Layout from "@theme/Layout";
2+
import { ReactNode, useEffect, useMemo, useState } from "react";
3+
import { FixedSizeList } from "react-window";
4+
import LoadingBanner from "../components/LoadingBanner";
5+
import ServerRow from "../components/ServerRow";
6+
import { ToastContainer } from "../components/Toast";
7+
import { API_ADDRESS } from "../constants";
8+
import { CoreServerData } from "../types";
9+
10+
const API_SERVERS = `${API_ADDRESS}/servers/`;
11+
12+
const getServers = async () => {
13+
try {
14+
const r: Response = await fetch(API_SERVERS);
15+
const servers: CoreServerData[] = await r.json();
16+
return servers;
17+
} catch (e) {
18+
return [];
19+
}
20+
};
21+
22+
type Stats = {
23+
players: number;
24+
servers: number;
25+
};
26+
27+
const getStats = (servers: CoreServerData[]): Stats => ({
28+
players: servers.map((s) => s.pc).reduce((acc, pc) => acc + pc, 0),
29+
servers: servers.length,
30+
});
31+
32+
type SortBy = "relevance" | "pc";
33+
34+
type Query = {
35+
search?: string;
36+
sort: SortBy;
37+
};
38+
39+
// Filters data
40+
const filterServers = (data: CoreServerData[], q: Query): CoreServerData[] => {
41+
let filteredData = [...data];
42+
43+
if (q.search) {
44+
const searchTerm = q.search.toLowerCase();
45+
filteredData = filteredData.filter(
46+
(s) =>
47+
s.ip.toLowerCase().includes(searchTerm) ||
48+
s.hn.toLowerCase().includes(searchTerm) ||
49+
s.gm.toLowerCase().includes(searchTerm)
50+
);
51+
}
52+
53+
// Sorting Logic
54+
if (q.sort === "pc") {
55+
filteredData.sort((a, b) => b.pc - a.pc); // Sort by players, descending
56+
}
57+
//Relevance would be the original order
58+
59+
return filteredData;
60+
};
61+
62+
const StatsComponent = ({ stats: { players, servers } }: { stats: Stats }) => {
63+
return (
64+
<div className="servers-center">
65+
<p className="servers-stats">
66+
<strong>{players}</strong> players on <strong>{servers}</strong> servers
67+
with an average of <strong>{(players / servers).toFixed(1)}</strong>{" "}
68+
players per server.
69+
</p>
70+
</div>
71+
);
72+
};
73+
74+
// List Component
75+
const List = ({ data }: { data: CoreServerData[] }) => {
76+
const [search, setSearch] = useState("");
77+
const [sort, setSort] = useState<SortBy>("relevance");
78+
79+
const filteredData = useMemo(() => {
80+
return filterServers(data, {
81+
search,
82+
sort,
83+
});
84+
}, [data, search, sort]);
85+
86+
const rowHeight = 134;
87+
const listHeight = 1000;
88+
const visibleItems = Math.floor(listHeight / rowHeight);
89+
90+
const Row = ({ index, style }) => {
91+
const server = filteredData[index];
92+
return (
93+
<div style={style}>
94+
<ServerRow key={server.ip} server={server} />
95+
</div>
96+
);
97+
};
98+
99+
return (
100+
<>
101+
<form className="servers-list-form">
102+
<div className="servers-controls">
103+
<select
104+
value={sort}
105+
onChange={(e) => setSort(e.target.value as SortBy)}
106+
className="servers-select"
107+
>
108+
<option value="relevance">Relevance</option>
109+
<option value="pc">Players</option>
110+
</select>
111+
112+
<input
113+
type="text"
114+
placeholder="Search by IP or Name"
115+
name="search"
116+
id="search"
117+
value={search}
118+
onChange={(e) => setSearch(e.target.value)}
119+
className="servers-search"
120+
/>
121+
</div>
122+
</form>
123+
124+
<StatsComponent stats={getStats(data)} />
125+
126+
<FixedSizeList
127+
height={(filteredData.length + 1) * rowHeight}
128+
width="100%"
129+
itemSize={rowHeight}
130+
itemCount={filteredData.length}
131+
overscanCount={visibleItems}
132+
>
133+
{Row}
134+
</FixedSizeList>
135+
</>
136+
);
137+
};
138+
139+
const Page = (): ReactNode => {
140+
const [loading, setLoading] = useState<boolean>(true);
141+
const [data, setData] = useState<CoreServerData[]>([]);
142+
143+
useEffect(() => {
144+
getServers().then((servers) => {
145+
setLoading(false);
146+
setData(servers.filter((server) => server.pr));
147+
});
148+
}, []);
149+
150+
return (
151+
<div>
152+
<Layout
153+
title={`Servers`}
154+
description="List of San Andreas servers using open.mp or SA-MP"
155+
>
156+
<section className="servers-container">
157+
<p>
158+
<b>
159+
Note: The partnership program application is temporarily closed as
160+
promised. Servers that have already reserved a slot can still
161+
join, but we are not accepting new requests at this time. If you
162+
have any questions, feel free to ask on our Discord. However, if
163+
your question is about new ways to get on the list, we currently
164+
have no plans for that.
165+
</b>
166+
</p>
167+
{loading ? <LoadingBanner /> : <List data={data} />}
168+
</section>
169+
</Layout>
170+
<ToastContainer />
171+
</div>
172+
);
173+
};
174+
175+
export default Page;

frontend/src/pages/servers.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,9 @@ import NProgress from "nprogress";
33
import React, {
44
FormEvent,
55
ReactNode,
6-
useCallback,
76
useEffect,
87
useMemo,
9-
useState,
8+
useState
109
} from "react";
1110
import { FixedSizeList } from "react-window";
1211
import LoadingBanner from "../components/LoadingBanner";

0 commit comments

Comments
 (0)