Files
infrapuzzle/website/grabengasse_frontend/src/components/HouseHero.vue
T

399 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>