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 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 ⛅</a>
|
<a href="/" class="hover:underline">METAR ⛅</a>
|
||||||
<a href="/taf" class="hover:underline">TAF ☔</a>
|
<a href="/taf" class="hover:underline">TAF ☔</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>
|
||||||
|
@ -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>
|
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