diff --git a/src/demo.spec.ts b/src/demo.spec.ts deleted file mode 100644 index e07cbbd..0000000 --- a/src/demo.spec.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { describe, it, expect } from 'vitest'; - -describe('sum test', () => { - it('adds 1 + 2 to equal 3', () => { - expect(1 + 2).toBe(3); - }); -}); diff --git a/src/lib/api/types/metar.ts b/src/lib/api/types/metar.ts new file mode 100644 index 0000000..1e4991e --- /dev/null +++ b/src/lib/api/types/metar.ts @@ -0,0 +1,42 @@ +export type MetarCloud = { + cover: string; + base: number | null; +}; + +export type Metar = { + metar_id: number; + icaoId: string; + receiptTime: string; + obsTime: number; + reportTime: string; + temp: number; + dewp: number; + wdir: number; + wspd: number; + wgst: number | null; + visib: string; + altim: number; + slp: number | null; + qcField: number; + wxString: string | null; + presTend: string | null; + maxT: number | null; + minT: number | null; + maxT24: number | null; + minT24: number | null; + precip: number | null; + pcp3hr: number | null; + pcp6hr: number | null; + pcp24hr: number | null; + snow: number | null; + vertVis: number | null; + metarType: string; + rawOb: string; + mostRecent: number; + lat: number; + lon: number; + elev: number; + prior: number; + name: string; + clouds: MetarCloud[]; +}; \ No newline at end of file diff --git a/src/lib/api/weatherservice.ts b/src/lib/api/weatherservice.ts new file mode 100644 index 0000000..bf84f89 --- /dev/null +++ b/src/lib/api/weatherservice.ts @@ -0,0 +1,16 @@ +import type { Metar } from '$lib/api/types/metar'; + +export const aviationWeatherApi = { + /** + * Fetches METAR data for a given ICAO code. + * @param icao - The ICAO code (e.g., "EGKK") + * @returns A Promise resolving to a Metar object or null if not found. + */ + async fetchMetar(icao: string): Promise { + const url = `/api/metar/${encodeURIComponent(icao)}`; + const response = await fetch(url); + if (!response.ok) throw new Error(`Failed to fetch METAR: ${response.statusText}`); + const data = await response.json(); + return data as Metar | null; + } +}; \ No newline at end of file diff --git a/src/lib/stores/icao.ts b/src/lib/stores/icao.ts new file mode 100644 index 0000000..8da2fcf --- /dev/null +++ b/src/lib/stores/icao.ts @@ -0,0 +1,3 @@ +import { writable } from 'svelte/store'; + +export const icao = writable(''); \ No newline at end of file diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index a5b6a2e..97e27ea 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -5,6 +5,19 @@ import Section from '$lib/components/Section.svelte'; import Card from '$lib/components/Cards/Card.svelte'; + import { icao } from '$lib/stores/icao'; + + let icaoInput = $state(''); + + $effect(() => { + // Watch for changes to icaoInput and update the store if exactly 4 chars + if (icaoInput.length === 4) { + icao.set(icaoInput); + } else { + icao.set(''); + } + }); + let { children } = $props(); @@ -15,7 +28,7 @@ class="text-xxl flex w-full items-center justify-center gap-10 px-8 py-4 font-semibold text-green-600" > METAR ⛅ - TAF ☔ + TAF ☔
@@ -24,12 +37,7 @@

METARIUS

Start Here

-
+

@@ -46,6 +54,7 @@ maxlength="4" required placeholder="EGKK" + bind:value={icaoInput} />

diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 36cf2ca..f1da9ad 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -3,8 +3,35 @@ import Card from '$lib/components/Cards/Card.svelte'; import { toasts } from 'svelte-toasts'; import Loading from '$lib/components/Loading.svelte'; - + import { icao } from '$lib/stores/icao'; + import { aviationWeatherApi } from '$lib/api/weatherservice'; + import type { Metar } from '$lib/api/types/metar' + import { onDestroy } from 'svelte'; + + let metar: Metar | null = null; + let loading = false; + let error: string | null = null; + + let unsubscribe = icao.subscribe(async (val) => { + metar = null; + error = null; + if (val && val.length === 4) { + loading = true; + try { + const result = await aviationWeatherApi.fetchMetar(val); + metar = result; + if (!metar) error = 'No METAR found for this ICAO code.'; + } catch (e) { + error = e instanceof Error ? e.message : 'Unknown error'; + } finally { + loading = false; + } + } + }); + + onDestroy(() => unsubscribe()); +
{toasts.add({ @@ -21,7 +48,25 @@

METAR

Results

- + {#if loading} + + {:else if error} + {error} + {:else if metar} +
+
ICAO: {metar.icaoId}
+
Airport: {metar.name}
+
Observed: {metar.reportTime}
+
Temperature: {metar.temp}°C
+
Dew Point: {metar.dewp}°C
+
Wind: {metar.wdir}° at {metar.wspd}kt
+
Visibility: {metar.visib}
+
Pressure: {metar.altim} hPa
+
Raw METAR: {metar.rawOb}
+
+ {:else} + + {/if}
\ No newline at end of file diff --git a/src/routes/api/metar/[icao]/+server.ts b/src/routes/api/metar/[icao]/+server.ts new file mode 100644 index 0000000..2e7dec1 --- /dev/null +++ b/src/routes/api/metar/[icao]/+server.ts @@ -0,0 +1,19 @@ +import type { RequestHandler } from '@sveltejs/kit'; +import type { Metar } from '$lib/api/types/metar'; + +export const GET: RequestHandler = async ({ params }) => { + const icao = params.icao?.toUpperCase(); + if (!icao || icao.length !== 4) { + return new Response(JSON.stringify({ error: 'Invalid ICAO' }), { status: 400 }); + } + + const url = `https://aviationweather.gov/api/data/metar?ids=${icao}&format=json`; + const res = await fetch(url); + if (!res.ok) { + return new Response(JSON.stringify({ error: 'Failed to fetch METAR' }), { status: 502 }); + } + const data: Metar[] = await res.json(); + return new Response(JSON.stringify(data[0] ?? null), { + headers: { 'Content-Type': 'application/json' } + }); +}; \ No newline at end of file diff --git a/src/routes/page.svelte.test.ts b/src/routes/page.svelte.test.ts deleted file mode 100644 index a110662..0000000 --- a/src/routes/page.svelte.test.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { describe, test, expect } from 'vitest'; -import '@testing-library/jest-dom/vitest'; -import { render, screen } from '@testing-library/svelte'; -import Page from './+page.svelte'; - -describe('/+page.svelte', () => { - test('should render h1', () => { - render(Page); - expect(screen.getByRole('heading', { level: 1 })).toBeInTheDocument(); - }); -});