Skip to content

Commit 787db27

Browse files
committed
Improve shire map toolbar layout
1 parent 314c42d commit 787db27

21 files changed

Lines changed: 1670 additions & 89 deletions
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
name: Validate Collections
2+
3+
on:
4+
pull_request:
5+
paths:
6+
- "collections/**"
7+
- "scripts/validate-collections.mjs"
8+
- ".github/workflows/validate-collections.yml"
9+
push:
10+
branches:
11+
- main
12+
paths:
13+
- "collections/**"
14+
- "scripts/validate-collections.mjs"
15+
- ".github/workflows/validate-collections.yml"
16+
17+
jobs:
18+
validate:
19+
runs-on: ubuntu-latest
20+
21+
steps:
22+
- name: Checkout
23+
uses: actions/checkout@v4
24+
25+
- name: Setup Node
26+
uses: actions/setup-node@v4
27+
with:
28+
node-version: 20
29+
30+
- name: Validate collection JSON
31+
run: node scripts/validate-collections.mjs

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,14 @@ pnpm dev
7676
3. Right-click to erase tiles, and click-drag to paint quickly.
7777
4. Save/load maps locally or export your map as PNG.
7878

79+
## Community Collections
80+
81+
- Browse shared maps at `/collections`.
82+
- Community map files are stored in `collections/maps`.
83+
- Contributors can open a pull request with a new JSON file.
84+
- JSON shape is documented in `collections/schema/map.schema.json` and `collections/README.md`.
85+
- PRs are auto-checked by `.github/workflows/validate-collections.yml`.
86+
7987
## Tech Stack
8088

8189
- Next.js 16

app/api/collections/[id]/route.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { NextResponse } from "next/server";
2+
import { getCollectionMapById } from "@/lib/collections";
3+
4+
export async function GET(
5+
_request: Request,
6+
context: { params: Promise<{ id: string }> },
7+
) {
8+
const { id } = await context.params;
9+
const map = await getCollectionMapById(id);
10+
11+
if (!map) {
12+
return NextResponse.json({ error: "Collection map not found." }, { status: 404 });
13+
}
14+
15+
return NextResponse.json(map);
16+
}

