← Public packages
@reng/pollen
Pollen index / levels for a city or lat/lon via Open-Meteo Air Quality API (zero OAuth).
src/app.ts
134 lines · 5.0 KB · TypeScriptimport { packageContext } from 'kody:runtime'
import { getPollen } from './getPollen'
function appUrl(path: string): URL {
if (!packageContext?.hostedUrl) {
throw new Error('This module must run as a package app.')
}
const relative = path.replace(/^\/+/, '')
const mount = packageContext.appBasePath.endsWith('/')
? packageContext.appBasePath
: `${packageContext.appBasePath}/`
return new URL(`${mount}${relative}`, packageContext.hostedUrl)
}
function renderPage(): Response {
const apiPath = appUrl('api/getPollen').pathname
const html = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Pollen levels</title>
<style>
:root { color-scheme: light dark; font-family: system-ui, sans-serif; }
body { margin: 0; min-height: 100vh; display: grid; place-items: center; background: #0f172a; color: #e2e8f0; }
main { width: min(640px, 92vw); background: #1e293b; border-radius: 16px; padding: 1.5rem; box-shadow: 0 20px 50px rgba(0,0,0,.35); }
h1 { margin: 0 0 .25rem; font-size: 1.4rem; }
p.lead { margin: 0 0 1rem; color: #94a3b8; font-size: .95rem; }
label { display: block; margin-bottom: .4rem; font-size: .9rem; color: #cbd5e1; }
input { width: 100%; border-radius: 10px; border: 1px solid #334155; background: #0f172a; color: inherit; padding: .75rem; font-size: 1.05rem; box-sizing: border-box; }
.row { display: flex; gap: .75rem; align-items: center; margin: .9rem 0 1rem; flex-wrap: wrap; }
button { border-radius: 999px; border: 0; padding: .55rem 1.1rem; font-size: .95rem; background: #34d399; color: #0f172a; font-weight: 600; cursor: pointer; }
button:disabled { opacity: .6; cursor: wait; }
.out { min-height: 3rem; padding: 1rem; border-radius: 10px; background: #0f172a; border: 1px solid #334155; white-space: pre-wrap; word-break: break-word; font-size: .95rem; }
.meta { margin-top: .6rem; color: #94a3b8; font-size: .85rem; }
.err { color: #fca5a5; margin-top: .6rem; }
.note { margin-top: 1rem; color: #64748b; font-size: .8rem; }
</style>
</head>
<body>
<main>
<h1>Pollen levels</h1>
<p class="lead">City name → Open-Meteo pollen grains/m³ (Europe coverage; elsewhere often unavailable).</p>
<label for="city">City</label>
<input id="city" value="Berlin" placeholder="e.g. Berlin, Paris, Vienna" />
<div class="row">
<button id="go" type="button">Check pollen</button>
</div>
<label>Result</label>
<div class="out" id="out">—</div>
<div class="meta" id="meta"></div>
<div class="err" id="err"></div>
<p class="note">Not medical advice. Data: Open-Meteo / CAMS.</p>
</main>
<script>
const apiPath = ${JSON.stringify(apiPath)};
const cityEl = document.getElementById('city');
const outEl = document.getElementById('out');
const metaEl = document.getElementById('meta');
const errEl = document.getElementById('err');
const goEl = document.getElementById('go');
async function run() {
errEl.textContent = '';
goEl.disabled = true;
try {
const res = await fetch(apiPath, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ location: cityEl.value, hours: 24 }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || ('HTTP ' + res.status));
outEl.textContent = data.summary || JSON.stringify(data.levels, null, 2);
const place = data.place ? (data.place.name + (data.place.country ? ', ' + data.place.country : '')) : '';
metaEl.textContent = [place, data.coverage, data.time].filter(Boolean).join(' · ');
} catch (e) {
outEl.textContent = '—';
metaEl.textContent = '';
errEl.textContent = String(e && e.message ? e.message : e);
} finally {
goEl.disabled = false;
}
}
goEl.addEventListener('click', run);
cityEl.addEventListener('keydown', (e) => {
if (e.key === 'Enter') run();
});
</script>
</body>
</html>`
return new Response(html, {
headers: {
'content-type': 'text/html; charset=utf-8',
'cache-control': 'no-store',
},
})
}
async function handleApi(request: Request): Promise<Response> {
if (request.method !== 'POST') {
return Response.json({ error: 'POST required' }, { status: 405 })
}
let body: {
location?: string
latitude?: number
longitude?: number
hours?: number
} = {}
try {
body = (await request.json()) as typeof body
} catch {
return Response.json({ error: 'Invalid JSON body' }, { status: 400 })
}
try {
const result = await getPollen(body)
return Response.json(result)
} catch (e) {
return Response.json(
{ error: e instanceof Error ? e.message : String(e) },
{ status: 400 },
)
}
}
export default {
async fetch(request: Request): Promise<Response> {
const path = new URL(request.url).pathname
if (path === '/' || path === '') return renderPage()
if (path === '/api/getPollen') return handleApi(request)
return new Response('Not found', { status: 404 })
},
}