Graphical PDS migrator for AT Protocol
15
fork

Configure Feed

Select the types of activity you want to include in your feed.

deno fmt

+1158 -710
-2
.github/workflows/deploy.yml
··· 32 32 project: "roscoerubin-airport-67" 33 33 entrypoint: "main.ts" 34 34 root: "." 35 - 36 -
+11 -4
README.md
··· 1 1 # Airport 2 2 3 - Your terminal for seamless AT Protocol PDS (Personal Data Server) migration and backup. 3 + Your terminal for seamless AT Protocol PDS (Personal Data Server) migration and 4 + backup. 4 5 5 - Airport is a web application built with Fresh and Deno that helps users safely migrate and backup their Bluesky PDS data. It provides a user-friendly interface for managing your AT Protocol data. 6 + Airport is a web application built with Fresh and Deno that helps users safely 7 + migrate and backup their Bluesky PDS data. It provides a user-friendly interface 8 + for managing your AT Protocol data. 6 9 7 10 ## Features 8 11 ··· 30 33 31 34 ## About 32 35 33 - Airport is developed with ❤️ by [Roscoe](https://bsky.app/profile/knotbin.com) for [Spark](https://sprk.so), a new short-video platform for AT Protocol. 36 + Airport is developed with ❤️ by [Roscoe](https://bsky.app/profile/knotbin.com) 37 + for [Spark](https://sprk.so), a new short-video platform for AT Protocol. 34 38 35 39 ## Contributing 36 40 37 - We welcome contributions! Please feel free to submit a Pull Request. Please only submit pull requests that are relevant to the project. This project targets people with a non-advanced understanding of AT Protocol, so please avoid submitting pull requests that add features that complicate the user experience. 41 + We welcome contributions! Please feel free to submit a Pull Request. Please only 42 + submit pull requests that are relevant to the project. This project targets 43 + people with a non-advanced understanding of AT Protocol, so please avoid 44 + submitting pull requests that add features that complicate the user experience. 38 45 39 46 ## License 40 47
+33 -11
components/Button.tsx
··· 9 9 condensed?: boolean; 10 10 }; 11 11 12 - type ButtonProps = ButtonBaseProps & Omit<JSX.HTMLAttributes<HTMLButtonElement>, keyof ButtonBaseProps>; 13 - type AnchorProps = ButtonBaseProps & Omit<JSX.HTMLAttributes<HTMLAnchorElement>, keyof ButtonBaseProps> & { href: string }; 12 + type ButtonProps = 13 + & ButtonBaseProps 14 + & Omit<JSX.HTMLAttributes<HTMLButtonElement>, keyof ButtonBaseProps>; 15 + type AnchorProps = 16 + & ButtonBaseProps 17 + & Omit<JSX.HTMLAttributes<HTMLAnchorElement>, keyof ButtonBaseProps> 18 + & { href: string }; 14 19 15 20 /** 16 21 * The button props or anchor props for a button or link. ··· 25 30 * @component 26 31 */ 27 32 export function Button(props: Props) { 28 - const { color = "blue", icon, iconAlt, label, className = "", condensed = false, ...rest } = props; 29 - const isAnchor = 'href' in props; 33 + const { 34 + color = "blue", 35 + icon, 36 + iconAlt, 37 + label, 38 + className = "", 39 + condensed = false, 40 + ...rest 41 + } = props; 42 + const isAnchor = "href" in props; 30 43 31 44 const baseStyles = "airport-sign flex items-center [transition:none]"; 32 - const paddingStyles = condensed ? 'px-2 py-1.5' : 'px-3 py-2 sm:px-6 sm:py-3'; 33 - const transformStyles = "translate-y-0 hover:translate-y-1 hover:transition-transform hover:duration-200 hover:ease-in-out"; 45 + const paddingStyles = condensed ? "px-2 py-1.5" : "px-3 py-2 sm:px-6 sm:py-3"; 46 + const transformStyles = 47 + "translate-y-0 hover:translate-y-1 hover:transition-transform hover:duration-200 hover:ease-in-out"; 34 48 const colorStyles = { 35 - blue: "bg-gradient-to-r from-blue-400 to-blue-500 text-white hover:from-blue-500 hover:to-blue-600", 36 - amber: "bg-gradient-to-r from-amber-400 to-amber-500 text-slate-900 hover:from-amber-500 hover:to-amber-600", 49 + blue: 50 + "bg-gradient-to-r from-blue-400 to-blue-500 text-white hover:from-blue-500 hover:to-blue-600", 51 + amber: 52 + "bg-gradient-to-r from-amber-400 to-amber-500 text-slate-900 hover:from-amber-500 hover:to-amber-600", 37 53 }; 38 54 39 55 const buttonContent = ( ··· 42 58 <img 43 59 src={icon} 44 60 alt={iconAlt || ""} 45 - className={`${condensed ? 'w-4 h-4' : 'w-6 h-6'} mr-2`} 46 - style={{ filter: color === 'blue' ? "brightness(0) invert(1)" : "brightness(0)" }} 61 + className={`${condensed ? "w-4 h-4" : "w-6 h-6"} mr-2`} 62 + style={{ 63 + filter: color === "blue" 64 + ? "brightness(0) invert(1)" 65 + : "brightness(0)", 66 + }} 47 67 /> 48 68 )} 49 69 {label && ( ··· 54 74 </> 55 75 ); 56 76 57 - const buttonStyles = `${baseStyles} ${paddingStyles} ${transformStyles} ${colorStyles[color]} ${className}`; 77 + const buttonStyles = `${baseStyles} ${paddingStyles} ${transformStyles} ${ 78 + colorStyles[color] 79 + } ${className}`; 58 80 59 81 if (isAnchor) { 60 82 return (
+3 -3
deno.json
··· 2 2 "tasks": { 3 3 "check": "deno fmt --check . && deno lint . && deno check **/*.ts && deno check **/*.tsx", 4 4 "dev": "deno run -A --env --watch=static/,routes/ dev.ts", 5 - "build": "deno run -A --unstable-otel dev.ts build", 6 - "start": "deno run -A --unstable-otel main.ts", 5 + "build": "deno run -A dev.ts build", 6 + "start": "deno run -A main.ts", 7 7 "update": "deno run -A -r jsr:@fresh/update ." 8 8 }, 9 9 "lint": { ··· 55 55 "kv", 56 56 "otel" 57 57 ] 58 - } 58 + }
+146 -146
islands/DidPlcProgress.tsx
··· 1 - import { useState, useEffect } from "preact/hooks"; 1 + import { useEffect, useState } from "preact/hooks"; 2 2 import { Link } from "../components/Link.tsx"; 3 3 4 4 interface PlcUpdateStep { ··· 97 97 account and identity 98 98 </li> 99 99 <li> 100 - Store securely, like a password (e.g. <strong>DO NOT</strong>{" "} 100 + Store securely, like a password (e.g. <strong>DO NOT</strong> 101 + {" "} 101 102 keep it in Notes or any easily accessible app on an unlocked 102 103 device). 103 104 </li> ··· 107 108 <div class="p-3 bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700"> 108 109 <p class="text-slate-700 dark:text-slate-300"> 109 110 💡 We recommend adding a custom rotation key but recommend{" "} 110 - <strong class="italic">against</strong> having more than one custom 111 - rotation key, as more than one increases risk. 111 + <strong class="italic">against</strong>{" "} 112 + having more than one custom rotation key, as more than one increases 113 + risk. 112 114 </p> 113 115 </div> 114 116 </> ··· 167 169 const updateStepStatus = ( 168 170 index: number, 169 171 status: PlcUpdateStep["status"], 170 - error?: string 172 + error?: string, 171 173 ) => { 172 174 console.log( 173 175 `Updating step ${index} to ${status}${ 174 176 error ? ` with error: ${error}` : "" 175 - }` 177 + }`, 176 178 ); 177 179 setSteps((prevSteps) => 178 180 prevSteps.map((step, i) => ··· 213 215 return "Requesting PLC Operation Token..."; 214 216 case 2: 215 217 return step.name === 216 - "Enter the code sent to your email to complete PLC update" 218 + "Enter the code sent to your email to complete PLC update" 217 219 ? step.name 218 220 : "Completing PLC Update..."; 219 221 } ··· 264 266 updateStepStatus( 265 267 1, 266 268 "error", 267 - "Please ensure you have the correct key loaded" 269 + "Please ensure you have the correct key loaded", 268 270 ); 269 271 return; 270 272 } ··· 304 306 prevSteps.map((step, i) => 305 307 i === 1 306 308 ? { 307 - ...step, 308 - name: "Enter the code sent to your email to complete PLC update", 309 - status: "in-progress", 310 - } 309 + ...step, 310 + name: "Enter the code sent to your email to complete PLC update", 311 + status: "in-progress", 312 + } 311 313 : step 312 314 ) 313 315 ); ··· 316 318 updateStepStatus( 317 319 1, 318 320 "error", 319 - error instanceof Error ? error.message : String(error) 321 + error instanceof Error ? error.message : String(error), 320 322 ); 321 323 } 322 324 }; ··· 404 406 } 405 407 406 408 if (!verifyRes.ok || !verifyData.success) { 407 - const errorMessage = 408 - verifyData.message || "Failed to verify PLC update"; 409 + const errorMessage = verifyData.message || 410 + "Failed to verify PLC update"; 409 411 console.error("Verification failed:", errorMessage); 410 412 throw new Error(errorMessage); 411 413 } ··· 418 420 updateStepStatus( 419 421 1, 420 422 "error", 421 - error instanceof Error ? error.message : String(error) 423 + error instanceof Error ? error.message : String(error), 422 424 ); 423 425 updateStepStatus(2, "pending"); // Reset the final step 424 426 setUpdateResult(error instanceof Error ? error.message : String(error)); ··· 527 529 updateStepStatus( 528 530 0, 529 531 "error", 530 - error instanceof Error ? error.message : String(error) 532 + error instanceof Error ? error.message : String(error), 531 533 ); 532 534 } 533 535 }; ··· 640 642 updateStepStatus( 641 643 1, 642 644 "error", 643 - error instanceof Error ? error.message : String(error) 645 + error instanceof Error ? error.message : String(error), 644 646 ); 645 647 } 646 648 }; ··· 667 669 <div class="flex justify-between items-center"> 668 670 <button 669 671 onClick={() => 670 - setCurrentChunkIndex((prev) => Math.max(0, prev - 1)) 671 - } 672 + setCurrentChunkIndex((prev) => Math.max(0, prev - 1))} 672 673 class={`px-4 py-2 font-mono text-slate-600 dark:text-slate-400 hover:text-slate-800 dark:hover:text-slate-200 transition-colors duration-200 flex items-center space-x-2 ${ 673 674 currentChunkIndex === 0 ? "invisible" : "" 674 675 }`} ··· 689 690 <span>Previous Gate</span> 690 691 </button> 691 692 692 - {currentChunkIndex === contentChunks.length - 1 ? ( 693 - <button 694 - onClick={handleStart} 695 - class="px-6 py-2 bg-amber-500 hover:bg-amber-600 text-white font-mono rounded-md transition-colors duration-200 flex items-center space-x-2" 696 - > 697 - <span>Begin Key Generation</span> 698 - <svg 699 - class="w-5 h-5" 700 - fill="none" 701 - stroke="currentColor" 702 - viewBox="0 0 24 24" 693 + {currentChunkIndex === contentChunks.length - 1 694 + ? ( 695 + <button 696 + onClick={handleStart} 697 + class="px-6 py-2 bg-amber-500 hover:bg-amber-600 text-white font-mono rounded-md transition-colors duration-200 flex items-center space-x-2" 703 698 > 704 - <path 705 - stroke-linecap="round" 706 - stroke-linejoin="round" 707 - stroke-width="2" 708 - d="M9 5l7 7-7 7" 709 - /> 710 - </svg> 711 - </button> 712 - ) : ( 713 - <button 714 - onClick={() => 715 - setCurrentChunkIndex((prev) => 716 - Math.min(contentChunks.length - 1, prev + 1) 717 - ) 718 - } 719 - class="px-4 py-2 font-mono text-slate-600 dark:text-slate-400 hover:text-slate-800 dark:hover:text-slate-200 transition-colors duration-200 flex items-center space-x-2" 720 - > 721 - <span>Next Gate</span> 722 - <svg 723 - class="w-5 h-5" 724 - fill="none" 725 - stroke="currentColor" 726 - viewBox="0 0 24 24" 699 + <span>Begin Key Generation</span> 700 + <svg 701 + class="w-5 h-5" 702 + fill="none" 703 + stroke="currentColor" 704 + viewBox="0 0 24 24" 705 + > 706 + <path 707 + stroke-linecap="round" 708 + stroke-linejoin="round" 709 + stroke-width="2" 710 + d="M9 5l7 7-7 7" 711 + /> 712 + </svg> 713 + </button> 714 + ) 715 + : ( 716 + <button 717 + onClick={() => 718 + setCurrentChunkIndex((prev) => 719 + Math.min(contentChunks.length - 1, prev + 1) 720 + )} 721 + class="px-4 py-2 font-mono text-slate-600 dark:text-slate-400 hover:text-slate-800 dark:hover:text-slate-200 transition-colors duration-200 flex items-center space-x-2" 727 722 > 728 - <path 729 - stroke-linecap="round" 730 - stroke-linejoin="round" 731 - stroke-width="2" 732 - d="M9 5l7 7-7 7" 733 - /> 734 - </svg> 735 - </button> 736 - )} 723 + <span>Next Gate</span> 724 + <svg 725 + class="w-5 h-5" 726 + fill="none" 727 + stroke="currentColor" 728 + viewBox="0 0 24 24" 729 + > 730 + <path 731 + stroke-linecap="round" 732 + stroke-linejoin="round" 733 + stroke-width="2" 734 + d="M9 5l7 7-7 7" 735 + /> 736 + </svg> 737 + </button> 738 + )} 737 739 </div> 738 740 739 741 {/* Progress Dots */} ··· 853 855 {index === 0 && 854 856 step.status === "completed" && 855 857 !hasDownloadedKey && ( 856 - <div class="mt-4 space-y-4"> 857 - <div class="bg-yellow-50 dark:bg-yellow-900/50 p-4 rounded-lg border border-yellow-200 dark:border-yellow-800"> 858 - <div class="flex items-start"> 859 - <div class="flex-shrink-0"> 860 - <svg 861 - class="h-5 w-5 text-yellow-400" 862 - viewBox="0 0 20 20" 863 - fill="currentColor" 864 - > 865 - <path 866 - fill-rule="evenodd" 867 - d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z" 868 - clip-rule="evenodd" 869 - /> 870 - </svg> 871 - </div> 872 - <div class="ml-3"> 873 - <h3 class="text-sm font-medium text-yellow-800 dark:text-yellow-200"> 874 - Critical Security Step 875 - </h3> 876 - <div class="mt-2 text-sm text-yellow-700 dark:text-yellow-300"> 877 - <p class="mb-2"> 878 - Your rotation key grants control over your 879 - identity: 880 - </p> 881 - <ul class="list-disc pl-5 space-y-2"> 882 - <li> 883 - <strong>Store Securely:</strong> Use a password 884 - manager 885 - </li> 886 - <li> 887 - <strong>Keep Private:</strong> Never share with 888 - anyone 889 - </li> 890 - <li> 891 - <strong>Backup:</strong> Keep a secure backup 892 - copy 893 - </li> 894 - <li> 895 - <strong>Required:</strong> Needed for future DID 896 - modifications 897 - </li> 898 - </ul> 899 - </div> 900 - </div> 901 - </div> 902 - </div> 903 - 904 - <div class="flex items-center justify-between"> 905 - <button 906 - onClick={handleDownload} 907 - class="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-md transition-colors duration-200 flex items-center space-x-2" 908 - > 858 + <div class="mt-4 space-y-4"> 859 + <div class="bg-yellow-50 dark:bg-yellow-900/50 p-4 rounded-lg border border-yellow-200 dark:border-yellow-800"> 860 + <div class="flex items-start"> 861 + <div class="flex-shrink-0"> 909 862 <svg 910 - class="w-5 h-5" 911 - fill="none" 912 - stroke="currentColor" 913 - viewBox="0 0 24 24" 914 - > 915 - <path 916 - stroke-linecap="round" 917 - stroke-linejoin="round" 918 - stroke-width="2" 919 - d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" 920 - /> 921 - </svg> 922 - <span>Download Key</span> 923 - </button> 924 - 925 - <div class="flex items-center text-sm text-red-600 dark:text-red-400"> 926 - <svg 927 - class="w-4 h-4 mr-1" 928 - fill="none" 929 - stroke="currentColor" 930 - viewBox="0 0 24 24" 863 + class="h-5 w-5 text-yellow-400" 864 + viewBox="0 0 20 20" 865 + fill="currentColor" 931 866 > 932 867 <path 933 - stroke-linecap="round" 934 - stroke-linejoin="round" 935 - stroke-width="2" 936 - d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" 868 + fill-rule="evenodd" 869 + d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z" 870 + clip-rule="evenodd" 937 871 /> 938 872 </svg> 939 - Download required to proceed 873 + </div> 874 + <div class="ml-3"> 875 + <h3 class="text-sm font-medium text-yellow-800 dark:text-yellow-200"> 876 + Critical Security Step 877 + </h3> 878 + <div class="mt-2 text-sm text-yellow-700 dark:text-yellow-300"> 879 + <p class="mb-2"> 880 + Your rotation key grants control over your identity: 881 + </p> 882 + <ul class="list-disc pl-5 space-y-2"> 883 + <li> 884 + <strong>Store Securely:</strong>{" "} 885 + Use a password manager 886 + </li> 887 + <li> 888 + <strong>Keep Private:</strong>{" "} 889 + Never share with anyone 890 + </li> 891 + <li> 892 + <strong>Backup:</strong> Keep a secure backup copy 893 + </li> 894 + <li> 895 + <strong>Required:</strong>{" "} 896 + Needed for future DID modifications 897 + </li> 898 + </ul> 899 + </div> 940 900 </div> 941 901 </div> 942 902 </div> 943 - )} 903 + 904 + <div class="flex items-center justify-between"> 905 + <button 906 + onClick={handleDownload} 907 + class="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-md transition-colors duration-200 flex items-center space-x-2" 908 + > 909 + <svg 910 + class="w-5 h-5" 911 + fill="none" 912 + stroke="currentColor" 913 + viewBox="0 0 24 24" 914 + > 915 + <path 916 + stroke-linecap="round" 917 + stroke-linejoin="round" 918 + stroke-width="2" 919 + d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" 920 + /> 921 + </svg> 922 + <span>Download Key</span> 923 + </button> 924 + 925 + <div class="flex items-center text-sm text-red-600 dark:text-red-400"> 926 + <svg 927 + class="w-4 h-4 mr-1" 928 + fill="none" 929 + stroke="currentColor" 930 + viewBox="0 0 24 24" 931 + > 932 + <path 933 + stroke-linecap="round" 934 + stroke-linejoin="round" 935 + stroke-width="2" 936 + d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" 937 + /> 938 + </svg> 939 + Download required to proceed 940 + </div> 941 + </div> 942 + </div> 943 + )} 944 944 945 945 {/* Email Code Input */} 946 946 {index === 1 && 947 947 (step.status === "in-progress" || 948 948 step.status === "verifying") && 949 949 step.name === 950 - "Enter the code sent to your email to complete PLC update" && ( 950 + "Enter the code sent to your email to complete PLC update" && 951 + ( 951 952 <div class="mt-4 space-y-4"> 952 953 <div class="bg-blue-50 dark:bg-blue-900/50 p-4 rounded-lg"> 953 954 <p class="text-sm text-blue-800 dark:text-blue-200 mb-3"> ··· 960 961 type="text" 961 962 value={emailToken} 962 963 onChange={(e) => 963 - setEmailToken(e.currentTarget.value) 964 - } 964 + setEmailToken(e.currentTarget.value)} 965 965 placeholder="Enter verification code" 966 966 class="w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:focus:border-blue-400 dark:focus:ring-blue-400" 967 967 />
+39 -37
islands/Header.tsx
··· 51 51 setUser( 52 52 userData 53 53 ? { 54 - did: userData.did, 55 - handle: userData.handle, 56 - } 57 - : null 54 + did: userData.did, 55 + handle: userData.handle, 56 + } 57 + : null, 58 58 ); 59 59 } catch (error) { 60 60 console.error("Failed to fetch user:", error); ··· 117 117 118 118 {/* Check-in (Login/Profile) */} 119 119 <div className="relative"> 120 - {user?.did ? ( 121 - <div className="relative"> 120 + {user?.did 121 + ? ( 122 + <div className="relative"> 123 + <Button 124 + color="amber" 125 + icon="/icons/account.svg" 126 + iconAlt="Check-in" 127 + label="CHECKED IN" 128 + onClick={() => setShowDropdown(!showDropdown)} 129 + /> 130 + {showDropdown && ( 131 + <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"> 132 + <div className="text-sm font-mono mb-2 pb-2 border-b border-slate-900/10"> 133 + <div title={user.handle || "Anonymous"}> 134 + {truncateText(user.handle || "Anonymous", 20)} 135 + </div> 136 + <div className="text-xs opacity-75" title={user.did}> 137 + {truncateText(user.did, 25)} 138 + </div> 139 + </div> 140 + <button 141 + type="button" 142 + onClick={handleLogout} 143 + className="text-sm font-mono text-slate-900 hover:text-slate-700 w-full text-left transition-colors" 144 + > 145 + Sign Out 146 + </button> 147 + </div> 148 + )} 149 + </div> 150 + ) 151 + : ( 122 152 <Button 153 + href="/login" 123 154 color="amber" 124 155 icon="/icons/account.svg" 125 156 iconAlt="Check-in" 126 - label="CHECKED IN" 127 - onClick={() => setShowDropdown(!showDropdown)} 157 + label="CHECK-IN" 128 158 /> 129 - {showDropdown && ( 130 - <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"> 131 - <div className="text-sm font-mono mb-2 pb-2 border-b border-slate-900/10"> 132 - <div title={user.handle || "Anonymous"}> 133 - {truncateText(user.handle || "Anonymous", 20)} 134 - </div> 135 - <div className="text-xs opacity-75" title={user.did}> 136 - {truncateText(user.did, 25)} 137 - </div> 138 - </div> 139 - <button 140 - type="button" 141 - onClick={handleLogout} 142 - className="text-sm font-mono text-slate-900 hover:text-slate-700 w-full text-left transition-colors" 143 - > 144 - Sign Out 145 - </button> 146 - </div> 147 - )} 148 - </div> 149 - ) : ( 150 - <Button 151 - href="/login" 152 - color="amber" 153 - icon="/icons/account.svg" 154 - iconAlt="Check-in" 155 - label="CHECK-IN" 156 - /> 157 - )} 159 + )} 158 160 </div> 159 161 </div> 160 162 </div>
+5 -3
islands/LoginButton.tsx
··· 13 13 checkMobile(); 14 14 15 15 // Listen for resize events 16 - globalThis.addEventListener('resize', checkMobile); 17 - return () => globalThis.removeEventListener('resize', checkMobile); 16 + globalThis.addEventListener("resize", checkMobile); 17 + return () => globalThis.removeEventListener("resize", checkMobile); 18 18 }, []); 19 19 20 20 return ( ··· 23 23 href={isMobile ? undefined : "/login"} 24 24 color="blue" 25 25 label={isMobile ? "MOBILE NOT SUPPORTED" : "GET STARTED"} 26 - className={isMobile ? "opacity-50 cursor-not-allowed" : "opacity-100 cursor-pointer"} 26 + className={isMobile 27 + ? "opacity-50 cursor-not-allowed" 28 + : "opacity-100 cursor-pointer"} 27 29 onClick={(e: MouseEvent) => { 28 30 if (isMobile) { 29 31 e.preventDefault();
+17 -15
islands/LoginSelector.tsx
··· 1 - import { useState } from "preact/hooks" 2 - import HandleInput from "./HandleInput.tsx" 3 - import CredLogin from "./CredLogin.tsx" 1 + import { useState } from "preact/hooks"; 2 + import HandleInput from "./HandleInput.tsx"; 3 + import CredLogin from "./CredLogin.tsx"; 4 4 5 5 /** 6 6 * The login method selector for OAuth or Credential. ··· 8 8 * @component 9 9 */ 10 10 export default function LoginMethodSelector() { 11 - const [loginMethod, setLoginMethod] = useState<'oauth' | 'password'>('password') 11 + const [loginMethod, setLoginMethod] = useState<"oauth" | "password">( 12 + "password", 13 + ); 12 14 13 15 return ( 14 16 <div className="flex flex-col gap-8"> ··· 18 20 <div className="flex gap-4 mb-6"> 19 21 <button 20 22 type="button" 21 - onClick={() => setLoginMethod('oauth')} 23 + onClick={() => setLoginMethod("oauth")} 22 24 className={`flex-1 px-4 py-2 rounded-md transition-colors ${ 23 - loginMethod === 'oauth' 24 - ? 'bg-blue-500 text-white' 25 - : 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300' 25 + loginMethod === "oauth" 26 + ? "bg-blue-500 text-white" 27 + : "bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300" 26 28 }`} 27 29 > 28 30 OAuth 29 31 </button> 30 32 <button 31 33 type="button" 32 - onClick={() => setLoginMethod('password')} 34 + onClick={() => setLoginMethod("password")} 33 35 className={`flex-1 px-4 py-2 rounded-md transition-colors ${ 34 - loginMethod === 'password' 35 - ? 'bg-blue-500 text-white' 36 - : 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300' 36 + loginMethod === "password" 37 + ? "bg-blue-500 text-white" 38 + : "bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300" 37 39 }`} 38 40 > 39 41 Credential 40 42 </button> 41 43 </div> 42 44 43 - {loginMethod === 'oauth' && ( 45 + {loginMethod === "oauth" && ( 44 46 <div className="mb-4 p-3 bg-amber-50 dark:bg-amber-900/30 text-amber-800 dark:text-amber-200 rounded-md text-sm"> 45 47 Note: OAuth login cannot be used for migrations. 46 48 </div> 47 49 )} 48 50 49 - {loginMethod === 'oauth' ? <HandleInput /> : <CredLogin />} 51 + {loginMethod === "oauth" ? <HandleInput /> : <CredLogin />} 50 52 51 53 <div className="mt-4 text-center"> 52 54 <a ··· 58 60 </div> 59 61 </div> 60 62 </div> 61 - ) 63 + ); 62 64 }
+285 -84
islands/MigrationSetup.tsx
··· 1 - import { useState, useEffect } from "preact/hooks"; 1 + import { useEffect, useState } from "preact/hooks"; 2 2 import { IS_BROWSER } from "fresh/runtime"; 3 3 4 4 /** ··· 64 64 const [showConfirmation, setShowConfirmation] = useState(false); 65 65 const [confirmationText, setConfirmationText] = useState(""); 66 66 const [passport, setPassport] = useState<UserPassport | null>(null); 67 - const [migrationState, setMigrationState] = useState<MigrationStateInfo | null>(null); 67 + const [migrationState, setMigrationState] = useState< 68 + MigrationStateInfo | null 69 + >(null); 68 70 69 71 const ensureServiceUrl = (url: string): string => { 70 72 if (!url) return url; ··· 100 102 const userData = await response.json(); 101 103 if (userData) { 102 104 // Get PDS URL from the current service 103 - const pdsResponse = await fetch(`/api/resolve-pds?did=${userData.did}`); 105 + const pdsResponse = await fetch( 106 + `/api/resolve-pds?did=${userData.did}`, 107 + ); 104 108 const pdsData = await pdsResponse.json(); 105 109 106 110 setPassport({ 107 111 did: userData.did, 108 112 handle: userData.handle, 109 113 pds: pdsData.pds || "Unknown", 110 - createdAt: new Date().toISOString() // TODO: Get actual creation date from API 114 + createdAt: new Date().toISOString(), // TODO: Get actual creation date from API 111 115 }); 112 116 } 113 117 } catch (error) { ··· 217 221 <div class="max-w-2xl mx-auto p-6 bg-gradient-to-b from-blue-50 to-white dark:from-gray-800 dark:to-gray-900 rounded-lg shadow-xl relative overflow-hidden"> 218 222 {/* Decorative airport elements */} 219 223 <div class="absolute top-0 left-0 w-full h-1 bg-blue-500"></div> 220 - <div class="absolute top-2 left-4 text-blue-500 text-sm font-mono">TERMINAL 1</div> 221 - <div class="absolute top-2 right-4 text-blue-500 text-sm font-mono">GATE M1</div> 224 + <div class="absolute top-2 left-4 text-blue-500 text-sm font-mono"> 225 + TERMINAL 1 226 + </div> 227 + <div class="absolute top-2 right-4 text-blue-500 text-sm font-mono"> 228 + GATE M1 229 + </div> 222 230 223 231 {/* Migration state alert */} 224 232 {migrationState && !migrationState.allowMigration && ( 225 - <div class={`mb-6 mt-4 p-4 rounded-lg border ${ 226 - migrationState.state === "maintenance" 227 - ? "bg-yellow-50 border-yellow-200 text-yellow-800 dark:bg-yellow-900/20 dark:border-yellow-800 dark:text-yellow-200" 228 - : "bg-red-50 border-red-200 text-red-800 dark:bg-red-900/20 dark:border-red-800 dark:text-red-200" 229 - }`}> 233 + <div 234 + class={`mb-6 mt-4 p-4 rounded-lg border ${ 235 + migrationState.state === "maintenance" 236 + ? "bg-yellow-50 border-yellow-200 text-yellow-800 dark:bg-yellow-900/20 dark:border-yellow-800 dark:text-yellow-200" 237 + : "bg-red-50 border-red-200 text-red-800 dark:bg-red-900/20 dark:border-red-800 dark:text-red-200" 238 + }`} 239 + > 230 240 <div class="flex items-center"> 231 - <div class={`mr-3 ${ 232 - migrationState.state === "maintenance" ? "text-yellow-600 dark:text-yellow-400" : "text-red-600 dark:text-red-400" 233 - }`}> 241 + <div 242 + class={`mr-3 ${ 243 + migrationState.state === "maintenance" 244 + ? "text-yellow-600 dark:text-yellow-400" 245 + : "text-red-600 dark:text-red-400" 246 + }`} 247 + > 234 248 {migrationState.state === "maintenance" ? "⚠️" : "🚫"} 235 249 </div> 236 250 <div> 237 251 <h3 class="font-semibold mb-1"> 238 - {migrationState.state === "maintenance" ? "Maintenance Mode" : "Service Unavailable"} 252 + {migrationState.state === "maintenance" 253 + ? "Maintenance Mode" 254 + : "Service Unavailable"} 239 255 </h3> 240 256 <p class="text-sm">{migrationState.message}</p> 241 257 </div> ··· 244 260 )} 245 261 246 262 <div class="text-center mb-8 relative"> 247 - <p class="text-gray-600 dark:text-gray-400 mt-4">Please complete your migration check-in</p> 248 - <div class="mt-2 text-sm text-gray-500 dark:text-gray-400 font-mono">FLIGHT: MIG-2024</div> 263 + <p class="text-gray-600 dark:text-gray-400 mt-4"> 264 + Please complete your migration check-in 265 + </p> 266 + <div class="mt-2 text-sm text-gray-500 dark:text-gray-400 font-mono"> 267 + FLIGHT: MIG-2024 268 + </div> 249 269 </div> 250 270 251 271 {/* Passport Section */} 252 272 {passport && ( 253 273 <div class="mb-8 bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 border border-gray-200 dark:border-gray-700"> 254 274 <div class="flex items-center justify-between mb-4"> 255 - <h3 class="text-lg font-semibold text-gray-900 dark:text-white">Current Passport</h3> 256 - <div class="text-xs text-gray-500 dark:text-gray-400 font-mono">ISSUED: {new Date().toLocaleDateString()}</div> 275 + <h3 class="text-lg font-semibold text-gray-900 dark:text-white"> 276 + Current Passport 277 + </h3> 278 + <div class="text-xs text-gray-500 dark:text-gray-400 font-mono"> 279 + ISSUED: {new Date().toLocaleDateString()} 280 + </div> 257 281 </div> 258 282 <div class="grid grid-cols-2 gap-4 text-sm"> 259 283 <div> 260 284 <div class="text-gray-500 dark:text-gray-400 mb-1">Handle</div> 261 - <div class="font-mono text-gray-900 dark:text-white">{passport.handle}</div> 285 + <div class="font-mono text-gray-900 dark:text-white"> 286 + {passport.handle} 287 + </div> 262 288 </div> 263 289 <div> 264 290 <div class="text-gray-500 dark:text-gray-400 mb-1">DID</div> 265 - <div class="font-mono text-gray-900 dark:text-white break-all">{passport.did}</div> 291 + <div class="font-mono text-gray-900 dark:text-white break-all"> 292 + {passport.did} 293 + </div> 266 294 </div> 267 295 <div> 268 - <div class="text-gray-500 dark:text-gray-400 mb-1">Citizen of PDS</div> 269 - <div class="font-mono text-gray-900 dark:text-white break-all">{passport.pds}</div> 296 + <div class="text-gray-500 dark:text-gray-400 mb-1"> 297 + Citizen of PDS 298 + </div> 299 + <div class="font-mono text-gray-900 dark:text-white break-all"> 300 + {passport.pds} 301 + </div> 270 302 </div> 271 303 <div> 272 - <div class="text-gray-500 dark:text-gray-400 mb-1">Account Age</div> 304 + <div class="text-gray-500 dark:text-gray-400 mb-1"> 305 + Account Age 306 + </div> 273 307 <div class="font-mono text-gray-900 dark:text-white"> 274 - {passport.createdAt ? new Date(passport.createdAt).toLocaleDateString() : "Unknown"} 308 + {passport.createdAt 309 + ? new Date(passport.createdAt).toLocaleDateString() 310 + : "Unknown"} 275 311 </div> 276 312 </div> 277 313 </div> ··· 282 318 {error && ( 283 319 <div class="bg-red-50 dark:bg-red-900 rounded-lg "> 284 320 <p class="text-red-800 dark:text-red-200 flex items-center"> 285 - <svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 286 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path> 321 + <svg 322 + class="w-5 h-5 mr-2" 323 + fill="none" 324 + stroke="currentColor" 325 + viewBox="0 0 24 24" 326 + > 327 + <path 328 + stroke-linecap="round" 329 + stroke-linejoin="round" 330 + stroke-width="2" 331 + d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" 332 + > 333 + </path> 287 334 </svg> 288 335 {error} 289 336 </p> ··· 294 341 <div> 295 342 <label class="block text-sm font-medium text-gray-700 dark:text-gray-300"> 296 343 Destination Server 297 - <span class="text-xs text-gray-500 ml-1">(Final Destination)</span> 344 + <span class="text-xs text-gray-500 ml-1"> 345 + (Final Destination) 346 + </span> 298 347 </label> 299 348 <div class="relative"> 300 349 <input ··· 307 356 class="mt-1 block w-full rounded-md bg-white dark:bg-gray-700 shadow-sm focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50 dark:text-white disabled:opacity-50 disabled:cursor-not-allowed pl-10" 308 357 /> 309 358 <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none"> 310 - <svg class="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 311 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9"></path> 359 + <svg 360 + class="h-5 w-5 text-gray-400" 361 + fill="none" 362 + stroke="currentColor" 363 + viewBox="0 0 24 24" 364 + > 365 + <path 366 + stroke-linecap="round" 367 + stroke-linejoin="round" 368 + stroke-width="2" 369 + d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" 370 + > 371 + </path> 312 372 </svg> 313 373 </div> 314 374 </div> 315 375 {isLoading && ( 316 376 <p class="mt-1 text-sm text-gray-500 dark:text-gray-400 flex items-center"> 317 - <svg class="animate-spin -ml-1 mr-2 h-4 w-4 text-blue-500" fill="none" viewBox="0 0 24 24"> 318 - <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> 319 - <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> 377 + <svg 378 + class="animate-spin -ml-1 mr-2 h-4 w-4 text-blue-500" 379 + fill="none" 380 + viewBox="0 0 24 24" 381 + > 382 + <circle 383 + class="opacity-25" 384 + cx="12" 385 + cy="12" 386 + r="10" 387 + stroke="currentColor" 388 + stroke-width="4" 389 + > 390 + </circle> 391 + <path 392 + class="opacity-75" 393 + fill="currentColor" 394 + d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" 395 + > 396 + </path> 320 397 </svg> 321 398 Verifying destination server... 322 399 </p> ··· 328 405 New Account Handle 329 406 <span class="text-xs text-gray-500 ml-1">(Passport ID)</span> 330 407 <div class="inline-block relative group ml-2"> 331 - <svg class="w-4 h-4 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 cursor-help" fill="currentColor" viewBox="0 0 20 20"> 332 - <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-3a1 1 0 00-.867.5 1 1 0 11-1.731-1A3 3 0 0113 8a3.001 3.001 0 01-2 2.83V11a1 1 0 11-2 0v-1a1 1 0 011-1 1 1 0 100-2zm0 8a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd" /> 408 + <svg 409 + class="w-4 h-4 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 cursor-help" 410 + fill="currentColor" 411 + viewBox="0 0 20 20" 412 + > 413 + <path 414 + fill-rule="evenodd" 415 + d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-3a1 1 0 00-.867.5 1 1 0 11-1.731-1A3 3 0 0113 8a3.001 3.001 0 01-2 2.83V11a1 1 0 11-2 0v-1a1 1 0 011-1 1 1 0 100-2zm0 8a1 1 0 100-2 1 1 0 000 2z" 416 + clip-rule="evenodd" 417 + /> 333 418 </svg> 334 419 <div class="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-3 py-2 bg-gray-900 text-white text-sm rounded-lg opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none whitespace-nowrap z-10"> 335 420 You can change your handle to a custom domain later 336 - <div class="absolute top-full left-1/2 transform -translate-x-1/2 border-4 border-transparent border-t-gray-900"></div> 421 + <div class="absolute top-full left-1/2 transform -translate-x-1/2 border-4 border-transparent border-t-gray-900"> 422 + </div> 337 423 </div> 338 424 </div> 339 425 </label> ··· 347 433 placeholder="username" 348 434 required 349 435 class="w-full rounded-md bg-white dark:bg-gray-700 shadow-sm focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50 dark:text-white pl-10 pr-32" 350 - style={{ fontFamily: 'inherit' }} 436 + style={{ fontFamily: "inherit" }} 351 437 /> 352 438 <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none"> 353 - <svg class="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 354 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path> 439 + <svg 440 + class="h-5 w-5 text-gray-400" 441 + fill="none" 442 + stroke="currentColor" 443 + viewBox="0 0 24 24" 444 + > 445 + <path 446 + stroke-linecap="round" 447 + stroke-linejoin="round" 448 + stroke-width="2" 449 + d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" 450 + > 451 + </path> 355 452 </svg> 356 453 </div> 357 454 {/* Suffix for domain ending */} 358 - {availableDomains.length > 0 ? ( 359 - availableDomains.length === 1 ? ( 455 + {availableDomains.length > 0 456 + ? ( 457 + availableDomains.length === 1 458 + ? ( 459 + <span class="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-400 select-none pointer-events-none font-mono text-base"> 460 + {availableDomains[0]} 461 + </span> 462 + ) 463 + : ( 464 + <span class="absolute inset-y-0 right-0 flex items-center pr-1"> 465 + <select 466 + value={selectedDomain} 467 + onChange={(e) => 468 + setSelectedDomain(e.currentTarget.value)} 469 + class="bg-transparent text-gray-400 font-mono text-base focus:outline-none focus:ring-0 border-0 pr-2" 470 + style={{ appearance: "none" }} 471 + > 472 + {availableDomains.map((domain) => ( 473 + <option key={domain} value={domain}> 474 + {domain} 475 + </option> 476 + ))} 477 + </select> 478 + </span> 479 + ) 480 + ) 481 + : ( 360 482 <span class="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-400 select-none pointer-events-none font-mono text-base"> 361 - {availableDomains[0]} 362 - </span> 363 - ) : ( 364 - <span class="absolute inset-y-0 right-0 flex items-center pr-1"> 365 - <select 366 - value={selectedDomain} 367 - onChange={(e) => setSelectedDomain(e.currentTarget.value)} 368 - class="bg-transparent text-gray-400 font-mono text-base focus:outline-none focus:ring-0 border-0 pr-2" 369 - style={{ appearance: 'none' }} 370 - > 371 - {availableDomains.map((domain) => ( 372 - <option key={domain} value={domain}>{domain}</option> 373 - ))} 374 - </select> 483 + .example.com 375 484 </span> 376 - ) 377 - ) : ( 378 - <span class="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-400 select-none pointer-events-none font-mono text-base"> 379 - .example.com 380 - </span> 381 - )} 485 + )} 382 486 </div> 383 487 </div> 384 488 </div> ··· 387 491 <div> 388 492 <label class="block text-sm font-medium text-gray-700 dark:text-gray-300"> 389 493 Email 390 - <span class="text-xs text-gray-500 ml-1">(Emergency Contact)</span> 494 + <span class="text-xs text-gray-500 ml-1"> 495 + (Emergency Contact) 496 + </span> 391 497 </label> 392 498 <div class="relative"> 393 499 <input ··· 398 504 class="mt-1 block w-full rounded-md bg-white dark:bg-gray-700 shadow-sm focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50 dark:text-white pl-10" 399 505 /> 400 506 <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none"> 401 - <svg class="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 402 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path> 507 + <svg 508 + class="h-5 w-5 text-gray-400" 509 + fill="none" 510 + stroke="currentColor" 511 + viewBox="0 0 24 24" 512 + > 513 + <path 514 + stroke-linecap="round" 515 + stroke-linejoin="round" 516 + stroke-width="2" 517 + d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" 518 + > 519 + </path> 403 520 </svg> 404 521 </div> 405 522 </div> ··· 408 525 <div> 409 526 <label class="block text-sm font-medium text-gray-700 dark:text-gray-300"> 410 527 New Account Password 411 - <span class="text-xs text-gray-500 ml-1">(Security Clearance)</span> 528 + <span class="text-xs text-gray-500 ml-1"> 529 + (Security Clearance) 530 + </span> 412 531 </label> 413 532 <div class="relative"> 414 533 <input ··· 419 538 class="mt-1 block w-full rounded-md bg-white dark:bg-gray-700 shadow-sm focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50 dark:text-white pl-10" 420 539 /> 421 540 <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none"> 422 - <svg class="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 423 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path> 541 + <svg 542 + class="h-5 w-5 text-gray-400" 543 + fill="none" 544 + stroke="currentColor" 545 + viewBox="0 0 24 24" 546 + > 547 + <path 548 + stroke-linecap="round" 549 + stroke-linejoin="round" 550 + stroke-width="2" 551 + d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" 552 + > 553 + </path> 424 554 </svg> 425 555 </div> 426 556 </div> ··· 441 571 class="mt-1 block w-full rounded-md bg-white dark:bg-gray-700 shadow-sm focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50 dark:text-white pl-10" 442 572 /> 443 573 <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none"> 444 - <svg class="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 445 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 5v2m0 4v2m0 4v2M5 5a2 2 0 00-2 2v3a2 2 0 110 4v3a2 2 0 002 2h14a2 2 0 002-2v-3a2 2 0 110-4V7a2 2 0 00-2-2H5z"></path> 574 + <svg 575 + class="h-5 w-5 text-gray-400" 576 + fill="none" 577 + stroke="currentColor" 578 + viewBox="0 0 24 24" 579 + > 580 + <path 581 + stroke-linecap="round" 582 + stroke-linejoin="round" 583 + stroke-width="2" 584 + d="M15 5v2m0 4v2m0 4v2M5 5a2 2 0 00-2 2v3a2 2 0 110 4v3a2 2 0 002 2h14a2 2 0 002-2v-3a2 2 0 110-4V7a2 2 0 00-2-2H5z" 585 + > 586 + </path> 446 587 </svg> 447 588 </div> 448 589 </div> ··· 452 593 453 594 <button 454 595 type="submit" 455 - disabled={isLoading || Boolean(migrationState && !migrationState.allowMigration)} 596 + disabled={isLoading || 597 + Boolean(migrationState && !migrationState.allowMigration)} 456 598 class="w-full flex justify-center items-center py-3 px-4 rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed" 457 599 > 458 - <svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 459 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path> 600 + <svg 601 + class="w-5 h-5 mr-2" 602 + fill="none" 603 + stroke="currentColor" 604 + viewBox="0 0 24 24" 605 + > 606 + <path 607 + stroke-linecap="round" 608 + stroke-linejoin="round" 609 + stroke-width="2" 610 + d="M5 13l4 4L19 7" 611 + > 612 + </path> 460 613 </svg> 461 614 Proceed to Check-in 462 615 </button> ··· 466 619 <div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50"> 467 620 <div 468 621 class="bg-white dark:bg-gray-800 rounded-xl p-8 max-w-md w-full shadow-2xl border-0 relative animate-popin" 469 - style={{ boxShadow: '0 8px 32px 0 rgba(255, 0, 0, 0.15), 0 1.5px 8px 0 rgba(0,0,0,0.10)' }} 622 + style={{ 623 + boxShadow: 624 + "0 8px 32px 0 rgba(255, 0, 0, 0.15), 0 1.5px 8px 0 rgba(0,0,0,0.10)", 625 + }} 470 626 > 471 627 <div class="absolute -top-8 left-1/2 -translate-x-1/2"> 472 628 <div class="bg-red-500 rounded-full p-3 shadow-lg animate-bounce-short"> 473 - <svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 474 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /> 629 + <svg 630 + class="w-8 h-8 text-white" 631 + fill="none" 632 + stroke="currentColor" 633 + viewBox="0 0 24 24" 634 + > 635 + <path 636 + stroke-linecap="round" 637 + stroke-linejoin="round" 638 + stroke-width="2" 639 + d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" 640 + /> 475 641 </svg> 476 642 </div> 477 643 </div> 478 644 <div class="text-center mb-4 mt-6"> 479 - <h3 class="text-2xl font-bold text-red-600 mb-2 tracking-wide">Final Boarding Call</h3> 645 + <h3 class="text-2xl font-bold text-red-600 mb-2 tracking-wide"> 646 + Final Boarding Call 647 + </h3> 480 648 <p class="text-gray-700 dark:text-gray-300 mb-2 text-base"> 481 - <span class="font-semibold text-red-500">Warning:</span> This migration is <strong>irreversible</strong> if coming from Bluesky servers.<br />Bluesky does not recommend it for main accounts. Migrate at your own risk. We reccomend backing up your data before proceeding. 649 + <span class="font-semibold text-red-500">Warning:</span>{" "} 650 + This migration is <strong>irreversible</strong>{" "} 651 + if coming from Bluesky servers.<br />Bluesky does not recommend 652 + it for main accounts. Migrate at your own risk. We reccomend 653 + backing up your data before proceeding. 482 654 </p> 483 655 <p class="text-gray-700 dark:text-gray-300 mb-4 text-base"> 484 - Please type <span class="font-mono font-bold text-blue-600">MIGRATE</span> below to confirm and proceed. 656 + Please type{" "} 657 + <span class="font-mono font-bold text-blue-600">MIGRATE</span> 658 + {" "} 659 + below to confirm and proceed. 485 660 </p> 486 661 </div> 487 662 <div class="relative"> ··· 500 675 class="px-4 py-2 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md flex items-center transition" 501 676 type="button" 502 677 > 503 - <svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 504 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path> 678 + <svg 679 + class="w-5 h-5 mr-2" 680 + fill="none" 681 + stroke="currentColor" 682 + viewBox="0 0 24 24" 683 + > 684 + <path 685 + stroke-linecap="round" 686 + stroke-linejoin="round" 687 + stroke-width="2" 688 + d="M6 18L18 6M6 6l12 12" 689 + > 690 + </path> 505 691 </svg> 506 692 Cancel 507 693 </button> 508 694 <button 509 695 onClick={handleConfirmation} 510 - class={`px-4 py-2 rounded-md flex items-center transition font-semibold ${confirmationText.trim().toLowerCase() === 'migrate' ? 'bg-red-600 text-white hover:bg-red-700 cursor-pointer' : 'bg-red-300 text-white cursor-not-allowed'}`} 696 + class={`px-4 py-2 rounded-md flex items-center transition font-semibold ${ 697 + confirmationText.trim().toLowerCase() === "migrate" 698 + ? "bg-red-600 text-white hover:bg-red-700 cursor-pointer" 699 + : "bg-red-300 text-white cursor-not-allowed" 700 + }`} 511 701 type="button" 512 - disabled={confirmationText.trim().toLowerCase() !== 'migrate'} 702 + disabled={confirmationText.trim().toLowerCase() !== "migrate"} 513 703 > 514 - <svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 515 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path> 704 + <svg 705 + class="w-5 h-5 mr-2" 706 + fill="none" 707 + stroke="currentColor" 708 + viewBox="0 0 24 24" 709 + > 710 + <path 711 + stroke-linecap="round" 712 + stroke-linejoin="round" 713 + stroke-width="2" 714 + d="M5 13l4 4L19 7" 715 + > 716 + </path> 516 717 </svg> 517 718 Confirm Migration 518 719 </button>
+8 -8
islands/SocialLinks.tsx
··· 1 1 import { useEffect, useState } from "preact/hooks"; 2 - import * as Icon from 'npm:preact-feather'; 2 + import * as Icon from "npm:preact-feather"; 3 3 4 4 /** 5 5 * The GitHub repository. ··· 18 18 const [starCount, setStarCount] = useState<number | null>(null); 19 19 20 20 useEffect(() => { 21 - const CACHE_KEY = 'github_stars'; 21 + const CACHE_KEY = "github_stars"; 22 22 const CACHE_DURATION = 15 * 60 * 1000; // 15 minutes in milliseconds 23 23 24 24 const fetchRepoInfo = async () => { 25 25 try { 26 - const response = await fetch("https://api.github.com/repos/knotbin/airport"); 26 + const response = await fetch( 27 + "https://api.github.com/repos/knotbin/airport", 28 + ); 27 29 const data: GitHubRepo = await response.json(); 28 30 const cacheData = { 29 31 count: data.stargazers_count, 30 - timestamp: Date.now() 32 + timestamp: Date.now(), 31 33 }; 32 34 localStorage.setItem(CACHE_KEY, JSON.stringify(cacheData)); 33 35 setStarCount(data.stargazers_count); ··· 78 80 stroke-linejoin="round" 79 81 xmlns="http://www.w3.org/2000/svg" 80 82 > 81 - <path 82 - 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" 83 - /> 83 + <path 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" /> 84 84 </svg> 85 85 </a> 86 86 <a ··· 102 102 </a> 103 103 </div> 104 104 ); 105 - } 105 + }
+4 -4
islands/Ticket.tsx
··· 34 34 setUser( 35 35 userData 36 36 ? { 37 - did: userData.did, 38 - handle: userData.handle, 39 - } 40 - : null 37 + did: userData.did, 38 + handle: userData.handle, 39 + } 40 + : null, 41 41 ); 42 42 } catch (error) { 43 43 console.error("Failed to fetch user:", error);
+5 -5
lib/cred/sessions.ts
··· 1 1 import { Agent } from "npm:@atproto/api"; 2 2 import { getIronSession, SessionOptions } from "npm:iron-session"; 3 - import { CredentialSession, createSessionOptions } from "../types.ts"; 3 + import { createSessionOptions, CredentialSession } from "../types.ts"; 4 4 5 5 let migrationSessionOptions: SessionOptions; 6 6 let credentialSessionOptions: SessionOptions; ··· 34 34 export async function getCredentialSession( 35 35 req: Request, 36 36 res: Response = new Response(), 37 - isMigration: boolean = false 37 + isMigration: boolean = false, 38 38 ) { 39 39 const options = await getOptions(isMigration); 40 40 return getIronSession<CredentialSession>(req, res, options); ··· 50 50 export async function getCredentialAgent( 51 51 req: Request, 52 52 res: Response = new Response(), 53 - isMigration: boolean = false 53 + isMigration: boolean = false, 54 54 ) { 55 55 const session = await getCredentialSession(req, res, isMigration); 56 56 if ( ··· 104 104 req: Request, 105 105 res: Response, 106 106 data: CredentialSession, 107 - isMigration: boolean = false 107 + isMigration: boolean = false, 108 108 ) { 109 109 const session = await getCredentialSession(req, res, isMigration); 110 110 session.did = data.did; ··· 125 125 export async function getCredentialSessionAgent( 126 126 req: Request, 127 127 res: Response = new Response(), 128 - isMigration: boolean = false 128 + isMigration: boolean = false, 129 129 ) { 130 130 const session = await getCredentialSession(req, res, isMigration); 131 131
+3 -3
lib/id-resolver.ts
··· 55 55 }, 56 56 57 57 async resolveHandleToDid(handle: string) { 58 - return await resolver.handle.resolve(handle) as Did 58 + return await resolver.handle.resolve(handle) as Did; 59 59 }, 60 60 61 61 async resolveDidToPdsUrl(did: string): Promise<string | undefined> { ··· 68 68 return didDoc.pds; 69 69 } else { 70 70 const forcedDidDoc = await resolver.did.resolveAtprotoData( 71 - did, 71 + did, 72 72 true, 73 - ) 73 + ); 74 74 if (forcedDidDoc.pds) { 75 75 return forcedDidDoc.pds; 76 76 }
+6 -3
lib/migration-state.ts
··· 15 15 * @returns The migration state information 16 16 */ 17 17 export function getMigrationState(): MigrationStateInfo { 18 - const state = (Deno.env.get("MIGRATION_STATE") || "up").toLowerCase() as MigrationState; 18 + const state = (Deno.env.get("MIGRATION_STATE") || "up") 19 + .toLowerCase() as MigrationState; 19 20 20 21 switch (state) { 21 22 case "issue": 22 23 return { 23 24 state: "issue", 24 - message: "Migration services are temporarily unavailable as we investigate an issue. Please try again later.", 25 + message: 26 + "Migration services are temporarily unavailable as we investigate an issue. Please try again later.", 25 27 allowMigration: false, 26 28 }; 27 29 28 30 case "maintenance": 29 31 return { 30 32 state: "maintenance", 31 - message: "Migration services are temporarily unavailable for maintenance. Please try again later.", 33 + message: 34 + "Migration services are temporarily unavailable for maintenance. Please try again later.", 32 35 allowMigration: false, 33 36 }; 34 37
+2 -2
lib/oauth/sessions.ts
··· 1 1 import { Agent } from "npm:@atproto/api"; 2 2 import { getIronSession, SessionOptions } from "npm:iron-session"; 3 3 import { oauthClient } from "./client.ts"; 4 - import { OauthSession, createSessionOptions } from "../types.ts"; 4 + import { createSessionOptions, OauthSession } from "../types.ts"; 5 5 6 6 let oauthSessionOptions: SessionOptions; 7 7 ··· 22 22 * @returns The OAuth session agent 23 23 */ 24 24 export async function getOauthSessionAgent( 25 - req: Request 25 + req: Request, 26 26 ) { 27 27 try { 28 28 console.log("Getting OAuth session...");
+32 -26
lib/types.ts
··· 15 15 * @param db - The Deno KV instance for the database 16 16 * @returns The unlock function 17 17 */ 18 - async function createLock(key: string, db: Deno.Kv): Promise<() => Promise<void>> { 18 + async function createLock( 19 + key: string, 20 + db: Deno.Kv, 21 + ): Promise<() => Promise<void>> { 19 22 const lockKey = ["session_lock", key]; 20 23 const lockValue = Date.now(); 21 - 24 + 22 25 // Try to acquire lock 23 26 const result = await db.atomic() 24 - .check({ key: lockKey, versionstamp: null }) // Only if key doesn't exist 25 - .set(lockKey, lockValue, { expireIn: 5000 }) // 5 second TTL 27 + .check({ key: lockKey, versionstamp: null }) // Only if key doesn't exist 28 + .set(lockKey, lockValue, { expireIn: 5000 }) // 5 second TTL 26 29 .commit(); 27 30 28 31 if (!result.ok) { ··· 40 43 * @type {OauthSession} 41 44 */ 42 45 export interface OauthSession { 43 - did: string 46 + did: string; 44 47 } 45 48 46 49 /** ··· 68 71 * @param cookieName - The name of the iron session cookie 69 72 * @returns The session options for iron session 70 73 */ 71 - export const createSessionOptions = async (cookieName: string): Promise<SessionOptions> => { 72 - const cookieSecret = Deno.env.get("COOKIE_SECRET"); 73 - if (!cookieSecret) { 74 - throw new Error("COOKIE_SECRET is not set"); 75 - } 74 + export const createSessionOptions = async ( 75 + cookieName: string, 76 + ): Promise<SessionOptions> => { 77 + const cookieSecret = Deno.env.get("COOKIE_SECRET"); 78 + if (!cookieSecret) { 79 + throw new Error("COOKIE_SECRET is not set"); 80 + } 76 81 77 - if (!db) { 78 - db = await Deno.openKv(); 79 - } 82 + if (!db) { 83 + db = await Deno.openKv(); 84 + } 80 85 81 - return { 82 - cookieName: cookieName, 83 - password: cookieSecret, 84 - cookieOptions: { 85 - secure: Deno.env.get("NODE_ENV") === "production" || Deno.env.get("NODE_ENV") === "staging", 86 - httpOnly: true, 87 - sameSite: "lax", 88 - path: "/", 89 - domain: undefined, 90 - }, 91 - lockFn: (key: string) => createLock(key, db) 92 - } 93 - }; 86 + return { 87 + cookieName: cookieName, 88 + password: cookieSecret, 89 + cookieOptions: { 90 + secure: Deno.env.get("NODE_ENV") === "production" || 91 + Deno.env.get("NODE_ENV") === "staging", 92 + httpOnly: true, 93 + sameSite: "lax", 94 + path: "/", 95 + domain: undefined, 96 + }, 97 + lockFn: (key: string) => createLock(key, db), 98 + }; 99 + };
+5 -5
routes/_error.tsx
··· 1 - import { PageProps, HttpError } from "fresh"; 1 + import { HttpError, PageProps } from "fresh"; 2 2 import posthog from "posthog-js"; 3 3 4 4 export default function ErrorPage(props: PageProps) { 5 5 const error = props.error; // Contains the thrown Error or HTTPError 6 6 if (error instanceof HttpError) { 7 - posthog.default.capture('error', { 7 + posthog.default.capture("error", { 8 8 error: error.message, 9 9 status: error.status, 10 10 }); ··· 32 32 FLIGHT NOT FOUND 33 33 </p> 34 34 <p class="text-lg sm:text-xl text-slate-600 dark:text-white/70 max-w-2xl"> 35 - We couldn't locate the destination you're looking for. Please 36 - check your flight number and try again. 35 + We couldn't locate the destination you're looking for. 36 + Please check your flight number and try again. 37 37 </p> 38 38 <div class="mt-8"> 39 39 <a ··· 48 48 </div> 49 49 </div> 50 50 </> 51 - ) 51 + ); 52 52 } 53 53 } 54 54
+68 -38
routes/about.tsx
··· 6 6 <div class="px-2 sm:px-4 py-4 sm:py-8 mx-auto"> 7 7 <div class="max-w-screen-lg mx-auto flex flex-col items-center justify-center"> 8 8 <div class="prose dark:prose-invert max-w-none w-full mb-0"> 9 - <h1 class="text-3xl font-bold text-center mb-8">About AT Protocol</h1> 9 + <h1 class="text-3xl font-bold text-center mb-8"> 10 + About AT Protocol 11 + </h1> 10 12 11 13 <div class="space-y-6"> 12 14 <section> 13 - <h2 class="text-2xl font-semibold mb-4">What is AT Protocol?</h2> 15 + <h2 class="text-2xl font-semibold mb-4"> 16 + What is AT Protocol? 17 + </h2> 14 18 <p class="text-gray-600 dark:text-gray-300"> 15 19 AT Protocol (Authenticated Transfer Protocol) is the 16 20 foundation of Bluesky and other social apps like 17 21 <a href="https://tangled.sh">Tangled</a>, 18 - <a href="https://spark.com">Spark</a>, and more. 19 - Unlike traditional social platforms that lock your 20 - data and identity to a single service, AT Protocol 21 - gives you complete control over your digital presence. 22 - Think of it as an open standard for social networking, 23 - similar to how email works across different providers. 22 + <a href="https://spark.com">Spark</a>, and more. Unlike 23 + traditional social platforms that lock your data and identity 24 + to a single service, AT Protocol gives you complete control 25 + over your digital presence. Think of it as an open standard 26 + for social networking, similar to how email works across 27 + different providers. 24 28 </p> 25 29 </section> 26 30 ··· 28 32 <h2 class="text-2xl font-semibold mb-4">Key Features</h2> 29 33 <ul class="list-disc pl-6 space-y-4 text-gray-600 dark:text-gray-300"> 30 34 <li> 31 - <strong>PDS Servers:</strong> PDS servers are where your data is stored. 32 - They can be run by anyone, and they are very lightweight, allowing you to 33 - choose which one to use or run your own. PDS servers just store your data, 34 - meaning you don't have to switch PDS servers to use a different app or service. 35 - You can have one PDS while using many different apps and services with the 35 + <strong>PDS Servers:</strong>{" "} 36 + PDS servers are where your data is stored. They can be run 37 + by anyone, and they are very lightweight, allowing you to 38 + choose which one to use or run your own. PDS servers just 39 + store your data, meaning you don't have to switch PDS 40 + servers to use a different app or service. You can have one 41 + PDS while using many different apps and services with the 36 42 same account. 37 43 </li> 38 44 <li> 39 - <strong>Decentralized Identity:</strong> Your account is tied to a DID 40 - (Decentralized Identifier) rather than your handle/username. 41 - This means you can move your entire account, including your followers 42 - and content, to any PDS by changing where your DID points. 43 - It's also the reason you can use any domain as your handle, because 44 - your identity is not tied to your handle. Your handle can change, 45 + <strong>Decentralized Identity:</strong>{" "} 46 + Your account is tied to a DID (Decentralized Identifier) 47 + rather than your handle/username. This means you can move 48 + your entire account, including your followers and content, 49 + to any PDS by changing where your DID points. It's also the 50 + reason you can use any domain as your handle, because your 51 + identity is not tied to your handle. Your handle can change, 45 52 but your DID will always remain the same. 46 53 </li> 47 54 <li> 48 - <strong>Portable Content:</strong> All your posts, likes, and other social 49 - data are stored in your Personal Data Server (PDS). 50 - You can switch PDS providers without losing any content or connections. 55 + <strong>Portable Content:</strong>{" "} 56 + All your posts, likes, and other social data are stored in 57 + your Personal Data Server (PDS). You can switch PDS 58 + providers without losing any content or connections. 51 59 </li> 52 60 <li> 53 - <strong>Architecture:</strong> The protocol uses a three-tier architecture: 54 - Personal Data Servers (PDS) store your content, 55 - relays broadcast a stream of all events on all PDSes, 56 - and AppViews process and serve that stream into content for users. 57 - This means when you make a post, the content is stored on your PDS, 58 - picked up by relays, and AppViews listen to those relays to deliver 59 - that post to all users. 61 + <strong>Architecture:</strong>{" "} 62 + The protocol uses a three-tier architecture: Personal Data 63 + Servers (PDS) store your content, relays broadcast a stream 64 + of all events on all PDSes, and AppViews process and serve 65 + that stream into content for users. This means when you make 66 + a post, the content is stored on your PDS, picked up by 67 + relays, and AppViews listen to those relays to deliver that 68 + post to all users. 60 69 </li> 61 70 <li> 62 - <strong>Algorithmic Choice:</strong> You're not locked into a single algorithm 63 - for your feed. Different services can offer different ways of curating content, 64 - and you can choose which one you prefer. Bluesky offers a way to make custom 65 - feeds, but even if it didn't, different apps could still offer their own 66 - algorithms for curating content. 71 + <strong>Algorithmic Choice:</strong>{" "} 72 + You're not locked into a single algorithm for your feed. 73 + Different services can offer different ways of curating 74 + content, and you can choose which one you prefer. Bluesky 75 + offers a way to make custom feeds, but even if it didn't, 76 + different apps could still offer their own algorithms for 77 + curating content. 67 78 </li> 68 79 </ul> 69 80 </section> ··· 72 83 <h2 class="text-2xl font-semibold mb-4">Learn More</h2> 73 84 <div class="space-y-4"> 74 85 <p class="text-gray-600 dark:text-gray-300"> 75 - Want to dive deeper into AT Protocol? Check out these resources: 86 + Want to dive deeper into AT Protocol? Check out these 87 + resources: 76 88 </p> 77 89 <ul class="list-none space-y-2"> 78 90 <li> 79 - <a href="https://atproto.com" class="text-blue-500 hover:underline">Official AT Protocol Docs</a> - The main source for protocol specs and information 91 + <a 92 + href="https://atproto.com" 93 + class="text-blue-500 hover:underline" 94 + > 95 + Official AT Protocol Docs 96 + </a>{" "} 97 + - The main source for protocol specs and information 80 98 </li> 81 99 <li> 82 - <a href="https://github.com/bluesky-social/atproto" class="text-blue-500 hover:underline">GitHub Repository</a> - View the protocol implementation 100 + <a 101 + href="https://github.com/bluesky-social/atproto" 102 + class="text-blue-500 hover:underline" 103 + > 104 + GitHub Repository 105 + </a>{" "} 106 + - View the protocol implementation 83 107 </li> 84 108 <li> 85 - <a href="https://atproto.wiki" class="text-blue-500 hover:underline">AT Protocol Wiki</a> - Community-driven documentation and resources 109 + <a 110 + href="https://atproto.wiki" 111 + class="text-blue-500 hover:underline" 112 + > 113 + AT Protocol Wiki 114 + </a>{" "} 115 + - Community-driven documentation and resources 86 116 </li> 87 117 </ul> 88 118 </div>
+56 -41
routes/api/cred/login.ts
··· 17 17 const { handle, password } = body; 18 18 19 19 if (!handle || !password) { 20 - return new Response(JSON.stringify({ 21 - success: false, 22 - message: "Handle and password are required" 23 - }), { 24 - status: 400, 25 - headers: { "Content-Type": "application/json" } 26 - }); 20 + return new Response( 21 + JSON.stringify({ 22 + success: false, 23 + message: "Handle and password are required", 24 + }), 25 + { 26 + status: 400, 27 + headers: { "Content-Type": "application/json" }, 28 + }, 29 + ); 27 30 } 28 31 29 32 console.log("Resolving handle:", handle); 30 - const did = await resolver.resolveHandleToDid(handle) 31 - const service = await resolver.resolveDidToPdsUrl(did) 33 + const did = await resolver.resolveHandleToDid(handle); 34 + const service = await resolver.resolveDidToPdsUrl(did); 32 35 console.log("Resolved service:", service); 33 36 34 37 if (!service) { 35 - return new Response(JSON.stringify({ 36 - success: false, 37 - message: "Invalid handle" 38 - }), { 39 - status: 400, 40 - }) 38 + return new Response( 39 + JSON.stringify({ 40 + success: false, 41 + message: "Invalid handle", 42 + }), 43 + { 44 + status: 400, 45 + }, 46 + ); 41 47 } 42 48 43 49 try { ··· 51 57 console.log("Created ATProto session:", { 52 58 did: sessionRes.data.did, 53 59 handle: sessionRes.data.handle, 54 - hasAccessJwt: !!sessionRes.data.accessJwt 60 + hasAccessJwt: !!sessionRes.data.accessJwt, 55 61 }); 56 62 57 63 // Create response for setting cookies 58 - const response = new Response(JSON.stringify({ 59 - success: true, 60 - did, 61 - handle 62 - }), { 63 - status: 200, 64 - headers: { "Content-Type": "application/json" } 65 - }); 64 + const response = new Response( 65 + JSON.stringify({ 66 + success: true, 67 + did, 68 + handle, 69 + }), 70 + { 71 + status: 200, 72 + headers: { "Content-Type": "application/json" }, 73 + }, 74 + ); 66 75 67 76 // Create and save our client session with tokens 68 77 await setCredentialSession(ctx.req, response, { ··· 70 79 service, 71 80 password, 72 81 handle, 73 - accessJwt: sessionRes.data.accessJwt 82 + accessJwt: sessionRes.data.accessJwt, 74 83 }); 75 84 76 85 // Log the response headers 77 86 console.log("Response headers:", { 78 87 cookies: response.headers.get("Set-Cookie"), 79 - allHeaders: Object.fromEntries(response.headers.entries()) 88 + allHeaders: Object.fromEntries(response.headers.entries()), 80 89 }); 81 90 82 91 return response; 83 92 } catch (err) { 84 93 const message = err instanceof Error ? err.message : String(err); 85 94 console.error("Login failed:", message); 86 - return new Response(JSON.stringify({ 87 - success: false, 88 - message: "Invalid credentials" 89 - }), { 90 - status: 401, 91 - headers: { "Content-Type": "application/json" } 92 - }); 95 + return new Response( 96 + JSON.stringify({ 97 + success: false, 98 + message: "Invalid credentials", 99 + }), 100 + { 101 + status: 401, 102 + headers: { "Content-Type": "application/json" }, 103 + }, 104 + ); 93 105 } 94 106 } catch (error) { 95 107 const message = error instanceof Error ? error.message : String(error); 96 108 console.error("Login error:", message); 97 - return new Response(JSON.stringify({ 98 - success: false, 99 - message: error instanceof Error ? error.message : "An error occurred" 100 - }), { 101 - status: 500, 102 - headers: { "Content-Type": "application/json" } 103 - }); 109 + return new Response( 110 + JSON.stringify({ 111 + success: false, 112 + message: error instanceof Error ? error.message : "An error occurred", 113 + }), 114 + { 115 + status: 500, 116 + headers: { "Content-Type": "application/json" }, 117 + }, 118 + ); 104 119 } 105 - } 120 + }, 106 121 });
+12 -9
routes/api/me.ts
··· 8 8 const res = new Response(); 9 9 10 10 try { 11 - console.log("[/api/me] Request headers:", Object.fromEntries(req.headers.entries())); 11 + console.log( 12 + "[/api/me] Request headers:", 13 + Object.fromEntries(req.headers.entries()), 14 + ); 12 15 13 16 const agent = await getSessionAgent(req, res); 14 17 if (!agent) { ··· 17 20 status: 200, 18 21 headers: { 19 22 "Content-Type": "application/json", 20 - "X-Response-Type": "null" 21 - } 23 + "X-Response-Type": "null", 24 + }, 22 25 }); 23 26 } 24 27 ··· 28 31 29 32 const responseData = { 30 33 did: session.data.did, 31 - handle 34 + handle, 32 35 }; 33 36 34 37 return new Response(JSON.stringify(responseData), { 35 38 status: 200, 36 39 headers: { 37 40 "Content-Type": "application/json", 38 - "X-Response-Type": "user" 39 - } 41 + "X-Response-Type": "user", 42 + }, 40 43 }); 41 44 } catch (err) { 42 45 const message = err instanceof Error ? err.message : String(err); ··· 45 48 stack: err instanceof Error ? err.stack : undefined, 46 49 url: req.url, 47 50 method: req.method, 48 - headers: Object.fromEntries(req.headers.entries()) 51 + headers: Object.fromEntries(req.headers.entries()), 49 52 }); 50 53 51 54 return new Response(JSON.stringify(null), { ··· 53 56 headers: { 54 57 "Content-Type": "application/json", 55 58 "X-Response-Type": "error", 56 - "X-Error-Message": encodeURIComponent(message) 57 - } 59 + "X-Error-Message": encodeURIComponent(message), 60 + }, 58 61 }); 59 62 } 60 63 },
+44 -37
routes/api/migrate/next-step.ts
··· 2 2 import { define } from "../../../utils.ts"; 3 3 4 4 export const handler = define.handlers({ 5 - async GET(ctx) { 6 - let nextStep = null; 7 - const oldAgent = await getSessionAgent(ctx.req); 8 - const newAgent = await getSessionAgent(ctx.req, new Response(), true); 5 + async GET(ctx) { 6 + let nextStep = null; 7 + const oldAgent = await getSessionAgent(ctx.req); 8 + const newAgent = await getSessionAgent(ctx.req, new Response(), true); 9 9 10 - if (!newAgent) return Response.json({ nextStep: 1, completed: false }); 11 - if (!oldAgent) return new Response("Unauthorized", { status: 401 }); 10 + if (!newAgent) return Response.json({ nextStep: 1, completed: false }); 11 + if (!oldAgent) return new Response("Unauthorized", { status: 401 }); 12 12 13 - const oldStatus = await oldAgent.com.atproto.server.checkAccountStatus(); 14 - const newStatus = await newAgent.com.atproto.server.checkAccountStatus(); 15 - if (!oldStatus.data || !newStatus.data) return new Response("Could not verify status", { status: 500 }); 13 + const oldStatus = await oldAgent.com.atproto.server.checkAccountStatus(); 14 + const newStatus = await newAgent.com.atproto.server.checkAccountStatus(); 15 + if (!oldStatus.data || !newStatus.data) { 16 + return new Response("Could not verify status", { status: 500 }); 17 + } 16 18 17 - // Check conditions in sequence to determine the next step 18 - if (!newStatus.data) { 19 - nextStep = 1; 20 - } else if (!(newStatus.data.repoCommit && 21 - newStatus.data.indexedRecords === oldStatus.data.indexedRecords && 22 - newStatus.data.privateStateValues === oldStatus.data.privateStateValues && 23 - newStatus.data.expectedBlobs === newStatus.data.importedBlobs && 24 - newStatus.data.importedBlobs === oldStatus.data.importedBlobs)) { 25 - nextStep = 2; 26 - } else if (!newStatus.data.validDid) { 27 - nextStep = 3; 28 - } else if (!(newStatus.data.activated === true && oldStatus.data.activated === false)) { 29 - nextStep = 4; 30 - } 19 + // Check conditions in sequence to determine the next step 20 + if (!newStatus.data) { 21 + nextStep = 1; 22 + } else if ( 23 + !(newStatus.data.repoCommit && 24 + newStatus.data.indexedRecords === oldStatus.data.indexedRecords && 25 + newStatus.data.privateStateValues === 26 + oldStatus.data.privateStateValues && 27 + newStatus.data.expectedBlobs === newStatus.data.importedBlobs && 28 + newStatus.data.importedBlobs === oldStatus.data.importedBlobs) 29 + ) { 30 + nextStep = 2; 31 + } else if (!newStatus.data.validDid) { 32 + nextStep = 3; 33 + } else if ( 34 + !(newStatus.data.activated === true && oldStatus.data.activated === false) 35 + ) { 36 + nextStep = 4; 37 + } 31 38 32 - return Response.json({ 33 - nextStep, 34 - completed: nextStep === null, 35 - currentStatus: { 36 - activated: newStatus.data.activated, 37 - validDid: newStatus.data.validDid, 38 - repoCommit: newStatus.data.repoCommit, 39 - indexedRecords: newStatus.data.indexedRecords, 40 - privateStateValues: newStatus.data.privateStateValues, 41 - importedBlobs: newStatus.data.importedBlobs 42 - } 43 - }); 44 - } 45 - }) 39 + return Response.json({ 40 + nextStep, 41 + completed: nextStep === null, 42 + currentStatus: { 43 + activated: newStatus.data.activated, 44 + validDid: newStatus.data.validDid, 45 + repoCommit: newStatus.data.repoCommit, 46 + indexedRecords: newStatus.data.indexedRecords, 47 + privateStateValues: newStatus.data.privateStateValues, 48 + importedBlobs: newStatus.data.importedBlobs, 49 + }, 50 + }); 51 + }, 52 + });
+4 -3
routes/api/migration-state.ts
··· 21 21 headers: { 22 22 "Content-Type": "application/json", 23 23 }, 24 - } 24 + }, 25 25 ); 26 26 } catch (error) { 27 27 console.error("Error checking migration state:", error); ··· 29 29 return new Response( 30 30 JSON.stringify({ 31 31 state: "issue", 32 - message: "Unable to determine migration state. Please try again later.", 32 + message: 33 + "Unable to determine migration state. Please try again later.", 33 34 allowMigration: false, 34 35 }), 35 36 { ··· 37 38 headers: { 38 39 "Content-Type": "application/json", 39 40 }, 40 - } 41 + }, 41 42 ); 42 43 } 43 44 },
+13 -13
routes/api/oauth/initiate.ts
··· 1 - import { isValidHandle } from 'npm:@atproto/syntax' 1 + import { isValidHandle } from "npm:@atproto/syntax"; 2 2 import { oauthClient } from "../../../lib/oauth/client.ts"; 3 3 import { define } from "../../../utils.ts"; 4 4 5 5 function isValidUrl(url: string): boolean { 6 6 try { 7 - const urlp = new URL(url) 7 + const urlp = new URL(url); 8 8 // http or https 9 - return urlp.protocol === 'http:' || urlp.protocol === 'https:' 9 + return urlp.protocol === "http:" || urlp.protocol === "https:"; 10 10 } catch { 11 - return false 11 + return false; 12 12 } 13 13 } 14 14 15 15 export const handler = define.handlers({ 16 16 async POST(ctx) { 17 - const data = await ctx.req.json() 18 - const handle = data.handle 17 + const data = await ctx.req.json(); 18 + const handle = data.handle; 19 19 if ( 20 - typeof handle !== 'string' || 20 + typeof handle !== "string" || 21 21 !(isValidHandle(handle) || isValidUrl(handle)) 22 22 ) { 23 - return new Response("Invalid Handle", {status: 400}) 23 + return new Response("Invalid Handle", { status: 400 }); 24 24 } 25 25 26 26 // Initiate the OAuth flow 27 27 try { 28 28 const url = await oauthClient.authorize(handle, { 29 - scope: 'atproto transition:generic transition:chat.bsky', 30 - }) 31 - return Response.json({ redirectUrl: url.toString() }) 29 + scope: "atproto transition:generic transition:chat.bsky", 30 + }); 31 + return Response.json({ redirectUrl: url.toString() }); 32 32 } catch (err) { 33 - console.error({ err }, 'oauth authorize failed') 34 - return new Response("Couldn't initiate login", {status: 500}) 33 + console.error({ err }, "oauth authorize failed"); 34 + return new Response("Couldn't initiate login", { status: 500 }); 35 35 } 36 36 }, 37 37 });
+1 -1
routes/api/plc/keys.ts
··· 36 36 }), 37 37 { 38 38 headers: { "Content-Type": "application/json" }, 39 - } 39 + }, 40 40 ); 41 41 }, 42 42 });
+5 -6
routes/api/plc/token.ts
··· 42 42 "Content-Type": "application/json", 43 43 ...Object.fromEntries(res.headers), // Include session cookie headers 44 44 }, 45 - } 45 + }, 46 46 ); 47 47 } catch (error) { 48 48 console.error("PLC signature request error:", error); 49 49 return new Response( 50 50 JSON.stringify({ 51 51 success: false, 52 - message: 53 - error instanceof Error 54 - ? error.message 55 - : "Failed to get PLC operation signature (sending confirmation email)", 52 + message: error instanceof Error 53 + ? error.message 54 + : "Failed to get PLC operation signature (sending confirmation email)", 56 55 }), 57 56 { 58 57 status: 400, 59 58 headers: { "Content-Type": "application/json" }, 60 - } 59 + }, 61 60 ); 62 61 } 63 62 },
+15 -15
routes/api/plc/update.ts
··· 51 51 { 52 52 status: 400, 53 53 headers: { "Content-Type": "application/json" }, 54 - } 54 + }, 55 55 ); 56 56 } 57 57 console.log("Using agent DID:", did); ··· 70 70 { 71 71 status: 400, 72 72 headers: { "Content-Type": "application/json" }, 73 - } 73 + }, 74 74 ); 75 75 } 76 76 console.log("Got DID document:", didDoc); ··· 92 92 { 93 93 status: 400, 94 94 headers: { "Content-Type": "application/json" }, 95 - } 95 + }, 96 96 ); 97 97 } 98 98 ··· 124 124 "Content-Type": "application/json", 125 125 ...Object.fromEntries(res.headers), // Include session cookie headers 126 126 }, 127 - } 127 + }, 128 128 ); 129 129 } catch (error) { 130 130 console.error("PLC update error:", error); 131 - const errorMessage = 132 - error instanceof Error ? error.message : "Failed to update your PLC"; 131 + const errorMessage = error instanceof Error 132 + ? error.message 133 + : "Failed to update your PLC"; 133 134 console.log("Sending error response:", errorMessage); 134 135 135 136 return new Response( 136 137 JSON.stringify({ 137 138 success: false, 138 139 message: errorMessage, 139 - error: 140 - error instanceof Error 141 - ? { 142 - name: error.name, 143 - message: error.message, 144 - stack: error.stack, 145 - } 146 - : String(error), 140 + error: error instanceof Error 141 + ? { 142 + name: error.name, 143 + message: error.message, 144 + stack: error.stack, 145 + } 146 + : String(error), 147 147 }), 148 148 { 149 149 status: 400, 150 150 headers: { "Content-Type": "application/json" }, 151 - } 151 + }, 152 152 ); 153 153 } 154 154 },
+8 -7
routes/api/plc/update/complete.ts
··· 21 21 { 22 22 status: 400, 23 23 headers: { "Content-Type": "application/json" }, 24 - } 24 + }, 25 25 ); 26 26 } 27 27 ··· 35 35 { 36 36 status: 401, 37 37 headers: { "Content-Type": "application/json" }, 38 - } 38 + }, 39 39 ); 40 40 } 41 41 ··· 49 49 { 50 50 status: 400, 51 51 headers: { "Content-Type": "application/json" }, 52 - } 52 + }, 53 53 ); 54 54 } 55 55 ··· 70 70 "Content-Type": "application/json", 71 71 ...Object.fromEntries(res.headers), // Include session cookie headers 72 72 }, 73 - } 73 + }, 74 74 ); 75 75 } catch (error) { 76 76 console.error("PLC update completion error:", error); 77 - const message = 78 - error instanceof Error ? error.message : "Unknown error occurred"; 77 + const message = error instanceof Error 78 + ? error.message 79 + : "Unknown error occurred"; 79 80 80 81 return new Response( 81 82 JSON.stringify({ ··· 85 86 { 86 87 status: 500, 87 88 headers: { "Content-Type": "application/json" }, 88 - } 89 + }, 89 90 ); 90 91 } 91 92 },
+15 -17
routes/api/plc/verify.ts
··· 42 42 { 43 43 status: 400, 44 44 headers: { "Content-Type": "application/json" }, 45 - } 45 + }, 46 46 ); 47 47 } 48 48 console.log("Using agent DID:", did); ··· 61 61 { 62 62 status: 400, 63 63 headers: { "Content-Type": "application/json" }, 64 - } 64 + }, 65 65 ); 66 66 } 67 67 console.log("Got DID document:", didDoc); ··· 85 85 "Content-Type": "application/json", 86 86 ...Object.fromEntries(res.headers), // Include session cookie headers 87 87 }, 88 - } 88 + }, 89 89 ); 90 90 } 91 91 ··· 98 98 { 99 99 status: 404, 100 100 headers: { "Content-Type": "application/json" }, 101 - } 101 + }, 102 102 ); 103 103 } catch (error) { 104 104 console.error("PLC verification error:", error); 105 - const errorMessage = 106 - error instanceof Error 107 - ? error.message 108 - : "Failed to verify rotation key"; 105 + const errorMessage = error instanceof Error 106 + ? error.message 107 + : "Failed to verify rotation key"; 109 108 console.log("Sending error response:", errorMessage); 110 109 111 110 return new Response( 112 111 JSON.stringify({ 113 112 success: false, 114 113 message: errorMessage, 115 - error: 116 - error instanceof Error 117 - ? { 118 - name: error.name, 119 - message: error.message, 120 - stack: error.stack, 121 - } 122 - : String(error), 114 + error: error instanceof Error 115 + ? { 116 + name: error.name, 117 + message: error.message, 118 + stack: error.stack, 119 + } 120 + : String(error), 123 121 }), 124 122 { 125 123 status: 400, 126 124 headers: { "Content-Type": "application/json" }, 127 - } 125 + }, 128 126 ); 129 127 } 130 128 },
+11 -8
routes/api/resolve-pds.ts
··· 7 7 const did = url.searchParams.get("did"); 8 8 9 9 if (!did) { 10 - return new Response(JSON.stringify({ error: "DID parameter is required" }), { 11 - status: 400, 12 - headers: { "Content-Type": "application/json" } 13 - }); 10 + return new Response( 11 + JSON.stringify({ error: "DID parameter is required" }), 12 + { 13 + status: 400, 14 + headers: { "Content-Type": "application/json" }, 15 + }, 16 + ); 14 17 } 15 18 16 19 try { 17 20 const pds = await resolver.resolveDidToPdsUrl(did); 18 21 return new Response(JSON.stringify({ pds }), { 19 22 status: 200, 20 - headers: { "Content-Type": "application/json" } 23 + headers: { "Content-Type": "application/json" }, 21 24 }); 22 25 } catch (error) { 23 26 console.error("Failed to resolve PDS:", error); 24 27 return new Response(JSON.stringify({ error: "Failed to resolve PDS" }), { 25 28 status: 500, 26 - headers: { "Content-Type": "application/json" } 29 + headers: { "Content-Type": "application/json" }, 27 30 }); 28 31 } 29 - } 30 - }); 32 + }, 33 + });
+1 -2
routes/api/server/describe.ts
··· 1 - 2 1 import { Agent } from "@atproto/api"; 3 2 import { getSessionAgent } from "../../../lib/sessions.ts"; 4 3 import { define } from "../../../utils.ts"; ··· 21 20 } 22 21 const result = await agent.com.atproto.server.describeServer(); 23 22 return Response.json(result); 24 - } 23 + }, 25 24 });
+20 -3
routes/index.tsx
··· 19 19 20 20 <LoginButton /> 21 21 <p class="font-mono text-lg sm:text-xl mb-4 mt-4 sm:mb-6 text-center text-gray-600 dark:text-gray-300"> 22 - Airport is made with love by <a class="text-blue-500 hover:underline" href="https://bsky.app/profile/knotbin.com">Roscoe</a> for <a class="text-blue-500 hover:underline" href="https://sprk.so">Spark</a>, a new short-video platform for AT Protocol. 22 + Airport is made with love by{" "} 23 + <a 24 + class="text-blue-500 hover:underline" 25 + href="https://bsky.app/profile/knotbin.com" 26 + > 27 + Roscoe 28 + </a>{" "} 29 + for{" "} 30 + <a class="text-blue-500 hover:underline" href="https://sprk.so"> 31 + Spark 32 + </a>, a new short-video platform for AT Protocol. 23 33 </p> 24 34 <div class="text-center mb-4"> 25 - <a href="/about" class="inline-flex items-center text-blue-500 hover:text-blue-600 transition-colors"> 26 - <img src="/icons/info_bold.svg" alt="Info" class="w-5 h-5 mr-2" /> 35 + <a 36 + href="/about" 37 + class="inline-flex items-center text-blue-500 hover:text-blue-600 transition-colors" 38 + > 39 + <img 40 + src="/icons/info_bold.svg" 41 + alt="Info" 42 + class="w-5 h-5 mr-2" 43 + /> 27 44 <span class="font-mono">Learn more about AT Protocol</span> 28 45 </a> 29 46 </div>
+1 -1
routes/login/index.tsx
··· 1 1 import { PageProps } from "fresh"; 2 - import LoginSelector from "../../islands/LoginSelector.tsx" 2 + import LoginSelector from "../../islands/LoginSelector.tsx"; 3 3 4 4 export async function submitHandle(handle: string) { 5 5 const response = await fetch("/api/oauth/initiate", {
+52 -9
static/favicon.svg
··· 1 - <svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="768px" height="768px"><svg width="768px" height="768px" viewBox="0 0 768 768" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> 1 + <svg 2 + xmlns="http://www.w3.org/2000/svg" 3 + version="1.1" 4 + xmlns:xlink="http://www.w3.org/1999/xlink" 5 + width="768px" 6 + height="768px" 7 + > 8 + <svg 9 + width="768px" 10 + height="768px" 11 + viewBox="0 0 768 768" 12 + version="1.1" 13 + xmlns="http://www.w3.org/2000/svg" 14 + xmlns:xlink="http://www.w3.org/1999/xlink" 15 + > 2 16 <title>Artboard</title> 3 - <g id="SvgjsG1018" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> 4 - <path d="M258.744161,673.532662 C220.757598,658.556745 187.761971,637.340862 159.757278,609.885015 C131.731315,582.675001 109.640415,549.95668 94.87512,513.789548 C79.62504,476.904419 72,436.829975 72,393.566215 C72,341.982502 79.9023142,296.222756 95.7069425,256.286977 C111.788845,216.351199 133.97078,182.794052 162.252746,155.615536 C190.638366,128.268343 224.327386,107.031355 261.239629,93.2158823 C298.948918,79.0719608 339.292311,72 382.269809,72 C434.951904,72 481.118055,80.1812879 520.768263,96.5438638 C560.418471,112.90644 593.414098,135.092983 619.755146,163.103494 C645.823967,190.667688 665.804365,223.408333 678.398635,259.198961 C691.153247,294.974763 696.976005,332.414555 695.866908,371.518338 C694.480537,425.320706 674.410807,486.092796 651.875912,513.807986 C627.109489,544.267686 603.766423,555.08515 561.350365,555.08515 C539.879106,555.08515 518.908303,544.647246 498.437956,523.771439 L520.642336,488.757018 L553.379562,469.399451 C566.854015,479.647575 578.145985,482.494276 587.255474,477.939554 C600.919708,471.107472 612.130106,436.413978 615.180122,422.270056 C618.73393,406.280102 620.684502,389.975506 621.002879,373.598326 C621.834702,330.611898 615.457396,294.420099 601.870961,265.022929 C588.284526,235.625759 569.845793,211.91389 546.554762,193.887324 C524.028964,175.836615 498.164782,162.407064 470.442999,154.367543 C442.715581,146.047589 415.1268,141.887612 387.676656,141.887612 C348.85827,141.887612 314.337635,148.127577 284.114749,160.607508 C253.891863,172.810107 228.382638,190.143344 207.587075,212.60722 C187.068785,234.793763 171.541431,261.140284 161.005012,291.646781 C150.745868,321.875947 145.893569,355.155762 146.448118,391.486227 C147.557214,427.53936 154.073158,459.98718 165.995948,488.829687 C177.28032,516.727243 194.28181,541.951359 215.9053,562.877276 C237.610095,583.620856 263.386636,599.628298 291.601152,609.885015 C320.714941,620.700955 352.601472,626.108925 387.260744,626.108925 C406.669937,626.108925 425.940493,623.89027 445.072411,619.452962 C464.481604,615.292985 482.227152,609.330351 498.309054,601.565061 L522.015997,666.460701 C500.665885,676.444645 478.206676,683.793938 454.63837,688.508578 C431.262297,693.509483 407.422332,696.019474 383.517543,696 C338.321851,696 296.730724,688.508578 258.744161,673.532662 Z" id="SvgjsPath1017" fill="#0083FF" fill-rule="nonzero"></path> 5 - <g id="SvgjsG1016" transform="translate(115.0733, 79.4049)" fill="#0083FF"> 6 - <path d="M329.828907,231.383849 L260.699401,110.602459 C251.63529,94.765511 234.795945,84.9961472 216.561535,84.9961472 L202.32856,84.9961472 C197.759942,84.9961472 194.291354,89.1141864 195.064554,93.6217527 L218.683383,231.383849 L146.044594,231.383849 L117.317307,191.126992 C112.543308,184.436292 104.83484,180.465281 96.6208666,180.465281 L77.9170684,180.465281 C74.6441326,180.465281 72.2041032,183.486025 72.8889188,186.689439 L90.5192033,269.169882 C90.542094,269.250079 90.5821529,269.322638 90.6349289,269.382467 C90.634293,269.446115 90.634293,269.509764 90.634293,269.572775 L90.6349289,269.760538 C90.5821529,269.821003 90.542094,269.892926 90.5192033,269.973759 L72.8889188,352.453566 C72.2041032,355.657617 74.6441326,358.677724 77.9170684,358.677724 L96.6208666,358.677724 C104.83484,358.677724 112.543308,354.706712 117.317307,348.016012 L146.042051,307.761702 L218.684019,307.761702 L195.064554,445.52889 C194.291354,450.036456 197.759942,454.154495 202.32856,454.154495 L216.561535,454.154495 C234.795945,454.154495 251.63529,444.385132 260.699401,428.548184 L329.83145,307.761702 L399.512242,307.761702 C415.470292,307.761702 431.243943,304.348885 445.777042,297.751748 L448.41584,296.553888 C458.994558,291.751631 465.788667,281.20003 465.788667,269.572775 C465.788667,257.946157 458.994558,247.394556 448.41584,242.592299 L445.777042,241.39444 C431.243943,234.797303 415.470292,231.383849 399.512242,231.383849 L329.828907,231.383849 Z" id="SvgjsPath1015" transform="translate(269.2809, 269.5753) rotate(-134) translate(-269.2809, -269.5753)"></path> 7 - </g> 17 + <g 18 + id="SvgjsG1018" 19 + stroke="none" 20 + stroke-width="1" 21 + fill="none" 22 + fill-rule="evenodd" 23 + > 24 + <path 25 + d="M258.744161,673.532662 C220.757598,658.556745 187.761971,637.340862 159.757278,609.885015 C131.731315,582.675001 109.640415,549.95668 94.87512,513.789548 C79.62504,476.904419 72,436.829975 72,393.566215 C72,341.982502 79.9023142,296.222756 95.7069425,256.286977 C111.788845,216.351199 133.97078,182.794052 162.252746,155.615536 C190.638366,128.268343 224.327386,107.031355 261.239629,93.2158823 C298.948918,79.0719608 339.292311,72 382.269809,72 C434.951904,72 481.118055,80.1812879 520.768263,96.5438638 C560.418471,112.90644 593.414098,135.092983 619.755146,163.103494 C645.823967,190.667688 665.804365,223.408333 678.398635,259.198961 C691.153247,294.974763 696.976005,332.414555 695.866908,371.518338 C694.480537,425.320706 674.410807,486.092796 651.875912,513.807986 C627.109489,544.267686 603.766423,555.08515 561.350365,555.08515 C539.879106,555.08515 518.908303,544.647246 498.437956,523.771439 L520.642336,488.757018 L553.379562,469.399451 C566.854015,479.647575 578.145985,482.494276 587.255474,477.939554 C600.919708,471.107472 612.130106,436.413978 615.180122,422.270056 C618.73393,406.280102 620.684502,389.975506 621.002879,373.598326 C621.834702,330.611898 615.457396,294.420099 601.870961,265.022929 C588.284526,235.625759 569.845793,211.91389 546.554762,193.887324 C524.028964,175.836615 498.164782,162.407064 470.442999,154.367543 C442.715581,146.047589 415.1268,141.887612 387.676656,141.887612 C348.85827,141.887612 314.337635,148.127577 284.114749,160.607508 C253.891863,172.810107 228.382638,190.143344 207.587075,212.60722 C187.068785,234.793763 171.541431,261.140284 161.005012,291.646781 C150.745868,321.875947 145.893569,355.155762 146.448118,391.486227 C147.557214,427.53936 154.073158,459.98718 165.995948,488.829687 C177.28032,516.727243 194.28181,541.951359 215.9053,562.877276 C237.610095,583.620856 263.386636,599.628298 291.601152,609.885015 C320.714941,620.700955 352.601472,626.108925 387.260744,626.108925 C406.669937,626.108925 425.940493,623.89027 445.072411,619.452962 C464.481604,615.292985 482.227152,609.330351 498.309054,601.565061 L522.015997,666.460701 C500.665885,676.444645 478.206676,683.793938 454.63837,688.508578 C431.262297,693.509483 407.422332,696.019474 383.517543,696 C338.321851,696 296.730724,688.508578 258.744161,673.532662 Z" 26 + id="SvgjsPath1017" 27 + fill="#0083FF" 28 + fill-rule="nonzero" 29 + ></path> 30 + <g 31 + id="SvgjsG1016" 32 + transform="translate(115.0733, 79.4049)" 33 + fill="#0083FF" 34 + > 35 + <path 36 + d="M329.828907,231.383849 L260.699401,110.602459 C251.63529,94.765511 234.795945,84.9961472 216.561535,84.9961472 L202.32856,84.9961472 C197.759942,84.9961472 194.291354,89.1141864 195.064554,93.6217527 L218.683383,231.383849 L146.044594,231.383849 L117.317307,191.126992 C112.543308,184.436292 104.83484,180.465281 96.6208666,180.465281 L77.9170684,180.465281 C74.6441326,180.465281 72.2041032,183.486025 72.8889188,186.689439 L90.5192033,269.169882 C90.542094,269.250079 90.5821529,269.322638 90.6349289,269.382467 C90.634293,269.446115 90.634293,269.509764 90.634293,269.572775 L90.6349289,269.760538 C90.5821529,269.821003 90.542094,269.892926 90.5192033,269.973759 L72.8889188,352.453566 C72.2041032,355.657617 74.6441326,358.677724 77.9170684,358.677724 L96.6208666,358.677724 C104.83484,358.677724 112.543308,354.706712 117.317307,348.016012 L146.042051,307.761702 L218.684019,307.761702 L195.064554,445.52889 C194.291354,450.036456 197.759942,454.154495 202.32856,454.154495 L216.561535,454.154495 C234.795945,454.154495 251.63529,444.385132 260.699401,428.548184 L329.83145,307.761702 L399.512242,307.761702 C415.470292,307.761702 431.243943,304.348885 445.777042,297.751748 L448.41584,296.553888 C458.994558,291.751631 465.788667,281.20003 465.788667,269.572775 C465.788667,257.946157 458.994558,247.394556 448.41584,242.592299 L445.777042,241.39444 C431.243943,234.797303 415.470292,231.383849 399.512242,231.383849 L329.828907,231.383849 Z" 37 + id="SvgjsPath1015" 38 + transform="translate(269.2809, 269.5753) rotate(-134) translate(-269.2809, -269.5753)" 39 + ></path> 40 + </g> 8 41 </g> 9 - </svg><style>@media (prefers-color-scheme: light) { :root { filter: none; } } 10 - @media (prefers-color-scheme: dark) { :root { filter: none; } } 11 - </style></svg> 42 + </svg><style> 43 + @media (prefers-color-scheme: light) { 44 + :root { 45 + filter: none; 46 + } 47 + } 48 + @media (prefers-color-scheme: dark) { 49 + :root { 50 + filter: none; 51 + } 52 + } 53 + </style> 54 + </svg>
+12 -4
static/icons/account.svg
··· 1 - <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#e3e3e3"> 2 - <path d="M0 0h24v24H0z" fill="none"/> 3 - <path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/> 4 - </svg> 1 + <svg 2 + xmlns="http://www.w3.org/2000/svg" 3 + height="24px" 4 + viewBox="0 0 24 24" 5 + width="24px" 6 + fill="#e3e3e3" 7 + > 8 + <path d="M0 0h24v24H0z" fill="none" /> 9 + <path 10 + d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z" 11 + /> 12 + </svg>
+9 -3
static/icons/bluesky.svg
··· 1 - <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="-20 -20 296 266" fill="none"> 2 - <path 1 + <svg 2 + xmlns="http://www.w3.org/2000/svg" 3 + width="24" 4 + height="24" 5 + viewBox="-20 -20 296 266" 6 + fill="none" 7 + > 8 + <path 3 9 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" 4 10 stroke="currentColor" 5 11 stroke-width="25" 6 12 fill="none" 7 13 stroke-linejoin="round" 8 14 /> 9 - </svg> 15 + </svg>
+29 -5
static/icons/info_bold.svg
··· 1 1 <?xml version="1.0" encoding="UTF-8"?> 2 - <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> 3 - <path d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> 4 - <path d="M12 16V12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> 5 - <path d="M12 8H12.01" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> 6 - </svg> 2 + <svg 3 + width="24" 4 + height="24" 5 + viewBox="0 0 24 24" 6 + fill="none" 7 + xmlns="http://www.w3.org/2000/svg" 8 + > 9 + <path 10 + d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z" 11 + stroke="currentColor" 12 + stroke-width="2" 13 + stroke-linecap="round" 14 + stroke-linejoin="round" 15 + /> 16 + <path 17 + d="M12 16V12" 18 + stroke="currentColor" 19 + stroke-width="2" 20 + stroke-linecap="round" 21 + stroke-linejoin="round" 22 + /> 23 + <path 24 + d="M12 8H12.01" 25 + stroke="currentColor" 26 + stroke-width="2" 27 + stroke-linecap="round" 28 + stroke-linejoin="round" 29 + /> 30 + </svg>
+21 -4
static/icons/plane-departure_bold.svg
··· 1 - <svg width="80" height="80" viewBox="4 20 70 36" fill="none" xmlns="http://www.w3.org/2000/svg"> 2 - <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" /> 3 - <path d="M7 64H75" stroke="#C2CCDE" stroke-width="6" stroke-linecap="round" stroke-linejoin="round" /> 4 - </svg> 1 + <svg 2 + width="80" 3 + height="80" 4 + viewBox="4 20 70 36" 5 + fill="none" 6 + xmlns="http://www.w3.org/2000/svg" 7 + > 8 + <path 9 + fill-rule="evenodd" 10 + clip-rule="evenodd" 11 + 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" 12 + fill="#FFFFFF" 13 + /> 14 + <path 15 + d="M7 64H75" 16 + stroke="#C2CCDE" 17 + stroke-width="6" 18 + stroke-linecap="round" 19 + stroke-linejoin="round" 20 + /> 21 + </svg>
+14 -3
static/icons/plane_bold.svg
··· 1 - <svg width="80" height="80" viewBox="8 10 64 60" fill="none" xmlns="http://www.w3.org/2000/svg"> 2 - <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" /> 3 - </svg> 1 + <svg 2 + width="80" 3 + height="80" 4 + viewBox="8 10 64 60" 5 + fill="none" 6 + xmlns="http://www.w3.org/2000/svg" 7 + > 8 + <path 9 + fill-rule="evenodd" 10 + clip-rule="evenodd" 11 + 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" 12 + fill="#C2CCDE" 13 + /> 14 + </svg>
+18 -4
static/icons/ticket_bold.svg
··· 1 - <svg width="80" height="80" viewBox="6 18 70 44" fill="none" xmlns="http://www.w3.org/2000/svg"> 2 - <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" /> 3 - <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" /> 4 - </svg> 1 + <svg 2 + width="80" 3 + height="80" 4 + viewBox="6 18 70 44" 5 + fill="none" 6 + xmlns="http://www.w3.org/2000/svg" 7 + > 8 + <path 9 + fill-rule="evenodd" 10 + clip-rule="evenodd" 11 + 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" 12 + fill="#FFFFFF" 13 + /> 14 + <path 15 + 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" 16 + fill="#FFFFFF" 17 + /> 18 + </svg>
+125 -116
static/styles.css
··· 2 2 @import url("https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=Space+Mono:ital,wght@0,400;0,700;1,400;1,700&display=swap"); 3 3 4 4 @font-face { 5 - font-family: "Skyfont"; 6 - src: url("fonts/skyfont.regular.otf") format("opentype"); 7 - font-weight: normal; 8 - font-style: normal; 5 + font-family: "Skyfont"; 6 + src: url("fonts/skyfont.regular.otf") format("opentype"); 7 + font-weight: normal; 8 + font-style: normal; 9 9 } 10 10 11 11 @font-face { 12 - font-family: "F25_Bank_Printer"; 13 - src: url("fonts/F25_Bank_Printer.ttf") format("truetype"); 14 - font-weight: normal; 15 - font-style: normal; 12 + font-family: "F25_Bank_Printer"; 13 + src: url("fonts/F25_Bank_Printer.ttf") format("truetype"); 14 + font-weight: normal; 15 + font-style: normal; 16 16 } 17 17 18 18 @tailwind base; ··· 20 20 @tailwind utilities; 21 21 22 22 @keyframes fadeOut { 23 - 0% { 24 - opacity: 1; 25 - } 26 - 75% { 27 - opacity: 1; 28 - } /* Hold full opacity for most of the animation */ 29 - 100% { 30 - opacity: 0; 31 - } 23 + 0% { 24 + opacity: 1; 25 + } 26 + 75% { 27 + opacity: 1; 28 + } /* Hold full opacity for most of the animation */ 29 + 100% { 30 + opacity: 0; 31 + } 32 32 } 33 33 34 34 .status-message-fade { 35 - animation: fadeOut 2s forwards; 35 + animation: fadeOut 2s forwards; 36 36 } 37 37 38 38 .font-spectral { 39 - font-family: "Spectral", serif; 39 + font-family: "Spectral", serif; 40 40 } 41 41 42 42 .grow-wrap { 43 - /* easy way to plop the elements on top of each other and have them both sized based on the tallest one's height */ 44 - display: grid; 43 + /* easy way to plop the elements on top of each other and have them both sized based on the tallest one's height */ 44 + display: grid; 45 45 } 46 46 .grow-wrap::after { 47 - /* Note the weird space! Needed to preventy jumpy behavior */ 48 - content: attr(data-replicated-value) " "; 47 + /* Note the weird space! Needed to preventy jumpy behavior */ 48 + content: attr(data-replicated-value) " "; 49 49 50 - /* This is how textarea text behaves */ 51 - white-space: pre-wrap; 50 + /* This is how textarea text behaves */ 51 + white-space: pre-wrap; 52 52 53 - /* Hidden from view, clicks, and screen readers */ 54 - visibility: hidden; 53 + /* Hidden from view, clicks, and screen readers */ 54 + visibility: hidden; 55 55 } 56 56 .grow-wrap > textarea { 57 - /* You could leave this, but after a user resizes, then it ruins the auto sizing */ 58 - resize: none; 57 + /* You could leave this, but after a user resizes, then it ruins the auto sizing */ 58 + resize: none; 59 59 60 - /* Firefox shows scrollbar on growth, you can hide like this. */ 61 - overflow: hidden; 60 + /* Firefox shows scrollbar on growth, you can hide like this. */ 61 + overflow: hidden; 62 62 } 63 63 .grow-wrap > textarea, 64 64 .grow-wrap::after { 65 - /* Identical styling required!! */ 66 - font: inherit; 65 + /* Identical styling required!! */ 66 + font: inherit; 67 67 68 - /* Place on top of each other */ 69 - grid-area: 1 / 1 / 2 / 2; 68 + /* Place on top of each other */ 69 + grid-area: 1 / 1 / 2 / 2; 70 70 } 71 71 72 72 /* Base styling */ 73 73 @layer base { 74 - body { 75 - @apply bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100; 76 - font-family: Space Mono; 77 - } 78 - button { 79 - @apply rounded-xl; 80 - } 74 + body { 75 + @apply bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100; 76 + font-family: Space Mono; 77 + } 78 + button { 79 + @apply rounded-xl; 80 + } 81 81 82 - input { 83 - @apply px-4 py-2; 84 - } 82 + input { 83 + @apply px-4 py-2; 84 + } 85 85 86 - h1, 87 - h2, 88 - h3, 89 - h4, 90 - h5 { 91 - font-family: 92 - Share Tech Mono, 93 - monospace; 94 - } 86 + h1, 87 + h2, 88 + h3, 89 + h4, 90 + h5 { 91 + font-family: Share Tech Mono, monospace; 92 + } 95 93 } 96 94 97 95 .ticket { 98 - font-family: F25_Bank_Printer, monospace; 99 - @apply bg-white dark:bg-gray-800 p-8 relative overflow-hidden; 100 - position: relative; 101 - /* Angled corners */ 102 - clip-path: polygon( 103 - 20px 0, 104 - /* Top left corner */ calc(100% - 20px) 0, 105 - /* Top right corner */ 100% 20px, 106 - /* Top right */ 100% calc(100% - 20px), 107 - /* Bottom right */ calc(100% - 20px) 100%, 108 - /* Bottom right corner */ 20px 100%, 109 - /* Bottom left corner */ 0 calc(100% - 20px), 110 - /* Bottom left */ 0 20px /* Back to top left */ 111 - ); 96 + font-family: F25_Bank_Printer, monospace; 97 + @apply bg-white dark:bg-gray-800 p-8 relative overflow-hidden; 98 + position: relative; 99 + /* Angled corners */ 100 + clip-path: polygon( 101 + 20px 0, 102 + /* Top left corner */ calc(100% - 20px) 0, 103 + /* Top right corner */ 100% 20px, 104 + /* Top right */ 100% calc(100% - 20px), 105 + /* Bottom right */ calc(100% - 20px) 100%, 106 + /* Bottom right corner */ 20px 100%, 107 + /* Bottom left corner */ 0 calc(100% - 20px), 108 + /* Bottom left */ 0 20px /* Back to top left */ 109 + ); 112 110 } 113 111 114 112 /* Create side perforations using pseudo-elements */ 115 113 .ticket::before, 116 114 .ticket::after { 117 - content: ""; 118 - position: absolute; 119 - top: 30px; 120 - bottom: 30px; 121 - width: 1px; 122 - background-image: linear-gradient( 123 - to bottom, 124 - transparent 0%, 125 - transparent 40%, 126 - currentColor 40%, 127 - currentColor 60%, 128 - transparent 60%, 129 - transparent 100% 130 - ); 131 - background-size: 100% 20px; 132 - background-repeat: repeat-y; 133 - opacity: 0.2; 115 + content: ""; 116 + position: absolute; 117 + top: 30px; 118 + bottom: 30px; 119 + width: 1px; 120 + background-image: linear-gradient( 121 + to bottom, 122 + transparent 0%, 123 + transparent 40%, 124 + currentColor 40%, 125 + currentColor 60%, 126 + transparent 60%, 127 + transparent 100% 128 + ); 129 + background-size: 100% 20px; 130 + background-repeat: repeat-y; 131 + opacity: 0.2; 134 132 } 135 133 136 134 .ticket::before { 137 - left: 8px; 135 + left: 8px; 138 136 } 139 137 140 138 .ticket::after { 141 - right: 8px; 139 + right: 8px; 142 140 } 143 141 144 142 .dark .ticket { 145 - background-image: 146 - radial-gradient( 147 - circle at 10px center, 148 - rgb(17 24 39) 4px, 149 - transparent 4px 150 - ), 151 - radial-gradient( 152 - circle at calc(100% - 10px) center, 153 - rgb(17 24 39) 4px, 154 - transparent 4px 155 - ); 143 + background-image: 144 + radial-gradient( 145 + circle at 10px center, 146 + rgb(17 24 39) 4px, 147 + transparent 4px 148 + ), 149 + radial-gradient( 150 + circle at calc(100% - 10px) center, 151 + rgb(17 24 39) 4px, 152 + transparent 4px 153 + ); 156 154 } 157 155 158 156 /* Remove the previous background images and corner cuts */ 159 157 .ticket::before, 160 158 .ticket::after { 161 - display: none; 159 + display: none; 162 160 } 163 161 164 162 .boarding-label { 165 - @apply absolute top-2 right-2 bg-blue-100 dark:bg-blue-900 px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider; 166 - transform: rotate(2deg); 163 + @apply absolute top-2 right-2 bg-blue-100 dark:bg-blue-900 px-3 py-1 164 + rounded-full text-xs font-bold uppercase tracking-wider; 165 + transform: rotate(2deg); 167 166 } 168 167 169 168 .flight-info { 170 - @apply flex justify-between items-center mt-4 pt-4 border-t border-dashed; 169 + @apply flex justify-between items-center mt-4 pt-4 border-t border-dashed; 171 170 } 172 171 173 172 .passenger-info { 174 - @apply text-sm text-gray-600 dark:text-gray-400 mt-2; 173 + @apply text-sm text-gray-600 dark:text-gray-400 mt-2; 175 174 } 176 175 177 176 /* Modern Airport Sign Styles */ 178 177 .airport-sign { 179 - position: relative; 180 - transform-origin: top; 181 - transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); 182 - border-radius: 0.5rem; 183 - backdrop-filter: blur(8px); 178 + position: relative; 179 + transform-origin: top; 180 + transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); 181 + border-radius: 0.5rem; 182 + backdrop-filter: blur(8px); 184 183 } 185 184 186 185 /* Dropdown panel styles */ 187 186 .airport-sign + div { 188 - border-radius: 0.5rem; 189 - backdrop-filter: blur(8px); 187 + border-radius: 0.5rem; 188 + backdrop-filter: blur(8px); 190 189 } 191 190 192 191 /* Remove old texture styles */ 193 192 .airport-sign, 194 193 .airport-sign + div { 195 - background-blend-mode: overlay; 194 + background-blend-mode: overlay; 196 195 } 197 196 198 197 @keyframes popin { 199 - 0% { opacity: 0; transform: scale(0.95); } 200 - 100% { opacity: 1; transform: scale(1); } 198 + 0% { 199 + opacity: 0; 200 + transform: scale(0.95); 201 + } 202 + 100% { 203 + opacity: 1; 204 + transform: scale(1); 205 + } 201 206 } 202 207 .animate-popin { 203 - animation: popin 0.25s cubic-bezier(0.4,0,0.2,1); 208 + animation: popin 0.25s cubic-bezier(0.4, 0, 0.2, 1); 204 209 } 205 210 @keyframes bounce-short { 206 - 0%, 100% { transform: translateY(0); } 207 - 50% { transform: translateY(-8px); } 211 + 0%, 100% { 212 + transform: translateY(0); 213 + } 214 + 50% { 215 + transform: translateY(-8px); 216 + } 208 217 } 209 218 .animate-bounce-short { 210 219 animation: bounce-short 0.5s;