app/collections/page.tsx

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import Toolbar from "@/components/toolbar";
2+
import CollectionMapCard from "@/components/collection-map-card";
3+
import {
4+
Pagination,
5+
PaginationContent,
6+
PaginationEllipsis,
7+
PaginationItem,
8+
PaginationLink,
9+
PaginationNext,
10+
PaginationPrevious,
11+
} from "@/components/ui/pagination";
12+
import { getCollectionMaps } from "@/lib/collections";
13+
14+
const PAGE_SIZE = 6;
15+
const MAX_PAGE_LINKS = 5;
16+
17+
const parsePage = (rawPage: string | string[] | undefined) => {
18+
const pageValue = Array.isArray(rawPage) ? rawPage[0] : rawPage;
19+
const parsed = Number.parseInt(pageValue ?? "1", 10);
20+
if (!Number.isFinite(parsed) || parsed < 1) return 1;
21+
return parsed;
22+
};
23+
24+
const getPaginationItems = (currentPage: number, totalPages: number) => {
25+
if (totalPages <= MAX_PAGE_LINKS) {
26+
return Array.from({ length: totalPages }, (_, index) => index + 1);
27+
}
28+
29+
const pages = new Set<number>([1, totalPages, currentPage]);
30+
if (currentPage > 1) pages.add(currentPage - 1);
31+
if (currentPage < totalPages) pages.add(currentPage + 1);
32+
33+
const sorted = [...pages].sort((a, b) => a - b);
34+
const result: Array<number | "ellipsis"> = [];
35+
36+
for (let i = 0; i < sorted.length; i += 1) {
37+
const page = sorted[i];
38+
const prev = sorted[i - 1];
39+
if (prev !== undefined && page - prev > 1) {
40+
result.push("ellipsis");
41+
}
42+
result.push(page);
43+
}
44+
45+
return result;
46+
};
47+
48+
export default async function CollectionsPage({
49+
searchParams,
50+
}: {
51+
searchParams: Promise<{ page?: string | string[] }>;
52+
}) {
53+
const { page } = await searchParams;
54+
const maps = await getCollectionMaps();
55+
const totalPages = Math.max(1, Math.ceil(maps.length / PAGE_SIZE));
56+
const currentPage = Math.min(parsePage(page), totalPages);
57+
const startIndex = (currentPage - 1) * PAGE_SIZE;
58+
const pageMaps = maps.slice(startIndex, startIndex + PAGE_SIZE);
59+
const prevPage = currentPage > 1 ? currentPage - 1 : null;
60+
const nextPage = currentPage < totalPages ? currentPage + 1 : null;
61+
62+
return (
63+
<main className="flex h-screen flex-col overflow-hidden bg-background">
64+
<Toolbar />
65+
<section className="flex-1 overflow-auto px-6 py-10">
66+
<div className="mx-auto max-w-4xl space-y-8">
67+
<header className="space-y-2">
68+
<h1 className="text-3xl font-semibold tracking-tight">
69+
Community Collections
70+
</h1>
71+
<p className="text-sm text-muted-foreground">
72+
Shared maps contributed through GitHub pull requests.
73+
</p>
74+
<p className="text-xs text-muted-foreground">
75+
Add your own map: <code>collections/maps/&lt;id&gt;.json</code>
76+
</p>
77+
</header>
78+
79+
{maps.length === 0 ? (
80+
<div className="rounded-lg border bg-card p-6 text-sm text-muted-foreground">
81+
No community maps yet.
82+
</div>
83+
) : (
84+
<div className="grid gap-4 lg:grid-cols-2">
85+
{pageMaps.map((map) => (
86+
<CollectionMapCard key={map.id} map={map} />
87+
))}
88+
</div>
89+
)}
90+
{maps.length > 0 ? (
91+
<div className="space-y-3 border-t pt-4">
92+
<p className="text-sm text-muted-foreground">
93+
Page {currentPage} of {totalPages}
94+
</p>
95+
<Pagination className="mx-0 justify-center">
96+
<PaginationContent>
97+
<PaginationItem>
98+
<PaginationPrevious
99+
href={prevPage ? `/collections?page=${prevPage}` : "#"}
100+
className={
101+
!prevPage ? "pointer-events-none opacity-50" : undefined
102+
}
103+
aria-disabled={!prevPage}
104+
tabIndex={prevPage ? undefined : -1}
105+
/>
106+
</PaginationItem>
107+
{getPaginationItems(currentPage, totalPages).map(
108+
(item, index) => {
109+
if (item === "ellipsis") {
110+
return (
111+
<PaginationItem key={`ellipsis-${index}`}>
112+
<PaginationEllipsis />
113+
</PaginationItem>
114+
);
115+
}
116+
return (
117+
<PaginationItem key={item}>
118+
<PaginationLink
119+
href={`/collections?page=${item}`}
120+
isActive={item === currentPage}
121+
>
122+
{item}
123+
</PaginationLink>
124+
</PaginationItem>
125+
);
126+
},
127+
)}
128+
<PaginationItem>
129+
<PaginationNext
130+
href={nextPage ? `/collections?page=${nextPage}` : "#"}
131+
className={
132+
!nextPage ? "pointer-events-none opacity-50" : undefined
133+
}
134+
aria-disabled={!nextPage}
135+
tabIndex={nextPage ? undefined : -1}
136+
/>
137+
</PaginationItem>
138+
</PaginationContent>
139+
</Pagination>
140+
</div>
141+
) : null}
142+
</div>
143+
</section>
144+
</main>
145+
);
146+
}

app/page.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
1+
import { Suspense } from "react";
12
import IsoCanvas from "@/components/iso-canvas";
23
import TilePicker from "@/components/tile-picker";
34
import Toolbar from "@/components/toolbar";
5+
import CollectionLoader from "@/components/collection-loader";
46

57

68

