Compare commits

...

2 Commits

Author SHA1 Message Date
jamesp 002ba4047d Menu improvement on mobile 2026-06-26 07:42:51 -04:00
jamesp 18a9b247c4 banner and fuel updates 2026-06-26 07:38:46 -04:00
10 changed files with 223 additions and 45 deletions
+24 -19
View File
@@ -26,30 +26,35 @@ const slides = images.length > 0 ? images : [{ src: '/images/banner.png', alt: '
{slides.length > 1 && ( {slides.length > 1 && (
<script> <script>
(() => { (() => {
const root = document.querySelector('[data-banner-rotator]'); if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
if (!root || window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
const slides = Array.from(root.querySelectorAll('[data-banner-slide]')); const roots = Array.from(document.querySelectorAll('[data-banner-rotator]:not([data-banner-rotator-ready])'));
if (slides.length < 2) return;
if (root.getAttribute('data-randomize-after-first') === 'true') { roots.forEach((root) => {
const firstSlide = slides[0]; root.setAttribute('data-banner-rotator-ready', 'true');
const restSlides = slides.slice(1);
for (let index = restSlides.length - 1; index > 0; index -= 1) { const slides = Array.from(root.querySelectorAll('[data-banner-slide]'));
const swapIndex = Math.floor(Math.random() * (index + 1)); if (slides.length < 2) return;
[restSlides[index], restSlides[swapIndex]] = [restSlides[swapIndex], restSlides[index]];
if (root.getAttribute('data-randomize-after-first') === 'true') {
const firstSlide = slides[0];
const restSlides = slides.slice(1);
for (let index = restSlides.length - 1; index > 0; index -= 1) {
const swapIndex = Math.floor(Math.random() * (index + 1));
[restSlides[index], restSlides[swapIndex]] = [restSlides[swapIndex], restSlides[index]];
}
slides.splice(0, slides.length, firstSlide, ...restSlides);
} }
slides.splice(0, slides.length, firstSlide, ...restSlides); let currentIndex = 0;
} window.setInterval(() => {
slides[currentIndex].classList.remove('active');
let currentIndex = 0; currentIndex = (currentIndex + 1) % slides.length;
window.setInterval(() => { slides[currentIndex].classList.add('active');
slides[currentIndex].classList.remove('active'); }, 3500);
currentIndex = (currentIndex + 1) % slides.length; });
slides[currentIndex].classList.add('active');
}, 3500);
})(); })();
</script> </script>
)} )}
+1 -1
View File
@@ -41,8 +41,8 @@ const contactRequestEndpoint = `${pprApiBase}/contact-requests/public`;
<select id="contact-enquiry-type" name="enquiry_type" required> <select id="contact-enquiry-type" name="enquiry_type" required>
<option value="">Select enquiry type</option> <option value="">Select enquiry type</option>
<option value="general">General enquiry</option> <option value="general">General enquiry</option>
<option value="pilot">Pilot or visiting aircraft / Fuel</option>
<option value="aviation_business">Aviation business / basing</option> <option value="aviation_business">Aviation business / basing</option>
<option value="pilot">Pilot or visiting aircraft</option>
<option value="events">Events and visits</option> <option value="events">Events and visits</option>
<option value="community">Community or local resident</option> <option value="community">Community or local resident</option>
</select> </select>
+80 -2
View File
@@ -3,10 +3,14 @@ import SectionHeading from './SectionHeading.astro';
import type { FuelPrice } from '../lib/fallback-data'; import type { FuelPrice } from '../lib/fallback-data';
type Props = { type Props = {
contactHref?: string;
fuelPrices: FuelPrice[]; fuelPrices: FuelPrice[];
moreInfoHref?: string;
sectionId?: string;
serviceNotes?: string[];
}; };
const { fuelPrices } = Astro.props as Props; const { contactHref, fuelPrices, moreInfoHref, sectionId, serviceNotes = [] } = Astro.props as Props;
function formatFuelPrice(value: unknown): string { function formatFuelPrice(value: unknown): string {
const numeric = Number(value); const numeric = Number(value);
@@ -18,8 +22,16 @@ function formatVatLabel(value: unknown): string {
} }
--- ---
<section> <section id={sectionId}>
<SectionHeading title="Fuel at Swansea" /> <SectionHeading title="Fuel at Swansea" />
{serviceNotes.length > 0 && (
<div class="fuel-service-notes">
{serviceNotes.map((note) => <p>{note}</p>)}
{contactHref && (
<a class="button primary fuel-contact-link" href={contactHref}>Contact Us</a>
)}
</div>
)}
<div class="cards-grid fuel-cards-grid"> <div class="cards-grid fuel-cards-grid">
{fuelPrices.map((fuel) => ( {fuelPrices.map((fuel) => (
<article class="card fuel-card"> <article class="card fuel-card">
@@ -37,6 +49,14 @@ function formatVatLabel(value: unknown): string {
</article> </article>
))} ))}
</div> </div>
{moreInfoHref && (
<div class="fuel-more-info">
<a class="button primary fuel-info-link" href={moreInfoHref}>
More fuel information
<span aria-hidden="true">-&gt;</span>
</a>
</div>
)}
</section> </section>
<style> <style>
@@ -44,6 +64,56 @@ function formatVatLabel(value: unknown): string {
gap: 1.1rem; gap: 1.1rem;
} }
.fuel-service-notes {
margin: 0 0 1.1rem;
}
.fuel-service-notes p {
margin: 0 0 0.65rem;
}
.fuel-service-notes p:last-child {
margin-bottom: 0;
}
.fuel-contact-link {
margin-top: 0.35rem;
}
.fuel-more-info {
margin-top: 1rem;
display: flex;
justify-content: flex-end;
}
.fuel-info-link {
gap: 0.5rem;
padding-inline: 1.15rem;
box-shadow: 0 12px 24px rgba(11, 79, 122, 0.18);
transition:
background 0.18s ease,
box-shadow 0.18s ease,
transform 0.18s ease;
}
.fuel-info-link span {
font-weight: 800;
transition: transform 0.18s ease;
}
.fuel-info-link:hover,
.fuel-info-link:focus-visible {
color: white;
background: var(--brand-2);
box-shadow: 0 14px 28px rgba(11, 79, 122, 0.24);
transform: translateY(-1px);
}
.fuel-info-link:hover span,
.fuel-info-link:focus-visible span {
transform: translateX(2px);
}
.fuel-card { .fuel-card {
border: 1px solid rgba(11, 79, 122, 0.18); border: 1px solid rgba(11, 79, 122, 0.18);
background: background:
@@ -104,6 +174,14 @@ function formatVatLabel(value: unknown): string {
} }
@media (max-width: 560px) { @media (max-width: 560px) {
.fuel-more-info {
justify-content: stretch;
}
.fuel-info-link {
width: 100%;
}
.fuel-row { .fuel-row {
grid-template-columns: 1fr; grid-template-columns: 1fr;
gap: 0.75rem; gap: 0.75rem;
+4 -1
View File
@@ -24,7 +24,10 @@ const pageTitle = title ? `${title} · ${site.name}` : site.name;
<div class="container topbar-inner"> <div class="container topbar-inner">
<a class="brand brand-mobile" href="/"><img src="/images/logo-text.webp" alt="Swansea Airport" /></a> <a class="brand brand-mobile" href="/"><img src="/images/logo-text.webp" alt="Swansea Airport" /></a>
<details class="mobile-nav"> <details class="mobile-nav">
<summary></summary> <summary aria-label="Main menu">
<span class="mobile-nav-icon" aria-hidden="true"></span>
<span>Menu</span>
</summary>
<div class="mobile-nav-panel"> <div class="mobile-nav-panel">
{site.navigation.map((item) => ( {site.navigation.map((item) => (
item.children ? ( item.children ? (
+6
View File
@@ -5,6 +5,7 @@ import {
fallbackEvents, fallbackEvents,
fallbackFuelPrices, fallbackFuelPrices,
fallbackHomepageBannerImages, fallbackHomepageBannerImages,
fallbackHomepageVolunteerImages,
fallbackNews, fallbackNews,
fallbackNotices, fallbackNotices,
type ContactItem, type ContactItem,
@@ -45,6 +46,7 @@ const directusAssetUrlTemplate =
const directusToken = process.env.DIRECTUS_TOKEN ?? process.env.DIRECTUS_ADMIN_TOKEN; const directusToken = process.env.DIRECTUS_TOKEN ?? process.env.DIRECTUS_ADMIN_TOKEN;
const directusDebug = ['1', 'true', 'yes', 'on'].includes((process.env.DIRECTUS_DEBUG ?? '').toLowerCase()); const directusDebug = ['1', 'true', 'yes', 'on'].includes((process.env.DIRECTUS_DEBUG ?? '').toLowerCase());
const homepageBannerFolder = process.env.DIRECTUS_HOMEPAGE_BANNER_FOLDER ?? 'homepage-banners'; const homepageBannerFolder = process.env.DIRECTUS_HOMEPAGE_BANNER_FOLDER ?? 'homepage-banners';
const homepageVolunteersFolder = process.env.DIRECTUS_HOMEPAGE_VOLUNTEERS_FOLDER ?? 'homepage-volunteers';
const cafePageFolder = process.env.DIRECTUS_CAFE_PAGE_FOLDER ?? 'cafe-page'; const cafePageFolder = process.env.DIRECTUS_CAFE_PAGE_FOLDER ?? 'cafe-page';
type DirectusFolder = { type DirectusFolder = {
@@ -293,6 +295,10 @@ export const getHomepageBannerImages = () =>
firstTag: 'first', firstTag: 'first',
shuffleRest: true, shuffleRest: true,
}); });
export const getHomepageVolunteerImages = () =>
getImagesFromFolder(homepageVolunteersFolder, fallbackHomepageVolunteerImages, {
shuffleRest: true,
});
export const getCafePageImages = () => getImagesFromFolder(cafePageFolder, fallbackCafePageImages); export const getCafePageImages = () => getImagesFromFolder(cafePageFolder, fallbackCafePageImages);
function stripHtml(value = ''): string { function stripHtml(value = ''): string {
+7
View File
@@ -100,6 +100,13 @@ export const fallbackCafePageImages: HomepageBannerImage[] = [
}, },
]; ];
export const fallbackHomepageVolunteerImages: HomepageBannerImage[] = [
{
src: '/images/cessna.jpg',
alt: 'A Cessna aircraft on the apron at Swansea Airport',
},
];
export const fallbackEvents: EventItem[] = [ export const fallbackEvents: EventItem[] = [
{ {
title: 'Airfield open day', title: 'Airfield open day',
+2 -1
View File
@@ -31,7 +31,8 @@ export const site = {
}, },
{ label: 'Events', href: '/events/' }, { label: 'Events', href: '/events/' },
{ label: 'News', href: '/news/' }, { label: 'News', href: '/news/' },
{ label: 'Documents', href: '/documents/' }, // Keep the documents page available, but hidden from the menu until needed.
// { label: 'Documents', href: '/documents/' },
{ label: 'Contact', href: '/contact/' }, { label: 'Contact', href: '/contact/' },
], ],
}; };
+5 -4
View File
@@ -6,13 +6,14 @@ import EventsList from '../components/EventsList.astro';
import FacebookWidget from '../components/FacebookWidget.astro'; import FacebookWidget from '../components/FacebookWidget.astro';
import NewsFeed from '../components/NewsFeed.astro'; import NewsFeed from '../components/NewsFeed.astro';
import { getUpcomingEvents } from '../lib/events'; import { getUpcomingEvents } from '../lib/events';
import { getEvents, getFuelPrices, getHomepageBannerImages, getNews } from '../lib/directus'; import { getEvents, getFuelPrices, getHomepageBannerImages, getHomepageVolunteerImages, getNews } from '../lib/directus';
const [fuelPrices, events, news, bannerImages] = await Promise.all([ const [fuelPrices, events, news, bannerImages, volunteerImages] = await Promise.all([
getFuelPrices(), getFuelPrices(),
getEvents(), getEvents(),
getNews(), getNews(),
getHomepageBannerImages(), getHomepageBannerImages(),
getHomepageVolunteerImages(),
]); ]);
const upcomingEvents = getUpcomingEvents(events); const upcomingEvents = getUpcomingEvents(events);
@@ -74,7 +75,7 @@ const businessPromos = [
</section> </section>
<div class="container stack"> <div class="container stack">
<FuelPricesWidget fuelPrices={fuelPrices} /> <FuelPricesWidget fuelPrices={fuelPrices} moreInfoHref="/pilot-info/#fuel" />
</div> </div>
<section class="container business-promo"> <section class="container business-promo">
@@ -140,7 +141,7 @@ const businessPromos = [
</article> </article>
<figure class="story-image"> <figure class="story-image">
<img src="/images/cessna.jpg" alt="A Cessna aircraft on the apron at Swansea Airport" loading="lazy" /> <BannerRotator images={volunteerImages} />
</figure> </figure>
</section> </section>
+5 -1
View File
@@ -19,6 +19,10 @@ const runwayRows = site.runwayFacts
const otherFacts = site.runwayFacts.filter((fact) => !fact.match(runwayPattern)); const otherFacts = site.runwayFacts.filter((fact) => !fact.match(runwayPattern));
const fuelPrices = await getFuelPrices(); const fuelPrices = await getFuelPrices();
const fuelServiceNotes = [
'Attended refuelling is available. Please advise on PPR, or after landing, if fuel is required.',
'JET A1 is available gravity fed or pressurised. Rotors-running and out-of-hours fuel may be available by prior arrangement.',
];
--- ---
<BaseLayout title="Pilot Info" description="Essential information for pilots planning a visit to Swansea Airport."> <BaseLayout title="Pilot Info" description="Essential information for pilots planning a visit to Swansea Airport.">
@@ -128,7 +132,7 @@ When the Tower is unavailable, this will be NOTAMed and blind calls to Swansea T
)} )}
<FuelPricesWidget fuelPrices={fuelPrices} /> <FuelPricesWidget contactHref="/contact/" fuelPrices={fuelPrices} sectionId="fuel" serviceNotes={fuelServiceNotes} />
</div> </div>
+89 -16
View File
@@ -190,34 +190,91 @@ img {
.mobile-nav > summary { .mobile-nav > summary {
list-style: none; list-style: none;
display: flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
min-height: 2.75rem; gap: 0.45rem;
min-width: 2.75rem; min-height: 3rem;
padding: 0; min-width: 5.5rem;
padding: 0 0.85rem;
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: 0.7rem; border-radius: 0.85rem;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.95), rgba(228, 240, 252, 0.95)); background: var(--brand);
color: var(--brand); color: #ffffff;
font-weight: 800; font-weight: 800;
font-size: 1.4rem; font-size: 0.96rem;
line-height: 1;
cursor: pointer; cursor: pointer;
box-shadow: var(--shadow); box-shadow: 0 12px 28px rgba(11, 79, 122, 0.22);
text-decoration: none;
}
.mobile-nav > summary span:not(.mobile-nav-icon) {
display: block;
line-height: 1;
}
.mobile-nav > summary:hover,
.mobile-nav > summary:focus-visible {
background: #083c5f;
}
.mobile-nav > summary:focus-visible {
outline: 3px solid rgba(246, 181, 56, 0.72);
outline-offset: 3px;
} }
.mobile-nav > summary::-webkit-details-marker { .mobile-nav > summary::-webkit-details-marker {
display: none; display: none;
} }
.mobile-nav > summary::before { .mobile-nav-icon {
content: '☰'; position: relative;
line-height: 1; display: block;
width: 1.2rem;
height: 0.9rem;
flex: 0 0 auto;
border-block: 0.18rem solid currentColor;
} }
.mobile-nav[open] > summary::before { .mobile-nav-icon::before,
content: '✕'; .mobile-nav-icon::after {
font-size: 1.2rem; content: '';
position: absolute;
left: 0;
width: 100%;
height: 0.18rem;
border-radius: 999px;
background: currentColor;
}
.mobile-nav-icon::before {
top: 50%;
transform: translateY(-50%);
}
.mobile-nav-icon::after {
display: none;
}
.mobile-nav[open] .mobile-nav-icon {
width: 1rem;
height: 1rem;
border-block: 0;
}
.mobile-nav[open] .mobile-nav-icon::before,
.mobile-nav[open] .mobile-nav-icon::after {
display: block;
top: 50%;
}
.mobile-nav[open] .mobile-nav-icon::before {
transform: translateY(-50%) rotate(45deg);
}
.mobile-nav[open] .mobile-nav-icon::after {
transform: translateY(-50%) rotate(-45deg);
} }
.mobile-nav-panel { .mobile-nav-panel {
@@ -406,12 +463,18 @@ section {
.story-image img { .story-image img {
width: 100%; width: 100%;
display: block; display: block;
aspect-ratio: 16 / 9; aspect-ratio: 4 / 3;
object-fit: cover; object-fit: cover;
border-radius: 0; border-radius: 0;
box-shadow: none; box-shadow: none;
} }
.story-image .banner-rotator {
min-height: 0;
aspect-ratio: 4 / 3;
background: transparent;
}
.business-promo { .business-promo {
display: grid; display: grid;
gap: 1rem; gap: 1rem;
@@ -897,6 +960,16 @@ section {
max-height: 2rem; max-height: 2rem;
} }
@media (max-width: 390px) {
.topbar-inner {
gap: 0.65rem;
}
.brand-mobile img {
max-width: min(12rem, 58vw);
}
}
.runway-facts-table { .runway-facts-table {
min-width: 38rem; min-width: 38rem;
} }