Initial commit

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
2026-05-11 15:55:14 -04:00
commit 290ff0bc1e
41 changed files with 7998 additions and 0 deletions
+84
View File
@@ -0,0 +1,84 @@
import {
fallbackContacts,
fallbackDocuments,
fallbackEvents,
fallbackFuelPrices,
fallbackNews,
fallbackNotices,
type ContactItem,
type DocumentItem,
type EventItem,
type FuelPrice,
type NewsItem,
type Notice,
} from './fallback-data';
type CollectionName = 'news' | 'events' | 'notices' | 'fuel_prices' | 'documents' | 'contacts';
const defaultSortByCollection: Partial<Record<CollectionName, string>> = {
news: '-publish_date',
events: '-start_datetime',
notices: '-priority',
fuel_prices: '-last_updated',
documents: '-uploaded_at',
contacts: 'order',
};
declare const process: {
env: Record<string, string | undefined>;
};
const directusUrl = process.env.DIRECTUS_URL ?? 'http://directus:8055';
const directusToken = process.env.DIRECTUS_ADMIN_TOKEN;
async function readCollection<T>(collection: CollectionName): Promise<T[]> {
const endpoint = new URL(`/items/${collection}`, directusUrl);
endpoint.searchParams.set('limit', '100');
const sort = defaultSortByCollection[collection];
if (sort) {
endpoint.searchParams.set('sort', sort);
}
try {
const headers: Record<string, string> = {};
if (directusToken) {
headers['Authorization'] = `Bearer ${directusToken}`;
}
const response = await fetch(endpoint, {
headers: Object.keys(headers).length > 0 ? headers : undefined,
});
if (!response.ok) {
throw new Error(`Directus responded with ${response.status}`);
}
const payload = (await response.json()) as { data?: T[] };
return payload.data ?? [];
} catch {
return fallbackFor(collection) as T[];
}
}
function fallbackFor(collection: CollectionName) {
switch (collection) {
case 'news':
return fallbackNews;
case 'events':
return fallbackEvents;
case 'notices':
return fallbackNotices;
case 'fuel_prices':
return fallbackFuelPrices;
case 'documents':
return fallbackDocuments;
case 'contacts':
return fallbackContacts;
}
}
export const getNews = () => readCollection<NewsItem>('news');
export const getEvents = () => readCollection<EventItem>('events');
export const getNotices = () => readCollection<Notice>('notices');
export const getFuelPrices = () => readCollection<FuelPrice>('fuel_prices');
export const getDocuments = () => readCollection<DocumentItem>('documents');
export const getContacts = () => readCollection<ContactItem>('contacts');
+121
View File
@@ -0,0 +1,121 @@
export type Notice = {
title: string;
message: string;
severity: 'info' | 'warning' | 'critical';
start_date?: string;
end_date?: string;
active?: boolean;
priority?: number;
};
export type FuelPrice = {
fuel_type: string;
price_per_litre: number;
currency: string;
last_updated: string;
notes?: string;
};
export type EventItem = {
title: string;
slug: string;
description: string;
start_datetime: string;
end_datetime?: string;
location_text?: string;
registration_link?: string;
status?: string;
is_featured?: boolean;
tags?: string[];
};
export type NewsItem = {
title: string;
slug: string;
summary: string;
body: string;
publish_date: string;
status?: string;
tags?: string[];
};
export type DocumentItem = {
title: string;
category: string;
description?: string;
fileUrl?: string;
uploaded_at?: string;
};
export type ContactItem = {
name: string;
role: string;
email: string;
phone?: string;
is_public?: boolean;
order?: number;
};
export const fallbackNotices: Notice[] = [
{
title: 'Welcome to Swansea Airport',
message: 'Operational notices and visitor information will appear here once Directus content is published.',
severity: 'info',
active: true,
priority: 1,
},
];
export const fallbackFuelPrices: FuelPrice[] = [
{
fuel_type: 'AVGAS',
price_per_litre: 2.35,
currency: 'GBP',
last_updated: '2026-05-11',
notes: 'Placeholder rate for the initial scaffold.',
},
];
export const fallbackEvents: EventItem[] = [
{
title: 'Airfield open day',
slug: 'airfield-open-day',
description: 'Example event to verify the listing and detail page flow.',
start_datetime: '2026-06-14T09:00:00Z',
end_datetime: '2026-06-14T16:00:00Z',
location_text: 'Main apron',
is_featured: true,
tags: ['Public'],
},
];
export const fallbackNews: NewsItem[] = [
{
title: 'Site scaffolding started',
slug: 'site-scaffolding-started',
summary: 'The new Astro and Directus architecture has been scaffolded.',
body: '<p>This is a starter article that proves the detail route and rich text rendering.</p>',
publish_date: '2026-05-11',
tags: ['Website'],
},
];
export const fallbackDocuments: DocumentItem[] = [
{
title: 'Pilot information pack',
category: 'Pilots',
description: 'Starter document entry for the documents listing.',
uploaded_at: '2026-05-11',
},
];
export const fallbackContacts: ContactItem[] = [
{
name: 'Airport office',
role: 'General enquiries',
email: 'info@swansea-airport.wales',
phone: '01792 687 042',
is_public: true,
order: 1,
},
];
+25
View File
@@ -0,0 +1,25 @@
export function formatDate(value?: string) {
if (!value) {
return 'To be confirmed';
}
return new Intl.DateTimeFormat('en-GB', {
day: '2-digit',
month: 'short',
year: 'numeric',
}).format(new Date(value));
}
export function formatDateTime(value?: string) {
if (!value) {
return 'To be confirmed';
}
return new Intl.DateTimeFormat('en-GB', {
day: '2-digit',
month: 'short',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
}).format(new Date(value));
}
+38
View File
@@ -0,0 +1,38 @@
export const site = {
name: 'Swansea Airport',
tagline: 'The gateway to Gower and Swansea',
address: 'Swansea Airport, Fairwood Common, Swansea, SA2 7JU',
phone: '01792 687 042',
openingHours: '7 days 0900-1600',
licensedHours: 'Friday to Sunday 0900-1700',
runwayFacts: [
'Runway 04/22 concrete 1351m x 30m licensed',
'Runway 10/28 asphalt 857m x 18m unlicensed',
'Category 1 RFFS',
'Air Ground Service 119.705',
],
navigation: [
{ label: 'Home', href: '/' },
{ label: 'Visiting Pilots', href: '/visiting-pilots/' },
{ label: 'Procedures', href: '/procedures-safety-noise-abatement/' },
{ label: 'Events', href: '/events/' },
{ label: 'News', href: '/news/' },
{ label: 'Documents', href: '/documents/' },
{ label: 'Contact', href: '/contact/' },
],
};
export const homepageHighlights = [
{
title: 'Operational clarity first',
body: 'Notices, fuel, events, and the latest news are kept at the top of the page for fast scanning on mobile.',
},
{
title: 'Layout controlled in code',
body: 'Astro owns the structure, spacing, and information hierarchy so the CMS only supplies content.',
},
{
title: 'Static output by default',
body: 'The public site remains available from built files even if Directus is offline after publication.',
},
];