Skip to content

Commit 6c4992d

Browse files
committed
Add NamingWizard feature expansion: airport lookup, companion namer, prefix matrix
- Extract inline data to src/lib/data/ (airports with lat/lng, ~220 cities, 64 landmarks) - Add auto airport lookup in Step 1 via Nominatim geocoding + haversine distance - Add pubkey conflict checking in Step 5 with inline warnings - Create interactive CompanionNamer component (emoji + handle + suffix strategies) - Create PrefixMatrix component (16x16 hex grid, search, crowded prefix highlighting) - Add prefix matrix section to home page after naming wizard - Add haversine distance utility
1 parent 3e49b85 commit 6c4992d

File tree

9 files changed

+1161
-296
lines changed

9 files changed

+1161
-296
lines changed

src/app/page.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import Link from 'next/link';
22
import { StatsSection } from '@/components';
33
import JsonLd from '@/components/JsonLd';
44
import NamingWizard from '@/components/NamingWizard';
5+
import PrefixMatrix from '@/components/PrefixMatrix';
56
import { generateBreadcrumbSchema } from '@/lib/schemas/breadcrumb';
67
import { BASE_URL } from '@/lib/constants';
78

@@ -119,6 +120,22 @@ export default function Home() {
119120
</div>
120121
</section>
121122

123+
{/* Prefix Utilization Matrix */}
124+
<section id="prefix-matrix" className="bg-background py-16 sm:py-24">
125+
<div className="mx-auto max-w-4xl px-4 sm:px-6 lg:px-8">
126+
<div className="text-center mb-12">
127+
<h2 className="text-3xl sm:text-4xl font-bold text-foreground mb-4">
128+
Public Key Prefix Map
129+
</h2>
130+
<p className="text-lg text-foreground-muted max-w-2xl mx-auto">
131+
See which public key prefixes are in use on the Denver mesh.
132+
Pick a <span className="text-mesh font-semibold">conflict-free prefix</span> for your node.
133+
</p>
134+
</div>
135+
<PrefixMatrix />
136+
</div>
137+
</section>
138+
122139
{/* Mission Statement */}
123140
<section className="bg-background py-16 sm:py-24">
124141
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">

src/components/CompanionNamer.tsx

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
"use client";
2+
3+
import { useState, useMemo } from "react";
4+
import { CopyButton } from "./CopyButton";
5+
6+
type SuffixStrategy = "pubkey" | "role" | "number";
7+
8+
export default function CompanionNamer() {
9+
const [emoji, setEmoji] = useState("");
10+
const [handle, setHandle] = useState("");
11+
const [strategy, setStrategy] = useState<SuffixStrategy>("pubkey");
12+
const [pubkeyPrefix, setPubkeyPrefix] = useState("");
13+
const [role, setRole] = useState("");
14+
const [number, setNumber] = useState("");
15+
16+
const suffix = useMemo(() => {
17+
switch (strategy) {
18+
case "pubkey":
19+
return pubkeyPrefix.toUpperCase();
20+
case "role":
21+
return role.toUpperCase();
22+
case "number":
23+
return number ? `MY${number.padStart(2, "0")}` : "";
24+
}
25+
}, [strategy, pubkeyPrefix, role, number]);
26+
27+
const generatedName = useMemo(() => {
28+
const parts: string[] = [];
29+
if (emoji) parts.push(emoji);
30+
if (handle) parts.push(handle.toUpperCase());
31+
if (suffix) parts.push(suffix);
32+
return parts.join(" ");
33+
}, [emoji, handle, suffix]);
34+
35+
// Count visible characters (emoji = ~2 display chars, but counts as 1 in MeshCore)
36+
const charCount = generatedName.length;
37+
const isOverLimit = charCount > 23;
38+
39+
const strategies: { value: SuffixStrategy; label: string; desc: string }[] = [
40+
{ value: "pubkey", label: "Public key prefix", desc: "4 hex chars from your public key (default)" },
41+
{ value: "role", label: "Role", desc: "2-4 chars: PRIM, SCND, HOME, etc." },
42+
{ value: "number", label: "Number", desc: "01-99 (auto-prefixed with MY)" },
43+
];
44+
45+
return (
46+
<div className="space-y-6">
47+
{/* Live Preview */}
48+
<div className="card-mesh p-5 text-center">
49+
<p className="text-xs text-foreground-muted uppercase tracking-wider mb-2">
50+
Companion Name Preview
51+
</p>
52+
<div className="flex items-center justify-center gap-2">
53+
<p
54+
className={`font-mono text-xl md:text-2xl font-bold ${
55+
isOverLimit ? "text-red-500" : generatedName ? "text-mesh" : "text-foreground-muted"
56+
}`}
57+
>
58+
{generatedName || "\uD83D\uDC7B M3SHGH\u00D8ST F4"}
59+
</p>
60+
{generatedName && <CopyButton text={generatedName} />}
61+
</div>
62+
<span
63+
className={`text-sm font-mono ${
64+
isOverLimit ? "text-red-500 font-bold" : "text-foreground-muted"
65+
}`}
66+
>
67+
{charCount}/23 chars
68+
</span>
69+
</div>
70+
71+
{/* Emoji */}
72+
<div>
73+
<label className="block text-sm font-semibold text-foreground mb-1">
74+
Emoji
75+
</label>
76+
<p className="text-xs text-foreground-muted mb-2">
77+
One emoji per person. Claim yours in Discord first.
78+
</p>
79+
<input
80+
type="text"
81+
value={emoji}
82+
onChange={(e) => {
83+
// Allow a single emoji (grapheme cluster)
84+
const val = e.target.value;
85+
const segments = [...new Intl.Segmenter().segment(val)];
86+
setEmoji(segments.length > 0 ? segments[0].segment : "");
87+
}}
88+
placeholder="\uD83D\uDC7B"
89+
className="w-20 bg-night-800/50 border border-card-border rounded-lg px-4 py-2.5 text-foreground text-2xl text-center focus:ring-2 focus:ring-mesh focus:border-mesh outline-none"
90+
/>
91+
</div>
92+
93+
{/* Handle */}
94+
<div>
95+
<label className="block text-sm font-semibold text-foreground mb-1">
96+
Handle
97+
</label>
98+
<p className="text-xs text-foreground-muted mb-2">
99+
Your mesh alias (not your real name). Max 10 characters.
100+
</p>
101+
<input
102+
type="text"
103+
value={handle}
104+
onChange={(e) =>
105+
setHandle(
106+
e.target.value.replace(/[^a-zA-Z0-9\u00D8]/g, "").slice(0, 10)
107+
)
108+
}
109+
placeholder="e.g. M3SHGH\u00D8ST"
110+
maxLength={10}
111+
className="w-full bg-night-800/50 border border-card-border rounded-lg px-4 py-2.5 text-foreground font-mono uppercase focus:ring-2 focus:ring-mesh focus:border-mesh outline-none placeholder:text-foreground-muted/50"
112+
/>
113+
</div>
114+
115+
{/* Suffix Strategy */}
116+
<div>
117+
<label className="block text-sm font-semibold text-foreground mb-1">
118+
Identification Suffix
119+
</label>
120+
<p className="text-xs text-foreground-muted mb-3">
121+
How do you want to identify this device?
122+
</p>
123+
<div className="space-y-2">
124+
{strategies.map((s) => (
125+
<label
126+
key={s.value}
127+
className={`flex items-start gap-3 p-3 rounded-lg border cursor-pointer transition-colors ${
128+
strategy === s.value
129+
? "border-mesh bg-mesh/10"
130+
: "border-card-border bg-night-800/20 hover:border-mesh/50"
131+
}`}
132+
>
133+
<input
134+
type="radio"
135+
name="suffix-strategy"
136+
value={s.value}
137+
checked={strategy === s.value}
138+
onChange={() => setStrategy(s.value)}
139+
className="mt-0.5 text-mesh focus:ring-mesh"
140+
/>
141+
<div>
142+
<span className="text-sm font-semibold text-foreground">
143+
{s.label}
144+
</span>
145+
<p className="text-xs text-foreground-muted">{s.desc}</p>
146+
</div>
147+
</label>
148+
))}
149+
</div>
150+
151+
{/* Strategy-specific input */}
152+
<div className="mt-3">
153+
{strategy === "pubkey" && (
154+
<input
155+
type="text"
156+
value={pubkeyPrefix}
157+
onChange={(e) =>
158+
setPubkeyPrefix(
159+
e.target.value.replace(/[^a-fA-F0-9]/g, "").slice(0, 4)
160+
)
161+
}
162+
placeholder="e.g. F4A2"
163+
maxLength={4}
164+
className="w-full bg-night-800/50 border border-card-border rounded-lg px-4 py-2.5 text-foreground font-mono uppercase focus:ring-2 focus:ring-mesh focus:border-mesh outline-none placeholder:text-foreground-muted/50"
165+
/>
166+
)}
167+
{strategy === "role" && (
168+
<input
169+
type="text"
170+
value={role}
171+
onChange={(e) =>
172+
setRole(
173+
e.target.value.replace(/[^a-zA-Z]/g, "").slice(0, 4)
174+
)
175+
}
176+
placeholder="e.g. PRIM, SCND, HOME"
177+
maxLength={4}
178+
className="w-full bg-night-800/50 border border-card-border rounded-lg px-4 py-2.5 text-foreground font-mono uppercase focus:ring-2 focus:ring-mesh focus:border-mesh outline-none placeholder:text-foreground-muted/50"
179+
/>
180+
)}
181+
{strategy === "number" && (
182+
<input
183+
type="text"
184+
value={number}
185+
onChange={(e) =>
186+
setNumber(
187+
e.target.value.replace(/[^0-9]/g, "").slice(0, 2)
188+
)
189+
}
190+
placeholder="e.g. 01"
191+
maxLength={2}
192+
className="w-full bg-night-800/50 border border-card-border rounded-lg px-4 py-2.5 text-foreground font-mono uppercase focus:ring-2 focus:ring-mesh focus:border-mesh outline-none placeholder:text-foreground-muted/50"
193+
/>
194+
)}
195+
</div>
196+
</div>
197+
198+
{/* Do Not rules */}
199+
<div className="card-mesh p-4 bg-sunset-500/5 border-sunset-500/20">
200+
<p className="text-sm font-semibold text-foreground mb-2">Do Not:</p>
201+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-1 text-sm text-foreground-muted">
202+
{[
203+
"Use your real name",
204+
"Put hardware in the name",
205+
"Use different emojis per device",
206+
"Take someone else\u2019s emoji",
207+
"Go over 23 characters",
208+
].map((rule, i) => (
209+
<div key={i} className="flex items-center gap-2">
210+
<span className="text-sunset-500">&#10005;</span>
211+
<span>{rule}</span>
212+
</div>
213+
))}
214+
</div>
215+
</div>
216+
</div>
217+
);
218+
}

0 commit comments

Comments
 (0)