Graphical PDS migrator for AT Protocol
1import { useEffect, useState } from "preact/hooks"; 2import { MigrationStateInfo } from "../lib/migration-types.ts"; 3import AccountCreationStep from "./migration-steps/AccountCreationStep.tsx"; 4import DataMigrationStep from "./migration-steps/DataMigrationStep.tsx"; 5import IdentityMigrationStep from "./migration-steps/IdentityMigrationStep.tsx"; 6import FinalizationStep from "./migration-steps/FinalizationStep.tsx"; 7import MigrationCompletion from "../components/MigrationCompletion.tsx"; 8 9/** 10 * The migration progress props. 11 * @type {MigrationProgressProps} 12 */ 13interface MigrationProgressProps { 14 service: string; 15 handle: string; 16 email: string; 17 password: string; 18 invite?: string; 19} 20 21/** 22 * The migration progress component. 23 * @param props - The migration progress props 24 * @returns The migration progress component 25 * @component 26 */ 27export default function MigrationProgress(props: MigrationProgressProps) { 28 const [migrationState, setMigrationState] = useState< 29 MigrationStateInfo | null 30 >(null); 31 const [currentStep, setCurrentStep] = useState(0); 32 const [completedSteps, setCompletedSteps] = useState<Set<number>>(new Set()); 33 const [hasError, setHasError] = useState(false); 34 35 const credentials = { 36 service: props.service, 37 handle: props.handle, 38 email: props.email, 39 password: props.password, 40 invite: props.invite, 41 }; 42 43 const validateParams = () => { 44 if (!props.service?.trim()) { 45 setHasError(true); 46 return false; 47 } 48 if (!props.handle?.trim()) { 49 setHasError(true); 50 return false; 51 } 52 if (!props.email?.trim()) { 53 setHasError(true); 54 return false; 55 } 56 if (!props.password?.trim()) { 57 setHasError(true); 58 return false; 59 } 60 return true; 61 }; 62 63 useEffect(() => { 64 console.log("Starting migration with props:", { 65 service: props.service, 66 handle: props.handle, 67 email: props.email, 68 hasPassword: !!props.password, 69 invite: props.invite, 70 }); 71 72 // Check migration state first 73 const checkMigrationState = async () => { 74 try { 75 const migrationResponse = await fetch("/api/migration-state"); 76 if (migrationResponse.ok) { 77 const migrationData = await migrationResponse.json(); 78 setMigrationState(migrationData); 79 80 if (!migrationData.allowMigration) { 81 setHasError(true); 82 return; 83 } 84 } 85 } catch (error) { 86 console.error("Failed to check migration state:", error); 87 setHasError(true); 88 return; 89 } 90 91 if (!validateParams()) { 92 console.log("Parameter validation failed"); 93 return; 94 } 95 96 // Start with the first step 97 setCurrentStep(0); 98 }; 99 100 checkMigrationState(); 101 }, []); 102 103 const handleStepComplete = (stepIndex: number) => { 104 console.log(`Step ${stepIndex} completed`); 105 setCompletedSteps((prev) => new Set([...prev, stepIndex])); 106 107 // Move to next step if not the last one 108 if (stepIndex < 3) { 109 setCurrentStep(stepIndex + 1); 110 } 111 }; 112 113 const handleStepError = ( 114 stepIndex: number, 115 error: string, 116 isVerificationError?: boolean, 117 ) => { 118 console.error(`Step ${stepIndex} error:`, error, { isVerificationError }); 119 // Errors are handled within each step component 120 }; 121 122 const isStepActive = (stepIndex: number) => { 123 return currentStep === stepIndex && !hasError; 124 }; 125 126 const _isStepCompleted = (stepIndex: number) => { 127 return completedSteps.has(stepIndex); 128 }; 129 130 const allStepsCompleted = completedSteps.size === 4; 131 132 return ( 133 <div class="space-y-8"> 134 {/* Migration state alert */} 135 {migrationState && !migrationState.allowMigration && ( 136 <div 137 class={`p-4 rounded-lg border ${ 138 migrationState.state === "maintenance" 139 ? "bg-yellow-50 border-yellow-200 text-yellow-800 dark:bg-yellow-900/20 dark:border-yellow-800 dark:text-yellow-200" 140 : "bg-red-50 border-red-200 text-red-800 dark:bg-red-900/20 dark:border-red-800 dark:text-red-200" 141 }`} 142 > 143 <div class="flex items-center"> 144 <div 145 class={`mr-3 ${ 146 migrationState.state === "maintenance" 147 ? "text-yellow-600 dark:text-yellow-400" 148 : "text-red-600 dark:text-red-400" 149 }`} 150 > 151 {migrationState.state === "maintenance" ? "⚠️" : "🚫"} 152 </div> 153 <div> 154 <h3 class="font-semibold mb-1"> 155 {migrationState.state === "maintenance" 156 ? "Maintenance Mode" 157 : "Service Unavailable"} 158 </h3> 159 <p class="text-sm">{migrationState.message}</p> 160 </div> 161 </div> 162 </div> 163 )} 164 165 <div class="space-y-4"> 166 <AccountCreationStep 167 credentials={credentials} 168 onStepComplete={() => handleStepComplete(0)} 169 onStepError={(error, isVerificationError) => 170 handleStepError(0, error, isVerificationError)} 171 isActive={isStepActive(0)} 172 /> 173 174 <DataMigrationStep 175 credentials={credentials} 176 onStepComplete={() => handleStepComplete(1)} 177 onStepError={(error, isVerificationError) => 178 handleStepError(1, error, isVerificationError)} 179 isActive={isStepActive(1)} 180 /> 181 182 <IdentityMigrationStep 183 credentials={credentials} 184 onStepComplete={() => handleStepComplete(2)} 185 onStepError={(error, isVerificationError) => 186 handleStepError(2, error, isVerificationError)} 187 isActive={isStepActive(2)} 188 /> 189 190 <FinalizationStep 191 credentials={credentials} 192 onStepComplete={() => handleStepComplete(3)} 193 onStepError={(error, isVerificationError) => 194 handleStepError(3, error, isVerificationError)} 195 isActive={isStepActive(3)} 196 /> 197 </div> 198 199 <MigrationCompletion isVisible={allStepsCompleted} /> 200 </div> 201 ); 202}