Monorepo for Tangled tangled.org
5

Configure Feed

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

appview: update larger element on htmx handlers

also see
https://discord.com/channels/1361963801993285692/1361991850327674932/1516041992570929285

Signed-off-by: Seongmin Lee <git@boltless.me>

author
Seongmin Lee
date (Jun 16, 2026, 2:08 AM +0900) commit ff8e2e0e parent 0d315506 change-id tuvnvtnm
+197 -184
+41 -45
appview/accountmigration/accountmigration.go
··· 71 71 if err := s.pages.AccountMigrateFromGitHub(w, pages.AccountMigrateFromGitHubParams{ 72 72 LoggedInUser: user, 73 73 Knots: knots, 74 - Repos: []pages.RepoImportParams{ 75 - { 76 - SourceKind: pages.RepoImportSourceGitHub, 77 - CloneUrl: "https://github.com/boltlessengineer/rest.nvim", 78 - Name: "rest.nvim", 79 - Description: "A very fast, powerful, extensible and asynchronous Neovim HTTP client written in Lua.", 80 - Website: "https://tangled.org", 81 - Topics: []string{"lua", "neovim", "curl", "http-client", "nvim", "neovim-plugin", "rest-client"}, 82 - Stars: 0, 83 - Selected: true, 84 - }, 85 - { 86 - SourceKind: pages.RepoImportSourceGitHub, 87 - CloneUrl: "https://github.com/boltlessengineer/dot", 88 - Name: "super-long-repo-name-fddasfsafdsfasdfaasdfasdfasfasdfasfdasdfasdfsadfsadfasdfaasdfasfasdfdsafd00000s", 89 - Description: "dotfiles", 90 - Website: "https://tangled.org", 91 - Topics: []string{}, 92 - Stars: 12, 93 - Selected: true, 94 - }, 95 - }, 96 74 }); err != nil { 97 75 s.logger.Error("failed to render", "err", err) 98 76 } ··· 119 97 } 120 98 121 99 func (s *AccountMigration) listGitHubRepos(w http.ResponseWriter, r *http.Request) { 100 + var notice string 101 + var params pages.AccountMigrateRepoListParams 102 + defer func() { 103 + s.pages.Notice(w, "listghrepos-error", notice) 104 + s.pages.AccountMigrateRepoListFragment(w, params) 105 + }() 106 + 122 107 user := s.oauth.GetMultiAccountUser(r) 123 108 124 109 username := strings.TrimSpace(r.FormValue("username")) 125 110 if username == "" { 126 - s.pages.Notice(w, "migrate-error", "GitHub username is required.") 111 + notice = "GitHub username is required." 127 112 return 128 113 } 129 114 ··· 134 119 endpoint := fmt.Sprintf("https://api.github.com/users/%s/repos?%s", url.PathEscape(username), query.Encode()) 135 120 req, err := http.NewRequestWithContext(r.Context(), http.MethodGet, endpoint, nil) 136 121 if err != nil { 137 - s.pages.Notice(w, "migrate-error", "Failed to build GitHub request.") 122 + notice = "Failed to build GitHub request." 138 123 return 139 124 } 140 125 req.Header.Set("Accept", "application/vnd.github+json") 141 126 142 127 resp, err := http.DefaultClient.Do(req) 143 128 if err != nil { 144 - s.logger.Error("github list repos failed", "username", username, "err", err) 145 - s.pages.Notice(w, "migrate-error", "Failed to reach GitHub. Try again.") 129 + s.logger.Error("github list repos request failed", "username", username, "err", err) 130 + notice = "Failed to reach GitHub. Try again." 146 131 return 147 132 } 148 133 defer resp.Body.Close() 149 134 150 135 if resp.StatusCode != http.StatusOK { 151 - s.pages.Notice(w, "migrate-error", fmt.Sprintf("GitHub returned %d. Check the username.", resp.StatusCode)) 136 + notice = fmt.Sprintf("GitHub returned %d. Check the username.", resp.StatusCode) 152 137 return 153 138 } 154 139 155 140 var githubRepos []githubUserRepo 156 141 if err := json.NewDecoder(resp.Body).Decode(&githubRepos); err != nil { 157 142 s.logger.Error("decode github response failed", "err", err) 158 - s.pages.Notice(w, "migrate-error", "Failed to parse GitHub response.") 143 + notice = "Failed to parse GitHub response." 159 144 return 160 145 } 161 146 ··· 199 184 return b.PushedAt.Compare(a.PushedAt) 200 185 }) 201 186 202 - if err := s.pages.AccountMigrateRepoListFragment(w, pages.AccountMigrateRepoListParams{ 187 + params = pages.AccountMigrateRepoListParams{ 203 188 Repos: importRepos, 204 189 Knots: knots, 205 - }); err != nil { 206 - s.logger.Error("render repo-list fragment failed", "err", err) 207 190 } 191 + // render is deferred 208 192 } 209 193 210 194 func (s *AccountMigration) startMigration(w http.ResponseWriter, r *http.Request) { 195 + var notice string 196 + defer func() { 197 + s.pages.Notice(w, "migration-error", notice) 198 + }() 199 + 211 200 user := s.oauth.GetMultiAccountUser(r) 212 201 213 202 if err := r.ParseForm(); err != nil { 214 - s.pages.Notice(w, "migrate-error", "Invalid form submission.") 203 + notice = "Invalid form submission." 215 204 return 216 205 } 217 206 218 207 knots, err := s.enforcer.GetKnotsForUser(user.Did) 219 208 if err != nil { 220 209 s.logger.Error("knots lookup failed", "did", user.Did, "err", err) 221 - s.pages.Notice(w, "migrate-error", "Failed to look up your knots.") 210 + notice = "Failed to look up your knots." 222 211 return 223 212 } 224 213 allowed := sets.Collect(slices.Values(knots)) 225 214 226 215 sessionId := s.oauth.GetSessIdFromCookie(r) 227 216 if sessionId == "" { 228 - s.pages.Notice(w, "migrate-error", "Session expired. Log in again.") 217 + notice = "Session expired. Log in again." 229 218 return 230 219 } 231 220 232 221 count, err := strconv.Atoi(r.FormValue("count")) 233 222 if err != nil || count <= 0 { 234 - s.pages.Notice(w, "migrate-error", "Invalid form submission.") 223 + notice = "Invalid form submission." 235 224 return 236 225 } 237 226 ··· 253 242 website := strings.TrimSpace(r.FormValue(fmt.Sprintf("website_%d", i))) 254 243 topics := r.FormValue(fmt.Sprintf("topics_%d", i)) 255 244 256 - if cloneUrl == "" || knot == "" || name == "" { 257 - s.pages.Notice(w, "migrate-error", "Each selected row needs a name, clone URL, and knot.") 245 + if cloneUrl == "" { 246 + notice = fmt.Sprintf("Row %d: clone URL is missing.", i+1) 247 + return 248 + } 249 + if knot == "" { 250 + notice = fmt.Sprintf("Row %d: knot is missing.", i+1) 251 + return 252 + } 253 + if name == "" { 254 + notice = fmt.Sprintf("Row %d: name is missing.", i+1) 258 255 return 259 256 } 260 257 if err := models.ValidateRepoName(name); err != nil { 261 - s.pages.Notice(w, "migrate-error", fmt.Sprintf("Row %d: %s", i+1, err.Error())) 258 + notice = fmt.Sprintf("Row %d: %s", i+1, err.Error()) 262 259 return 263 260 } 264 261 if len([]rune(desc)) > 140 { 265 - s.pages.Notice(w, "migrate-error", fmt.Sprintf("Row %d: description must be 140 characters or fewer.", i+1)) 262 + notice = fmt.Sprintf("Row %d: description must be 140 characters or fewer.", i+1) 266 263 return 267 264 } 268 265 if !allowed.Contains(knot) { 269 - s.pages.Notice(w, "migrate-error", fmt.Sprintf("You are not a member of knot %q.", knot)) 266 + notice = fmt.Sprintf("Row %d: You are not a member of knot %q.", i+1, knot) 270 267 return 271 268 } 272 269 if seen.Contains(name) { 273 - s.pages.Notice(w, "migrate-error", fmt.Sprintf("Duplicate repository name %q in selection.", name)) 270 + notice = fmt.Sprintf("Row %d: Duplicate repository name %q in selection.", i+1, name) 274 271 return 275 272 } 276 273 seen.Insert(name) ··· 302 299 } 303 300 304 301 if len(rows) == 0 { 305 - s.pages.Notice(w, "migrate-error", "Pick at least one repo.") 302 + notice = "Pick at least one repo." 306 303 return 307 304 } 308 305 ··· 310 307 311 308 if err := db.InsertGitRepoMigrations(r.Context(), s.db, rows); err != nil { 312 309 s.logger.Error("insert migrations failed", "err", err) 313 - s.pages.Notice(w, "migrate-error", "Failed to enqueue migrations.") 310 + notice = "Failed to enqueue migrations." 314 311 return 315 312 } 316 313 317 - w.Header().Set("HX-Redirect", "/settings/migration") 318 - http.Redirect(w, r, "/settings/migration", http.StatusSeeOther) 314 + s.pages.HxRedirect(w, "/settings/migration") 319 315 } 320 316 321 317 func (s *AccountMigration) progressRows(w http.ResponseWriter, r *http.Request) {
+1 -2
appview/pages/pages.go
··· 1811 1811 type AccountMigrateFromGitHubParams struct { 1812 1812 LoggedInUser *oauth.MultiAccountUser 1813 1813 Knots []string 1814 - Repos []RepoImportParams 1815 1814 } 1816 1815 1817 1816 type RepoImportSource string ··· 1845 1844 } 1846 1845 1847 1846 func (p *Pages) AccountMigrateRepoListFragment(w io.Writer, params AccountMigrateRepoListParams) error { 1848 - return p.executePlain("account/migrate/fragments/repoList", w, params) 1847 + return p.executePlain("account/migrate/fragments/step2", w, params) 1849 1848 } 1850 1849 1851 1850 type AccountMigrateProgressParams struct {
-93
appview/pages/templates/account/migrate/fragments/repoList.html
··· 1 - {{ define "account/migrate/fragments/repoList" }} 2 - {{ if not .Repos }} 3 - <p class="text-gray-500 dark:text-gray-400 mb-4">No importable repositories found.</p> 4 - {{ else }} 5 - <form action="/account/migrate/start" method="post" class="flex flex-col gap-3"> 6 - <input type="hidden" name="count" value="{{ len .Repos }}" /> 7 - 8 - <div class="w-full border border-gray-200 dark:border-gray-700 rounded rouded-sm divide-y divide-gray-200 dark:divide-gray-700"> 9 - {{ range $i, $repo := .Repos }} 10 - <details class="group"> 11 - <summary class="p-2 pl-3 flex items-center justify-between gap-2 cursor-pointer"> 12 - <div class="flex gap-2 min-w-0"> 13 - <label class="text-base py-0 min-w-0 flex items-center gap-2"> 14 - <input type="checkbox" id="selected_{{ $i }}" name="selected_{{ $i }}" value="1" {{ if $repo.Selected }}checked{{ end }} class="shrink-0" /> 15 - <span class="min-w-0 truncate">{{ $repo.Name }}</span> 16 - </label> 17 - {{ if gt .Stars 0 }} 18 - <div class="flex items-center gap-1"> 19 - {{ i "star" "size-3 fill-current" }} 20 - <span class="text-sm">{{ scaleFmt .Stars }}</span> 21 - </div> 22 - {{ end }} 23 - </div> 24 - 25 - <div class="flex items-center gap-2 shrink-0"> 26 - <!-- 27 - <span class="hidden sm:block text-gray-600 dark:text-gray-400"> 28 - Updated 2 days ago 29 - </span> 30 - --> 31 - <div class="p-1"> 32 - {{ i "chevron-right" "size-4 group-open:hidden" }} 33 - {{ i "chevron-down" "size-4 hidden group-open:block" }} 34 - </div> 35 - </div> 36 - </summary> 37 - <div class="border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900/50 pt-2 pb-4 px-3 flex flex-col gap-4"> 38 - <input type="hidden" name="website_{{ $i }}" value="{{ $repo.Website }}" /> 39 - <input type="hidden" name="topics_{{ $i }}" value="{{ join $repo.Topics "," }}" /> 40 - 41 - <label class="text-base py-0 space-y-1.5"> 42 - <span>Clone URL</span> 43 - <input type="text" name="clone_url_{{ $i }}" value="{{ $repo.CloneUrl }}" disabled class="w-full px-2 py-1" /> 44 - </label> 45 - 46 - <label class="text-base py-0 space-y-1.5"> 47 - <span>Name</span> 48 - <input type="text" name="name_{{ $i }}" value="{{ $repo.Name }}" required placeholder="repository-name" class="bg-white dark:bg-gray-900 w-full px-2 py-1" /> 49 - </label> 50 - 51 - <label class="text-base py-0 space-y-1.5"> 52 - <span>Description</span> 53 - <textarea 54 - name="description_{{ $i }}" 55 - placeholder="repository description" 56 - class="bg-white dark:bg-gray-900 w-full px-2 py-1" 57 - >{{ $repo.Description }}</textarea> 58 - </label> 59 - 60 - <fieldset class="space-y-1"> 61 - <legend class="text-base">Select a knot to migrate into</legend> 62 - <div class="w-full space-y-2"> 63 - {{ range $.Knots }} 64 - <label class="py-0 block flex items-center gap-2"> 65 - <input type="radio" name="knot_{{ $i }}" value="{{ . }}" required {{if eq (len $.Knots) 1}}checked{{end}} /> 66 - <span class="lowercase py-0.5">{{ . }}</span> 67 - </label> 68 - {{ else }} 69 - <p class="dark:text-white">No knots available.</p> 70 - {{ end }} 71 - </div> 72 - </fieldset> 73 - </div> 74 - </details> 75 - {{ end }} 76 - </div> 77 - 78 - <div class="pt-3 flex justify-end"> 79 - <button type="submit" class="btn-create flex items-center gap-2" {{ if (not .Repos) }}disabled{{ end }}> 80 - {{ i "import" "size-4" }} 81 - {{ if gt (len .Repos) 1 }} 82 - Migrate {{ len .Repos }} Repositories 83 - {{ else }} 84 - Migrate Repository 85 - {{ end }} 86 - <span id="spinner" class="group"> 87 - {{ i "loader-circle" "size-4 animate-spin hidden group-[.htmx-request]:inline" }} 88 - </span> 89 - </button> 90 - </div> 91 - </form> 92 - {{ end }} 93 - {{ end }}
+134
appview/pages/templates/account/migrate/fragments/step2.html
··· 1 + {{ define "account/migrate/fragments/step2" }} 2 + {{- $isPreview := eq .Repos nil -}} 3 + <div 4 + id="migration-step-2" 5 + class="flex relative border-l border-gray-200 dark:border-gray-700 ml-3 pl-7" 6 + > 7 + <div class="absolute -left-3 -top-0"> 8 + {{ template "account/migrate/fragments/stepIndicator" 2 }} 9 + </div> 10 + 11 + <!-- Content column --> 12 + <div class="flex-1 min-w-0 pb-12 space-y-3"> 13 + <div class="flex flex-wrap items-center justify-between gap-3"> 14 + <h2 class="text-lg font-semibold uppercase">Choose &amp; Configure Repositories</h2> 15 + <div class="ml-auto h-7 flex items-center"> 16 + <button 17 + class="btn-flat flex items-center gap-2" 18 + onclick="document.querySelectorAll('input[name^=selected_]').forEach(c => c.checked = !c.checked)" 19 + {{ if $isPreview }}disabled{{ end }} 20 + > 21 + {{ i "check-check" "size-4" }} 22 + Select all repositories 23 + </button> 24 + </div> 25 + </div> 26 + {{ if $isPreview }} 27 + <p class="text-gray-500 dark:text-gray-400 mb-4">No repositories yet, connect your account first.</p> 28 + {{ else if gt (len .Repos) 0 }} 29 + <form 30 + hx-post="/account/migrate/start" 31 + hx-target="none" 32 + hx-swap="none" 33 + class="flex flex-col gap-3" 34 + > 35 + <input type="hidden" name="count" value="{{ len .Repos }}" /> 36 + 37 + <div class="w-full border border-gray-200 dark:border-gray-700 rounded rouded-sm divide-y divide-gray-200 dark:divide-gray-700"> 38 + {{ range $i, $repo := .Repos }} 39 + {{ template "repo-row" (list $i $repo $) }} 40 + {{ end }} 41 + </div> 42 + 43 + <div class="pt-3 flex justify-between items-center"> 44 + <span id="migrate-error" class="error"></span> 45 + <button type="submit" class="btn-create flex items-center gap-2" {{ if (not .Repos) }}disabled{{ end }}> 46 + {{ i "import" "size-4" }} 47 + {{ if gt (len .Repos) 1 }} 48 + Migrate {{ len .Repos }} Repositories 49 + {{ else }} 50 + Migrate Repository 51 + {{ end }} 52 + <span id="spinner" class="group"> 53 + {{ i "loader-circle" "size-4 animate-spin hidden group-[.htmx-request]:inline" }} 54 + </span> 55 + </button> 56 + </div> 57 + </form> 58 + {{ else }} 59 + <p class="text-gray-500 dark:text-gray-400 mb-4">No importable repositories found.</p> 60 + {{ end }} 61 + </div> 62 + </div> 63 + {{ end }} 64 + 65 + {{ define "repo-row" }} 66 + {{- $i := index . 0 }} 67 + {{- $repo := index . 1 }} 68 + {{- $root := index . 2 }} 69 + <details class="group"> 70 + <summary class="p-2 pl-3 flex items-center justify-between gap-2 cursor-pointer"> 71 + <div class="flex gap-2 min-w-0"> 72 + <label class="text-base py-0 min-w-0 flex items-center gap-2"> 73 + <input type="checkbox" id="selected_{{ $i }}" name="selected_{{ $i }}" value="1" {{ if $repo.Selected }}checked{{ end }} class="shrink-0" /> 74 + <span class="min-w-0 truncate">{{ $repo.Name }}</span> 75 + </label> 76 + {{ if gt $repo.Stars 0 }} 77 + <div class="flex items-center gap-1"> 78 + {{ i "star" "size-3 fill-current" }} 79 + <span class="text-sm">{{ scaleFmt $repo.Stars }}</span> 80 + </div> 81 + {{ end }} 82 + </div> 83 + 84 + <div class="flex items-center gap-2 shrink-0"> 85 + <!-- 86 + <span class="hidden sm:block text-gray-600 dark:text-gray-400"> 87 + Updated 2 days ago 88 + </span> 89 + --> 90 + <div class="p-1"> 91 + {{ i "chevron-right" "size-4 group-open:hidden" }} 92 + {{ i "chevron-down" "size-4 hidden group-open:block" }} 93 + </div> 94 + </div> 95 + </summary> 96 + <div class="border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900/50 pt-2 pb-4 px-3 flex flex-col gap-4"> 97 + <input type="hidden" name="website_{{ $i }}" value="{{ $repo.Website }}" /> 98 + <input type="hidden" name="topics_{{ $i }}" value="{{ join $repo.Topics "," }}" /> 99 + 100 + <label class="text-base py-0 space-y-1.5"> 101 + <span>Clone URL</span> 102 + <input type="text" name="clone_url_{{ $i }}" value="{{ $repo.CloneUrl }}" readonly class="w-full px-2 py-1" /> 103 + </label> 104 + 105 + <label class="text-base py-0 space-y-1.5"> 106 + <span>Name</span> 107 + <input type="text" name="name_{{ $i }}" value="{{ $repo.Name }}" required placeholder="repository-name" class="bg-white dark:bg-gray-900 w-full px-2 py-1" /> 108 + </label> 109 + 110 + <label class="text-base py-0 space-y-1.5"> 111 + <span>Description</span> 112 + <textarea 113 + name="description_{{ $i }}" 114 + placeholder="repository description" 115 + class="bg-white dark:bg-gray-900 w-full px-2 py-1" 116 + >{{ $repo.Description }}</textarea> 117 + </label> 118 + 119 + <fieldset class="space-y-1"> 120 + <legend class="text-base">Select a knot to migrate into</legend> 121 + <div class="w-full space-y-2"> 122 + {{ range $root.Knots }} 123 + <label class="py-0 block flex items-center gap-2"> 124 + <input type="radio" name="knot_{{ $i }}" value="{{ . }}" required {{if eq (len $root.Knots) 1}}checked{{end}} /> 125 + <span class="lowercase py-0.5">{{ . }}</span> 126 + </label> 127 + {{ else }} 128 + <p class="dark:text-white">No knots available.</p> 129 + {{ end }} 130 + </div> 131 + </fieldset> 132 + </div> 133 + </details> 134 + {{ end }}
+5
appview/pages/templates/account/migrate/fragments/stepIndicator.html
··· 1 + {{ define "account/migrate/fragments/stepIndicator" }} 2 + <div class="size-7 bg-gray-200 dark:bg-gray-600 rounded-full flex items-center justify-center font-medium"> 3 + {{.}} 4 + </div> 5 + {{ end }}
+16 -44
appview/pages/templates/account/migrate/fromGitHub.html
··· 14 14 <p>Migration covers your repos and commit history.<br>Issues and PRs aren't part of the import yet.</p> 15 15 </div> 16 16 <div class="bg-white dark:bg-gray-800 p-8 pb-0 rounded w-full mx-auto drop-shadow-sm dark:text-white"> 17 - {{ template "step-1" . }} 18 - {{ template "step-2" . }} 19 - 20 - <span id="migrate-error" class="error"></span> 21 - 22 17 {{ if not .Knots }} 23 18 <p class="text-gray-500 dark:text-gray-400"> 24 19 You aren't a member of any knot yet. Join or register a knot first. 25 20 </p> 21 + {{ else }} 22 + <div id="migration-steps"> 23 + {{ template "step-1" . }} 24 + {{ template "step-2" . }} 25 + </div> 26 26 {{ end }} 27 27 28 28 </div> 29 29 {{ end }} 30 30 31 31 {{ define "step-1" }} 32 - <div class="flex relative border-l border-gray-200 dark:border-gray-700 ml-3 pl-7"> 32 + <div 33 + id="migration-step-1" 34 + class="flex relative border-l border-gray-200 dark:border-gray-700 ml-3 pl-7" 35 + > 33 36 <div class="absolute -left-3 -top-0"> 34 - {{ template "numberCircle" 1 }} 37 + {{ template "account/migrate/fragments/stepIndicator" 1 }} 35 38 </div> 36 39 37 40 <!-- Content column --> 38 41 <form 39 42 hx-post="/account/migrate/listGitHubRepos" 40 - hx-target="#source-repos" 41 - hx-swap="innerHTML" 43 + hx-target="#migration-step-2" 44 + hx-swap="outerHTML" 42 45 class="flex-1 min-w-0 pb-12 space-y-3" 43 46 > 44 - 45 47 <h2 class="text-lg font-semibold uppercase dark:text-white">Connect Your Account</h2> 46 48 <div class="flex gap-3 w-full"> 47 49 <input ··· 57 59 {{ i "loader-circle" "size-4 animate-spin hidden group-[.htmx-request]:inline" }} 58 60 </button> 59 61 </div> 62 + <div> 63 + <span id="listghrepos-error" class="error"></span> 64 + </div> 60 65 </form> 61 66 </div> 62 67 {{ end }} 63 68 64 69 {{ define "step-2" }} 65 - <div class="flex relative border-l border-gray-200 dark:border-gray-700 ml-3 pl-7"> 66 - <div class="absolute -left-3 -top-0"> 67 - {{ template "numberCircle" 2 }} 68 - </div> 69 - 70 - <!-- Content column --> 71 - <div class="flex-1 min-w-0 pb-12 space-y-3"> 72 - <div class="flex flex-wrap items-center justify-between gap-3"> 73 - <h2 class="text-lg font-semibold uppercase">Choose &amp; Configure Repositories</h2> 74 - <div class="ml-auto h-7 flex items-center"> 75 - <button 76 - class="btn-flat flex items-center gap-2" 77 - onclick="document.querySelectorAll('input[name^=selected_]').forEach(c => c.checked = !c.checked)" 78 - > 79 - {{ i "check-check" "size-4" }} 80 - Select all repositories 81 - </button> 82 - </div> 83 - </div> 84 - <div id="source-repos"> 85 - <!-- 86 - <p class="text-gray-500 dark:text-gray-400 mb-4">No repositories yet, connect your account first</p> 87 - --> 88 - 89 - {{ template "account/migrate/fragments/repoList" . }} 90 - </div> 91 - </div> 92 - </div> 93 - {{ end }} 94 - 95 - {{ define "numberCircle" }} 96 - <div class="size-7 bg-gray-200 dark:bg-gray-600 rounded-full flex items-center justify-center font-medium"> 97 - {{.}} 98 - </div> 70 + {{ template "account/migrate/fragments/step2" }} 99 71 {{ end }}