radio: public read-only tracker for Opa (NL/EN toggle, bigger type and tap targets)
Deploy production / deploy (push) Successful in 1m22s
Deploy production / deploy (push) Successful in 1m22s
Visitors see frequencies, banks, nets, ISS passes and the log without logging in; every write stays owner-only (actions check the owner, hooks refuse non-GET). The public payload drops reminder switches, log notes and inactive nets, and the page no longer shows the observer coordinates (only "Gouda"). Dutch/English toggle (?lang, cookie, Accept-Language, NL fallback). Larger fonts, high-contrast labels and 44px+ controls for phones and tablets; radio listed as public (no lock) and in the sitemap; banks CSV public.
This commit is contained in:
1 parent
e1e2bee590
commit
96d7333fe9
16 files changed
+1061
-420
No files matched your search
+115
-33
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
// Uniden UBC125XLT memory: 10 banks x 50 channels. Pick a bank, tap a channel to edit it.
|
||||
// Uniden UBC125XLT memory: 10 banks x 50 channels. Pick a bank, tap a channel to see it (and,
|
||||
// for the owner, edit it).
|
||||
import { enhance } from '$app/forms';
|
||||
import { page } from '$app/state';
|
||||
import { resolve } from '$app/paths';
|
||||
@@ -16,10 +17,17 @@
|
||||
bankRange,
|
||||
formatFreq
|
||||
} from './index.js';
|
||||
import { DICTS, getRadio } from './i18n.js';
|
||||
import type { Channel, Frequency } from '#lib/server/radio.js';
|
||||
|
||||
let { channels, frequencies }: { channels: Channel[]; frequencies: Frequency[] } = $props();
|
||||
|
||||
const r = getRadio();
|
||||
/** Bring the channel details into view (they sit under the 50 channels). */
|
||||
const reveal = (node: HTMLElement) => {
|
||||
node.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
||||
};
|
||||
const t = $derived(DICTS[r.lang]);
|
||||
const initialCh = Number(page.url.searchParams.get('ch')) || null;
|
||||
let bank = $state(initialCh ? bankOf(initialCh) : Number(page.url.searchParams.get('bank')) || 4);
|
||||
let selected = $state<number | null>(initialCh);
|
||||
@@ -46,7 +54,7 @@
|
||||
|
||||
<section class="card">
|
||||
<div class="bar">
|
||||
<div class="banks" role="tablist" aria-label="Scanner banks">
|
||||
<div class="banks" role="tablist" aria-label={t.banksLabel}>
|
||||
{#each perBank as n, i (i)}
|
||||
<button
|
||||
type="button"
|
||||
@@ -56,17 +64,18 @@
|
||||
onclick={() => {
|
||||
bank = i + 1;
|
||||
selected = null;
|
||||
}}><b>{i + 1}</b><span>{n}/{PER_BANK}</span></button
|
||||
}}
|
||||
aria-label="{t.bank} {i + 1}: {n}/{PER_BANK}"
|
||||
><b>{i + 1}</b><span>{n}/{PER_BANK}</span></button
|
||||
>
|
||||
{/each}
|
||||
</div>
|
||||
<a class="ghost" href={resolve('/trackers/radio/banks.csv')} download data-sveltekit-reload
|
||||
><Download size={14} />CSV</a
|
||||
><Download size={18} />{t.csv}</a
|
||||
>
|
||||
</div>
|
||||
<p class="meta hint">
|
||||
Bank {bank}: channels {bankRange(bank)[0]}–{bankRange(bank)[1]} · {channels.length} of 500 programmed
|
||||
· Uniden UBC125XLT layout (model unconfirmed)
|
||||
{t.bankHint(bank, bankRange(bank)[0], bankRange(bank)[1], channels.length)}
|
||||
</p>
|
||||
|
||||
<div class="grid">
|
||||
@@ -81,17 +90,50 @@
|
||||
>
|
||||
<span class="no">{ch}</span>
|
||||
{#if f && c}
|
||||
<BandIcon band={f.band} size={16} />
|
||||
<BandIcon band={f.band} size={26} />
|
||||
<span class="tg">{c.tag}</span>
|
||||
<span class="fq">{formatFreq(f.freqHz, false)} <i>{f.mode}</i></span>
|
||||
{:else}
|
||||
<span class="tg dim">empty</span>
|
||||
<span class="tg dim">{t.empty}</span>
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if selected}
|
||||
{#if selected && !r.owner}
|
||||
{#key selected}
|
||||
{@const f = sel ? byId.get(sel.frequencyId) : undefined}
|
||||
<div class="detail" role="region" use:reveal aria-label={t.channelN(selected)}>
|
||||
<div class="bar">
|
||||
<strong>{t.channelN(selected)}</strong>
|
||||
<button type="button" class="ghost" onclick={() => (selected = null)}
|
||||
><X size={18} />{t.close}</button
|
||||
>
|
||||
</div>
|
||||
{#if f && sel}
|
||||
<div class="drow">
|
||||
<BandIcon band={f.band} size={40} />
|
||||
<div>
|
||||
<p class="dname">{f.name}</p>
|
||||
<p class="dfreq">
|
||||
{formatFreq(f.freqHz)} · {f.mode} · {t.bands[f.band as keyof typeof t.bands] ??
|
||||
f.band}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p class="dflags">
|
||||
<span class="tag">{sel.tag}</span>
|
||||
{#if sel.priority}<span class="tag">{t.priority}</span>{/if}
|
||||
{#if sel.lockout}<span class="tag">{t.lockout}</span>{/if}
|
||||
{#if f.location}<span>{f.location}</span>{/if}
|
||||
</p>
|
||||
{#if f.notes}<p class="meta">{f.notes}</p>{/if}
|
||||
{:else}
|
||||
<p class="meta">{t.empty}</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/key}
|
||||
{:else if selected}
|
||||
{@const f = sel ? byId.get(sel.frequencyId) : undefined}
|
||||
<form
|
||||
class="edit chedit"
|
||||
@@ -162,27 +204,28 @@
|
||||
<style>
|
||||
.banks {
|
||||
display: flex;
|
||||
gap: 0.3rem;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
gap: 0.4rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.bank {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
min-width: 2.8rem;
|
||||
padding: 0.25rem 0.4rem;
|
||||
justify-content: center;
|
||||
min-width: 3.4rem;
|
||||
min-height: 3.4rem;
|
||||
padding: 0.3rem 0.5rem;
|
||||
border: 1px solid var(--site-border);
|
||||
border-radius: 0.55rem;
|
||||
border-radius: 0.7rem;
|
||||
color: var(--site-text);
|
||||
line-height: 1.1;
|
||||
line-height: 1.15;
|
||||
}
|
||||
.bank b {
|
||||
font-size: 0.95rem;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
.bank span {
|
||||
font-size: 0.62rem;
|
||||
color: var(--site-muted);
|
||||
font-size: 0.78rem;
|
||||
opacity: 0.75;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.bank.empty {
|
||||
@@ -203,18 +246,20 @@
|
||||
}
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(10.5rem, 1fr));
|
||||
gap: 0.3rem;
|
||||
grid-template-columns: repeat(auto-fill, minmax(13rem, 1fr));
|
||||
gap: 0.4rem;
|
||||
}
|
||||
.slot {
|
||||
display: grid;
|
||||
grid-template-columns: 1.9rem 16px 1fr;
|
||||
grid-template-columns: 2.4rem 26px 1fr;
|
||||
grid-template-rows: auto auto;
|
||||
column-gap: 0.4rem;
|
||||
column-gap: 0.5rem;
|
||||
align-items: center;
|
||||
padding: 0.35rem 0.45rem;
|
||||
min-height: 3.5rem;
|
||||
padding: 0.45rem 0.6rem;
|
||||
border: 1px solid var(--site-border);
|
||||
border-radius: 0.5rem;
|
||||
border-radius: 0.65rem;
|
||||
color: var(--site-text);
|
||||
text-align: left;
|
||||
background: var(--site-bg);
|
||||
min-width: 0;
|
||||
@@ -232,15 +277,17 @@
|
||||
}
|
||||
.no {
|
||||
grid-row: 1 / 3;
|
||||
font-size: 0.7rem;
|
||||
color: var(--site-muted);
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
opacity: 0.75;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.slot :global(.band-icon) {
|
||||
grid-row: 1 / 3;
|
||||
}
|
||||
.tg {
|
||||
font-size: 0.74rem;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -248,14 +295,14 @@
|
||||
}
|
||||
.tg.dim {
|
||||
grid-column: 2 / 4;
|
||||
color: var(--site-muted);
|
||||
opacity: 0.6;
|
||||
font-weight: 400;
|
||||
opacity: 0.55;
|
||||
text-transform: none;
|
||||
}
|
||||
.fq {
|
||||
grid-column: 3;
|
||||
font-size: 0.68rem;
|
||||
color: var(--site-muted);
|
||||
font-size: 0.85rem;
|
||||
opacity: 0.8;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.fq i {
|
||||
@@ -265,6 +312,38 @@
|
||||
.chedit {
|
||||
margin-top: 0.8rem;
|
||||
}
|
||||
.detail {
|
||||
margin-top: 0.9rem;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--site-text);
|
||||
border-radius: 0.8rem;
|
||||
background: var(--site-bg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
.detail strong {
|
||||
font-size: 1.15rem;
|
||||
}
|
||||
.drow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
.dname {
|
||||
font-size: 1.15rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.dfreq {
|
||||
font-size: 1.05rem;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.dflags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.4rem;
|
||||
align-items: center;
|
||||
}
|
||||
.checks {
|
||||
display: flex;
|
||||
gap: 0.8rem;
|
||||
@@ -279,7 +358,10 @@
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
.bank {
|
||||
min-width: 3.1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user