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}