development #1
@ -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);
|
||||
});
|
||||
});
|
42
src/lib/api/types/metar.ts
Normal file
42
src/lib/api/types/metar.ts
Normal 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[];
|
||||
};
|
16
src/lib/api/weatherservice.ts
Normal file
16
src/lib/api/weatherservice.ts
Normal 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
3
src/lib/stores/icao.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export const icao = writable('');
|
@ -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<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();
|
||||
</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"
|
||||
>
|
||||
<a href="/" class="hover:underline">METAR ⛅</a>
|
||||
<a href="/taf" class="hover:underline">TAF ☔</a>
|
||||
<a href="/taf" class="hover:underline">TAF ☔</a>
|
||||
</nav>
|
||||
|
||||
<div class="container mx-auto flex flex-col items-center justify-center">
|
||||
@ -24,12 +37,7 @@
|
||||
<Card>
|
||||
<h2 slot="headerLeft">METARIUS</h2>
|
||||
<h2 slot="headerRight">Start Here</h2>
|
||||
<form
|
||||
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"
|
||||
>
|
||||
<form slot="content" class="mx-auto flex w-full max-w-3xl flex-col gap-4 text-lg">
|
||||
<div class="flex-space-between flex flex-col gap-10 md:flex-row">
|
||||
<div class="flex-1">
|
||||
<p>
|
||||
@ -46,6 +54,7 @@
|
||||
maxlength="4"
|
||||
required
|
||||
placeholder="EGKK"
|
||||
bind:value={icaoInput}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -3,8 +3,35 @@
|
||||
import Card from '$lib/components/Cards/Card.svelte';
|
||||
import { toasts } from 'svelte-toasts';
|
||||
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;">
|
||||
{toasts.add({
|
||||
@ -21,7 +48,25 @@
|
||||
<h2 slot="headerLeft">METAR</h2>
|
||||
<h2 slot="headerRight">Results</h2>
|
||||
<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>
|
||||
</Card>
|
||||
</Section>
|
19
src/routes/api/metar/[icao]/+server.ts
Normal file
19
src/routes/api/metar/[icao]/+server.ts
Normal 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' }
|
||||
});
|
||||
};
|
@ -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();
|
||||
});
|
||||
});
|
Reference in New Issue
Block a user