Files
gashboard/apps/web/src/App.vue
2026-05-06 19:58:03 +01:00

222 lines
5.5 KiB
Vue

<script setup lang="ts">
import { computed, onMounted, ref, watch } from "vue";
import { RouterLink, RouterView, useRoute } from "vue-router";
import { useAuthStore } from "./stores/auth";
import { useStatsStore } from "./stores/stats";
import ChatDrawer from "./components/ChatDrawer.vue";
import ReleaseNotesDrawer from "./components/ReleaseNotesDrawer.vue";
const auth = useAuthStore();
const stats = useStatsStore();
const route = useRoute();
const crt = ref(false);
const chatOpen = ref(false);
const releaseOpen = ref(false);
const LOW_HASHRATE_THS = 10;
const TOP_HASHRATE_THS = 70;
const heatLevel = computed(() => {
const total = stats.snapshot?.pool.combinedHashrateThs ?? 0;
return Math.max(0, Math.min(1, (total - LOW_HASHRATE_THS) / (TOP_HASHRATE_THS - LOW_HASHRATE_THS)));
});
onMounted(() => {
const stored = localStorage.getItem("gashboard.crt") === "1";
crt.value = stored;
if (stored) document.body.classList.add("crt");
});
function toggleCrt(): void {
crt.value = !crt.value;
document.body.classList.toggle("crt", crt.value);
localStorage.setItem("gashboard.crt", crt.value ? "1" : "0");
}
function shortNpub(n: string): string {
return n.length > 16 ? `${n.slice(0, 8)}${n.slice(-4)}` : n;
}
watch(
heatLevel,
(level) => {
document.documentElement.style.setProperty("--heat", level.toFixed(3));
document.documentElement.style.setProperty("--heat-color", level > 0.75 ? "#ff4f78" : "#ff3df0");
document.body.classList.toggle("hot-hash", level > 0.75);
},
{ immediate: true },
);
</script>
<template>
<header class="topbar">
<div class="brand glow-magenta flicker">
GASHBOARD
<span class="muted">// solo lottery dashboard</span>
</div>
<nav v-if="auth.isLoggedIn" class="nav">
<RouterLink to="/">status</RouterLink>
<RouterLink to="/graphs">graphs</RouterLink>
</nav>
<div class="actions">
<button class="thin" @click="toggleCrt">CRT {{ crt ? "on" : "off" }}</button>
<button class="thin" @click="releaseOpen = !releaseOpen">notes</button>
<template v-if="auth.isLoggedIn">
<button class="thin" @click="chatOpen = !chatOpen">chat</button>
<span class="muted">{{ auth.npub ? shortNpub(auth.npub) : "" }}</span>
<button class="thin" @click="auth.logout()">logout</button>
</template>
</div>
</header>
<nav v-if="auth.isLoggedIn" class="mobile-tabbar" aria-label="mobile navigation">
<RouterLink to="/">status</RouterLink>
<RouterLink to="/graphs">graphs</RouterLink>
<button type="button" @click="releaseOpen = !releaseOpen">notes</button>
<button type="button" @click="chatOpen = !chatOpen">chat</button>
</nav>
<main :class="{ shellLogin: route.name === 'login' }">
<RouterView />
</main>
<footer class="footbar muted">
P(block) is a lifestyle, not a number · gashboard v0.1 · {{ new Date().getFullYear() }}
</footer>
<ChatDrawer v-if="auth.isLoggedIn" :open="chatOpen" @close="chatOpen = false" />
<ReleaseNotesDrawer :open="releaseOpen" @close="releaseOpen = false" />
</template>
<style scoped>
.topbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 24px;
border-bottom: 1px solid var(--line);
background: rgba(7, 9, 15, 0.85);
backdrop-filter: blur(6px);
position: sticky;
top: 0;
z-index: 10;
}
.brand {
font-weight: 700;
letter-spacing: 0.18em;
font-size: 15px;
}
.brand .muted {
margin-left: 12px;
letter-spacing: 0.04em;
font-weight: 400;
font-size: 11px;
}
.nav {
display: flex;
gap: 8px;
align-items: center;
margin-left: auto;
margin-right: 16px;
}
.nav a {
color: var(--fg-1);
border: 1px solid var(--line);
padding: 4px 10px;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.nav a:hover,
.nav a.router-link-active {
border-color: var(--neon-cyan);
color: var(--neon-cyan);
box-shadow: var(--shadow-cyan);
}
.mobile-tabbar {
display: none;
}
.actions {
display: flex;
gap: 12px;
align-items: center;
}
button.thin {
padding: 4px 10px;
font-size: 11px;
}
main {
padding: 24px;
max-width: 1400px;
margin: 0 auto;
}
main.shellLogin {
display: grid;
place-items: center;
min-height: calc(100vh - 110px);
}
.footbar {
padding: 14px 24px;
border-top: 1px solid var(--line);
font-size: 11px;
text-align: center;
letter-spacing: 0.08em;
}
@media (max-width: 760px) {
.topbar {
align-items: center;
padding: 10px 14px;
}
.brand .muted,
.nav {
display: none;
}
.actions {
width: auto;
justify-content: flex-end;
flex-wrap: wrap;
gap: 8px;
}
.actions button:nth-of-type(n + 2) {
display: none;
}
.actions .muted {
display: none;
}
.mobile-tabbar {
position: sticky;
top: 49px;
z-index: 9;
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 0;
border-bottom: 1px solid var(--line);
background: rgba(7, 9, 15, 0.94);
backdrop-filter: blur(6px);
}
.mobile-tabbar a,
.mobile-tabbar button {
min-width: 0;
border: 0;
border-right: 1px solid var(--line);
color: var(--fg-1);
padding: 10px 4px;
text-align: center;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.08em;
background: transparent;
}
.mobile-tabbar a:last-child,
.mobile-tabbar button:last-child {
border-right: 0;
}
.mobile-tabbar a.router-link-active,
.mobile-tabbar button:hover {
color: var(--neon-cyan);
box-shadow: inset 0 -1px 0 var(--neon-cyan), var(--shadow-cyan);
}
main {
padding: 16px;
}
}
</style>