tons of feature

This commit is contained in:
taDuc
2026-04-04 22:24:36 +07:00
commit 2a1b4f2f2a
25 changed files with 8539 additions and 0 deletions

BIN
app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

26
app/globals.css Normal file
View File

@@ -0,0 +1,26 @@
@import "tailwindcss";
:root {
--background: #ffffff;
--foreground: #171717;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
}

34
app/layout.tsx Normal file
View File

@@ -0,0 +1,34 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
</body>
</html>
);
}

86
app/page.tsx Normal file
View File

@@ -0,0 +1,86 @@
"use client";
import { useEffect, useState } from "react";
import Map from "@/components/Map";
import Editor from "@/components/Editor";
import {
FeatureCollection,
useEditorState,
} from "@/lib/useEditorState";
const EMPTY_FC: FeatureCollection = { type: "FeatureCollection", features: [] };
export default function Page() {
const [mode, setMode] = useState<"idle" | "draw" | "select" | "add-point">("idle");
const [initialData, setInitialData] = useState<FeatureCollection>(EMPTY_FC);
const [isSaving, setIsSaving] = useState(false);
const editor = useEditorState(initialData);
useEffect(() => {
async function loadInitial() {
try {
const params = new URLSearchParams({
minLng: "-180",
minLat: "-90",
maxLng: "180",
maxLat: "90",
});
const res = await fetch(`http://localhost:3000/geometries?${params.toString()}`);
if (!res.ok) return;
const data = await res.json();
setInitialData(data);
} catch (err) {
console.error("Load initial data failed", err);
}
}
loadInitial();
}, []);
const handleSave = async () => {
const payload = editor.buildPayload();
if (!payload.length) return;
setIsSaving(true);
try {
const res = await fetch("http://localhost:3000/geometries/batch", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ changes: payload }),
});
if (!res.ok) {
const text = await res.text();
console.error("Save failed", text);
return;
}
editor.clearChanges();
} catch (err) {
console.error("Save error", err);
} finally {
setIsSaving(false);
}
};
return (
<div style={{ display: "flex" }}>
<Editor
mode={mode}
setMode={setMode}
onUndo={editor.undo}
onSave={handleSave}
isSaving={isSaving}
changesCount={editor.changeCount}
undoStack={editor.undoStack}
/>
<Map
mode={mode}
draft={editor.draft}
onCreateFeature={editor.createFeature}
onDeleteFeature={editor.deleteFeature}
onUpdateFeature={editor.updateFeature}
/>
</div>
);
}