Graphical PDS migrator for AT Protocol
1import { IS_BROWSER } from "fresh/runtime"; 2import { ComponentChildren } from "preact"; 3 4export type StepStatus = 5 | "pending" 6 | "in-progress" 7 | "verifying" 8 | "completed" 9 | "error"; 10 11export interface MigrationStepProps { 12 name: string; 13 status: StepStatus; 14 error?: string; 15 isVerificationError?: boolean; 16 index: number; 17 onRetryVerification?: (index: number) => void; 18 children?: ComponentChildren; 19} 20 21export function MigrationStep({ 22 name, 23 status, 24 error, 25 isVerificationError, 26 index, 27 onRetryVerification, 28 children, 29}: MigrationStepProps) { 30 return ( 31 <div key={name} class={getStepClasses(status)}> 32 {getStepIcon(status)} 33 <div class="flex-1"> 34 <p 35 class={`font-medium ${ 36 status === "error" 37 ? "text-red-900 dark:text-red-200" 38 : status === "completed" 39 ? "text-green-900 dark:text-green-200" 40 : status === "in-progress" 41 ? "text-blue-900 dark:text-blue-200" 42 : "text-gray-900 dark:text-gray-200" 43 }`} 44 > 45 {getStepDisplayName( 46 { name, status, error, isVerificationError }, 47 index, 48 )} 49 </p> 50 {error && ( 51 <div class="mt-1"> 52 <p class="text-sm text-red-600 dark:text-red-400"> 53 {(() => { 54 try { 55 const err = JSON.parse(error); 56 return err.message || error; 57 } catch { 58 return error; 59 } 60 })()} 61 </p> 62 {isVerificationError && onRetryVerification && ( 63 <div class="flex space-x-2 mt-2"> 64 <button 65 type="button" 66 onClick={() => onRetryVerification(index)} 67 class="px-3 py-1 text-xs bg-blue-600 hover:bg-blue-700 text-white rounded transition-colors duration-200 dark:bg-blue-500 dark:hover:bg-blue-400" 68 disabled={!IS_BROWSER} 69 > 70 Retry Verification 71 </button> 72 </div> 73 )} 74 </div> 75 )} 76 {children} 77 </div> 78 </div> 79 ); 80} 81 82function getStepDisplayName( 83 step: Pick< 84 MigrationStepProps, 85 "name" | "status" | "error" | "isVerificationError" 86 >, 87 index: number, 88) { 89 if (step.status === "completed") { 90 switch (index) { 91 case 0: 92 return "Account Created"; 93 case 1: 94 return "Data Migrated"; 95 case 2: 96 return "Identity Migrated"; 97 case 3: 98 return "Migration Finalized"; 99 } 100 } 101 102 if (step.status === "in-progress") { 103 switch (index) { 104 case 0: 105 return "Creating your new account..."; 106 case 1: 107 return "Migrating your data..."; 108 case 2: 109 return step.name === 110 "Enter the token sent to your email to complete identity migration" 111 ? step.name 112 : "Migrating your identity..."; 113 case 3: 114 return "Finalizing migration..."; 115 } 116 } 117 118 if (step.status === "verifying") { 119 switch (index) { 120 case 0: 121 return "Verifying account creation..."; 122 case 1: 123 return "Verifying data migration..."; 124 case 2: 125 return "Verifying identity migration..."; 126 case 3: 127 return "Verifying migration completion..."; 128 } 129 } 130 131 return step.name; 132} 133 134function getStepIcon(status: StepStatus) { 135 switch (status) { 136 case "pending": 137 return ( 138 <div class="w-8 h-8 rounded-full border-2 border-gray-300 dark:border-gray-600 flex items-center justify-center"> 139 <div class="w-3 h-3 rounded-full bg-gray-300 dark:bg-gray-600" /> 140 </div> 141 ); 142 case "in-progress": 143 return ( 144 <div class="w-8 h-8 rounded-full border-2 border-blue-500 border-t-transparent animate-spin flex items-center justify-center"> 145 <div class="w-3 h-3 rounded-full bg-blue-500" /> 146 </div> 147 ); 148 case "verifying": 149 return ( 150 <div class="w-8 h-8 rounded-full border-2 border-yellow-500 border-t-transparent animate-spin flex items-center justify-center"> 151 <div class="w-3 h-3 rounded-full bg-yellow-500" /> 152 </div> 153 ); 154 case "completed": 155 return ( 156 <div class="w-8 h-8 rounded-full bg-green-500 flex items-center justify-center"> 157 <svg 158 class="w-5 h-5 text-white" 159 fill="none" 160 stroke="currentColor" 161 viewBox="0 0 24 24" 162 > 163 <path 164 stroke-linecap="round" 165 stroke-linejoin="round" 166 stroke-width="2" 167 d="M5 13l4 4L19 7" 168 /> 169 </svg> 170 </div> 171 ); 172 case "error": 173 return ( 174 <div class="w-8 h-8 rounded-full bg-red-500 flex items-center justify-center"> 175 <svg 176 class="w-5 h-5 text-white" 177 fill="none" 178 stroke="currentColor" 179 viewBox="0 0 24 24" 180 > 181 <path 182 stroke-linecap="round" 183 stroke-linejoin="round" 184 stroke-width="2" 185 d="M6 18L18 6M6 6l12 12" 186 /> 187 </svg> 188 </div> 189 ); 190 } 191} 192 193function getStepClasses(status: StepStatus) { 194 const baseClasses = 195 "flex items-center space-x-3 p-4 rounded-lg transition-colors duration-200"; 196 switch (status) { 197 case "pending": 198 return `${baseClasses} bg-gray-50 dark:bg-gray-800`; 199 case "in-progress": 200 return `${baseClasses} bg-blue-50 dark:bg-blue-900`; 201 case "verifying": 202 return `${baseClasses} bg-yellow-50 dark:bg-yellow-900`; 203 case "completed": 204 return `${baseClasses} bg-green-50 dark:bg-green-900`; 205 case "error": 206 return `${baseClasses} bg-red-50 dark:bg-red-900`; 207 } 208}