79
export default function Home() {
810
return (
911
<main className="flex flex-col h-screen overflow-hidden">
12+
<Suspense fallback={null}>
13+
<CollectionLoader />
14+
</Suspense>
1015
<Toolbar />
1116
<IsoCanvas />
1217
<TilePicker />

collections/README.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Community Collections
2+
3+
Community maps live in `collections/maps` and are submitted via GitHub pull request.
4+
5+
## File format
6+
7+
Each map must be a JSON file with this path pattern:
8+
9+
- `collections/maps/<id>.json`
10+
11+
Use the schema in `collections/schema/map.schema.json`.
12+
13+
### Required fields
14+
15+
- `schemaVersion`: currently `1`
16+
- `id`: kebab-case unique ID (must match filename)
17+
- `name`: display name
18+
- `author.name`: contributor name
19+
- `author.github`: GitHub username
20+
- `createdAt`: ISO date, for example `2026-02-16T00:00:00.000Z`
21+
- `location`: one of `shire`, `gondor`, `mordor`, `lothlorien`, `rohan`, `moria`, `rivendell`, `mixed`
22+
- `gridSize`: integer from `3` to `20`
23+
- `map`: matrix of tile coordinates
24+
25+
## Tile format
26+
27+
Each tile in the map matrix is one of:
28+
29+
- `[row, col]`
30+
- `[row, col, realm]`
31+
32+
Ranges:
33+
34+
- `row`: `0..5`
35+
- `col`: `0..11`
36+
- `realm`: only for mixed mode, one of `shire`, `gondor`, `mordor`, `lothlorien`, `rohan`, `moria`, `rivendell`
37+
38+
## Validate locally
39+
40+
```bash
41+
npm run validate:collections
42+
```
43+
44+
The same validation runs automatically in pull requests.

collections/maps/shire.json

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
{
2+
"schemaVersion": 1,
3+
"id": "shire-map",
4+
"name": "Shire Map",
5+
"author": {
6+
"name": "Hasan Harman",
7+
"github": "hasanharman"
8+
},
9+
"createdAt": "2026-02-16T18:05:07.118Z",
10+
"location": "mixed",
11+
"gridSize": 7,
12+
"map": [
13+
[
14+
[0, 0],
15+
[2, 6, "shire"],
16+
[2, 4, "shire"],
17+
[2, 9, "shire"],
18+
[0, 0],
19+
[0, 0],
20+
[4, 1, "shire"]
21+
],
22+
[
23+
[2, 5, "shire"],
24+
[0, 0],
25+
[3, 1, "shire"],
26+
[5, 1, "shire"],
27+
[0, 0],
28+
[4, 0, "shire"],
29+
[0, 6, "shire"]
30+
],
31+
[
32+
[1, 1, "shire"],
33+
[3, 0, "shire"],
34+
[0, 10, "shire"],
35+
[0, 7, "shire"],
36+
[0, 7, "shire"],
37+
[0, 7, "shire"],
38+
[0, 10, "shire"]
39+
],
40+
[
41+
[1, 1, "shire"],
42+
[5, 7, "shire"],
43+
[0, 6, "shire"],
44+
[1, 6, "shire"],
45+
[0, 0],
46+
[5, 4, "shire"],
47+
[0, 6, "shire"]
48+
],
49+
[
50+
[1, 1, "shire"],
51+
[0, 2, "shire"],
52+
[0, 6, "shire"],
53+
[2, 0, "shire"],
54+
[0, 0],
55+
[2, 8, "shire"],
56+
[0, 6, "shire"]
57+
],
58+
[
59+
[1, 1, "shire"],
60+
[0, 4, "shire"],
61+
[0, 6, "shire"],
62+
[0, 4, "shire"],
63+
[0, 0],
64+
[0, 0],
65+
[0, 6, "shire"]
66+
],
67+
[
68+
[1, 1, "shire"],
69+
[0, 2, "shire"],
70+
[0, 6, "shire"],
71+
[2, 8, "shire"],
72+
[0, 0],
73+
[0, 0],
74+
[0, 6, "shire"]
75+
]
76+
]
77+
}

0 commit comments

Comments
 (0)