Graphical PDS migrator for AT Protocol

a ton of frontend stuffs

+4
fresh.gen.ts
···
import * as $index from "./routes/index.tsx";
import * as $login_callback from "./routes/login/callback.tsx";
import * as $login_index from "./routes/login/index.tsx";
+
import * as $AirportSign from "./islands/AirportSign.tsx";
import * as $Counter from "./islands/Counter.tsx";
import * as $HandleInput from "./islands/HandleInput.tsx";
import * as $Header from "./islands/Header.tsx";
import * as $OAuthCallback from "./islands/OAuthCallback.tsx";
+
import * as $Ticket from "./islands/Ticket.tsx";
import type { Manifest } from "$fresh/server.ts";
const manifest = {
···
"./routes/login/index.tsx": $login_index,
},
islands: {
+
"./islands/AirportSign.tsx": $AirportSign,
"./islands/Counter.tsx": $Counter,
"./islands/HandleInput.tsx": $HandleInput,
"./islands/Header.tsx": $Header,
"./islands/OAuthCallback.tsx": $OAuthCallback,
+
"./islands/Ticket.tsx": $Ticket,
},
baseUrl: import.meta.url,
} satisfies Manifest;
+52
islands/AirportSign.tsx
···
+
export default function AirportSign() {
+
return (
+
<div class="relative inline-block mb-12">
+
{/* Left Pole */}
+
<div class="absolute left-8 -top-24 w-4 h-24 bg-gradient-to-r from-slate-800 via-slate-600 to-slate-800 rounded-t-lg"></div>
+
{/* Right Pole */}
+
<div class="absolute right-8 -top-24 w-4 h-24 bg-gradient-to-r from-slate-800 via-slate-600 to-slate-800 rounded-t-lg"></div>
+
{/* Display Board */}
+
<div class="relative bg-gradient-to-b from-slate-800 to-slate-900 p-1 rounded-lg shadow-[0_2px_10px_rgba(0,0,0,0.3)]">
+
{/* Metallic Frame */}
+
<div class="bg-gradient-to-b from-slate-600 via-slate-700 to-slate-800 p-2 rounded-[6px]">
+
{/* Inner Frame */}
+
<div class="bg-black px-4 py-0.5 rounded-[4px] relative overflow-hidden">
+
{/* Screen Background with Effects */}
+
<div class="absolute inset-0 bg-[#0a0a2f]">
+
{/* Scan lines */}
+
<div class="absolute inset-0 bg-[linear-gradient(transparent_0%,_rgba(255,255,255,0.02)_50%,_transparent_100%)] bg-[length:100%_4px]"></div>
+
{/* Screen noise */}
+
<div class="absolute inset-0 opacity-[0.03] [background-image:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMDAiIGhlaWdodD0iMzAwIj48ZmlsdGVyIGlkPSJhIiB4PSIwIiB5PSIwIj48ZmVUdXJidWxlbmNlIHR5cGU9ImZyYWN0YWxOb2lzZSIgYmFzZUZyZXF1ZW5jeT0iLjc1IiBzdGl0Y2hUaWxlcz0ic3RpdGNoIi8+PC9maWx0ZXI+PHJlY3Qgd2lkdGg9IjMwMCIgaGVpZ2h0PSIzMDAiIGZpbHRlcj0idXJsKCNhKSIgb3BhY2l0eT0iMC4wNSIvPjwvc3ZnPg==')]"></div>
+
</div>
+
+
{/* Display Board Text */}
+
<div className="relative flex justify-center items-center py-2 pb-4 px-4">
+
<div className="relative text-center">
+
<span
+
className="font-mono text-[3em] font-semibold tracking-[0.12em] leading-[0.9] text-white
+
[text-shadow:0_0_20px_rgba(255,255,255,0.2),0_0_40px_rgba(255,255,255,0.1)]
+
relative z-10"
+
>
+
ATP INTERNECTIONAL AIRPORT
+
</span>
+
{/* Text glow effect */}
+
<div className="absolute inset-0 blur-[2px] opacity-50">
+
<span
+
className="font-mono text-[3em] font-semibold tracking-[0.12em] leading-[0.9] text-white"
+
>
+
ATP INTERNECTIONAL AIRPORT
+
</span>
+
</div>
+
</div>
+
</div>
+
+
{/* Screen reflection overlay */}
+
<div className="absolute inset-0 bg-gradient-to-b from-transparent via-transparent to-white/[0.03]"></div>
+
{/* Vignette effect */}
+
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,_transparent_0%,_rgba(0,0,0,0.2)_100%)]"></div>
+
</div>
+
</div>
+
</div>
+
</div>
+
);
+
}
+17 -25
islands/Header.tsx
···
export default function Header() {
const [user, setUser] = useState<User | null>(null);
-
const [loading, setLoading] = useState(true);
useEffect(() => {
if (!IS_BROWSER) return;
···
} catch (error) {
console.error("Failed to fetch user:", error);
setUser(null);
-
} finally {
-
setLoading(false);
}
};
···
};
return (
-
<header className="bg-slate-900 text-white relative z-10">
+
<header className="bg-white dark:bg-slate-900 text-slate-900 dark:text-white relative z-10 border-b border-slate-200 dark:border-slate-800">
<div className="max-w-7xl mx-auto px-4">
<div className="flex items-center justify-between py-4">
{/* Home Link */}
-
<a href="/" className="airport-sign bg-gradient-to-r from-blue-500 to-blue-600 text-white flex items-center px-6 py-3 hover:translate-y-1 transition-all duration-200 hover:shadow-lg hover:from-blue-600 hover:to-blue-700">
+
<a href="/" className="airport-sign bg-gradient-to-r from-blue-500 to-blue-600 text-white flex items-center px-6 py-3 hover:translate-y-1 transition-all duration-200 hover:from-blue-600 hover:to-blue-700">
<img src="/icons/plane_bold.svg" alt="Plane" className="w-6 h-6 mr-2" style={{ filter: 'brightness(0) invert(1)' }} />
-
<span className="font-bold tracking-wider">AIRPORT</span>
+
<span className="font-mono font-bold tracking-wider">AIRPORT</span>
</a>
<div className="flex items-center gap-3">
{/* Departures (Migration) */}
<div className="relative group">
-
<a href="/migrate" className="airport-sign bg-gradient-to-r from-amber-400 to-amber-500 text-slate-900 flex items-center px-6 py-3 hover:translate-y-1 transition-all duration-200 hover:shadow-lg hover:from-amber-500 hover:to-amber-600">
+
<a href="/migrate" className="airport-sign bg-gradient-to-r from-amber-400 to-amber-500 text-slate-900 flex items-center px-6 py-3 hover:translate-y-1 transition-all duration-200 hover:from-amber-500 hover:to-amber-600">
<img src="/icons/plane-departure_bold.svg" alt="Departures" className="w-6 h-6 mr-2" style={{ filter: 'brightness(0)' }} />
-
<span className="font-bold tracking-wider">DEPARTURES</span>
+
<span className="font-mono font-bold tracking-wider">DEPARTURES</span>
</a>
-
<div className="absolute top-full left-0 w-full bg-gradient-to-r from-amber-400 to-amber-500 text-slate-900 py-3 px-4 transform scale-y-0 origin-top group-hover:scale-y-100 transition-transform duration-200 shadow-lg">
-
<div className="text-sm font-medium">Start Migration</div>
-
</div>
</div>
{/* Check-in (Login/Profile) */}
-
<div className="relative group">
-
{loading ? (
-
<div className="airport-sign bg-gradient-to-r from-amber-400 to-amber-500 text-slate-900 flex items-center px-6 py-3">
-
<div className="animate-pulse w-24 h-6 bg-amber-600/30 rounded"></div>
-
</div>
-
) : user?.did ? (
-
<div className="airport-sign bg-gradient-to-r from-amber-400 to-amber-500 text-slate-900 flex items-center px-6 py-3 hover:translate-y-1 transition-all duration-200 hover:shadow-lg hover:from-amber-500 hover:to-amber-600 cursor-pointer">
-
<img src="/icons/ticket_bold.svg" alt="Check-in" className="w-6 h-6 mr-2" style={{ filter: 'brightness(0)' }} />
-
<span className="font-bold tracking-wider">CHECKED IN</span>
-
<div className="absolute top-full right-0 w-56 bg-gradient-to-r from-amber-400 to-amber-500 text-slate-900 py-3 px-4 transform scale-y-0 origin-top group-hover:scale-y-100 transition-transform duration-200 shadow-lg rounded-md">
-
<div className="text-sm font-medium mb-2 pb-2 border-b border-slate-900/10">
+
<div className="relative">
+
{user?.did ? (
+
<div className="relative group">
+
<div className="airport-sign bg-gradient-to-r from-amber-400 to-amber-500 text-slate-900 flex items-center px-6 py-3 hover:translate-y-1 transition-all duration-200 hover:from-amber-500 hover:to-amber-600 cursor-pointer">
+
<img src="/icons/ticket_bold.svg" alt="Check-in" className="w-6 h-6 mr-2" style={{ filter: 'brightness(0)' }} />
+
<span className="font-mono font-bold tracking-wider">CHECKED IN</span>
+
</div>
+
<div className="absolute opacity-0 translate-y-[-8px] pointer-events-none group-hover:opacity-100 group-hover:translate-y-0 group-hover:pointer-events-auto top-full right-0 w-56 bg-gradient-to-r from-amber-400 to-amber-500 text-slate-900 py-3 px-4 rounded-md transition-all duration-200">
+
<div className="text-sm font-mono mb-2 pb-2 border-b border-slate-900/10">
<div>{user.handle || 'Anonymous'}</div>
<div className="text-xs opacity-75 truncate">{user.did}</div>
</div>
<button
type="button"
onClick={handleLogout}
-
className="text-sm font-medium text-slate-900 hover:text-slate-700 w-full text-left transition-colors"
+
className="text-sm font-mono text-slate-900 hover:text-slate-700 w-full text-left transition-colors"
>
Sign Out
</button>
</div>
</div>
) : (
-
<a href="/login" className="airport-sign bg-gradient-to-r from-amber-400 to-amber-500 text-slate-900 flex items-center px-6 py-3 hover:translate-y-1 transition-all duration-200 hover:shadow-lg hover:from-amber-500 hover:to-amber-600">
+
<a href="/login" className="airport-sign bg-gradient-to-r from-amber-400 to-amber-500 text-slate-900 flex items-center px-6 py-3 hover:translate-y-1 transition-all duration-200 hover:from-amber-500 hover:to-amber-600">
<img src="/icons/ticket_bold.svg" alt="Check-in" className="w-6 h-6 mr-2" style={{ filter: 'brightness(0)' }} />
-
<span className="font-bold tracking-wider">CHECK-IN</span>
+
<span className="font-mono font-bold tracking-wider">CHECK-IN</span>
</a>
)}
</div>
+91
islands/Ticket.tsx
···
+
import { useEffect, useState } from "preact/hooks";
+
import { IS_BROWSER } from "$fresh/runtime.ts";
+
+
interface User {
+
did: string;
+
handle?: string;
+
}
+
+
export default function Ticket() {
+
const [user, setUser] = useState<User | null>(null);
+
+
useEffect(() => {
+
if (!IS_BROWSER) return;
+
+
const fetchUser = async () => {
+
try {
+
const response = await fetch("/api/me", {
+
credentials: "include",
+
});
+
if (!response.ok) {
+
throw new Error("Failed to fetch user profile");
+
}
+
const userData = await response.json();
+
setUser(userData ? {
+
did: userData.did,
+
handle: userData.handle
+
} : null);
+
} catch (error) {
+
console.error("Failed to fetch user:", error);
+
setUser(null);
+
}
+
};
+
+
fetchUser();
+
}, []);
+
+
return (
+
<div class="max-w-4xl mx-auto">
+
<div class="ticket mb-8 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 p-6 relative before:absolute before:inset-[1px] before:bg-white dark:before:bg-slate-800 before:-z-10 after:absolute after:inset-0 after:bg-slate-200 dark:after:bg-slate-700 after:-z-20 [clip-path:polygon(0_0,20px_0,100%_0,100%_calc(100%-20px),calc(100%-20px)_100%,0_100%,0_calc(100%-20px),20px_100%)] after:[clip-path:polygon(0_0,20px_0,100%_0,100%_calc(100%-20px),calc(100%-20px)_100%,0_100%,0_calc(100%-20px),20px_100%)]">
+
<div class="boarding-label text-amber-500 dark:text-amber-400 font-mono font-bold tracking-wider text-sm mb-4">BOARDING PASS</div>
+
<div class="flex justify-between items-start mb-4">
+
<h3 class="text-2xl font-mono">WHAT IS AIRPORT?</h3>
+
<div class="text-sm font-mono text-gray-500 dark:text-gray-400">GATE A1</div>
+
</div>
+
<div class="passenger-info text-slate-600 dark:text-slate-300 font-mono text-sm mb-4">
+
PASSENGER: {(user?.handle || "UNKNOWN").toUpperCase()}
+
<br />
+
DESTINATION: NEW PDS
+
</div>
+
<p class="mb-4">
+
Think of Airport as your digital terminal for AT Protocol migrations. We help you smoothly
+
transfer your PDS account between different providers – no lost luggage, just a first-class
+
experience for your data's journey to its new home.
+
</p>
+
</div>
+
+
<div class="ticket bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 p-6 relative before:absolute before:inset-[1px] before:bg-white dark:before:bg-slate-800 before:-z-10 after:absolute after:inset-0 after:bg-slate-200 dark:after:bg-slate-700 after:-z-20 [clip-path:polygon(0_0,20px_0,100%_0,100%_calc(100%-20px),calc(100%-20px)_100%,0_100%,0_calc(100%-20px),20px_100%)] after:[clip-path:polygon(0_0,20px_0,100%_0,100%_calc(100%-20px),calc(100%-20px)_100%,0_100%,0_calc(100%-20px),20px_100%)]">
+
<div class="boarding-label text-amber-500 dark:text-amber-400 font-mono font-bold tracking-wider text-sm mb-4">FLIGHT DETAILS</div>
+
<div class="flex justify-between items-start mb-4">
+
<h3 class="text-2xl font-mono">GET READY TO FLY</h3>
+
<div class="text-sm font-mono text-gray-500 dark:text-gray-400">SEAT: 1A</div>
+
</div>
+
<div class="passenger-info mb-4 text-slate-600 dark:text-slate-300 font-mono text-sm">
+
CLASS: FIRST CLASS MIGRATION
+
<br />
+
FLIGHT: ATP-2024
+
</div>
+
<ol class="list-decimal list-inside space-y-3">
+
<li>Check in with your current PDS credentials</li>
+
<li>Select your destination PDS</li>
+
<li>Go through our streamlined security check</li>
+
<li>Sit back while we handle your data transfer</li>
+
</ol>
+
<div class="mt-6 text-sm text-gray-600 dark:text-gray-400 border-t border-dashed pt-4 border-slate-200 dark:border-slate-700">
+
Your data travels securely with our top-tier encryption – we're like the TSA, but actually fast.
+
</div>
+
<div class="flight-info mt-6 flex items-center justify-between text-slate-600 dark:text-slate-300 font-mono text-sm">
+
<div>
+
<div class="text-xs text-gray-500 dark:text-gray-400">FROM</div>
+
<div>CURRENT PDS</div>
+
</div>
+
<div class="text-amber-500 dark:text-amber-400">➜</div>
+
<div>
+
<div class="text-xs text-gray-500 dark:text-gray-400">TO</div>
+
<div>NEW PDS</div>
+
</div>
+
</div>
+
</div>
+
</div>
+
);
+
}
-1
oauth/session.ts
···
}
try {
-
console.log("restoring session: ", session.did);
const oauthSession = await oauthClient.restore(session.did);
return oauthSession ? new Agent(oauthSession) : null;
} catch (err) {
+14 -9
routes/_404.tsx
···
<>
<Head>
<title>404 - Flight Not Found</title>
+
<style dangerouslySetInnerHTML={{ __html: `
+
:root { --is-dark: 0; }
+
@media (prefers-color-scheme: dark) {
+
:root { --is-dark: 1; }
+
}
+
`}} />
</Head>
-
<div class="min-h-screen bg-slate-900 px-4 py-16 sm:py-24 mx-auto flex items-center">
-
<div class="max-w-screen-xl mx-auto flex flex-col items-center justify-center text-center">
+
<div class="px-4">
+
<div class="max-w-screen-xl mx-auto flex flex-col items-center text-center">
<div class="relative mb-4">
<img
src="/icons/plane_bold.svg"
-
class="w-32 h-32 sm:w-48 sm:h-48 opacity-90"
+
class="w-32 h-32 sm:w-35 sm:h-35 brightness-[0.1] dark:invert dark:filter-none"
alt="Plane icon"
-
style={{ filter: 'brightness(0) invert(1)' }}
/>
</div>
-
<div class="airport-board bg-black/20 backdrop-blur-sm p-8 sm:p-12 rounded-lg border border-white/10">
-
<h1 class="text-6xl sm:text-8xl md:text-9xl font-mono tracking-wider text-amber-400 font-bold mb-6">
+
<div class="bg-white dark:bg-slate-900 airport-board p-8 sm:p-12 rounded-lg border border-slate-200 dark:border-white/10">
+
<h1 class="text-6xl sm:text-8xl md:text-9xl font-mono tracking-wider text-amber-500 dark:text-amber-400 font-bold mb-6">
404
</h1>
<div class="space-y-4">
-
<p class="text-2xl sm:text-3xl md:text-4xl font-mono text-white/90">
+
<p class="text-2xl sm:text-3xl md:text-4xl font-mono text-slate-900 dark:text-white/90">
FLIGHT NOT FOUND
</p>
-
<p class="text-lg sm:text-xl text-white/70 max-w-2xl">
+
<p class="text-lg sm:text-xl text-slate-600 dark:text-white/70 max-w-2xl">
We couldn't locate the destination you're looking for. Please check your flight number and try again.
</p>
<div class="mt-8">
<a
href="/"
-
class="inline-flex items-center px-8 py-4 bg-amber-400 text-slate-900 rounded-md font-bold text-lg hover:bg-amber-500 transition-colors duration-200"
+
class="inline-flex items-center px-8 py-4 bg-amber-500 dark:bg-amber-400 text-slate-900 rounded-md font-bold text-lg hover:bg-amber-600 dark:hover:bg-amber-500 transition-colors duration-200"
>
Return to Terminal
</a>
-6
routes/api/me.ts
···
import { getSessionAgent } from "../../oauth/session.ts";
import { resolver } from "../../utils/id-resolver.ts";
-
interface BskyProfile {
-
handle: string;
-
displayName?: string;
-
description?: string;
-
}
-
export const handler: Handlers = {
async GET(req, ctx) {
const agent = await getSessionAgent(req, ctx);
+20 -64
routes/index.tsx
···
+
import Ticket from "../islands/Ticket.tsx";
+
import AirportSign from "../islands/AirportSign.tsx";
+
export default function Home() {
return (
-
<div class="px-4 py-8 mx-auto">
-
<div class="max-w-screen-md mx-auto flex flex-col items-center justify-center">
-
<h1 class="text-7xl font-normal mb-8 text-center">Welcome to Airport</h1>
+
<>
+
<div class="px-4 py-8 mx-auto">
+
<div class="max-w-screen-lg mx-auto flex flex-col items-center justify-center">
+
<AirportSign />
-
<div class="prose dark:prose-invert max-w-none w-full mb-0">
-
<p class="text-xl mb-6 mt-0 text-center text-gray-600 dark:text-gray-300">
-
Your gateway to seamless AT Protocol PDS migration
-
</p>
-
-
<div class="ticket mb-8">
-
<div class="boarding-label">BOARDING PASS</div>
-
<div class="flex justify-between items-start mb-4">
-
<h3 class="text-2xl font-normal">WHAT IS AIRPORT?</h3>
-
<div class="text-sm text-gray-500">GATE A1</div>
-
</div>
-
<div class="passenger-info">
-
PASSENGER: BLUESKY USER
-
<br />
-
DESTINATION: NEW PDS
-
</div>
-
<p class="mb-4">
-
Think of Airport as your digital terminal for AT Protocol migrations. We help you smoothly
-
transfer your PDS account between different providers – no lost luggage, just a first-class
-
experience for your data's journey to its new home.
+
<div class="prose dark:prose-invert max-w-none w-full mb-0">
+
<p class="text-xl mb-6 mt-0 text-center text-gray-600 dark:text-gray-300">
+
Your terminal for seamless AT Protocol PDS migration and backup
</p>
-
</div>
-
<div class="ticket">
-
<div class="boarding-label">FLIGHT DETAILS</div>
-
<div class="flex justify-between items-start mb-4">
-
<h3 class="text-2xl font-normal">GET READY TO FLY</h3>
-
<div class="text-sm text-gray-500">SEAT: 1A</div>
-
</div>
-
<div class="passenger-info mb-4">
-
CLASS: FIRST CLASS MIGRATION
-
<br />
-
FLIGHT: ATP-2024
-
</div>
-
<ol class="list-decimal list-inside space-y-3">
-
<li>Check in with your current PDS credentials</li>
-
<li>Select your destination PDS</li>
-
<li>Go through our streamlined security check</li>
-
<li>Sit back while we handle your data transfer</li>
-
</ol>
-
<div class="mt-6 text-sm text-gray-600 dark:text-gray-400 border-t border-dashed pt-4">
-
Your data travels securely with our top-tier encryption – we're like the TSA, but actually fast.
-
</div>
-
<div class="flight-info">
-
<div>
-
<div class="text-xs text-gray-500">FROM</div>
-
<div>CURRENT PDS</div>
-
</div>
-
<div>➜</div>
-
<div>
-
<div class="text-xs text-gray-500">TO</div>
-
<div>NEW PDS</div>
-
</div>
+
<Ticket />
+
+
<div class="mt-8 text-center">
+
<a
+
href="/login"
+
class="inline-flex items-center px-6 py-3 border border-transparent text-lg font-mono rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
+
>
+
Begin Your Journey
+
</a>
</div>
</div>
-
-
<div class="mt-8 text-center">
-
<a
-
href="/login"
-
class="inline-flex items-center px-6 py-3 border border-transparent text-lg font-normal rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
-
>
-
Begin Your Journey
-
</a>
-
</div>
</div>
</div>
-
</div>
+
</>
);
}
static/fonts/din-condensed-bold.ttf

This is a binary file and will not be displayed.

+2 -2
static/icons/plane-departure_bold.svg
···
-
<svg width="80" height="80" viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg">
-
<path fill-rule="evenodd" clip-rule="evenodd" d="M50.8224 33.2841L31.008 22.743C29.2561 21.8109 27.1909 21.6663 25.3262 22.345L19.7985 24.3569C19.3667 24.5141 19.2513 25.0706 19.5849 25.3865L34.2839 39.3037L18.5107 45.0447L13.6805 42.0448C12.6378 41.3972 11.3555 41.2642 10.202 41.684L5.45108 43.4133C4.99314 43.5799 4.83855 44.15 5.14954 44.5252L13.9345 55.124C13.9589 55.1535 13.9934 55.1711 14.0299 55.1746C14.0582 55.1928 14.0913 55.2035 14.1264 55.2046L22.9607 55.4824C27.707 55.6317 32.4381 54.874 36.9004 53.2498L63.8001 43.4591C66.6965 42.405 69.3245 40.7247 71.4968 38.5381L72.708 37.3189C73.9986 36.0199 74.4226 34.0923 73.7963 32.3716C73.1701 30.6509 71.6062 29.4468 69.7826 29.2812L68.071 29.1259C65.0014 28.8472 61.9082 29.2493 59.0119 30.3034L50.8224 33.2841Z" fill="#C2CCDE" />
+
<svg width="80" height="80" viewBox="4 20 70 36" fill="none" xmlns="http://www.w3.org/2000/svg">
+
<path fill-rule="evenodd" clip-rule="evenodd" d="M50.8224 33.2841L31.008 22.743C29.2561 21.8109 27.1909 21.6663 25.3262 22.345L19.7985 24.3569C19.3667 24.5141 19.2513 25.0706 19.5849 25.3865L34.2839 39.3037L18.5107 45.0447L13.6805 42.0448C12.6378 41.3972 11.3555 41.2642 10.202 41.684L5.45108 43.4133C4.99314 43.5799 4.83855 44.15 5.14954 44.5252L13.9345 55.124C13.9589 55.1535 13.9934 55.1711 14.0299 55.1746C14.0582 55.1928 14.0913 55.2035 14.1264 55.2046L22.9607 55.4824C27.707 55.6317 32.4381 54.874 36.9004 53.2498L63.8001 43.4591C66.6965 42.405 69.3245 40.7247 71.4968 38.5381L72.708 37.3189C73.9986 36.0199 74.4226 34.0923 73.7963 32.3716C73.1701 30.6509 71.6062 29.4468 69.7826 29.2812L68.071 29.1259C65.0014 28.8472 61.9082 29.2493 59.0119 30.3034L50.8224 33.2841Z" fill="#FFFFFF" />
<path d="M7 64H75" stroke="#C2CCDE" stroke-width="6" stroke-linecap="round" stroke-linejoin="round" />
</svg>
+1 -1
static/icons/plane_bold.svg
···
-
<svg width="80" height="80" viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg">
+
<svg width="80" height="80" viewBox="8 10 64 60" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M49.4268 34L38.5549 15.0236C37.1294 12.5354 34.4811 11.0005 31.6134 11.0005H29.375C28.6565 11.0005 28.111 11.6475 28.2326 12.3557L31.9471 34H20.5233L16.0054 27.6751C15.2546 26.6239 14.0423 26 12.7505 26H9.80898C9.29425 26 8.91051 26.4746 9.01821 26.9779L11.7909 39.9367C11.7945 39.9493 11.8008 39.9607 11.8091 39.9701C11.809 39.9801 11.809 39.9901 11.809 40L11.8091 40.0295C11.8008 40.039 11.7945 40.0503 11.7909 40.063L9.01821 53.0217C8.91051 53.5251 9.29425 53.9996 9.80898 53.9996H12.7505C14.0423 53.9996 15.2546 53.3757 16.0054 52.3245L20.5229 46H31.9472L28.2326 67.6451C28.111 68.3533 28.6565 69.0003 29.375 69.0003H31.6134C34.4811 69.0003 37.1294 67.4654 38.5549 64.9772L49.4272 46H60.3858C62.8955 46 65.3762 45.4638 67.6618 44.4273L68.0768 44.2391C69.7405 43.4846 70.809 41.8268 70.809 40C70.809 38.1733 69.7405 36.5155 68.0768 35.761L67.6618 35.5728C65.3762 34.5363 62.8955 34 60.3858 34H49.4268Z" fill="#C2CCDE" />
</svg>
+3 -3
static/icons/ticket_bold.svg
···
-
<svg width="80" height="80" viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg">
-
<path fill-rule="evenodd" clip-rule="evenodd" d="M8 24C8 21.7909 9.79086 20 12 20H68C70.2091 20 72 21.7909 72 24V33.6052C71.6647 33.6578 71.3304 33.7373 71 33.8446C68.3333 34.7111 66.5279 37.1961 66.5279 40C66.5279 42.8039 68.3333 45.2889 71 46.1554C71.3304 46.2627 71.6647 46.3422 72 46.3948V56C72 58.2091 70.2091 60 68 60H12C9.79086 60 8 58.2091 8 56V46.4726C8.66685 46.4727 9.3412 46.3694 10 46.1554C12.6667 45.2889 14.4721 42.8039 14.4721 40C14.4721 37.1961 12.6667 34.7111 10 33.8446C9.3412 33.6306 8.66685 33.5273 8 33.5274V24Z" fill="#C2CCDE" />
-
<path d="M72 33.6052L72.3097 35.5811C73.2828 35.4286 74 34.5903 74 33.6052H72ZM71 33.8446L70.382 31.9425L70.382 31.9425L71 33.8446ZM71 46.1554L70.382 48.0575H70.382L71 46.1554ZM72 46.3948H74C74 45.4097 73.2828 44.5714 72.3097 44.4189L72 46.3948ZM8 46.4726L8.00027 44.4726C7.46979 44.4725 6.96101 44.6832 6.58588 45.0583C6.21075 45.4333 6 45.9421 6 46.4726H8ZM10 46.1554L10.618 48.0575H10.618L10 46.1554ZM14.4721 40H12.4721H14.4721ZM10 33.8446L10.618 31.9425H10.618L10 33.8446ZM8 33.5274H6C6 34.0579 6.21075 34.5667 6.58588 34.9417C6.96101 35.3168 7.46979 35.5275 8.00027 35.5274L8 33.5274ZM12 18C8.68629 18 6 20.6863 6 24H10C10 22.8954 10.8954 22 12 22V18ZM68 18H12V22H68V18ZM74 24C74 20.6863 71.3137 18 68 18V22C69.1046 22 70 22.8954 70 24H74ZM74 33.6052V24H70V33.6052H74ZM71.6903 31.6294C71.2511 31.6982 70.8136 31.8023 70.382 31.9425L71.618 35.7467C71.8472 35.6723 72.0783 35.6174 72.3097 35.5811L71.6903 31.6294ZM70.382 31.9425C66.8913 33.0767 64.5279 36.3296 64.5279 40H68.5279C68.5279 38.0626 69.7754 36.3454 71.618 35.7467L70.382 31.9425ZM64.5279 40C64.5279 43.6704 66.8913 46.9233 70.382 48.0575L71.618 44.2533C69.7754 43.6546 68.5279 41.9374 68.5279 40H64.5279ZM70.382 48.0575C70.8136 48.1977 71.2511 48.3018 71.6903 48.3706L72.3097 44.4189C72.0783 44.3826 71.8472 44.3277 71.618 44.2533L70.382 48.0575ZM74 56V46.3948H70V56H74ZM68 62C71.3137 62 74 59.3137 74 56H70C70 57.1046 69.1046 58 68 58V62ZM12 62H68V58H12V62ZM6 56C6 59.3137 8.68629 62 12 62V58C10.8954 58 10 57.1046 10 56H6ZM6 46.4726V56H10V46.4726H6ZM9.38197 44.2533C8.92559 44.4015 8.46006 44.4726 8.00027 44.4726L7.99973 48.4726C8.87364 48.4727 9.7568 48.3373 10.618 48.0575L9.38197 44.2533ZM12.4721 40C12.4721 41.9374 11.2246 43.6545 9.38197 44.2533L10.618 48.0575C14.1087 46.9233 16.4721 43.6704 16.4721 40H12.4721ZM9.38197 35.7467C11.2246 36.3454 12.4721 38.0626 12.4721 40H16.4721C16.4721 36.3296 14.1087 33.0767 10.618 31.9425L9.38197 35.7467ZM8.00027 35.5274C8.46006 35.5274 8.92559 35.5985 9.38197 35.7467L10.618 31.9425C9.7568 31.6627 8.87364 31.5273 7.99973 31.5274L8.00027 35.5274ZM6 24V33.5274H10V24H6Z" fill="#C2CCDE" />
+
<svg width="80" height="80" viewBox="6 18 70 44" fill="none" xmlns="http://www.w3.org/2000/svg">
+
<path fill-rule="evenodd" clip-rule="evenodd" d="M8 24C8 21.7909 9.79086 20 12 20H68C70.2091 20 72 21.7909 72 24V33.6052C71.6647 33.6578 71.3304 33.7373 71 33.8446C68.3333 34.7111 66.5279 37.1961 66.5279 40C66.5279 42.8039 68.3333 45.2889 71 46.1554C71.3304 46.2627 71.6647 46.3422 72 46.3948V56C72 58.2091 70.2091 60 68 60H12C9.79086 60 8 58.2091 8 56V46.4726C8.66685 46.4727 9.3412 46.3694 10 46.1554C12.6667 45.2889 14.4721 42.8039 14.4721 40C14.4721 37.1961 12.6667 34.7111 10 33.8446C9.3412 33.6306 8.66685 33.5273 8 33.5274V24Z" fill="#FFFFFF" />
+
<path d="M72 33.6052L72.3097 35.5811C73.2828 35.4286 74 34.5903 74 33.6052H72ZM71 33.8446L70.382 31.9425L70.382 31.9425L71 33.8446ZM71 46.1554L70.382 48.0575H70.382L71 46.1554ZM72 46.3948H74C74 45.4097 73.2828 44.5714 72.3097 44.4189L72 46.3948ZM8 46.4726L8.00027 44.4726C7.46979 44.4725 6.96101 44.6832 6.58588 45.0583C6.21075 45.4333 6 45.9421 6 46.4726H8ZM10 46.1554L10.618 48.0575H10.618L10 46.1554ZM14.4721 40H12.4721H14.4721ZM10 33.8446L10.618 31.9425H10.618L10 33.8446ZM8 33.5274H6C6 34.0579 6.21075 34.5667 6.58588 34.9417C6.96101 35.3168 7.46979 35.5275 8.00027 35.5274L8 33.5274ZM12 18C8.68629 18 6 20.6863 6 24H10C10 22.8954 10.8954 22 12 22V18ZM68 18H12V22H68V18ZM74 24C74 20.6863 71.3137 18 68 18V22C69.1046 22 70 22.8954 70 24H74ZM74 33.6052V24H70V33.6052H74ZM71.6903 31.6294C71.2511 31.6982 70.8136 31.8023 70.382 31.9425L71.618 35.7467C71.8472 35.6723 72.0783 35.6174 72.3097 35.5811L71.6903 31.6294ZM70.382 31.9425C66.8913 33.0767 64.5279 36.3296 64.5279 40H68.5279C68.5279 38.0626 69.7754 36.3454 71.618 35.7467L70.382 31.9425ZM64.5279 40C64.5279 43.6704 66.8913 46.9233 70.382 48.0575L71.618 44.2533C69.7754 43.6546 68.5279 41.9374 68.5279 40H64.5279ZM70.382 48.0575C70.8136 48.1977 71.2511 48.3018 71.6903 48.3706L72.3097 44.4189C72.0783 44.3826 71.8472 44.3277 71.618 44.2533L70.382 48.0575ZM74 56V46.3948H70V56H74ZM68 62C71.3137 62 74 59.3137 74 56H70C70 57.1046 69.1046 58 68 58V62ZM12 62H68V58H12V62ZM6 56C6 59.3137 8.68629 62 12 62V58C10.8954 58 10 57.1046 10 56H6ZM6 46.4726V56H10V46.4726H6ZM9.38197 44.2533C8.92559 44.4015 8.46006 44.4726 8.00027 44.4726L7.99973 48.4726C8.87364 48.4727 9.7568 48.3373 10.618 48.0575L9.38197 44.2533ZM12.4721 40C12.4721 41.9374 11.2246 43.6545 9.38197 44.2533L10.618 48.0575C14.1087 46.9233 16.4721 43.6704 16.4721 40H12.4721ZM9.38197 35.7467C11.2246 36.3454 12.4721 38.0626 12.4721 40H16.4721C16.4721 36.3296 14.1087 33.0767 10.618 31.9425L9.38197 35.7467ZM8.00027 35.5274C8.46006 35.5274 8.92559 35.5985 9.38197 35.7467L10.618 31.9425C9.7568 31.6627 8.87364 31.5273 7.99973 31.5274L8.00027 35.5274ZM6 24V33.5274H10V24H6Z" fill="#FFFFFF" />
</svg>
+2 -9
static/styles.css
···
+
@import url('https://fonts.googleapis.com/css2?family=Share+Tech+Mono&display=swap');
+
@font-face {
font-family: "Skyfont";
src: url("fonts/skyfont.regular.otf") format("opentype");
···
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border-radius: 0.5rem;
backdrop-filter: blur(8px);
-
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1),
-
0 2px 4px -1px rgba(0, 0, 0, 0.06);
-
}
-
-
.airport-sign:hover {
-
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1),
-
0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
/* Dropdown panel styles */
.airport-sign + div {
border-radius: 0.5rem;
backdrop-filter: blur(8px);
-
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1),
-
0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
/* Remove old texture styles */