radio: classic ham-site public view (Opa's Radiohoek) with link directory
Deploy production / deploy (push) Successful in 1m23s

- PublicRadio.svelte: websdr.org-style grey page, steel-blue bars, dense tables,
  NL default / EN toggle, radio facts, frequency list per band, banks, ISS, nets
- links.ts: all 34 showcase links, status (online/warn/offline) + archive links
- owner keeps the dashboard; ?view=public lets the owner preview the public page
This commit is contained in:
stan committed 2026-10-27 09:49:00 +00:00
1 parent 3a2d3cc78c
commit 2c8d1bd881
4 files changed
+1684 -107

No files matched your search

+4 -1
View File
@@ -20,7 +20,9 @@ import type { Actions, PageServerLoad } from './$types';
// below check the owner, and hooks.server.ts refuses non-GET requests here (ownerWritePrefixes).
// Language: ?lang=nl|en (remembered in a cookie), else the cookie, else Accept-Language, else NL.
export const load: PageServerLoad = async (event) => {
const owner = isOwner(event);
const isOwnerReq = isOwner(event);
// ?view=public lets the owner preview exactly what visitors get.
const owner = isOwnerReq && event.url.searchParams.get('view') !== 'public';
// Varies by login, cookie and Accept-Language, so never cache it in between.
event.setHeaders({ 'cache-control': 'private, no-store', vary: 'cookie, accept-language' });
const asked = event.url.searchParams.get('lang');
@@ -41,6 +43,7 @@ export const load: PageServerLoad = async (event) => {
return {
...(owner ? radio : publicRadio(radio)),
owner,
ownerPreview: isOwnerReq && !owner,
lang,
now: Date.now(),
seo: {
+124 -106
View File
@@ -11,6 +11,7 @@
import Nets from '#lib/radio/Nets.svelte';
import IssPasses from '#lib/radio/IssPasses.svelte';
import LogList from '#lib/radio/LogList.svelte';
import PublicRadio from '#lib/radio/PublicRadio.svelte';
import { BANDS } from '#lib/radio/index.js';
import { DICTS, LANGS, setRadio } from '#lib/radio/i18n.js';
import LayoutDashboard from 'lucide-svelte/icons/layout-dashboard';
@@ -91,125 +92,142 @@
resolve(`/trackers/radio?tab=frequencies&band=${band}` as '/');
</script>
<div class="radio" lang={data.lang}>
<header class="head">
<a href={resolve('/trackers/radio')} class="title-link" aria-label={t.title}
><TrackerIcon name="radio" class="size-12" /></a
>
<div class="title">
<h1>{t.title}</h1>
<p>{data.owner ? t.intro : t.introPublic}</p>
</div>
<nav class="lang" aria-label={t.language}>
<span class="lang-icon" aria-hidden="true"><Languages size={18} /></span>
{#each LANGS as l (l)}
<!-- Visitors (and the owner with ?view=public) get the classic ham-site page; the owner gets
the dashboard with edit controls. -->
{#if !data.owner}
<PublicRadio
frequencies={data.frequencies}
channels={data.channels}
nets={data.nets}
passes={data.passes}
log={data.log}
logTotal={data.logTotal}
lang={data.lang}
{now}
{langHref}
ownerPreview={data.ownerPreview}
/>
{:else}
<div class="radio" lang={data.lang}>
<header class="head">
<a href={resolve('/trackers/radio')} class="title-link" aria-label={t.title}
><TrackerIcon name="radio" class="size-12" /></a
>
<div class="title">
<h1>{t.title}</h1>
<p>{data.owner ? t.intro : t.introPublic}</p>
</div>
<nav class="lang" aria-label={t.language}>
<span class="lang-icon" aria-hidden="true"><Languages size={18} /></span>
{#each LANGS as l (l)}
<a
href={langHref(l)}
hreflang={l}
class={{ active: data.lang === l }}
aria-current={data.lang === l ? 'true' : undefined}
data-sveltekit-noscroll
data-sveltekit-replacestate>{l === 'nl' ? 'NL' : 'EN'}</a
>
{/each}
</nav>
</header>
<nav class="tabs" aria-label={t.tabsLabel}>
{#each TABS as x (x.id)}
{@const n = count(x.id)}
<a
href={langHref(l)}
hreflang={l}
class={{ active: data.lang === l }}
aria-current={data.lang === l ? 'true' : undefined}
data-sveltekit-noscroll
data-sveltekit-replacestate>{l === 'nl' ? 'NL' : 'EN'}</a
href={tabHref(x.id)}
class={{ active: tab === x.id }}
aria-current={tab === x.id ? 'page' : undefined}
><Tile icon={x.icon} from={x.c[0]} to={x.c[1]} size={28} /><span class="lbl"
>{t.tabs[x.id]}</span
>{#if n !== null}<span class="n">{n}</span>{/if}</a
>
{/each}
</nav>
</header>
<nav class="tabs" aria-label={t.tabsLabel}>
{#each TABS as x (x.id)}
{@const n = count(x.id)}
<a
href={tabHref(x.id)}
class={{ active: tab === x.id }}
aria-current={tab === x.id ? 'page' : undefined}
><Tile icon={x.icon} from={x.c[0]} to={x.c[1]} size={28} /><span class="lbl"
>{t.tabs[x.id]}</span
>{#if n !== null}<span class="n">{n}</span>{/if}</a
>
{/each}
</nav>
{#if tab === 'overview'}
<div class="overview">
<section class="card">
<h2 class="card-title">
<Tile icon={TABS[3].icon} from="#fbbf24" to="#b45309" size={24} />{t.upNext}
</h2>
<UpNext nets={data.nets} passes={data.passes} frequencies={data.frequencies} {now} />
</section>
{#if tab === 'overview'}
<div class="overview">
<section class="card">
<h2 class="card-title">
<Tile icon={TABS[3].icon} from="#fbbf24" to="#b45309" size={24} />{t.upNext}
</h2>
<UpNext nets={data.nets} passes={data.passes} frequencies={data.frequencies} {now} />
</section>
{#if data.owner}
<section class="card">
<h2 class="card-title">
<Tile icon={TABS[5].icon} from="#34d399" to="#047857" size={24} />{t.quickLog}
</h2>
<QuickLog frequencies={data.frequencies} message={logMessage} error={logError} />
<div class="recent">
<LogList log={data.log} frequencies={data.frequencies} {now} limit={5} />
</div>
</section>
{:else}
<section class="card">
<h2 class="card-title">
<Tile icon={TABS[1].icon} from="#a78bfa" to="#5b21b6" size={24} />{t.byBand}
</h2>
<ul class="bands">
{#each bandCounts as b (b.band)}
<li>
<a href={freqsHref(b.band)}
><BandIcon band={b.band} size={32} /><span class="bn">{t.bands[b.band]}</span
><span class="bc">{b.n}</span></a
>
</li>
{/each}
</ul>
</section>
{/if}
<section class="card">
<h2 class="card-title">
<Tile icon={TABS[4].icon} from="#2dd4bf" to="#0f766e" size={24} />{t.nextIss}
<a class="more" href={tabHref('iss')}>{t.all}</a>
</h2>
<IssPasses passes={data.passes} {now} limit={4} compact />
</section>
<section class="card stats">
<h2 class="card-title">
<Tile icon={TABS[2].icon} from="#38bdf8" to="#0369a1" size={24} />{t.scanner}
</h2>
<p>{t.channelsProgrammed(data.channels.length, data.frequencies.length)}</p>
<p class="meta">{t.scanHelp}</p>
<a class="ghost" href={tabHref('banks')}>{t.openBanks}</a>
</section>
{#if !data.owner && data.log.length}
<section class="card">
<h2 class="card-title">
<Tile icon={TABS[5].icon} from="#34d399" to="#047857" size={24} />{t.recentLog}
</h2>
<LogList log={data.log} frequencies={data.frequencies} {now} limit={5} />
</section>
{/if}
</div>
{:else if tab === 'frequencies'}
<FreqList frequencies={data.frequencies} />
{:else if tab === 'banks'}
<Banks channels={data.channels} frequencies={data.frequencies} />
{:else if tab === 'nets'}
<Nets nets={data.nets} frequencies={data.frequencies} {now} />
{:else if tab === 'iss'}
<section class="card"><IssPasses passes={data.passes} tle={data.tle} {now} /></section>
{:else}
{#if data.owner}
<section class="card">
<h2 class="card-title">
<Tile icon={TABS[5].icon} from="#34d399" to="#047857" size={24} />{t.quickLog}
</h2>
<QuickLog frequencies={data.frequencies} message={logMessage} error={logError} />
<div class="recent">
<LogList log={data.log} frequencies={data.frequencies} {now} limit={5} />
</div>
</section>
{:else}
<section class="card">
<h2 class="card-title">
<Tile icon={TABS[1].icon} from="#a78bfa" to="#5b21b6" size={24} />{t.byBand}
</h2>
<ul class="bands">
{#each bandCounts as b (b.band)}
<li>
<a href={freqsHref(b.band)}
><BandIcon band={b.band} size={32} /><span class="bn">{t.bands[b.band]}</span
><span class="bc">{b.n}</span></a
>
</li>
{/each}
</ul>
</section>
{/if}
<section class="card">
<h2 class="card-title">
<Tile icon={TABS[4].icon} from="#2dd4bf" to="#0f766e" size={24} />{t.nextIss}
<a class="more" href={tabHref('iss')}>{t.all}</a>
</h2>
<IssPasses passes={data.passes} {now} limit={4} compact />
</section>
<section class="card stats">
<h2 class="card-title">
<Tile icon={TABS[2].icon} from="#38bdf8" to="#0369a1" size={24} />{t.scanner}
</h2>
<p>{t.channelsProgrammed(data.channels.length, data.frequencies.length)}</p>
<p class="meta">{t.scanHelp}</p>
<a class="ghost" href={tabHref('banks')}>{t.openBanks}</a>
</section>
{#if !data.owner && data.log.length}
<section class="card">
<h2 class="card-title">
<Tile icon={TABS[5].icon} from="#34d399" to="#047857" size={24} />{t.recentLog}
</h2>
<LogList log={data.log} frequencies={data.frequencies} {now} limit={5} />
</section>
{/if}
</div>
{:else if tab === 'frequencies'}
<FreqList frequencies={data.frequencies} />
{:else if tab === 'banks'}
<Banks channels={data.channels} frequencies={data.frequencies} />
{:else if tab === 'nets'}
<Nets nets={data.nets} frequencies={data.frequencies} {now} />
{:else if tab === 'iss'}
<section class="card"><IssPasses passes={data.passes} tle={data.tle} {now} /></section>
{:else}
{#if data.owner}
<section class="card">
<QuickLog frequencies={data.frequencies} message={logMessage} error={logError} />
{#if logFreq}<p class="meta">
{t.oneFreq} <a href={tabHref('log')} class="u">{t.showAllLog}</a>
</p>{/if}
<LogList log={data.log} frequencies={data.frequencies} {now} frequencyId={logFreq} />
</section>
{/if}
<section class="card">
{#if logFreq}<p class="meta">
{t.oneFreq} <a href={tabHref('log')} class="u">{t.showAllLog}</a>
</p>{/if}
<LogList log={data.log} frequencies={data.frequencies} {now} frequencyId={logFreq} />
</section>
{/if}
</div>
</div>
{/if}
<style>
/* Built to be read by an older visitor on a phone or tablet: big type, strong contrast, large