Graphical PDS migrator for AT Protocol

socials

Changed files
+224 -79
components
islands
routes
static
+64 -6
components/Button.tsx
··· 1 1 import { JSX } from "preact"; 2 - import { IS_BROWSER } from "fresh/runtime"; 2 + 3 + type ButtonBaseProps = { 4 + color?: "blue" | "amber"; 5 + icon?: string; 6 + iconAlt?: string; 7 + label?: string; 8 + className?: string; 9 + condensed?: boolean; 10 + }; 11 + 12 + type ButtonProps = ButtonBaseProps & Omit<JSX.HTMLAttributes<HTMLButtonElement>, keyof ButtonBaseProps>; 13 + type AnchorProps = ButtonBaseProps & Omit<JSX.HTMLAttributes<HTMLAnchorElement>, keyof ButtonBaseProps> & { href: string }; 14 + 15 + type Props = ButtonProps | AnchorProps; 16 + 17 + export function Button(props: Props) { 18 + const { color = "blue", icon, iconAlt, label, className = "", condensed = false, ...rest } = props; 19 + const isAnchor = 'href' in props; 20 + 21 + const baseStyles = "airport-sign flex items-center [transition:none]"; 22 + const paddingStyles = condensed ? 'px-2 py-1.5' : 'px-3 py-2 sm:px-6 sm:py-3'; 23 + const transformStyles = "translate-y-0 hover:translate-y-1 hover:transition-transform hover:duration-200 hover:ease-in-out"; 24 + const colorStyles = { 25 + blue: "bg-gradient-to-r from-blue-400 to-blue-500 text-white hover:from-blue-500 hover:to-blue-600", 26 + amber: "bg-gradient-to-r from-amber-400 to-amber-500 text-slate-900 hover:from-amber-500 hover:to-amber-600", 27 + }; 3 28 4 - export function Button(props: JSX.HTMLAttributes<HTMLButtonElement>) { 29 + const buttonContent = ( 30 + <> 31 + {icon && ( 32 + <img 33 + src={icon} 34 + alt={iconAlt || ""} 35 + className={`${condensed ? 'w-4 h-4' : 'w-6 h-6'} mr-2`} 36 + style={{ filter: color === 'blue' ? "brightness(0) invert(1)" : "brightness(0)" }} 37 + /> 38 + )} 39 + {label && ( 40 + <span className="font-mono font-bold tracking-wider"> 41 + {label} 42 + </span> 43 + )} 44 + </> 45 + ); 46 + 47 + const buttonStyles = `${baseStyles} ${paddingStyles} ${transformStyles} ${colorStyles[color]} ${className}`; 48 + 49 + if (isAnchor) { 50 + return ( 51 + <a 52 + href={props.href} 53 + className={buttonStyles} 54 + {...rest as JSX.HTMLAttributes<HTMLAnchorElement>} 55 + > 56 + {buttonContent} 57 + </a> 58 + ); 59 + } 60 + 61 + const buttonProps = rest as JSX.HTMLAttributes<HTMLButtonElement>; 5 62 return ( 6 63 <button 7 - {...props} 8 - disabled={!IS_BROWSER || props.disabled} 9 - class="px-2 py-1 border-gray-500 border-2 rounded bg-white hover:bg-gray-200 transition-colors" 10 - /> 64 + {...buttonProps} 65 + className={buttonStyles} 66 + > 67 + {buttonContent} 68 + </button> 11 69 ); 12 70 }
+37 -4
deno.lock
··· 25 25 "jsr:@std/path@^1.0.9": "1.0.9", 26 26 "jsr:@std/semver@1": "1.0.5", 27 27 "npm:@atproto-labs/handle-resolver-node@~0.1.14": "0.1.15", 28 + "npm:@atproto-labs/simple-store@~0.1.2": "0.1.2", 28 29 "npm:@atproto/api@*": "0.15.6", 29 30 "npm:@atproto/api@~0.15.6": "0.15.6", 30 31 "npm:@atproto/crypto@*": "0.4.4", 31 32 "npm:@atproto/identity@*": "0.4.8", 32 33 "npm:@atproto/jwk@0.1.4": "0.1.4", 33 34 "npm:@atproto/oauth-client@~0.3.13": "0.3.16", 35 + "npm:@atproto/oauth-types@~0.2.4": "0.2.7", 34 36 "npm:@atproto/syntax@*": "0.4.0", 35 37 "npm:@atproto/xrpc@*": "0.7.0", 38 + "npm:@lucide/lab@*": "0.1.2", 36 39 "npm:@opentelemetry/api@^1.9.0": "1.9.0", 37 40 "npm:@preact/signals@^1.2.3": "1.3.2_preact@10.26.6", 38 41 "npm:@preact/signals@^2.0.4": "2.0.4_preact@10.26.6", 42 + "npm:@types/node@*": "22.15.15", 39 43 "npm:autoprefixer@10.4.17": "10.4.17_postcss@8.4.35", 40 44 "npm:cssnano@6.0.3": "6.0.3_postcss@8.4.35", 41 45 "npm:esbuild-wasm@0.23.1": "0.23.1", 42 46 "npm:esbuild@0.23.1": "0.23.1", 43 47 "npm:iron-session@*": "8.0.4", 44 48 "npm:jose@5.9.6": "5.9.6", 49 + "npm:lucide-preact@*": "0.511.0_preact@10.26.6", 45 50 "npm:postcss@8.4.35": "8.4.35", 51 + "npm:preact-feather@*": "4.2.1_preact@10.26.6", 46 52 "npm:preact-render-to-string@^6.5.11": "6.5.13_preact@10.26.6", 47 53 "npm:preact@^10.25.1": "10.26.6", 48 54 "npm:preact@^10.26.6": "10.26.6", ··· 166 172 "dependencies": [ 167 173 "@atproto-labs/fetch", 168 174 "@atproto-labs/pipe", 169 - "@atproto-labs/simple-store", 175 + "@atproto-labs/simple-store@0.2.0", 170 176 "@atproto-labs/simple-store-memory", 171 177 "@atproto/did", 172 178 "zod" ··· 199 205 "@atproto-labs/handle-resolver@0.1.8": { 200 206 "integrity": "sha512-Y0ckccoCGDo/3g4thPkgp9QcORmc+qqEaCBCYCZYtfLIQp4775u22wd+4fyEyJP4DqoReKacninkICgRGfs3dQ==", 201 207 "dependencies": [ 202 - "@atproto-labs/simple-store", 208 + "@atproto-labs/simple-store@0.2.0", 203 209 "@atproto-labs/simple-store-memory", 204 210 "@atproto/did", 205 211 "zod" ··· 219 225 "@atproto-labs/simple-store-memory@0.1.3": { 220 226 "integrity": "sha512-jkitT9+AtU+0b28DoN92iURLaCt/q/q4yX8q6V+9LSwYlUTqKoj/5NFKvF7x6EBuG+gpUdlcycbH7e60gjOhRQ==", 221 227 "dependencies": [ 222 - "@atproto-labs/simple-store", 228 + "@atproto-labs/simple-store@0.2.0", 223 229 "lru-cache" 224 230 ] 231 + }, 232 + "@atproto-labs/simple-store@0.1.2": { 233 + "integrity": "sha512-9vTNvyPPBs44tKVFht16wGlilW8u4wpEtKwLkWbuNEh3h9TTQ8zjVhEoGZh/v73G4Otr9JUOSIq+/5+8OZD2mQ==" 225 234 }, 226 235 "@atproto-labs/simple-store@0.2.0": { 227 236 "integrity": "sha512-0bRbAlI8Ayh03wRwncAMEAyUKtZ+AuTS1jgPrfym1WVOAOiottI/ZmgccqLl6w5MbxVcClNQF7WYGKvGwGoIhA==" ··· 300 309 "@atproto-labs/fetch", 301 310 "@atproto-labs/handle-resolver", 302 311 "@atproto-labs/identity-resolver", 303 - "@atproto-labs/simple-store", 312 + "@atproto-labs/simple-store@0.2.0", 304 313 "@atproto-labs/simple-store-memory", 305 314 "@atproto/did", 306 315 "@atproto/jwk@0.1.5", ··· 482 491 "@jridgewell/sourcemap-codec" 483 492 ] 484 493 }, 494 + "@lucide/lab@0.1.2": { 495 + "integrity": "sha512-VprF2BJa7ZuTGOhUd5cf8tHJXyL63wdxcGieAiVVoR9hO0YmPsnZO0AGqDiX2/br+/MC6n8BoJcmPilltOXIJA==" 496 + }, 485 497 "@noble/curves@1.9.1": { 486 498 "integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==", 487 499 "dependencies": [ ··· 534 546 "@trysound/sax@0.2.0": { 535 547 "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==" 536 548 }, 549 + "@types/node@22.15.15": { 550 + "integrity": "sha512-R5muMcZob3/Jjchn5LcO8jdKwSCbzqmPB6ruBxMcf9kbxtniZHP327s6C37iOfuw8mbKK3cAQa7sEl7afLrQ8A==", 551 + "dependencies": [ 552 + "undici-types" 553 + ] 554 + }, 537 555 "ansi-regex@5.0.1": { 538 556 "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" 539 557 }, ··· 996 1014 "lru-cache@10.4.3": { 997 1015 "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" 998 1016 }, 1017 + "lucide-preact@0.511.0_preact@10.26.6": { 1018 + "integrity": "sha512-7MhxCepYkNOfXZTWahbDVODh/BkhLUeCTY5mh6WmIvWcCWssul7TeIM/SkNARifRWZ9KUwYcl9oeV6VTIlqJog==", 1019 + "dependencies": [ 1020 + "preact" 1021 + ] 1022 + }, 999 1023 "mdn-data@2.0.28": { 1000 1024 "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==" 1001 1025 }, ··· 1349 1373 "source-map-js" 1350 1374 ] 1351 1375 }, 1376 + "preact-feather@4.2.1_preact@10.26.6": { 1377 + "integrity": "sha512-yK5kYW64AoOkm+xTtUjwcFx0zNrqVTbwmtww8G2AmAB6f8wyQgwZgc6oRXllSYeg7q1I8VbkUpErJuKJ6Vq2eA==", 1378 + "dependencies": [ 1379 + "preact" 1380 + ] 1381 + }, 1352 1382 "preact-render-to-string@6.5.13_preact@10.26.6": { 1353 1383 "integrity": "sha512-iGPd+hKPMFKsfpR2vL4kJ6ZPcFIoWZEcBf0Dpm3zOpdVvj77aY8RlLiQji5OMrngEyaxGogeakTb54uS2FvA6w==", 1354 1384 "dependencies": [ ··· 1547 1577 }, 1548 1578 "uncrypto@0.1.3": { 1549 1579 "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==" 1580 + }, 1581 + "undici-types@6.21.0": { 1582 + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==" 1550 1583 }, 1551 1584 "undici@6.21.2": { 1552 1585 "integrity": "sha512-uROZWze0R0itiAKVPsYhFov9LxrPMHLMEQFszeI2gCN6bnIIZ8twzBCJcN2LJrBBLfrP0t1FW0g+JmKVl8Vk1g=="
+37 -60
islands/Header.tsx
··· 1 1 import { useEffect, useState } from "preact/hooks"; 2 2 import { IS_BROWSER } from "fresh/runtime"; 3 + import { Button } from "../components/Button.tsx"; 3 4 4 5 interface User { 5 6 did: string; ··· 18 19 19 20 export default function Header() { 20 21 const [user, setUser] = useState<User | null>(null); 22 + const [showDropdown, setShowDropdown] = useState(false); 21 23 22 24 useEffect(() => { 23 25 if (!IS_BROWSER) return; ··· 71 73 <div className="max-w-7xl mx-auto px-4"> 72 74 <div className="flex items-center justify-between py-4"> 73 75 {/* Home Link */} 74 - <a 76 + <Button 75 77 href="/" 76 - className="airport-sign bg-gradient-to-r from-blue-500 to-blue-600 text-white flex items-center px-3 sm:px-6 py-2 sm:py-3 transform translate-y-0 transition-transform duration-200 ease-in-out hover:translate-y-1 hover:from-blue-600 hover:to-blue-700" 77 - > 78 - <img 79 - src="/icons/plane_bold.svg" 80 - alt="Plane" 81 - className="w-6 h-6 mr-2" 82 - style={{ filter: "brightness(0) invert(1)" }} 83 - /> 84 - <span className="font-mono font-bold tracking-wider">AIRPORT</span> 85 - </a> 78 + color="blue" 79 + icon="/icons/plane_bold.svg" 80 + iconAlt="Plane" 81 + label="AIRPORT" 82 + /> 86 83 87 84 <div className="flex items-center gap-3"> 88 85 {/* Departures (Migration) */} 89 - <div className="relative group"> 90 - <a 91 - href="/migrate" 92 - className="airport-sign bg-gradient-to-r from-amber-400 to-amber-500 text-slate-900 flex items-center px-3 sm:px-6 py-2 sm:py-3 transform translate-y-0 transition-transform duration-200 ease-in-out hover:translate-y-1 hover:from-amber-500 hover:to-amber-600" 93 - > 94 - <img 95 - src="/icons/plane-departure_bold.svg" 96 - alt="Departures" 97 - className="w-6 h-6 mr-2" 98 - style={{ filter: "brightness(0)" }} 99 - /> 100 - <span className="font-mono font-bold tracking-wider"> 101 - DEPARTURES 102 - </span> 103 - </a> 104 - </div> 86 + <Button 87 + href="/migrate" 88 + color="amber" 89 + icon="/icons/plane-departure_bold.svg" 90 + iconAlt="Departures" 91 + label="DEPARTURES" 92 + /> 105 93 106 94 {/* Check-in (Login/Profile) */} 107 95 <div className="relative"> 108 - {user?.did 109 - ? ( 110 - <div className="relative group"> 111 - <div className="airport-sign bg-gradient-to-r from-amber-400 to-amber-500 text-slate-900 flex items-center px-3 sm:px-6 py-2 sm:py-3 transform translate-y-0 transition-transform duration-200 ease-in-out hover:translate-y-1 hover:from-amber-500 hover:to-amber-600 cursor-pointer"> 112 - <img 113 - src="/icons/ticket_bold.svg" 114 - alt="Check-in" 115 - className="w-6 h-6 mr-2" 116 - style={{ filter: "brightness(0)" }} 117 - /> 118 - <span className="font-mono font-bold tracking-wider"> 119 - CHECKED IN 120 - </span> 121 - </div> 122 - <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"> 96 + {user?.did ? ( 97 + <div className="relative"> 98 + <Button 99 + color="amber" 100 + icon="/icons/ticket_bold.svg" 101 + iconAlt="Check-in" 102 + label="CHECKED IN" 103 + onClick={() => setShowDropdown(!showDropdown)} 104 + /> 105 + {showDropdown && ( 106 + <div className="absolute right-0 mt-2 w-64 bg-white dark:bg-slate-800 rounded-lg shadow-lg p-4 border border-slate-200 dark:border-slate-700"> 123 107 <div className="text-sm font-mono mb-2 pb-2 border-b border-slate-900/10"> 124 108 <div title={user.handle || "Anonymous"}> 125 109 {truncateText(user.handle || "Anonymous", 20)} ··· 136 120 Sign Out 137 121 </button> 138 122 </div> 139 - </div> 140 - ) 141 - : ( 142 - <a 143 - href="/login" 144 - className="airport-sign bg-gradient-to-r from-amber-400 to-amber-500 text-slate-900 flex items-center px-3 sm:px-6 py-2 sm:py-3 transform translate-y-0 transition-transform duration-200 ease-in-out hover:translate-y-1 hover:from-amber-500 hover:to-amber-600" 145 - > 146 - <img 147 - src="/icons/ticket_bold.svg" 148 - alt="Check-in" 149 - className="w-6 h-6 mr-2" 150 - style={{ filter: "brightness(0)" }} 151 - /> 152 - <span className="font-mono font-bold tracking-wider"> 153 - CHECK-IN 154 - </span> 155 - </a> 156 - )} 123 + )} 124 + </div> 125 + ) : ( 126 + <Button 127 + href="/login" 128 + color="amber" 129 + icon="/icons/ticket_bold.svg" 130 + iconAlt="Check-in" 131 + label="CHECK-IN" 132 + /> 133 + )} 157 134 </div> 158 135 </div> 159 136 </div>
+74
islands/SocialLinks.tsx
··· 1 + import { useEffect, useState } from "preact/hooks"; 2 + import * as Icon from 'npm:preact-feather'; 3 + 4 + interface GitHubRepo { 5 + stargazers_count: number; 6 + } 7 + 8 + export default function SocialLinks() { 9 + const [starCount, setStarCount] = useState<number | null>(null); 10 + 11 + useEffect(() => { 12 + const fetchRepoInfo = async () => { 13 + try { 14 + const response = await fetch("https://api.github.com/repos/knotbin/airport"); 15 + const data: GitHubRepo = await response.json(); 16 + setStarCount(data.stargazers_count); 17 + } catch (error) { 18 + console.error("Failed to fetch GitHub repo info:", error); 19 + } 20 + }; 21 + 22 + fetchRepoInfo(); 23 + }, []); 24 + 25 + const formatStarCount = (count: number | null) => { 26 + if (count === null) return "..."; 27 + if (count >= 1000) { 28 + return `${(count / 1000).toFixed(1)}k`; 29 + } 30 + return count.toString(); 31 + }; 32 + 33 + return ( 34 + <div class="mt-8 flex justify-center items-center gap-6"> 35 + <a 36 + href="https://bsky.app/profile/knotbin.com" 37 + class="text-gray-600 hover:text-blue-500 dark:text-gray-400 dark:hover:text-blue-400 transition-colors" 38 + target="_blank" 39 + rel="noopener noreferrer" 40 + > 41 + <svg 42 + class="w-6 h-6" 43 + viewBox="-20 -20 296 266" 44 + fill="none" 45 + stroke="currentColor" 46 + stroke-width="25" 47 + stroke-linejoin="round" 48 + xmlns="http://www.w3.org/2000/svg" 49 + > 50 + <path 51 + d="M55.491 15.172c29.35 22.035 60.917 66.712 72.509 90.686 11.592-23.974 43.159-68.651 72.509-90.686C221.686-.727 256-13.028 256 26.116c0 7.818-4.482 65.674-7.111 75.068-9.138 32.654-42.436 40.983-72.057 35.942 51.775 8.812 64.946 38 36.501 67.187-54.021 55.433-77.644-13.908-83.696-31.676-1.11-3.257-1.63-4.78-1.637-3.485-.008-1.296-.527.228-1.637 3.485-6.052 17.768-29.675 87.11-83.696 31.676-28.445-29.187-15.274-58.375 36.5-67.187-29.62 5.041-62.918-3.288-72.056-35.942C4.482 91.79 0 33.934 0 26.116 0-13.028 34.314-.727 55.491 15.172Z" 52 + /> 53 + </svg> 54 + </a> 55 + <a 56 + href="https://ko-fi.com/knotbin" 57 + class="text-gray-600 hover:text-red-500 dark:text-gray-400 dark:hover:text-red-400 transition-colors" 58 + target="_blank" 59 + rel="noopener noreferrer" 60 + > 61 + <Icon.Coffee class="w-6 h-6" /> 62 + </a> 63 + <a 64 + href="https://github.com/knotbin/airport" 65 + class="text-gray-600 hover:text-purple-500 dark:text-gray-400 dark:hover:text-purple-400 transition-colors flex items-center gap-1" 66 + target="_blank" 67 + rel="noopener noreferrer" 68 + > 69 + <Icon.Github class="w-6 h-6" /> 70 + <span class="text-sm font-mono">{formatStarCount(starCount)}</span> 71 + </a> 72 + </div> 73 + ); 74 + }
+2 -2
islands/Ticket.tsx
··· 40 40 41 41 return ( 42 42 <div class="max-w-4xl mx-auto"> 43 - <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%)]"> 43 + <div class="ticket mb-8 bg-white dark:bg-slate-800 p-6 relative before:absolute 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%)]"> 44 44 <div class="boarding-label text-amber-500 dark:text-amber-400 font-mono font-bold tracking-wider text-sm mb-4"> 45 45 BOARDING PASS 46 46 </div> ··· 71 71 </p> 72 72 </div> 73 73 74 - <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%)]"> 74 + <div class="ticket mb-8 bg-white dark:bg-slate-800 p-6 relative before:absolute 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%)]"> 75 75 <div class="boarding-label text-amber-500 dark:text-amber-400 font-mono font-bold tracking-wider text-sm mb-4"> 76 76 FLIGHT DETAILS 77 77 </div>
+9 -6
routes/index.tsx
··· 1 1 import Ticket from "../islands/Ticket.tsx"; 2 2 import AirportSign from "../islands/AirportSign.tsx"; 3 + import SocialLinks from "../islands/SocialLinks.tsx"; 4 + import { Button } from "../components/Button.tsx"; 3 5 4 6 export default function Home() { 5 7 return ( ··· 15 17 16 18 <Ticket /> 17 19 18 - <div class="mt-6 sm:mt-8 text-center"> 19 - <a 20 + <div class="mt-6 sm:mt-8 text-center w-fit mx-auto"> 21 + <Button 20 22 href="/login" 21 - class="inline-flex items-center px-4 sm:px-6 py-2 sm:py-3 border border-transparent text-base sm: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" 22 - > 23 - Begin Your Journey 24 - </a> 23 + color="blue" 24 + label="BEGIN YOUR JOURNEY" 25 + /> 25 26 </div> 27 + 28 + <SocialLinks /> 26 29 </div> 27 30 </div> 28 31 </div>
+1 -1
static/styles.css
··· 178 178 .airport-sign { 179 179 position: relative; 180 180 transform-origin: top; 181 - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); 181 + transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); 182 182 border-radius: 0.5rem; 183 183 backdrop-filter: blur(8px); 184 184 }