From 1307436e65ef1caf5286a3bbc3ec22e9de0173a1 Mon Sep 17 00:00:00 2001 From: pavel444-byte Date: Wed, 7 Oct 2026 19:44:49 +0500 Subject: [PATCH] Add map import and export with durable refresh progress --- app/api/v1/[...path]/route.ts | 1 + app/globals.css | 23 ++ app/servers/[id]/page.tsx | 4 + app/servers/page.tsx | 15 +- components/CatalogBrowser.tsx | 30 +- components/Dashboard.tsx | 2 + components/DraftEditor.tsx | 52 +++- components/MapRefresh.tsx | 52 +++- components/MapTransfer.tsx | 136 +++++++++ components/ProcessingProgress.tsx | 67 ++++- docs/map-sharing.md | 26 ++ docs/shared-assets.md | 5 +- docs/vercel-deployment.md | 15 +- lib/api.ts | 150 ++++++++-- lib/fabric-images.ts | 98 +++++++ lib/fabric-jobs.ts | 198 +++++++++++++ lib/map-archive-signature.ts | 23 ++ lib/map-transfer.ts | 133 +++++++++ lib/multiplayer-api.ts | 6 +- lib/process-uploads.ts | 22 ++ scripts/test-map-sharing-browser.ts | 259 ++++++++++++++++++ scripts/test-map-sharing-sql.mjs | 22 ++ shared/account.ts | 1 + shared/database.ts | 22 ++ shared/locales/ru.json | 34 ++- shared/map-archive.ts | 199 ++++++++++++++ shared/map-refresh.ts | 17 ++ shared/processing-progress.ts | 2 + shared/protocol.ts | 31 ++- .../migrations/20261007145000_map_sharing.sql | 155 +++++++++++ tests/account.test.ts | 1 + tests/fabric-processing.test.ts | 228 +++++++++++++++ tests/map-refresh.test.ts | 27 ++ tests/map-sharing.test.ts | 170 ++++++++++++ tests/sql/map-sharing.sql | 38 +++ vercel.json | 4 + worker/main.ts | 24 +- 37 files changed, 2207 insertions(+), 85 deletions(-) create mode 100644 components/MapTransfer.tsx create mode 100644 docs/map-sharing.md create mode 100644 lib/fabric-images.ts create mode 100644 lib/fabric-jobs.ts create mode 100644 lib/map-archive-signature.ts create mode 100644 lib/map-transfer.ts create mode 100644 lib/process-uploads.ts create mode 100644 scripts/test-map-sharing-browser.ts create mode 100644 scripts/test-map-sharing-sql.mjs create mode 100644 shared/map-archive.ts create mode 100644 shared/map-refresh.ts create mode 100644 supabase/migrations/20261007145000_map_sharing.sql create mode 100644 tests/fabric-processing.test.ts create mode 100644 tests/map-refresh.test.ts create mode 100644 tests/map-sharing.test.ts create mode 100644 tests/sql/map-sharing.sql create mode 100644 vercel.json diff --git a/app/api/v1/[...path]/route.ts b/app/api/v1/[...path]/route.ts index b3df3d4..8009a7b 100644 --- a/app/api/v1/[...path]/route.ts +++ b/app/api/v1/[...path]/route.ts @@ -2,6 +2,7 @@ import { NextRequest } from "next/server"; import { handle } from "@/lib/api"; export const runtime = "nodejs"; export const dynamic = "force-dynamic"; +export const maxDuration = 300; async function route( request: NextRequest, { params }: { params: Promise<{ path: string[] }> }, diff --git a/app/globals.css b/app/globals.css index 2a5bbfa..c5be507 100644 --- a/app/globals.css +++ b/app/globals.css @@ -706,10 +706,32 @@ nav a:hover, } .catalog-controls { display: flex; + align-items: center; + flex-wrap: wrap; gap: 20px; justify-content: space-between; margin: 35px 0; } +.map-import-dialog { + margin: auto; + background: var(--panel); + color: var(--text); + border: 1px solid var(--line); + border-radius: 12px; + padding: 24px; + width: min(540px, calc(100vw - 32px)); + max-height: calc(100dvh - 32px); + overflow: auto; +} +.map-import-dialog::backdrop { background: rgb(0 0 0 / 65%); } +.map-import-heading { display: flex; align-items: center; justify-content: space-between; gap: 16px; } +.map-import-heading h2 { margin: 0; } +.map-import-dialog .field input { max-width: 100%; } +.map-export { margin: 16px 0; } +.catalog-search { display: flex; align-items: center; gap: 12px; flex: 1; min-width: 0; } +.catalog-search .search-field { flex: 1; min-width: 0; } +.catalog-search .search-field input { min-width: 0; } +.catalog-search > .button { flex-shrink: 0; } .search-field { border: 1px solid var(--line); background: var(--panel); @@ -1399,6 +1421,7 @@ pre { } .catalog-controls { flex-direction: column; + align-items: stretch; } .difficulty-options { gap: 8px; diff --git a/app/servers/[id]/page.tsx b/app/servers/[id]/page.tsx index d92c7e4..025ce39 100644 --- a/app/servers/[id]/page.tsx +++ b/app/servers/[id]/page.tsx @@ -6,6 +6,8 @@ import { StartGame } from "@/components/StartGame"; import { getMap } from "@/lib/catalog"; import { ContributionInstructions } from "@/components/ContributionInstructions"; import { ReportContent } from "@/components/ReportContent"; +import { user } from "@/lib/supabase"; +import { ExportMap } from "@/components/MapTransfer"; export default async function ServerPage({ params, }: { @@ -15,6 +17,7 @@ export default async function ServerPage({ const { id } = await params; const map = await getMap(id); if (!map) notFound(); + const account = await user(); return ( <>
@@ -31,6 +34,7 @@ export default async function ServerPage({ {t("Created by")} {map.credit}

+ {account?.id === map.owner_id && }

{t("Map ID:")} {map.id}

diff --git a/app/servers/page.tsx b/app/servers/page.tsx index 33b49a6..5be25a2 100644 --- a/app/servers/page.tsx +++ b/app/servers/page.tsx @@ -1,11 +1,12 @@ import { getI18n } from "@/lib/i18n"; -import { Header, Footer, EmptyState } from "@/components/Shell"; +import { Header, Footer } from "@/components/Shell"; import { catalog } from "@/lib/catalog"; import { CatalogBrowser } from "@/components/CatalogBrowser"; +import { user } from "@/lib/supabase"; export const dynamic = "force-dynamic"; export default async function Servers() { const { t } = await getI18n(); - const maps = await catalog(); + const [maps, account] = await Promise.all([catalog(), user()]); return ( <>
@@ -19,15 +20,7 @@ export default async function Servers() { "From familiar spawn towns to places nobody’s quite sure about. Pick your next expedition.", )}

- {maps.length ? ( - - ) : ( - - {t( - "No maps have been approved yet. Your server could be the first.", - )} - - )} +