everything rendering

This commit is contained in:
2025-09-25 21:07:24 +02:00
parent 7b7855709c
commit 459c0c7b74
9 changed files with 264 additions and 71 deletions

View File

@ -4,6 +4,7 @@
"": {
"name": "nguhmap",
"dependencies": {
"@types/bun": "^1.2.22",
"@types/jsdom": "^21.1.7",
"@types/leaflet": "^1.9.20",
"@types/react-leaflet": "^3.0.0",
@ -11,6 +12,7 @@
"jsdom": "^27.0.0",
"leaflet": "^1.9.4",
"next": "15.5.4",
"next-auth": "^5.0.0-beta.29",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-leaflet": "^5.0.0",
@ -34,6 +36,8 @@
"@asamuzakjp/nwsapi": ["@asamuzakjp/nwsapi@2.3.9", "", {}, "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q=="],
"@auth/core": ["@auth/core@0.40.0", "", { "dependencies": { "@panva/hkdf": "^1.2.1", "jose": "^6.0.6", "oauth4webapi": "^3.3.0", "preact": "10.24.3", "preact-render-to-string": "6.5.11" }, "peerDependencies": { "@simplewebauthn/browser": "^9.0.1", "@simplewebauthn/server": "^9.0.2", "nodemailer": "^6.8.0" }, "optionalPeers": ["@simplewebauthn/browser", "@simplewebauthn/server", "nodemailer"] }, "sha512-n53uJE0RH5SqZ7N1xZoMKekbHfQgjd0sAEyUbE+IYJnmuQkbvuZnXItCU7d+i7Fj8VGOgqvNO7Mw4YfBTlZeQw=="],
"@csstools/color-helpers": ["@csstools/color-helpers@5.1.0", "", {}, "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA=="],
"@csstools/css-calc": ["@csstools/css-calc@2.1.4", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ=="],
@ -154,6 +158,8 @@
"@nolyfill/is-core-module": ["@nolyfill/is-core-module@1.0.39", "", {}, "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA=="],
"@panva/hkdf": ["@panva/hkdf@1.2.1", "", {}, "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw=="],
"@react-leaflet/core": ["@react-leaflet/core@3.0.0", "", { "peerDependencies": { "leaflet": "^1.9.0", "react": "^19.0.0", "react-dom": "^19.0.0" } }, "sha512-3EWmekh4Nz+pGcr+xjf0KNyYfC3U2JjnkWsh0zcqaexYqmmB5ZhH37kz41JXGmKzpaMZCnPofBBm64i+YrEvGQ=="],
"@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="],
@ -164,6 +170,8 @@
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
"@types/bun": ["@types/bun@1.2.22", "", { "dependencies": { "bun-types": "1.2.22" } }, "sha512-5A/KrKos2ZcN0c6ljRSOa1fYIyCKhZfIVYeuyb4snnvomnpFqC0tTsEkdqNxbAgExV384OETQ//WAjl3XbYqQA=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@types/geojson": ["@types/geojson@7946.0.16", "", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="],
@ -294,6 +302,8 @@
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
"bun-types": ["bun-types@1.2.22", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-hwaAu8tct/Zn6Zft4U9BsZcXkYomzpHJX28ofvx7k0Zz2HNz54n1n+tDgxoWFGB4PcFvJXJQloPhaV2eP3Q6EA=="],
"call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="],
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
@ -542,6 +552,8 @@
"iterator.prototype": ["iterator.prototype@1.1.5", "", { "dependencies": { "define-data-property": "^1.1.4", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "get-proto": "^1.0.0", "has-symbols": "^1.1.0", "set-function-name": "^2.0.2" } }, "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g=="],
"jose": ["jose@6.1.0", "", {}, "sha512-TTQJyoEoKcC1lscpVDCSsVgYzUDg/0Bt3WE//WiTPK6uOCQC2KZS4MpugbMWt/zyjkopgZoXhZuCi00gLudfUA=="],
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
"js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="],
@ -598,6 +610,10 @@
"next": ["next@15.5.4", "", { "dependencies": { "@next/env": "15.5.4", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.5.4", "@next/swc-darwin-x64": "15.5.4", "@next/swc-linux-arm64-gnu": "15.5.4", "@next/swc-linux-arm64-musl": "15.5.4", "@next/swc-linux-x64-gnu": "15.5.4", "@next/swc-linux-x64-musl": "15.5.4", "@next/swc-win32-arm64-msvc": "15.5.4", "@next/swc-win32-x64-msvc": "15.5.4", "sharp": "^0.34.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-xH4Yjhb82sFYQfY3vbkJfgSDgXvBB6a8xPs9i35k6oZJRoQRihZH+4s9Yo2qsWpzBmZ3lPXaJ2KPXLfkvW4LnA=="],
"next-auth": ["next-auth@5.0.0-beta.29", "", { "dependencies": { "@auth/core": "0.40.0" }, "peerDependencies": { "@simplewebauthn/browser": "^9.0.1", "@simplewebauthn/server": "^9.0.2", "next": "^14.0.0-0 || ^15.0.0-0", "nodemailer": "^6.6.5", "react": "^18.2.0 || ^19.0.0-0" }, "optionalPeers": ["@simplewebauthn/browser", "@simplewebauthn/server", "nodemailer"] }, "sha512-Ukpnuk3NMc/LiOl32njZPySk7pABEzbjhMUFd5/n10I0ZNC7NCuVv8IY2JgbDek2t/PUOifQEoUiOOTLy4os5A=="],
"oauth4webapi": ["oauth4webapi@3.8.1", "", {}, "sha512-olkZDELNycOWQf9LrsELFq8n05LwJgV8UkrS0cburk6FOwf8GvLam+YB+Uj5Qvryee+vwWOfQVeI5Vm0MVg7SA=="],
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
@ -640,6 +656,10 @@
"postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
"preact": ["preact@10.24.3", "", {}, "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA=="],
"preact-render-to-string": ["preact-render-to-string@6.5.11", "", { "peerDependencies": { "preact": ">=10" } }, "sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw=="],
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
"prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],

View File

@ -9,6 +9,7 @@
"lint": "eslint"
},
"dependencies": {
"@types/bun": "^1.2.22",
"@types/jsdom": "^21.1.7",
"@types/leaflet": "^1.9.20",
"@types/react-leaflet": "^3.0.0",
@ -16,6 +17,7 @@
"jsdom": "^27.0.0",
"leaflet": "^1.9.4",
"next": "15.5.4",
"next-auth": "^5.0.0-beta.29",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-leaflet": "^5.0.0",

10
public/legend/poi.svg Normal file
View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
<svg
xmlns="http://www.w3.org/2000/svg"
width="12px"
height="12x"
viewBox="0 0 12 12"
version="1.1"
>
<circle style="stroke-width:2px;stroke:#000;fill:#fff" x="1" y="1" r="5" transform="translate(6,6)"/>
</svg>

After

Width:  |  Height:  |  Size: 286 B

View File

@ -0,0 +1,2 @@
import { handlers } from "@/auth"
export const { GET, POST } = handlers

15
src/app/client.tsx Normal file
View File

@ -0,0 +1,15 @@
"use client";
import dynamic from "next/dynamic";
import { useMemo } from "react";
import type {MapData} from "@/app/map"
export function Map(props: {data: MapData}) {
const It = useMemo(() => dynamic(
() => import("@/app/map"),
{
loading: () => <p>Loading</p>,
ssr: false,
}
), [])
return <It {...props} />;
}

View File

@ -1,20 +1,135 @@
import { MapContainer, Rectangle, TileLayer } from "react-leaflet";
import L from "leaflet";
import { MapContainer, Marker, Rectangle, TileLayer, Tooltip, Polyline, Polygon } from "react-leaflet";
import L, { Icon } from "leaflet";
export default function Map() {
export type CountryEntry = {
code2: string;
code3: string;
common_name: string;
name?: string;
ruler?: string | string[];
ruler_title?: string;
ruler_link?: string;
founded?: string;
capital?: string;
ung?: "MEMBER" | "OBSERVER" | "FORMER";
ung_joined?: string;
ung_demoted?: string;
ung_left?: string;
dissolved?: string | true;
dissolved_date?: string;
disputed?: string | true;
not_ngation?: true;
condominium?: string[]
};
export type POI = {
coordinates: string;
label: string;
id: number;
last_editor: number | string
};
export type Road = {
id: number;
path: string;
network: string;
name: string;
last_editor: number | string
}
export type CountryPart = {
id: number;
country: string;
shape: string;
last_editor: number | string;
}
export type RailLine = {
id: String;
path: String;
last_editor: number | string;
}
export type Settlement = {
id: number;
coordinates: string;
name: string;
last_editor: number | string
}
export type TransitStop = {
id: string;
coordinates: string;
last_editor: number | string;
}
export type MapData = {
countries: CountryEntry[];
parts: CountryPart[];
pois: POI[];
farms: POI[];
rails: RailLine[];
roads: Road[];
settlements: Settlement[];
stops: TransitStop[];
}
function find_entry(code: string, entries: CountryEntry[]) : CountryEntry|undefined {
return entries.find((it) => it.code2 === code || it.code3 === code)
}
function parseCoords(desc: string) : [number, number][] {
console.log(desc);
const foo = [...desc.matchAll(/\((-?\d+),(-?\d+)\)/g)]
return foo.map(it => [-+it[2] - .5, +it[1] + .5])
}
export default function Map(props: { data: MapData }) {
const { data } = props;
return (
<MapContainer
id="map"
center={[0, 0]}
maxBounds={[[-8192, -8192], [8192, 8192]]}
crs={L.CRS.Simple}
zoom={0}>
<TileLayer
url="/tiles/{z}/{x}_{y}.png"
tileSize={512}
minZoom={-4}
maxNativeZoom={0} />
<Rectangle bounds={[[-8000, -8000], [8000, 8000]]} pathOptions={{ color: "#000", stroke: true, fill: false, weight: 2 }} />
</MapContainer>
)
<>
<MapContainer
id="map"
center={[0, 0]}
maxBounds={[[-8192, -8192], [8192, 8192]]}
crs={L.CRS.Simple}
zoom={0}>
<TileLayer
url="/tiles/{z}/{x}_{y}.png"
tileSize={512}
minZoom={-4}
maxNativeZoom={0} />
<Rectangle bounds={[[-8000, -8000], [8000, 8000]]} pathOptions={{ color: "#000", stroke: true, fill: false, weight: 2 }} />
{
data.pois.map(it =>
<Marker key={it.id} position={parseCoords(it.coordinates)[0]} icon={new Icon({iconUrl: "/legend/poi.svg", iconAnchor: [6, 6]})}>
<Tooltip>{it.label}</Tooltip>
</Marker>
)
}
{
data.settlements.map(it =>
<Marker key={it.id} position={parseCoords(it.coordinates)[0]} icon={new Icon({iconUrl: "/legend/city.svg", iconAnchor: [6, 6]})}>
<Tooltip>{it.name}</Tooltip>
</Marker>
)
}
{
data.roads.map(it =>
<Polyline key={it.id} positions={parseCoords(it.path)} stroke={true} color="#FFF">
<Tooltip>{find_entry(it.network, data.countries)?.common_name} {it.name}</Tooltip>
</Polyline>
)
}
{
data.parts.map(it =>
<Polygon key={it.id} positions={parseCoords(it.shape)} stroke={true} color="#A00" fill={true} fillOpacity={.5} fillColor="#A00">
<Tooltip position={parseCoords(it.shape)[0]}>{find_entry(it.country, data.countries)?.common_name}</Tooltip>
</Polygon>
)
}
{data.stops.map(it => <Marker key={it.id} position={parseCoords(it.coordinates)[0]}><Tooltip>{it.id}</Tooltip></Marker>)}
</MapContainer>
</>)
}

View File

@ -1,60 +1,46 @@
"use client";
import { useMemo } from "react";
import Modal from "react-modal";
import dynamic from "next/dynamic";
import { auth, signIn } from "@/auth";
import { Map } from "@/app/client";
import type {MapData, CountryEntry, CountryPart, POI, RailLine, Settlement, TransitStop, Road} from "@/app/map"
import { sql } from "bun"
import { JSDOM } from "jsdom"
type SetState<T> = (_: T) => void;
type StateInit<T> = [T, SetState<T>];
function Login(params: { modal: StateInit<boolean>, login: StateInit<string | undefined>; }) {
const { modal, login } = params;
const customStyles = {
content: {
top: '50%',
left: '50%',
right: 'auto',
bottom: 'auto',
marginRight: '-50%',
transform: 'translate(-50%, -50%)',
},
};
const openModal = () => modal[1](true)
const afterOpenModal = () => { }
const closeModal = () => modal[1](false)
return (<>
<button type="button" onClick={openModal}>Log In</button>
<Modal
isOpen={modal[0]}
onAfterOpen={afterOpenModal}
onRequestClose={closeModal}
style={customStyles}
contentLabel="Log In"
ariaHideApp={false}
>
<h2>Authentification</h2>
<form action={(e: FormData) => {
// TODO Check Auth
const l = e.get("login")
if (l == null) return;
closeModal()
login[1](l.toString())
}}>
<input name="login" type="text" placeholder="Login" required={true} /><br />
<input name="password" type="password" placeholder="Password" required={true} /><br />
<button id="auth!submit" type="submit">Log In</button>
</form>
</Modal>
</>)
export async function getCountries () : Promise<CountryEntry[]> {
let data_raw = await fetch("https://mc.nguh.org/w/api.php?action=parse&page=Data:UŊCDSO%2FCountries&prop=text&format=json")
let data = await data_raw.json()
let parsed_text : string = data.parse.text["*"];
let parsed_xml = new JSDOM(parsed_text);
let json_text = parsed_xml.window.document.querySelector("#jsondata")?.textContent
if (json_text != undefined)
return JSON.parse(json_text);
else return [];
}
export default function Home() {
const Map = useMemo(() => dynamic(
() => import("@/app/map"),
{
loading: () => <p>Loading</p>,
ssr: false,
}
), [])
return <Map />
async function getMapData() : Promise<MapData> {
const [countries, parts, pois, farms, rails, roads, settlements, stops] :
[CountryEntry[], CountryPart[], POI[], POI[], RailLine[], Road[], Settlement[], TransitStop[]] = await Promise.all([
getCountries(),
sql`SELECT * from country_parts`,
sql`SELECT * from points_of_interest`,
sql`SELECT * from public_farms`,
sql`SELECT * from rail_lines`,
sql`SELECT * from roads`,
sql`SELECT * from settlements`,
sql`SELECT * from transit_stops`
])
return { countries, parts, pois, farms, rails, roads, settlements, stops }
}
export default async function Home() {
const session = await auth();
return <>
<Map data={await getMapData()}/>
{(session) ? null : <form action={async () => {
'use server';
await signIn("discord")
}}><button type="submit">Log In</button></form>}
</>
}

42
src/auth.ts Normal file
View File

@ -0,0 +1,42 @@
import NextAuth from "next-auth"
import Discord from 'next-auth/providers/discord';
declare module 'next-auth' {
interface Session {
access_token: string | undefined,
discord_id?: string
}
}
export const { handlers, signIn, signOut, auth } = NextAuth({
providers: [Discord({
authorization: {
params: {
scope: "identify guilds"
}
},
clientId: process.env.AUTH_DISCORD_ID!,
clientSecret: process.env.AUTH_DISCORD_SECRET!,
})],
callbacks: {
async jwt({token, account, profile, user}) {
// During sign-in, 'user' is set; this is when we need to save the access_token etc.
if (user) return {
...token,
access_token: account?.access_token,
discord_id: profile?.id
}
// Otherwise, were fetching an existing token, so take care not to override it.
return token
},
async session({token, session}) {
session.access_token = token.access_token as string
session.discord_id = token.discord_id as string
return session
}
},
trustHost: true,
secret: process.env.AUTH_SECRET!,
})

1
src/middleware.ts Normal file
View File

@ -0,0 +1 @@
export { auth as middleware } from "@/auth"