Compare commits
1 Commits
459c0c7b74
...
main
Author | SHA1 | Date | |
---|---|---|---|
4547ef8be2
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -42,3 +42,4 @@ next-env.d.ts
|
|||||||
|
|
||||||
# Large images
|
# Large images
|
||||||
/public/tiles
|
/public/tiles
|
||||||
|
/public/fonts
|
6
bun.lock
6
bun.lock
@ -13,10 +13,12 @@
|
|||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"next": "15.5.4",
|
"next": "15.5.4",
|
||||||
"next-auth": "^5.0.0-beta.29",
|
"next-auth": "^5.0.0-beta.29",
|
||||||
|
"next-safe-action": "^8.0.11",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
"react-leaflet": "^5.0.0",
|
"react-leaflet": "^5.0.0",
|
||||||
"react-modal": "^3.16.3",
|
"react-modal": "^3.16.3",
|
||||||
|
"zod": "^4.1.11",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3",
|
"@eslint/eslintrc": "^3",
|
||||||
@ -612,6 +614,8 @@
|
|||||||
|
|
||||||
"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=="],
|
"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=="],
|
||||||
|
|
||||||
|
"next-safe-action": ["next-safe-action@8.0.11", "", { "peerDependencies": { "next": ">= 14.0.0", "react": ">= 18.2.0", "react-dom": ">= 18.2.0" } }, "sha512-gqJLmnQLAoFCq1kRBopN46New+vx1n9J9Y/qDQLXpv/VqU40AWxDakvshwwnWAt8R0kLvlakNYNLX5PqlXWSMg=="],
|
||||||
|
|
||||||
"oauth4webapi": ["oauth4webapi@3.8.1", "", {}, "sha512-olkZDELNycOWQf9LrsELFq8n05LwJgV8UkrS0cburk6FOwf8GvLam+YB+Uj5Qvryee+vwWOfQVeI5Vm0MVg7SA=="],
|
"oauth4webapi": ["oauth4webapi@3.8.1", "", {}, "sha512-olkZDELNycOWQf9LrsELFq8n05LwJgV8UkrS0cburk6FOwf8GvLam+YB+Uj5Qvryee+vwWOfQVeI5Vm0MVg7SA=="],
|
||||||
|
|
||||||
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
||||||
@ -832,6 +836,8 @@
|
|||||||
|
|
||||||
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||||
|
|
||||||
|
"zod": ["zod@4.1.11", "", {}, "sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg=="],
|
||||||
|
|
||||||
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
||||||
|
|
||||||
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
|
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
|
||||||
|
@ -18,10 +18,12 @@
|
|||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"next": "15.5.4",
|
"next": "15.5.4",
|
||||||
"next-auth": "^5.0.0-beta.29",
|
"next-auth": "^5.0.0-beta.29",
|
||||||
|
"next-safe-action": "^8.0.11",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
"react-leaflet": "^5.0.0",
|
"react-leaflet": "^5.0.0",
|
||||||
"react-modal": "^3.16.3"
|
"react-modal": "^3.16.3",
|
||||||
|
"zod": "^4.1.11"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5",
|
"typescript": "^5",
|
||||||
|
@ -1,9 +1,64 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import dynamic from "next/dynamic";
|
|
||||||
import { useMemo } from "react";
|
|
||||||
import type {MapData} from "@/app/map"
|
|
||||||
|
|
||||||
export function Map(props: {data: MapData}) {
|
import dynamic from "next/dynamic";
|
||||||
|
import { ReactNode, RefObject, useMemo, useRef, useState } from "react";
|
||||||
|
import { Coords, EditStates, MapData, Setter } from "@/app/types"
|
||||||
|
import { addPOI } from "@/app/db-actions";
|
||||||
|
import { useAction } from "next-safe-action/hooks";
|
||||||
|
import { signIn } from "@/auth";
|
||||||
|
|
||||||
|
export function Dialog({
|
||||||
|
ref,
|
||||||
|
title,
|
||||||
|
buttons,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
ref: RefObject<HTMLDialogElement | null>
|
||||||
|
title: ReactNode,
|
||||||
|
children?: ReactNode,
|
||||||
|
buttons: {
|
||||||
|
label: ReactNode,
|
||||||
|
action?: () => any,
|
||||||
|
}[]
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<dialog ref={ref} className="dialog-main">
|
||||||
|
<div className='dialog-title'>
|
||||||
|
{title}
|
||||||
|
</div>
|
||||||
|
<div className='dialog-body'>
|
||||||
|
<div className='dialog-elems'>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
<div className='dialog-buttons'>
|
||||||
|
{buttons.map(({ label, action }, index) =>
|
||||||
|
<button key={index}
|
||||||
|
onClick={async () => {
|
||||||
|
if (action) {
|
||||||
|
const a = action()
|
||||||
|
if (a instanceof Promise) await a
|
||||||
|
}
|
||||||
|
ref.current?.close()
|
||||||
|
}}
|
||||||
|
>{label}</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Map(props: {
|
||||||
|
data: MapData,
|
||||||
|
coords: Coords,
|
||||||
|
setCoords: Setter<Coords>,
|
||||||
|
curPath?: Coords[],
|
||||||
|
setCurPath: Setter<Coords[]>,
|
||||||
|
editState?: EditStates,
|
||||||
|
setEditState: Setter<EditStates>
|
||||||
|
}) {
|
||||||
const It = useMemo(() => dynamic(
|
const It = useMemo(() => dynamic(
|
||||||
() => import("@/app/map"),
|
() => import("@/app/map"),
|
||||||
{
|
{
|
||||||
@ -13,3 +68,36 @@ export function Map(props: {data: MapData}) {
|
|||||||
), [])
|
), [])
|
||||||
return <It {...props} />;
|
return <It {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function Toolbar(props: { editState: EditStates, setEditState: Setter<EditStates> }) {
|
||||||
|
const addPOIAction = useAction(addPOI, { onSuccess: () => { alert("Doing stuff I guess"); addPOIAction.reset() } })
|
||||||
|
return <>
|
||||||
|
<div className='toolbar'>
|
||||||
|
{
|
||||||
|
[
|
||||||
|
{ label: "Add PoI", state: EditStates.ADD_POI },
|
||||||
|
{ label: "Add Settlement", state: EditStates.ADD_SETTLEMENT },
|
||||||
|
{ label: "Add Road", state: EditStates.ADD_ROAD },
|
||||||
|
{ label: "Add Rail", state: EditStates.ADD_TRAIN },
|
||||||
|
{ label: "Add Transit Station", state: EditStates.ADD_STOP },
|
||||||
|
{ label: "Add Country Part", state: EditStates.ADD_COUNTRY },
|
||||||
|
].map(it =>
|
||||||
|
<button key={it.state} onClick={() => props.setEditState(it.state)}>{it.label}</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<p>Current State: {EditStates[props.editState]}</p>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Client({ data, id }: { data: MapData, id?: string }) {
|
||||||
|
const [coords, setCoords]: [Coords, Setter<Coords>] = useState([0, 0])
|
||||||
|
const [editState, setEditState]: [EditStates | undefined, Setter<EditStates>] = useState()
|
||||||
|
const [curPath, setCurPath]: [Coords[] | undefined, Setter<Coords[]>] = useState()
|
||||||
|
return <>
|
||||||
|
<Map {...{ data, coords, setCoords, curPath, setCurPath, editState, setEditState }} />
|
||||||
|
{
|
||||||
|
(!id) ? null : <Toolbar {...{editState: editState ?? EditStates.NONE, setEditState}}/>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
}
|
31
src/app/db-actions.ts
Normal file
31
src/app/db-actions.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
"use server";
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
import { returnValidationErrors } from "next-safe-action";
|
||||||
|
import { actionClient } from "@/app/safe-action";
|
||||||
|
import { auth } from "@/auth";
|
||||||
|
import { notFound } from "next/navigation";
|
||||||
|
import { sql } from "bun";
|
||||||
|
import { revalidatePath } from "next/cache";
|
||||||
|
|
||||||
|
const addPOISchema = z.object({
|
||||||
|
label: z.string().trim().min(1).max(256),
|
||||||
|
coordinates: z.object({
|
||||||
|
x: z.number().min(-8000).max(8000),
|
||||||
|
y: z.number().min(-8000).max(8000)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
export const addPOI = actionClient
|
||||||
|
.inputSchema(addPOISchema)
|
||||||
|
.action(async ({parsedInput: { label, coordinates }}) => {
|
||||||
|
console.log("Arrived in the server action")
|
||||||
|
const id = (await auth())?.discord_id
|
||||||
|
if (!id) notFound();
|
||||||
|
const coords_formatted = `(${coordinates.x},${coordinates.y})`
|
||||||
|
await sql.begin(async (tx) =>
|
||||||
|
await tx`INSERT INTO points_of_interest (label, coordinates, last_editor) VALUES (${label}, ${coords_formatted}, ${id}) RETURNING *`
|
||||||
|
)
|
||||||
|
revalidatePath("/");
|
||||||
|
return {};
|
||||||
|
})
|
@ -1,12 +1,45 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: Andika;
|
||||||
|
font-feature-settings: "ss13" on;
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
src: url("/fonts/Andika-Regular.ttf");
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: Andika;
|
||||||
|
font-feature-settings: "ss13" on;
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: italic;
|
||||||
|
src: url("/fonts/Andika-Italic.ttf");
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: Andika;
|
||||||
|
font-feature-settings: "ss13" on;
|
||||||
|
font-weight: bold;
|
||||||
|
font-style: normal;
|
||||||
|
src: url("/fonts/Andika-Bold.ttf");
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: Andika;
|
||||||
|
font-feature-settings: "ss13" on;
|
||||||
|
font-weight: bold;
|
||||||
|
font-style: italic;
|
||||||
|
src: url("/fonts/Andika-BoldItalic.ttf");
|
||||||
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--background: #fff;
|
--background: #fff;
|
||||||
--foreground: #000;
|
--foreground: #000;
|
||||||
|
--background-accent: #ccc;
|
||||||
|
--background-accent2: #bbb;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
:root {
|
:root {
|
||||||
--background: #000;
|
--background: #000;
|
||||||
--foreground: #fff;
|
--foreground: #fff;
|
||||||
|
--background-accent: #444;
|
||||||
|
--background-accent2: #555;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -24,6 +57,7 @@ body {
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
font-family: 'Andika', sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
@ -31,3 +65,54 @@ body {
|
|||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dialog-main {
|
||||||
|
position: absolute;
|
||||||
|
inset: 50%;
|
||||||
|
transform: translate(-50%);
|
||||||
|
width: 40ch;
|
||||||
|
max-width: 60ch;
|
||||||
|
height: 10rem;
|
||||||
|
color: var(--foreground);
|
||||||
|
&:open {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
background-color: var(--background);
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.dialog-title {
|
||||||
|
width: 100%;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
text-align: center;
|
||||||
|
background-color: var(--background-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-elems {
|
||||||
|
padding: .5rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.toolbar, .dialog-buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
margin-top: auto;
|
||||||
|
justify-content: space-around;
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: .75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar button, dialog button {
|
||||||
|
min-width: 10ch;
|
||||||
|
background-color: var(--background-accent);
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--background-accent2);
|
||||||
|
}
|
||||||
|
}
|
155
src/app/map.tsx
155
src/app/map.tsx
@ -1,91 +1,60 @@
|
|||||||
|
"use client";
|
||||||
import { MapContainer, Marker, Rectangle, TileLayer, Tooltip, Polyline, Polygon } from "react-leaflet";
|
import { MapContainer, Marker, Rectangle, TileLayer, Tooltip, Polyline, Polygon } from "react-leaflet";
|
||||||
import L, { Icon } from "leaflet";
|
import L, { Icon } from "leaflet";
|
||||||
|
import { CountryEntry, MapData, Coords, Setter, EditStates } from "@/app/types";
|
||||||
export type CountryEntry = {
|
import { ReactNode, RefObject, useCallback, useRef, useState } from "react";
|
||||||
code2: string;
|
import { useAction } from "next-safe-action/hooks";
|
||||||
code3: string;
|
import { addPOI } from "@/app/db-actions";
|
||||||
common_name: string;
|
import { Dialog } from "@/app/client";
|
||||||
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 {
|
function find_entry(code: string, entries: CountryEntry[]): CountryEntry | undefined {
|
||||||
return entries.find((it) => it.code2 === code || it.code3 === code)
|
return entries.find((it) => it.code2 === code || it.code3 === code)
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseCoords(desc: string): [number, number][] {
|
function parseCoords(desc: string): [number, number][] {
|
||||||
console.log(desc);
|
|
||||||
const foo = [...desc.matchAll(/\((-?\d+),(-?\d+)\)/g)]
|
const foo = [...desc.matchAll(/\((-?\d+),(-?\d+)\)/g)]
|
||||||
return foo.map(it => [-+it[2] - .5, +it[1] + .5])
|
return foo.map(it => [-+it[2] - .5, +it[1] + .5])
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Map(props: { data: MapData }) {
|
export default function Map(props: {
|
||||||
const { data } = props;
|
data: MapData,
|
||||||
|
coords: Coords,
|
||||||
|
setCoords: Setter<Coords>,
|
||||||
|
curPath?: Coords[],
|
||||||
|
setCurPath: Setter<Coords[]>,
|
||||||
|
editState?: EditStates,
|
||||||
|
setEditState: Setter<EditStates>
|
||||||
|
}) {
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
coords,
|
||||||
|
setCoords,
|
||||||
|
curPath,
|
||||||
|
setCurPath,
|
||||||
|
editState,
|
||||||
|
setEditState
|
||||||
|
} = props;
|
||||||
|
// refs to dialogs
|
||||||
|
const poiDialog = useRef<HTMLDialogElement>(null);
|
||||||
|
const poiDialogLabel = useRef<HTMLInputElement>(null);
|
||||||
|
const addPOIAction = useAction(addPOI, { onSuccess: () => { console.log("running the action’s callback"); addPOIAction.reset() } })
|
||||||
|
const map = useCallback((it: L.Map | null) => {
|
||||||
|
|
||||||
|
if (!it) return;
|
||||||
|
it.off("click");
|
||||||
|
it.on("click", e => {
|
||||||
|
setCoords([Math.round(e.latlng.lng), Math.round(-e.latlng.lat)])
|
||||||
|
|
||||||
|
console.log("running the on click handler of the map", coords)
|
||||||
|
//switch (editState) {
|
||||||
|
//case EditStates.NONE: return;
|
||||||
|
//case EditStates.ADD_POI: {
|
||||||
|
poiDialog.current?.showModal();
|
||||||
|
return;
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
})
|
||||||
|
}, [editState, curPath])
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<MapContainer
|
<MapContainer
|
||||||
@ -93,7 +62,8 @@ export default function Map(props: { data: MapData }) {
|
|||||||
center={[0, 0]}
|
center={[0, 0]}
|
||||||
maxBounds={[[-8192, -8192], [8192, 8192]]}
|
maxBounds={[[-8192, -8192], [8192, 8192]]}
|
||||||
crs={L.CRS.Simple}
|
crs={L.CRS.Simple}
|
||||||
zoom={0}>
|
zoom={0}
|
||||||
|
ref={map}>
|
||||||
<TileLayer
|
<TileLayer
|
||||||
url="/tiles/{z}/{x}_{y}.png"
|
url="/tiles/{z}/{x}_{y}.png"
|
||||||
tileSize={512}
|
tileSize={512}
|
||||||
@ -124,12 +94,37 @@ export default function Map(props: { data: MapData }) {
|
|||||||
{
|
{
|
||||||
data.parts.map(it =>
|
data.parts.map(it =>
|
||||||
<Polygon key={it.id} positions={parseCoords(it.shape)} stroke={true} color="#A00" fill={true} fillOpacity={.5} fillColor="#A00">
|
<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>
|
<Tooltip>{find_entry(it.country, data.countries)?.common_name}</Tooltip>
|
||||||
</Polygon>
|
</Polygon>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
{data.stops.map(it => <Marker key={it.id} position={parseCoords(it.coordinates)[0]}><Tooltip>{it.id}</Tooltip></Marker>)}
|
{
|
||||||
|
data.stops.map(it =>
|
||||||
|
<Marker key={it.id} position={parseCoords(it.coordinates)[0]}>
|
||||||
|
<Tooltip>{it.id}</Tooltip>
|
||||||
|
</Marker>
|
||||||
|
)
|
||||||
|
}
|
||||||
</MapContainer>
|
</MapContainer>
|
||||||
|
<Dialog
|
||||||
|
title="Add Point of Interest"
|
||||||
|
ref={poiDialog}
|
||||||
|
buttons={[
|
||||||
|
{
|
||||||
|
label: "Add", action: () => {
|
||||||
|
if (!poiDialogLabel.current) throw Error("WHAT THE FUCK????")
|
||||||
|
console.log("running the dialog’s action")
|
||||||
|
console.log(poiDialogLabel.current.value, coords[0], coords[1])
|
||||||
|
addPOIAction.execute({
|
||||||
|
label: poiDialogLabel.current.value,
|
||||||
|
coordinates: { x: coords[0], y: coords[1] }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ label: "Cancel" },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<input ref={poiDialogLabel} type="text" placeholder="Point of Interest" />
|
||||||
|
</Dialog>
|
||||||
</>)
|
</>)
|
||||||
}
|
}
|
@ -1,12 +1,9 @@
|
|||||||
import { auth, signIn } from "@/auth";
|
import { auth, signIn } from "@/auth";
|
||||||
import { Map } from "@/app/client";
|
import { Client, /*Map,*/ Toolbar } from "@/app/client";
|
||||||
import type {MapData, CountryEntry, CountryPart, POI, RailLine, Settlement, TransitStop, Road} from "@/app/map"
|
import { MapData, CountryEntry, CountryPart, POI, RailLine, Settlement, TransitStop, Road, EditStates } from "@/app/types"
|
||||||
import { sql } from "bun"
|
import { sql } from "bun"
|
||||||
import { JSDOM } from "jsdom"
|
import { JSDOM } from "jsdom"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export async function getCountries(): Promise<CountryEntry[]> {
|
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_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 data = await data_raw.json()
|
||||||
@ -18,7 +15,8 @@ export async function getCountries () : Promise<CountryEntry[]> {
|
|||||||
else return [];
|
else return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getMapData() : Promise<MapData> {
|
export async function getMapData(): Promise<MapData> {
|
||||||
|
console.log("Fetched the data");
|
||||||
const [countries, parts, pois, farms, rails, roads, settlements, stops]:
|
const [countries, parts, pois, farms, rails, roads, settlements, stops]:
|
||||||
[CountryEntry[], CountryPart[], POI[], POI[], RailLine[], Road[], Settlement[], TransitStop[]] = await Promise.all([
|
[CountryEntry[], CountryPart[], POI[], POI[], RailLine[], Road[], Settlement[], TransitStop[]] = await Promise.all([
|
||||||
getCountries(),
|
getCountries(),
|
||||||
@ -36,11 +34,14 @@ async function getMapData() : Promise<MapData> {
|
|||||||
|
|
||||||
export default async function Home() {
|
export default async function Home() {
|
||||||
const session = await auth();
|
const session = await auth();
|
||||||
|
const data = await getMapData()
|
||||||
return <>
|
return <>
|
||||||
<Map data={await getMapData()}/>
|
<Client data={data} id={session?.discord_id} />
|
||||||
{(session) ? null : <form action={async () => {
|
{
|
||||||
|
(session) ? null : <form action={async () => {
|
||||||
'use server';
|
'use server';
|
||||||
await signIn("discord")
|
await signIn("discord")
|
||||||
}}><button type="submit">Log In</button></form>}
|
}}><button type="submit">Log In</button></form>
|
||||||
|
}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
3
src/app/safe-action.ts
Normal file
3
src/app/safe-action.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import { createSafeActionClient } from "next-safe-action";
|
||||||
|
|
||||||
|
export const actionClient = createSafeActionClient();
|
89
src/app/types.ts
Normal file
89
src/app/types.ts
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
|
||||||
|
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[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Coords = [number, number];
|
||||||
|
export type Setter<T> = (it: T) => void;
|
||||||
|
|
||||||
|
export enum EditStates {
|
||||||
|
NONE,
|
||||||
|
ADD_POI,
|
||||||
|
ADD_SETTLEMENT,
|
||||||
|
ADD_FARM,
|
||||||
|
ADD_STOP,
|
||||||
|
ADD_COUNTRY,
|
||||||
|
ADD_COUNTRY_PARTIAL,
|
||||||
|
ADD_ROAD,
|
||||||
|
ADD_ROAD_PARTIAL,
|
||||||
|
ADD_TRAIN,
|
||||||
|
ADD_TRAIN_PARTIAL,
|
||||||
|
}
|
Reference in New Issue
Block a user