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}