development #1

Merged
luke-else merged 4 commits from development into main 2025-06-18 20:50:12 +00:00
8 changed files with 143 additions and 27 deletions

View File

@ -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);
});
});

View File

@ -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[];
};

View File

@ -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<Metar | null> {
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;
}
};

3
src/lib/stores/icao.ts Normal file
View File

@ -0,0 +1,3 @@
import { writable } from 'svelte/store';
export const icao = writable('');

View File

@ -5,6 +5,19 @@
import Section from '$lib/components/Section.svelte'; import Section from '$lib/components/Section.svelte';
import Card from '$lib/components/Cards/Card.svelte'; import Card from '$lib/components/Cards/Card.svelte';
import { icao } from '$lib/stores/icao';
let icaoInput = $state<string>('');
$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(); let { children } = $props();
</script> </script>
@ -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" class="text-xxl flex w-full items-center justify-center gap-10 px-8 py-4 font-semibold text-green-600"
> >
<a href="/" class="hover:underline">METAR &#9925;</a> <a href="/" class="hover:underline">METAR &#9925;</a>
<a href="/taf" class="hover:underline">TAF &#9748;</a> <a href="/taf" class="hover:underline">TAF &#9748;</a>
</nav> </nav>
<div class="container mx-auto flex flex-col items-center justify-center"> <div class="container mx-auto flex flex-col items-center justify-center">
@ -24,12 +37,7 @@
<Card> <Card>
<h2 slot="headerLeft">METARIUS</h2> <h2 slot="headerLeft">METARIUS</h2>
<h2 slot="headerRight">Start Here</h2> <h2 slot="headerRight">Start Here</h2>
<form <form slot="content" class="mx-auto flex w-full max-w-3xl flex-col gap-4 text-lg">
slot="content"
class="mx-auto flex w-full max-w-3xl flex-col gap-4 text-lg"
action="https://api.staticforms.xyz/submit"
method="post"
>
<div class="flex-space-between flex flex-col gap-10 md:flex-row"> <div class="flex-space-between flex flex-col gap-10 md:flex-row">
<div class="flex-1"> <div class="flex-1">
<p> <p>
@ -46,6 +54,7 @@
maxlength="4" maxlength="4"
required required
placeholder="EGKK" placeholder="EGKK"
bind:value={icaoInput}
/> />
</div> </div>
</div> </div>

View File

@ -3,8 +3,35 @@
import Card from '$lib/components/Cards/Card.svelte'; import Card from '$lib/components/Cards/Card.svelte';
import { toasts } from 'svelte-toasts'; import { toasts } from 'svelte-toasts';
import Loading from '$lib/components/Loading.svelte'; import Loading from '$lib/components/Loading.svelte';
</script>
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());
</script>
<div style="display: none;"> <div style="display: none;">
{toasts.add({ {toasts.add({
@ -21,7 +48,25 @@
<h2 slot="headerLeft">METAR</h2> <h2 slot="headerLeft">METAR</h2>
<h2 slot="headerRight">Results</h2> <h2 slot="headerRight">Results</h2>
<div slot="content" class="flex h-96 w-full items-center justify-center"> <div slot="content" class="flex h-96 w-full items-center justify-center">
<Loading></Loading> {#if loading}
<Loading />
{:else if error}
<span class="text-red-600 text-xl">{error}</span>
{:else if metar}
<div class="text-left text-lg space-y-2">
<div><strong>ICAO:</strong> {metar.icaoId}</div>
<div><strong>Airport:</strong> {metar.name}</div>
<div><strong>Observed:</strong> {metar.reportTime}</div>
<div><strong>Temperature:</strong> {metar.temp}°C</div>
<div><strong>Dew Point:</strong> {metar.dewp}°C</div>
<div><strong>Wind:</strong> {metar.wdir}° at {metar.wspd}kt</div>
<div><strong>Visibility:</strong> {metar.visib}</div>
<div><strong>Pressure:</strong> {metar.altim} hPa</div>
<div><strong>Raw METAR:</strong> <code>{metar.rawOb}</code></div>
</div>
{:else}
<Loading />
{/if}
</div> </div>
</Card> </Card>
</Section> </Section>

View File

@ -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' }
});
};

View File

@ -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();
});
});