399 lines
11 KiB
Vue
399 lines
11 KiB
Vue
<template>
|
||
<header class="retro-window hero-header">
|
||
<!-- Top System Window Titlebar -->
|
||
<div class="retro-window-header">
|
||
<div class="retro-window-title">
|
||
<span class="system-icon">🕹️</span>
|
||
<span>CITY TELEMETRY // GRABENGASSE 7</span>
|
||
</div>
|
||
<div class="retro-window-controls">
|
||
<span class="retro-btn-box">_</span>
|
||
<span class="retro-btn-box">□</span>
|
||
<span class="retro-btn-box">×</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Hero Content Board -->
|
||
<div class="retro-window-content hero-content">
|
||
<div class="hero-top-row">
|
||
<!-- Brand & Crest -->
|
||
<div class="hero-brand">
|
||
<div class="city-crest">
|
||
<span class="crest-icon">🏛️</span>
|
||
</div>
|
||
<div class="brand-text">
|
||
<h1 class="brand-title">GRABENGASSE 7</h1>
|
||
<div class="brand-meta">
|
||
<span class="meta-tag">REGENSBURG</span>
|
||
<span class="meta-separator">•</span>
|
||
<span class="meta-tag">ELEV 342M</span>
|
||
<span class="meta-separator">•</span>
|
||
<span class="meta-tag">WS-90</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Action / Controls Panel -->
|
||
<div class="hero-actions">
|
||
<!-- Live Station Badge -->
|
||
<div class="retro-badge badge-green action-badge">
|
||
<span class="retro-blinker"></span>
|
||
<span>WS-90 LIVE</span>
|
||
</div>
|
||
|
||
<!-- Theme Toggle (Dark / Light) -->
|
||
<button
|
||
class="retro-btn theme-toggle"
|
||
@click="toggleTheme"
|
||
:title="isDark ? 'Switch to 16-Bit Bright Theme (Amiga/SNES Day)' : 'Switch to 16-Bit Dark Theme (CRT Night)'"
|
||
>
|
||
<span>{{ isDark ? '☀️' : '🌙' }}</span>
|
||
<span>{{ isDark ? 'DAY' : 'NIGHT' }}</span>
|
||
</button>
|
||
|
||
<!-- CRT Scanline Toggle -->
|
||
<button
|
||
class="retro-btn crt-toggle"
|
||
:class="{ active: crtEnabled }"
|
||
@click="toggleCrt"
|
||
title="Toggle Retro CRT Scanline Filter"
|
||
>
|
||
<span>📺</span>
|
||
<span>{{ crtEnabled ? 'CRT ON' : 'CRT OFF' }}</span>
|
||
</button>
|
||
|
||
<!-- PWA Install App Button (When installable or on mobile) -->
|
||
<button
|
||
class="retro-btn install-btn"
|
||
:class="{ 'btn-accent': installable }"
|
||
@click="handleInstallClick"
|
||
title="Install Grabengasse 7 as a native Android/Apple Home Screen App"
|
||
>
|
||
<span>📲</span>
|
||
<span>INSTALL</span>
|
||
</button>
|
||
|
||
<!-- Manual Refresh -->
|
||
<button class="retro-btn sync-btn" @click="$emit('refresh')" title="Poll latest metrics">
|
||
<span>🔄</span>
|
||
<span>SYNC</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- iOS / Safari Installation Guide Banner (if clicked on iOS) -->
|
||
<div v-if="showIosGuide" class="retro-well ios-install-well">
|
||
<div class="ios-guide-header">
|
||
<span>🍎 INSTALL ON APPLE iOS / SAFARI:</span>
|
||
<button class="retro-btn-box" @click="showIosGuide = false">×</button>
|
||
</div>
|
||
<div class="ios-guide-body">
|
||
<span>1. Tap the <strong>Share button (⎋)</strong> in Safari</span>
|
||
<span>2. Scroll and select <strong>'Add to Home Screen (⊞)'</strong></span>
|
||
<span>3. Tap <strong>'Add'</strong> to launch as a standalone app!</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Animated SimCity Weather Advisory Ticker -->
|
||
<div class="retro-well ticker-well">
|
||
<div class="ticker-label">NEWS:</div>
|
||
<div class="ticker-container">
|
||
<div class="ticker-text">
|
||
<span>*** GRABENGASSE 7 WEATHER REPORT: METEOROLOGICAL TELEMETRY ACTIVE ***</span>
|
||
<span class="ticker-item">STATION: SHELLY WS90 (PROMETHEUS)</span>
|
||
<span class="ticker-item">LOCATION: REGENSBURG HISTORIC DISTRICT</span>
|
||
<span class="ticker-item">PWA READY: INSTALLABLE AS STANDALONE ANDROID & APPLE APP</span>
|
||
<span class="ticker-item">LAST SYNC: {{ lastUpdated ? formatTime(lastUpdated) : 'CONNECTING...' }}</span>
|
||
<span class="ticker-item">ADVISORY: CLICK ANY METRIC FOR 16-BIT HISTORICAL GRAPH INSPECTOR</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, onMounted } from 'vue'
|
||
|
||
defineProps<{
|
||
lastUpdated?: string
|
||
}>()
|
||
|
||
const emit = defineEmits<{
|
||
(e: 'refresh'): void
|
||
(e: 'theme-changed', theme: 'dark' | 'light'): void
|
||
(e: 'crt-changed', enabled: boolean): void
|
||
}>()
|
||
|
||
const isDark = ref(true)
|
||
const crtEnabled = ref(true)
|
||
const installable = ref(false)
|
||
const showIosGuide = ref(false)
|
||
let deferredPrompt: any = null
|
||
|
||
function formatTime(isoString?: string): string {
|
||
if (!isoString) return ''
|
||
try {
|
||
const date = new Date(isoString)
|
||
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' })
|
||
} catch {
|
||
return isoString
|
||
}
|
||
}
|
||
|
||
function toggleTheme() {
|
||
isDark.value = !isDark.value
|
||
const theme = isDark.value ? 'dark' : 'light'
|
||
document.documentElement.setAttribute('data-theme', theme)
|
||
localStorage.setItem('grabengasse_theme', theme)
|
||
emit('theme-changed', theme)
|
||
}
|
||
|
||
function toggleCrt() {
|
||
crtEnabled.value = !crtEnabled.value
|
||
localStorage.setItem('grabengasse_crt', crtEnabled.value ? 'true' : 'false')
|
||
emit('crt-changed', crtEnabled.value)
|
||
}
|
||
|
||
async function handleInstallClick() {
|
||
if (deferredPrompt) {
|
||
deferredPrompt.prompt()
|
||
const { outcome } = await deferredPrompt.userChoice
|
||
if (outcome === 'accepted') {
|
||
installable.value = false
|
||
deferredPrompt = null
|
||
}
|
||
} else {
|
||
// Check if on iOS or desktop where native prompt isn't supported directly
|
||
const isIos = /iPad|iPhone|iPod/.test(navigator.userAgent) && !(window as any).MSStream
|
||
if (isIos) {
|
||
showIosGuide.value = !showIosGuide.value
|
||
} else {
|
||
alert("To install this app on your device:\n\n• Android: Tap the browser menu (⋮) -> 'Install App' or 'Add to Home screen'\n• iPhone/iPad: Tap Share (⎋) -> 'Add to Home Screen (⊞)'\n• Chrome/Edge: Click the install icon in the URL bar (⊕)")
|
||
}
|
||
}
|
||
}
|
||
|
||
onMounted(() => {
|
||
const savedTheme = localStorage.getItem('grabengasse_theme') || 'dark'
|
||
isDark.value = savedTheme === 'dark'
|
||
document.documentElement.setAttribute('data-theme', savedTheme)
|
||
|
||
const savedCrt = localStorage.getItem('grabengasse_crt')
|
||
if (savedCrt !== null) {
|
||
crtEnabled.value = savedCrt === 'true'
|
||
}
|
||
|
||
// Listen for PWA beforeinstallprompt on Android / Chrome
|
||
window.addEventListener('beforeinstallprompt', (e) => {
|
||
e.preventDefault()
|
||
deferredPrompt = e
|
||
installable.value = true
|
||
})
|
||
|
||
window.addEventListener('appinstalled', () => {
|
||
installable.value = false
|
||
deferredPrompt = null
|
||
})
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.hero-header {
|
||
margin-top: 0.25rem;
|
||
}
|
||
|
||
.hero-content {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.75rem;
|
||
}
|
||
|
||
.hero-top-row {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 0.75rem;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.hero-brand {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.75rem;
|
||
}
|
||
|
||
.city-crest {
|
||
width: 42px;
|
||
height: 42px;
|
||
min-width: 42px;
|
||
background: var(--retro-window-inner);
|
||
border-top: 2px solid var(--retro-bevel-dark);
|
||
border-left: 2px solid var(--retro-bevel-dark);
|
||
border-right: 2px solid var(--retro-bevel-light);
|
||
border-bottom: 2px solid var(--retro-bevel-light);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 1.4rem;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.brand-text {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.2rem;
|
||
}
|
||
|
||
.brand-title {
|
||
font-family: var(--font-pixel);
|
||
font-size: clamp(0.95rem, 3.8vw, 1.35rem);
|
||
color: var(--retro-accent-amber);
|
||
letter-spacing: 0.03em;
|
||
text-shadow: 2px 2px 0px rgba(0, 0, 0, 0.5);
|
||
line-height: 1.2;
|
||
}
|
||
|
||
.brand-meta {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.35rem;
|
||
font-family: var(--font-ui);
|
||
font-size: clamp(0.55rem, 1.8vw, 0.65rem);
|
||
color: var(--retro-text-dim);
|
||
text-transform: uppercase;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.meta-tag {
|
||
color: var(--retro-accent-cyan);
|
||
}
|
||
|
||
.meta-separator {
|
||
color: var(--retro-text-muted);
|
||
}
|
||
|
||
.hero-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.45rem;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.action-badge {
|
||
padding: 0.35rem 0.55rem;
|
||
}
|
||
|
||
/* iOS Guide */
|
||
.ios-install-well {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.4rem;
|
||
padding: 0.55rem 0.75rem;
|
||
border-color: #0c4a6e #38bdf8 #38bdf8 #0c4a6e;
|
||
background: var(--retro-window-inner);
|
||
}
|
||
|
||
.ios-guide-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
font-family: var(--font-pixel);
|
||
font-size: 0.65rem;
|
||
color: var(--retro-accent-cyan);
|
||
}
|
||
|
||
.ios-guide-body {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.25rem;
|
||
font-family: var(--font-mono);
|
||
font-size: 0.7rem;
|
||
color: var(--retro-text-main);
|
||
}
|
||
|
||
/* Ticker */
|
||
.ticker-well {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.6rem;
|
||
padding: 0.35rem 0.55rem;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.ticker-label {
|
||
font-family: var(--font-pixel);
|
||
font-size: clamp(0.55rem, 2vw, 0.65rem);
|
||
color: var(--retro-accent-red);
|
||
background: var(--retro-window-bg);
|
||
padding: 0.15rem 0.35rem;
|
||
border-top: 1px solid var(--retro-bevel-light);
|
||
border-left: 1px solid var(--retro-bevel-light);
|
||
border-right: 1px solid var(--retro-bevel-dark);
|
||
border-bottom: 1px solid var(--retro-bevel-dark);
|
||
white-space: nowrap;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.ticker-container {
|
||
overflow: hidden;
|
||
white-space: nowrap;
|
||
flex-grow: 1;
|
||
}
|
||
|
||
.ticker-text {
|
||
display: inline-block;
|
||
white-space: nowrap;
|
||
font-family: var(--font-mono);
|
||
font-size: clamp(0.68rem, 2.2vw, 0.75rem);
|
||
color: var(--retro-text-main);
|
||
animation: tickerSlide 25s linear infinite;
|
||
}
|
||
|
||
.ticker-item {
|
||
margin-left: 1.5rem;
|
||
color: var(--retro-accent-amber);
|
||
}
|
||
|
||
@keyframes tickerSlide {
|
||
0% { transform: translateX(100%); }
|
||
100% { transform: translateX(-100%); }
|
||
}
|
||
|
||
@media (max-width: 640px) {
|
||
.hero-top-row {
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
gap: 0.65rem;
|
||
}
|
||
|
||
.city-crest {
|
||
width: 36px;
|
||
height: 36px;
|
||
min-width: 36px;
|
||
font-size: 1.2rem;
|
||
}
|
||
|
||
/* Clean 2x2 Grid of Actions on Mobile for balanced layout */
|
||
.hero-actions {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 0.35rem;
|
||
width: 100%;
|
||
}
|
||
|
||
.hero-actions .action-badge {
|
||
grid-column: span 2;
|
||
justify-content: center;
|
||
padding: 0.35rem;
|
||
}
|
||
|
||
.hero-actions .retro-btn {
|
||
width: 100%;
|
||
justify-content: center;
|
||
padding: 0.4rem 0.3rem;
|
||
font-size: 0.65rem;
|
||
}
|
||
}
|
||
</style>
|