Cyberpunk visual overhaul
Dark CARTO basemap (was default bright OSM tiles), Orbitron/Share Tech Mono fonts, neon cyan/magenta HUD chrome with glassmorphism panels, glowing divIcon portal markers (pulsing when claimed) replacing plain circle dots, neon drop-shadow filters on links/fields, and a subtle scanline overlay. Login and team-pick screens restyled to match.
This commit is contained in:
@@ -4,6 +4,12 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Regress</title>
|
<title>Regress</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Orbitron:wght@500;700;900&family=Share+Tech+Mono&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
<!-- No-op outside an Archipelago iframe (see nostr-provider.js's own
|
<!-- No-op outside an Archipelago iframe (see nostr-provider.js's own
|
||||||
window === window.top guard) — safe to always include. Provides
|
window === window.top guard) — safe to always include. Provides
|
||||||
window.nostr + auto sign-in via the node's selected nostr identity
|
window.nostr + auto sign-in via the node's selected nostr identity
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ onMounted(() => auth.ensureLoaded());
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="h-full w-full bg-neutral-950 text-white">
|
<div class="h-full w-full bg-void text-white">
|
||||||
<div v-if="!auth.loaded" class="flex h-full items-center justify-center text-neutral-400">
|
<div v-if="!auth.loaded" class="flex h-full items-center justify-center font-display text-cyan-neon text-glow-cyan">
|
||||||
Loading…
|
INITIALIZING…
|
||||||
</div>
|
</div>
|
||||||
<LoginView v-else-if="!auth.pubkey" />
|
<LoginView v-else-if="!auth.pubkey" />
|
||||||
<TeamPickView v-else-if="!auth.team" />
|
<TeamPickView v-else-if="!auth.team" />
|
||||||
|
|||||||
@@ -6,3 +6,80 @@ html, body, #app {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: #050508;
|
||||||
|
color: #d8fbff;
|
||||||
|
font-family: 'Share Tech Mono', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, .font-display {
|
||||||
|
font-family: 'Orbitron', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Subtle animated scanline field over the whole app — cheap, pure-CSS. */
|
||||||
|
#app::before {
|
||||||
|
content: '';
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 2000;
|
||||||
|
background-image: repeating-linear-gradient(
|
||||||
|
0deg,
|
||||||
|
rgba(0, 255, 242, 0.035) 0px,
|
||||||
|
rgba(0, 255, 242, 0.035) 1px,
|
||||||
|
transparent 1px,
|
||||||
|
transparent 3px
|
||||||
|
);
|
||||||
|
mix-blend-mode: screen;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-glow-cyan { text-shadow: 0 0 8px rgba(0, 255, 242, 0.85); }
|
||||||
|
.text-glow-orange { text-shadow: 0 0 8px rgba(255, 95, 31, 0.9); }
|
||||||
|
.text-glow-green { text-shadow: 0 0 8px rgba(57, 255, 20, 0.9); }
|
||||||
|
|
||||||
|
.hud-panel {
|
||||||
|
background: rgba(8, 10, 20, 0.88);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border: 1px solid rgba(0, 255, 242, 0.35);
|
||||||
|
box-shadow: 0 0 20px rgba(0, 255, 242, 0.12), inset 0 0 30px rgba(0, 255, 242, 0.04);
|
||||||
|
clip-path: polygon(0 0, calc(100% - 12px) 0, 100% 12px, 100% 100%, 12px 100%, 0 calc(100% - 12px));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Darken + tint the CARTO dark tiles a bit further and desaturate slightly
|
||||||
|
so our neon markers/links pop harder against the basemap. */
|
||||||
|
.leaflet-tile-pane {
|
||||||
|
filter: brightness(0.75) saturate(1.3) hue-rotate(-6deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-container {
|
||||||
|
background: #050508 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-popup-content-wrapper,
|
||||||
|
.leaflet-tooltip {
|
||||||
|
background: rgba(8, 10, 20, 0.92) !important;
|
||||||
|
color: #d8fbff !important;
|
||||||
|
border: 1px solid rgba(0, 255, 242, 0.4) !important;
|
||||||
|
font-family: 'Share Tech Mono', monospace !important;
|
||||||
|
box-shadow: 0 0 12px rgba(0, 255, 242, 0.25) !important;
|
||||||
|
}
|
||||||
|
.leaflet-popup-tip {
|
||||||
|
background: rgba(8, 10, 20, 0.92) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.regress-portal-marker {
|
||||||
|
filter: drop-shadow(0 0 4px var(--portal-glow, #9ca3af));
|
||||||
|
}
|
||||||
|
.regress-portal-marker.claimed {
|
||||||
|
animation: portal-pulse 2.4s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
@keyframes portal-pulse {
|
||||||
|
0%, 100% { filter: drop-shadow(0 0 4px var(--portal-glow, #9ca3af)); }
|
||||||
|
50% { filter: drop-shadow(0 0 10px var(--portal-glow, #9ca3af)); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.regress-link-orange { filter: drop-shadow(0 0 4px #ff5f1f); }
|
||||||
|
.regress-link-green { filter: drop-shadow(0 0 4px #39ff14); }
|
||||||
|
.regress-field-orange { filter: drop-shadow(0 0 6px #ff5f1f); }
|
||||||
|
.regress-field-green { filter: drop-shadow(0 0 6px #39ff14); }
|
||||||
|
|||||||
@@ -36,32 +36,34 @@ async function handleNsecLogin() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex h-full flex-col items-center justify-center gap-4 px-6 text-center">
|
<div class="relative flex h-full flex-col items-center justify-center gap-5 overflow-hidden px-6 text-center">
|
||||||
<h1 class="text-3xl font-bold">Regress</h1>
|
<div
|
||||||
<p class="max-w-sm text-neutral-400">
|
class="pointer-events-none absolute inset-0"
|
||||||
Claim real Bitcoin-accepting businesses for your team. Log in with Nostr to start.
|
style="background: radial-gradient(circle at 50% 40%, rgba(0,255,242,0.08), transparent 60%)"
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<h1 class="font-display text-5xl font-black tracking-[0.3em] text-cyan-neon text-glow-cyan">REGRESS</h1>
|
||||||
|
<p class="max-w-sm text-cyan-neon/60">
|
||||||
|
Real-world nodes. Two factions. Claim them for orange or green — sign in with Nostr to jack in.
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
class="rounded-lg bg-white px-6 py-3 font-semibold text-black transition hover:bg-neutral-200 disabled:opacity-50"
|
class="rounded border-2 border-cyan-neon px-6 py-3 font-display font-bold uppercase tracking-wider text-cyan-neon transition hover:bg-cyan-neon/10 hover:shadow-glow-cyan disabled:opacity-50"
|
||||||
:disabled="loggingIn"
|
:disabled="loggingIn"
|
||||||
@click="handleLogin"
|
@click="handleLogin"
|
||||||
>
|
>
|
||||||
{{ loggingIn ? 'Signing in…' : 'Log in with Nostr' }}
|
{{ loggingIn ? 'Authenticating…' : 'Connect with Nostr' }}
|
||||||
</button>
|
</button>
|
||||||
<p v-if="!hasNip07()" class="text-sm text-neutral-500">
|
<p v-if="!hasNip07()" class="text-sm text-cyan-neon/40">
|
||||||
No NIP-07 extension detected — install Alby or nos2x, or open this app from the Archipelago dashboard.
|
No NIP-07 extension detected — install Alby or nos2x, or open this app from the Archipelago dashboard.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<button
|
<button class="text-xs text-cyan-neon/30 underline hover:text-magenta-neon" @click="showNsecForm = !showNsecForm">
|
||||||
class="text-xs text-neutral-600 underline hover:text-neutral-400"
|
|
||||||
@click="showNsecForm = !showNsecForm"
|
|
||||||
>
|
|
||||||
{{ showNsecForm ? 'Hide' : 'Or paste an nsec (unsafe)' }}
|
{{ showNsecForm ? 'Hide' : 'Or paste an nsec (unsafe)' }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div v-if="showNsecForm" class="flex w-full max-w-xs flex-col gap-2">
|
<div v-if="showNsecForm" class="hud-panel flex w-full max-w-xs flex-col gap-2 p-4">
|
||||||
<p class="rounded bg-red-950 px-3 py-2 text-left text-xs text-red-300">
|
<p class="rounded border border-magenta-neon/50 bg-magenta-neon/10 px-3 py-2 text-left text-xs text-magenta-neon">
|
||||||
⚠ Unsafe: your private key is decoded and used to sign right in this
|
⚠ UNSAFE: your private key is decoded and used to sign right in this
|
||||||
page. Only paste a throwaway/test nsec — never your real identity's.
|
page. Only paste a throwaway/test nsec — never your real identity's.
|
||||||
The key isn't saved anywhere; it's used once and discarded.
|
The key isn't saved anywhere; it's used once and discarded.
|
||||||
</p>
|
</p>
|
||||||
@@ -69,18 +71,18 @@ async function handleNsecLogin() {
|
|||||||
v-model="nsecInput"
|
v-model="nsecInput"
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="nsec1…"
|
placeholder="nsec1…"
|
||||||
class="rounded bg-neutral-900 px-3 py-2 text-sm text-white outline-none ring-1 ring-neutral-700 focus:ring-red-500"
|
class="rounded border border-cyan-neon/30 bg-black/60 px-3 py-2 text-sm text-cyan-neon outline-none focus:border-magenta-neon"
|
||||||
@keyup.enter="handleNsecLogin"
|
@keyup.enter="handleNsecLogin"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
class="rounded bg-red-900 px-3 py-2 text-sm font-semibold text-red-100 transition hover:bg-red-800 disabled:opacity-50"
|
class="rounded border border-magenta-neon px-3 py-2 text-sm font-bold uppercase text-magenta-neon transition hover:shadow-glow-magenta disabled:opacity-50"
|
||||||
:disabled="loggingIn || !nsecInput"
|
:disabled="loggingIn || !nsecInput"
|
||||||
@click="handleNsecLogin"
|
@click="handleNsecLogin"
|
||||||
>
|
>
|
||||||
{{ loggingIn ? 'Signing in…' : 'Log in with nsec (unsafe)' }}
|
{{ loggingIn ? 'Authenticating…' : 'Connect with nsec (unsafe)' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p v-if="error" class="text-sm text-red-400">{{ error }}</p>
|
<p v-if="error" class="text-sm text-magenta-neon">{{ error }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -26,13 +26,25 @@ const linkSourcePlace = computed<Place | null>(
|
|||||||
() => game.places.find((p) => p.id === linkSourceId.value) ?? null,
|
() => game.places.find((p) => p.id === linkSourceId.value) ?? null,
|
||||||
);
|
);
|
||||||
|
|
||||||
const TEAM_COLOR: Record<string, string> = { orange: '#f97316', green: '#22c55e' };
|
const TEAM_COLOR: Record<string, string> = { orange: '#ff5f1f', green: '#39ff14' };
|
||||||
const NEUTRAL_COLOR = '#9ca3af';
|
const NEUTRAL_COLOR = '#3fb8c9';
|
||||||
|
|
||||||
function colorFor(team: string | null): string {
|
function colorFor(team: string | null): string {
|
||||||
return team ? TEAM_COLOR[team] : NEUTRAL_COLOR;
|
return team ? TEAM_COLOR[team] : NEUTRAL_COLOR;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function portalIcon(place: Place, isSource: boolean): L.DivIcon {
|
||||||
|
const color = isSource ? '#ffffff' : colorFor(place.claim_team);
|
||||||
|
const size = isSource ? 22 : place.claim_team ? 18 : 14;
|
||||||
|
const claimedClass = place.claim_team ? 'claimed' : '';
|
||||||
|
return L.divIcon({
|
||||||
|
className: `regress-portal-marker ${claimedClass}`,
|
||||||
|
html: `<div style="--portal-glow:${color}; width:${size}px; height:${size}px; border-radius:50%; background:radial-gradient(circle at 35% 35%, ${color}, ${color}33 70%); border:2px solid ${color};"></div>`,
|
||||||
|
iconSize: [size, size],
|
||||||
|
iconAnchor: [size / 2, size / 2],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function redraw() {
|
function redraw() {
|
||||||
if (!map || !markersLayer || !linksLayer || !fieldsLayer) return;
|
if (!map || !markersLayer || !linksLayer || !fieldsLayer) return;
|
||||||
markersLayer.clearLayers();
|
markersLayer.clearLayers();
|
||||||
@@ -43,13 +55,7 @@ function redraw() {
|
|||||||
|
|
||||||
for (const place of game.places) {
|
for (const place of game.places) {
|
||||||
const isSource = place.id === linkSourceId.value;
|
const isSource = place.id === linkSourceId.value;
|
||||||
const marker = L.circleMarker([place.lat, place.lon], {
|
const marker = L.marker([place.lat, place.lon], { icon: portalIcon(place, isSource) });
|
||||||
radius: isSource ? 10 : 7,
|
|
||||||
color: isSource ? '#ffffff' : colorFor(place.claim_team),
|
|
||||||
weight: isSource ? 3 : 2,
|
|
||||||
fillColor: colorFor(place.claim_team),
|
|
||||||
fillOpacity: 0.85,
|
|
||||||
});
|
|
||||||
marker.on('click', () => onMarkerClick(place));
|
marker.on('click', () => onMarkerClick(place));
|
||||||
marker.bindTooltip(place.name || `Place ${place.id}`, { direction: 'top' });
|
marker.bindTooltip(place.name || `Place ${place.id}`, { direction: 'top' });
|
||||||
markersLayer.addLayer(marker);
|
markersLayer.addLayer(marker);
|
||||||
@@ -65,7 +71,12 @@ function redraw() {
|
|||||||
[from.lat, from.lon],
|
[from.lat, from.lon],
|
||||||
[to.lat, to.lon],
|
[to.lat, to.lon],
|
||||||
],
|
],
|
||||||
{ color: TEAM_COLOR[link.team], weight: 2, opacity: 0.8 },
|
{
|
||||||
|
color: TEAM_COLOR[link.team],
|
||||||
|
weight: 2,
|
||||||
|
opacity: 0.9,
|
||||||
|
className: link.team === 'orange' ? 'regress-link-orange' : 'regress-link-green',
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -77,7 +88,13 @@ function redraw() {
|
|||||||
.map((p) => [p.lat, p.lon] as [number, number]);
|
.map((p) => [p.lat, p.lon] as [number, number]);
|
||||||
if (points.length !== 3) continue;
|
if (points.length !== 3) continue;
|
||||||
fieldsLayer.addLayer(
|
fieldsLayer.addLayer(
|
||||||
L.polygon(points, { color: TEAM_COLOR[field.team], weight: 1, fillOpacity: 0.15, stroke: false }),
|
L.polygon(points, {
|
||||||
|
color: TEAM_COLOR[field.team],
|
||||||
|
weight: 1,
|
||||||
|
fillOpacity: 0.18,
|
||||||
|
stroke: false,
|
||||||
|
className: field.team === 'orange' ? 'regress-field-orange' : 'regress-field-green',
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -139,9 +156,11 @@ const greenScore = computed(() => game.scores.find((s) => s.team === 'green'));
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (mapEl.value) {
|
if (mapEl.value) {
|
||||||
map = L.map(mapEl.value).setView([32.7607, -16.9595], 12);
|
map = L.map(mapEl.value, { zoomControl: false }).setView([32.7607, -16.9595], 12);
|
||||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
L.control.zoom({ position: 'bottomright' }).addTo(map);
|
||||||
attribution: '© OpenStreetMap contributors',
|
L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
|
||||||
|
attribution: '© OpenStreetMap contributors © CARTO',
|
||||||
|
subdomains: 'abcd',
|
||||||
maxZoom: 19,
|
maxZoom: 19,
|
||||||
}).addTo(map);
|
}).addTo(map);
|
||||||
fieldsLayer = L.layerGroup().addTo(map);
|
fieldsLayer = L.layerGroup().addTo(map);
|
||||||
@@ -157,29 +176,41 @@ watch(linkSourceId, redraw);
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex h-full flex-col">
|
<div class="flex h-full flex-col bg-void">
|
||||||
<header class="flex items-center justify-between gap-4 bg-neutral-900 px-4 py-2 text-sm">
|
<header class="hud-panel z-[1000] m-2 flex items-center justify-between gap-4 px-4 py-2 text-sm">
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-5">
|
||||||
<span class="font-bold">Regress</span>
|
<span class="font-display text-lg font-black tracking-widest text-cyan-neon text-glow-cyan">REGRESS</span>
|
||||||
<span class="flex items-center gap-1 text-orange-500">
|
<span class="flex items-center gap-1 font-bold text-orange-neon text-glow-orange">
|
||||||
● {{ orangeScore?.claimedPlaces ?? 0 }} places · {{ orangeScore?.fields ?? 0 }} fields ·
|
▲ {{ orangeScore?.claimedPlaces ?? 0 }} <span class="text-orange-neon/60">nodes</span> ·
|
||||||
|
{{ orangeScore?.fields ?? 0 }} <span class="text-orange-neon/60">fields</span> ·
|
||||||
{{ (orangeScore?.areaKm2 ?? 0).toFixed(2) }} km²
|
{{ (orangeScore?.areaKm2 ?? 0).toFixed(2) }} km²
|
||||||
</span>
|
</span>
|
||||||
<span class="flex items-center gap-1 text-green-500">
|
<span class="flex items-center gap-1 font-bold text-green-neon text-glow-green">
|
||||||
● {{ greenScore?.claimedPlaces ?? 0 }} places · {{ greenScore?.fields ?? 0 }} fields ·
|
▲ {{ greenScore?.claimedPlaces ?? 0 }} <span class="text-green-neon/60">nodes</span> ·
|
||||||
|
{{ greenScore?.fields ?? 0 }} <span class="text-green-neon/60">fields</span> ·
|
||||||
{{ (greenScore?.areaKm2 ?? 0).toFixed(2) }} km²
|
{{ (greenScore?.areaKm2 ?? 0).toFixed(2) }} km²
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<span class="text-neutral-400">
|
<span class="text-cyan-neon/70">
|
||||||
{{ auth.displayName ?? auth.pubkey?.slice(0, 8) }} ·
|
{{ auth.displayName ?? auth.pubkey?.slice(0, 8) }}//<span
|
||||||
<span :class="auth.team === 'orange' ? 'text-orange-500' : 'text-green-500'">{{ auth.team }}</span>
|
:class="auth.team === 'orange' ? 'text-orange-neon text-glow-orange' : 'text-green-neon text-glow-green'"
|
||||||
|
class="font-bold uppercase"
|
||||||
|
>{{ auth.team }}</span
|
||||||
|
>
|
||||||
</span>
|
</span>
|
||||||
<button class="rounded bg-neutral-800 px-3 py-1 hover:bg-neutral-700" :disabled="actionBusy" @click="doSync">
|
<button
|
||||||
Sync from BTC Map
|
class="rounded border border-cyan-neon/50 bg-black/40 px-3 py-1 text-cyan-neon transition hover:border-cyan-neon hover:shadow-glow-cyan"
|
||||||
|
:disabled="actionBusy"
|
||||||
|
@click="doSync"
|
||||||
|
>
|
||||||
|
↻ Sync BTC Map
|
||||||
</button>
|
</button>
|
||||||
<button class="rounded bg-neutral-800 px-3 py-1 hover:bg-neutral-700" @click="auth.logout()">
|
<button
|
||||||
Log out
|
class="rounded border border-magenta-neon/50 bg-black/40 px-3 py-1 text-magenta-neon transition hover:border-magenta-neon hover:shadow-glow-magenta"
|
||||||
|
@click="auth.logout()"
|
||||||
|
>
|
||||||
|
Disconnect
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -187,35 +218,40 @@ watch(linkSourceId, redraw);
|
|||||||
<div class="relative flex-1">
|
<div class="relative flex-1">
|
||||||
<div ref="mapEl" class="h-full w-full"></div>
|
<div ref="mapEl" class="h-full w-full"></div>
|
||||||
|
|
||||||
<div
|
<div v-if="linkSourcePlace" class="hud-panel absolute left-4 top-4 z-[1000] px-4 py-2 text-sm text-cyan-neon">
|
||||||
v-if="linkSourcePlace"
|
LINKING FROM <strong class="text-glow-cyan">{{ linkSourcePlace.name }}</strong> — select a friendly node, or
|
||||||
class="absolute left-4 top-4 z-[1000] rounded-lg bg-neutral-900/95 px-4 py-2 text-sm shadow-lg"
|
<button class="ml-2 text-magenta-neon underline" @click="cancelLink">abort</button>
|
||||||
>
|
|
||||||
Linking from <strong>{{ linkSourcePlace.name }}</strong> — click a friendly place to complete, or
|
|
||||||
<button class="ml-2 text-neutral-400 underline" @click="cancelLink">cancel</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div v-if="selectedPlace" class="hud-panel absolute right-4 top-4 z-[1000] w-72 p-4 text-sm">
|
||||||
v-if="selectedPlace"
|
|
||||||
class="absolute right-4 top-4 z-[1000] w-72 rounded-lg bg-neutral-900/95 p-4 text-sm shadow-lg"
|
|
||||||
>
|
|
||||||
<div class="mb-1 flex items-start justify-between gap-2">
|
<div class="mb-1 flex items-start justify-between gap-2">
|
||||||
<h2 class="font-bold">{{ selectedPlace.name || `Place ${selectedPlace.id}` }}</h2>
|
<h2 class="font-display font-bold text-cyan-neon text-glow-cyan">
|
||||||
<button class="text-neutral-500" @click="selectedPlaceId = null">✕</button>
|
{{ selectedPlace.name || `Node ${selectedPlace.id}` }}
|
||||||
|
</h2>
|
||||||
|
<button class="text-cyan-neon/50 hover:text-cyan-neon" @click="selectedPlaceId = null">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="selectedPlace.address" class="mb-2 text-neutral-400">{{ selectedPlace.address }}</p>
|
<p v-if="selectedPlace.address" class="mb-2 text-cyan-neon/50">{{ selectedPlace.address }}</p>
|
||||||
<p class="mb-3">
|
<p class="mb-3">
|
||||||
Status:
|
STATUS:
|
||||||
<span v-if="!selectedPlace.claim_team" class="text-neutral-400">unclaimed</span>
|
<span v-if="!selectedPlace.claim_team" class="text-cyan-neon/60">UNCLAIMED</span>
|
||||||
<span v-else :class="selectedPlace.claim_team === 'orange' ? 'text-orange-500' : 'text-green-500'">
|
<span
|
||||||
claimed by {{ selectedPlace.claim_team }}
|
v-else
|
||||||
|
class="font-bold uppercase"
|
||||||
|
:class="selectedPlace.claim_team === 'orange' ? 'text-orange-neon text-glow-orange' : 'text-green-neon text-glow-green'"
|
||||||
|
>
|
||||||
|
held by {{ selectedPlace.claim_team }}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<button
|
<button
|
||||||
v-if="selectedPlace.claim_team !== auth.team"
|
v-if="selectedPlace.claim_team !== auth.team"
|
||||||
class="rounded bg-white px-3 py-2 font-semibold text-black hover:bg-neutral-200 disabled:opacity-50"
|
class="rounded border px-3 py-2 font-bold uppercase transition disabled:opacity-50"
|
||||||
|
:class="
|
||||||
|
auth.team === 'orange'
|
||||||
|
? 'border-orange-neon text-orange-neon hover:shadow-glow-orange'
|
||||||
|
: 'border-green-neon text-green-neon hover:shadow-glow-green'
|
||||||
|
"
|
||||||
:disabled="actionBusy"
|
:disabled="actionBusy"
|
||||||
@click="doClaim(selectedPlace)"
|
@click="doClaim(selectedPlace)"
|
||||||
>
|
>
|
||||||
@@ -223,7 +259,7 @@ watch(linkSourceId, redraw);
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-else-if="linkSourceId !== selectedPlace.id"
|
v-else-if="linkSourceId !== selectedPlace.id"
|
||||||
class="rounded bg-neutral-800 px-3 py-2 hover:bg-neutral-700 disabled:opacity-50"
|
class="rounded border border-cyan-neon/50 px-3 py-2 text-cyan-neon transition hover:border-cyan-neon hover:shadow-glow-cyan disabled:opacity-50"
|
||||||
:disabled="actionBusy"
|
:disabled="actionBusy"
|
||||||
@click="startLink(selectedPlace)"
|
@click="startLink(selectedPlace)"
|
||||||
>
|
>
|
||||||
@@ -231,7 +267,7 @@ watch(linkSourceId, redraw);
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="linkSourceId && linkSourceId !== selectedPlace.id && selectedPlace.claim_team === auth.team"
|
v-if="linkSourceId && linkSourceId !== selectedPlace.id && selectedPlace.claim_team === auth.team"
|
||||||
class="rounded bg-neutral-800 px-3 py-2 hover:bg-neutral-700 disabled:opacity-50"
|
class="rounded border border-cyan-neon/50 px-3 py-2 text-cyan-neon transition hover:border-cyan-neon hover:shadow-glow-cyan disabled:opacity-50"
|
||||||
:disabled="actionBusy"
|
:disabled="actionBusy"
|
||||||
@click="completeLink(selectedPlace)"
|
@click="completeLink(selectedPlace)"
|
||||||
>
|
>
|
||||||
@@ -239,17 +275,21 @@ watch(linkSourceId, redraw);
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p v-if="actionError" class="mt-3 text-red-400">{{ actionError }}</p>
|
<p v-if="actionError" class="mt-3 text-magenta-neon">{{ actionError }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="!game.places.length && !game.loading"
|
v-if="!game.places.length && !game.loading"
|
||||||
class="absolute inset-0 z-[999] flex items-center justify-center bg-black/60"
|
class="absolute inset-0 z-[999] flex items-center justify-center bg-black/70"
|
||||||
>
|
>
|
||||||
<div class="rounded-lg bg-neutral-900 p-6 text-center">
|
<div class="hud-panel p-6 text-center">
|
||||||
<p class="mb-3">No places loaded yet — pull the current Madeira dataset from BTC Map.</p>
|
<p class="mb-3 text-cyan-neon">NO NODES DETECTED — pull the live Madeira dataset from BTC Map.</p>
|
||||||
<button class="rounded bg-white px-4 py-2 font-semibold text-black" :disabled="actionBusy" @click="doSync">
|
<button
|
||||||
Sync from BTC Map
|
class="rounded border border-cyan-neon px-4 py-2 font-bold text-cyan-neon hover:shadow-glow-cyan"
|
||||||
|
:disabled="actionBusy"
|
||||||
|
@click="doSync"
|
||||||
|
>
|
||||||
|
↻ Sync from BTC Map
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -22,26 +22,26 @@ async function pick(team: Team) {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex h-full flex-col items-center justify-center gap-6 px-6 text-center">
|
<div class="flex h-full flex-col items-center justify-center gap-6 px-6 text-center">
|
||||||
<h1 class="text-2xl font-bold">Pick your faction</h1>
|
<h1 class="font-display text-3xl font-black tracking-widest text-cyan-neon text-glow-cyan">CHOOSE YOUR FACTION</h1>
|
||||||
<p class="max-w-sm text-neutral-400">
|
<p class="max-w-sm text-cyan-neon/60">
|
||||||
This choice is permanent — you can't switch teams later, so pick the side you're actually playing for.
|
This choice is permanent — you can't switch teams later, so pick the side you're actually playing for.
|
||||||
</p>
|
</p>
|
||||||
<div class="flex gap-4">
|
<div class="flex gap-6">
|
||||||
<button
|
<button
|
||||||
class="rounded-lg bg-orange-500 px-8 py-4 text-lg font-bold text-black transition hover:bg-orange-400 disabled:opacity-50"
|
class="rounded border-2 border-orange-neon px-10 py-6 font-display text-xl font-black uppercase tracking-wider text-orange-neon transition hover:bg-orange-neon/10 hover:shadow-glow-orange disabled:opacity-50"
|
||||||
:disabled="picking"
|
:disabled="picking"
|
||||||
@click="pick('orange')"
|
@click="pick('orange')"
|
||||||
>
|
>
|
||||||
Orange
|
Orange
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="rounded-lg bg-green-500 px-8 py-4 text-lg font-bold text-black transition hover:bg-green-400 disabled:opacity-50"
|
class="rounded border-2 border-green-neon px-10 py-6 font-display text-xl font-black uppercase tracking-wider text-green-neon transition hover:bg-green-neon/10 hover:shadow-glow-green disabled:opacity-50"
|
||||||
:disabled="picking"
|
:disabled="picking"
|
||||||
@click="pick('green')"
|
@click="pick('green')"
|
||||||
>
|
>
|
||||||
Green
|
Green
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="error" class="text-sm text-red-400">{{ error }}</p>
|
<p v-if="error" class="text-sm text-magenta-neon">{{ error }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -3,9 +3,37 @@ export default {
|
|||||||
content: ['./index.html', './src/**/*.{vue,ts}'],
|
content: ['./index.html', './src/**/*.{vue,ts}'],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
|
fontFamily: {
|
||||||
|
display: ['Orbitron', 'sans-serif'],
|
||||||
|
mono: ['"Share Tech Mono"', 'monospace'],
|
||||||
|
},
|
||||||
colors: {
|
colors: {
|
||||||
orange: { team: '#f97316' },
|
void: '#050508',
|
||||||
green: { team: '#22c55e' },
|
panel: '#0b0d16',
|
||||||
|
cyan: { neon: '#00fff2' },
|
||||||
|
magenta: { neon: '#ff00e5' },
|
||||||
|
orange: { neon: '#ff5f1f' },
|
||||||
|
green: { neon: '#39ff14' },
|
||||||
|
},
|
||||||
|
boxShadow: {
|
||||||
|
'glow-cyan': '0 0 6px #00fff2, 0 0 18px rgba(0,255,242,0.5)',
|
||||||
|
'glow-magenta': '0 0 6px #ff00e5, 0 0 18px rgba(255,0,229,0.5)',
|
||||||
|
'glow-orange': '0 0 6px #ff5f1f, 0 0 18px rgba(255,95,31,0.6)',
|
||||||
|
'glow-green': '0 0 6px #39ff14, 0 0 18px rgba(57,255,20,0.6)',
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
'pulse-glow': 'pulse-glow 2.4s ease-in-out infinite',
|
||||||
|
scan: 'scan 6s linear infinite',
|
||||||
|
},
|
||||||
|
keyframes: {
|
||||||
|
'pulse-glow': {
|
||||||
|
'0%, 100%': { opacity: 1 },
|
||||||
|
'50%': { opacity: 0.55 },
|
||||||
|
},
|
||||||
|
scan: {
|
||||||
|
'0%': { backgroundPosition: '0 0' },
|
||||||
|
'100%': { backgroundPosition: '0 64px' },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user