| 1 | <!DOCTYPE html> |
| 2 | <% |
| 3 | const pageLocale = typeof locale !== 'undefined' ? locale : 'en-US' |
| 4 | const t = typeof messages !== 'undefined' ? messages : { |
| 5 | title: 'Authorization', |
| 6 | selectTitle: 'Choose accounts to connect', |
| 7 | selectDescription: 'Select the accounts you want AiToEarn to manage for publishing and analytics.', |
| 8 | accountCount: 'Available accounts', |
| 9 | selectedCount: 'Selected', |
| 10 | noAccountsTitle: 'No accounts available', |
| 11 | noAccountsDescription: 'This authorization did not return any accounts that can be connected.', |
| 12 | continueButton: 'Continue', |
| 13 | selectRequired: 'Select at least one account to continue.', |
| 14 | platformUid: 'Platform ID', |
| 15 | account: 'Account' |
| 16 | } |
| 17 | const pageAccounts = Array.isArray(accounts) ? accounts : [] |
| 18 | const pagePlatformDisplayName = typeof platformDisplayName !== 'undefined' ? platformDisplayName : '' |
| 19 | const pagePlatformLogoUrl = typeof platformLogoUrl !== 'undefined' ? platformLogoUrl : '' |
| 20 | const pageEmptyAccountHint = typeof emptyAccountHint !== 'undefined' && emptyAccountHint ? emptyAccountHint : undefined |
| 21 | const formAction = '/api/v2/channels/accounts/auth/selections' |
| 22 | %> |
| 23 | <html lang="<%= pageLocale %>"> |
| 24 | |
| 25 | <head> |
| 26 | <meta charset="UTF-8" /> |
| 27 | <meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
| 28 | <title><%= pagePlatformDisplayName || t.title %></title> |
| 29 | <style> |
| 30 | * { |
| 31 | box-sizing: border-box; |
| 32 | } |
| 33 | |
| 34 | :root { |
| 35 | --background: #f7f8fb; |
| 36 | --foreground: #171717; |
| 37 | --muted: #6b7280; |
| 38 | --muted-bg: #f3f4f6; |
| 39 | --border: #e5e7eb; |
| 40 | --primary: #2563eb; |
| 41 | --primary-soft: #eff6ff; |
| 42 | --destructive: #dc2626; |
| 43 | --card: #fff; |
| 44 | } |
| 45 | |
| 46 | body { |
| 47 | margin: 0; |
| 48 | min-height: 100vh; |
| 49 | font-family: Suisseintl, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif; |
| 50 | color: var(--foreground); |
| 51 | background: |
| 52 | radial-gradient(circle at 1px 1px, rgba(17, 24, 39, 0.08) 1px, transparent 0), |
| 53 | var(--background); |
| 54 | background-size: 18px 18px; |
| 55 | display: flex; |
| 56 | align-items: center; |
| 57 | justify-content: center; |
| 58 | padding: 20px; |
| 59 | } |
| 60 | |
| 61 | main { |
| 62 | width: min(100%, 720px); |
| 63 | background: var(--card); |
| 64 | border: 1px solid var(--border); |
| 65 | border-radius: 12px; |
| 66 | box-shadow: 0 18px 45px rgba(15, 23, 42, 0.08); |
| 67 | overflow: hidden; |
| 68 | } |
| 69 | |
| 70 | header { |
| 71 | padding: 24px 24px 18px; |
| 72 | border-bottom: 1px solid var(--border); |
| 73 | } |
| 74 | |
| 75 | .platform { |
| 76 | display: flex; |
| 77 | align-items: center; |
| 78 | gap: 12px; |
| 79 | min-width: 0; |
| 80 | margin-bottom: 14px; |
| 81 | } |
| 82 | |
| 83 | .platform-logo { |
| 84 | width: 38px; |
| 85 | height: 38px; |
| 86 | border-radius: 10px; |
| 87 | border: 1px solid var(--border); |
| 88 | object-fit: contain; |
| 89 | background: #fff; |
| 90 | flex: 0 0 auto; |
| 91 | } |
| 92 | |
| 93 | h1 { |
| 94 | margin: 0; |
| 95 | font-size: 22px; |
| 96 | line-height: 1.25; |
| 97 | } |
| 98 | |
| 99 | .platform-name { |
| 100 | margin: 3px 0 0; |
| 101 | color: var(--muted); |
| 102 | font-size: 14px; |
| 103 | line-height: 1.4; |
| 104 | } |
| 105 | |
| 106 | .description { |
| 107 | margin: 0; |
| 108 | color: var(--muted); |
| 109 | font-size: 14px; |
| 110 | line-height: 1.6; |
| 111 | } |
| 112 | |
| 113 | form { |
| 114 | display: flex; |
| 115 | flex-direction: column; |
| 116 | min-height: 300px; |
| 117 | } |
| 118 | |
| 119 | .summary { |
| 120 | display: flex; |
| 121 | align-items: center; |
| 122 | justify-content: space-between; |
| 123 | gap: 12px; |
| 124 | padding: 18px 24px 10px; |
| 125 | color: var(--muted); |
| 126 | font-size: 13px; |
| 127 | } |
| 128 | |
| 129 | .selected-count { |
| 130 | color: var(--foreground); |
| 131 | font-weight: 600; |
| 132 | } |
| 133 | |
| 134 | .account-list { |
| 135 | display: grid; |
| 136 | grid-template-columns: repeat(2, minmax(0, 1fr)); |
| 137 | gap: 10px; |
| 138 | margin: 0; |
| 139 | padding: 0 24px 20px; |
| 140 | list-style: none; |
| 141 | } |
| 142 | |
| 143 | .account-row { |
| 144 | position: relative; |
| 145 | display: flex; |
| 146 | align-items: center; |
| 147 | gap: 12px; |
| 148 | min-height: 70px; |
| 149 | border: 1px solid var(--border); |
| 150 | border-radius: 10px; |
| 151 | padding: 12px 42px 12px 12px; |
| 152 | cursor: pointer; |
| 153 | background: #fff; |
| 154 | transition: border-color 0.15s ease, background 0.15s ease, box-shadow 0.15s ease; |
| 155 | } |
| 156 | |
| 157 | .account-row.is-selected { |
| 158 | border-color: var(--primary); |
| 159 | background: var(--primary-soft); |
| 160 | box-shadow: inset 0 0 0 1px rgba(37, 99, 235, 0.2); |
| 161 | } |
| 162 | |
| 163 | .account-row input { |
| 164 | position: absolute; |
| 165 | opacity: 0; |
| 166 | pointer-events: none; |
| 167 | } |
| 168 | |
| 169 | .avatar { |
| 170 | width: 40px; |
| 171 | height: 40px; |
| 172 | border-radius: 50%; |
| 173 | background: var(--muted-bg); |
| 174 | border: 1px solid var(--border); |
| 175 | flex: 0 0 auto; |
| 176 | object-fit: cover; |
| 177 | } |
| 178 | |
| 179 | .account-text { |
| 180 | min-width: 0; |
| 181 | display: block; |
| 182 | } |
| 183 | |
| 184 | .name { |
| 185 | display: block; |
| 186 | font-size: 15px; |
| 187 | font-weight: 600; |
| 188 | line-height: 1.35; |
| 189 | overflow: hidden; |
| 190 | text-overflow: ellipsis; |
| 191 | white-space: nowrap; |
| 192 | } |
| 193 | |
| 194 | .uid { |
| 195 | display: block; |
| 196 | margin-top: 3px; |
| 197 | font-size: 12px; |
| 198 | color: var(--muted); |
| 199 | overflow: hidden; |
| 200 | text-overflow: ellipsis; |
| 201 | white-space: nowrap; |
| 202 | } |
| 203 | |
| 204 | .checkmark { |
| 205 | position: absolute; |
| 206 | top: 50%; |
| 207 | right: 12px; |
| 208 | width: 22px; |
| 209 | height: 22px; |
| 210 | transform: translateY(-50%); |
| 211 | border-radius: 50%; |
| 212 | border: 1px solid #cbd5e1; |
| 213 | background: #fff; |
| 214 | color: #fff; |
| 215 | display: flex; |
| 216 | align-items: center; |
| 217 | justify-content: center; |
| 218 | font-size: 14px; |
| 219 | font-weight: 700; |
| 220 | } |
| 221 | |
| 222 | .account-row.is-selected .checkmark { |
| 223 | border-color: var(--primary); |
| 224 | background: var(--primary); |
| 225 | } |
| 226 | |
| 227 | .empty { |
| 228 | margin: 0 24px 20px; |
| 229 | padding: 22px; |
| 230 | border: 1px dashed var(--border); |
| 231 | border-radius: 10px; |
| 232 | background: #fafafa; |
| 233 | text-align: center; |
| 234 | } |
| 235 | |
| 236 | .empty-title { |
| 237 | margin: 0 0 6px; |
| 238 | font-size: 15px; |
| 239 | font-weight: 700; |
| 240 | } |
| 241 | |
| 242 | .empty-description { |
| 243 | margin: 0; |
| 244 | color: var(--muted); |
| 245 | font-size: 13px; |
| 246 | line-height: 1.5; |
| 247 | } |
| 248 | |
| 249 | .empty-action { |
| 250 | display: inline-flex; |
| 251 | align-items: center; |
| 252 | justify-content: center; |
| 253 | min-height: 40px; |
| 254 | margin-top: 14px; |
| 255 | padding: 0 16px; |
| 256 | border-radius: 8px; |
| 257 | background: var(--primary); |
| 258 | color: #fff; |
| 259 | font-size: 14px; |
| 260 | font-weight: 700; |
| 261 | text-decoration: none; |
| 262 | } |
| 263 | |
| 264 | footer { |
| 265 | margin-top: auto; |
| 266 | border-top: 1px solid var(--border); |
| 267 | padding: 16px 24px 20px; |
| 268 | background: rgba(255, 255, 255, 0.92); |
| 269 | } |
| 270 | |
| 271 | .error { |
| 272 | min-height: 18px; |
| 273 | margin: 0 0 10px; |
| 274 | color: var(--destructive); |
| 275 | font-size: 13px; |
| 276 | line-height: 1.4; |
| 277 | } |
| 278 | |
| 279 | button { |
| 280 | width: 100%; |
| 281 | min-height: 44px; |
| 282 | border: 0; |
| 283 | border-radius: 10px; |
| 284 | background: linear-gradient(90deg, #7c3aed 0%, #06b6d4 100%); |
| 285 | color: #fff; |
| 286 | font-size: 15px; |
| 287 | font-weight: 700; |
| 288 | cursor: pointer; |
| 289 | transition: opacity 0.15s ease; |
| 290 | } |
| 291 | |
| 292 | button:disabled { |
| 293 | cursor: not-allowed; |
| 294 | opacity: 0.45; |
| 295 | } |
| 296 | |
| 297 | @media (max-width: 640px) { |
| 298 | body { |
| 299 | align-items: stretch; |
| 300 | padding: 12px; |
| 301 | } |
| 302 | |
| 303 | main { |
| 304 | width: 100%; |
| 305 | } |
| 306 | |
| 307 | header, |
| 308 | .summary, |
| 309 | footer { |
| 310 | padding-left: 18px; |
| 311 | padding-right: 18px; |
| 312 | } |
| 313 | |
| 314 | .account-list { |
| 315 | grid-template-columns: 1fr; |
| 316 | padding-left: 18px; |
| 317 | padding-right: 18px; |
| 318 | } |
| 319 | |
| 320 | .empty { |
| 321 | margin-left: 18px; |
| 322 | margin-right: 18px; |
| 323 | } |
| 324 | } |
| 325 | </style> |
| 326 | </head> |
| 327 | |
| 328 | <body> |
| 329 | <main> |
| 330 | <header> |
| 331 | <div class="platform"> |
| 332 | <% if (pagePlatformLogoUrl) { %> |
| 333 | <img class="platform-logo" src="<%= pagePlatformLogoUrl %>" alt="" /> |
| 334 | <% } %> |
| 335 | <div> |
| 336 | <h1><%= t.selectTitle %></h1> |
| 337 | <p class="platform-name"><%= pagePlatformDisplayName %></p> |
| 338 | </div> |
| 339 | </div> |
| 340 | <p class="description"><%= t.selectDescription %></p> |
| 341 | </header> |
| 342 | |
| 343 | <form id="accountForm" method="POST" action="<%= formAction %>"> |
| 344 | <div class="summary"> |
| 345 | <span><%= t.accountCount %>: <%= pageAccounts.length %></span> |
| 346 | <span class="selected-count"><%= t.selectedCount %>: <span id="selectedCount"><%= pageAccounts.length > 0 ? 1 : 0 %></span></span> |
| 347 | </div> |
| 348 | |
| 349 | <% if (pageAccounts.length > 0) { %> |
| 350 | <ul class="account-list"> |
| 351 | <% pageAccounts.forEach(function(account, index) { %> |
| 352 | <li> |
| 353 | <label class="account-row<%= index === 0 ? ' is-selected' : '' %>"> |
| 354 | <input type="checkbox" name="selectedAccounts" value="<%= account.platformUid %>" data-platform-uid="<%= account.platformUid %>" <% if (account.account) { %>data-account="<%= account.account %>"<% } %> <%= index === 0 ? 'checked' : '' %> /> |
| 355 | <% if (account.avatarUrl) { %> |
| 356 | <img class="avatar" src="<%= account.avatarUrl %>" alt="" /> |
| 357 | <% } else { %> |
| 358 | <span class="avatar"></span> |
| 359 | <% } %> |
| 360 | <span class="account-text"> |
| 361 | <span class="name"><%= account.displayName %></span> |
| 362 | <span class="uid"><%= t.platformUid %>: <%= account.platformUid %></span> |
| 363 | <% if (account.account) { %> |
| 364 | <span class="uid"><%= t.account %>: <%= account.account %></span> |
| 365 | <% } %> |
| 366 | </span> |
| 367 | <span class="checkmark">✓</span> |
| 368 | </label> |
| 369 | </li> |
| 370 | <% }) %> |
| 371 | </ul> |
| 372 | <% } else { %> |
| 373 | <div class="empty"> |
| 374 | <p class="empty-title"><%= pageEmptyAccountHint ? pageEmptyAccountHint.title : t.noAccountsTitle %></p> |
| 375 | <p class="empty-description"><%= pageEmptyAccountHint ? pageEmptyAccountHint.description : t.noAccountsDescription %></p> |
| 376 | <% if (pageEmptyAccountHint && pageEmptyAccountHint.action) { %> |
| 377 | <a class="empty-action" href="<%= pageEmptyAccountHint.action.url %>" target="_blank" rel="noopener noreferrer"><%= pageEmptyAccountHint.action.label %></a> |
| 378 | <% } %> |
| 379 | </div> |
| 380 | <% } %> |
| 381 | |
| 382 | <footer> |
| 383 | <p id="selectError" class="error"></p> |
| 384 | <button id="continueButton" type="submit" <%= pageAccounts.length === 0 ? 'disabled' : '' %>><%= t.continueButton %></button> |
| 385 | </footer> |
| 386 | </form> |
| 387 | </main> |
| 388 | |
| 389 | <script> |
| 390 | const form = document.getElementById('accountForm'); |
| 391 | const selectedCount = document.getElementById('selectedCount'); |
| 392 | const continueButton = document.getElementById('continueButton'); |
| 393 | const selectError = document.getElementById('selectError'); |
| 394 | const requiredMessage = '<%= t.selectRequired %>'; |
| 395 | const inputs = Array.from(document.querySelectorAll('input[name="selectedAccounts"]')); |
| 396 | |
| 397 | function updateSelection() { |
| 398 | const checkedCount = inputs.filter(function(input) { |
| 399 | return input.checked; |
| 400 | }).length; |
| 401 | |
| 402 | selectedCount.textContent = String(checkedCount); |
| 403 | continueButton.disabled = checkedCount === 0; |
| 404 | selectError.textContent = checkedCount === 0 && inputs.length > 0 ? requiredMessage : ''; |
| 405 | |
| 406 | inputs.forEach(function(input) { |
| 407 | input.closest('.account-row').classList.toggle('is-selected', input.checked); |
| 408 | }); |
| 409 | } |
| 410 | |
| 411 | inputs.forEach(function(input) { |
| 412 | input.addEventListener('change', updateSelection); |
| 413 | }); |
| 414 | |
| 415 | form.addEventListener('submit', async function(event) { |
| 416 | event.preventDefault(); |
| 417 | const checkedInputs = inputs.filter(function(input) { return input.checked; }); |
| 418 | if (checkedInputs.length === 0) { |
| 419 | selectError.textContent = requiredMessage; |
| 420 | return; |
| 421 | } |
| 422 | |
| 423 | const accounts = checkedInputs.map(function(input) { |
| 424 | const account = { |
| 425 | platformUid: input.dataset.platformUid || input.value |
| 426 | }; |
| 427 | if (input.dataset.account) { |
| 428 | account.account = input.dataset.account; |
| 429 | } |
| 430 | return account; |
| 431 | }); |
| 432 | |
| 433 | continueButton.disabled = true; |
| 434 | selectError.textContent = ''; |
| 435 | |
| 436 | try { |
| 437 | const response = await fetch(form.action, { |
| 438 | method: 'POST', |
| 439 | credentials: 'same-origin', |
| 440 | headers: { |
| 441 | 'Content-Type': 'application/json' |
| 442 | }, |
| 443 | body: JSON.stringify({ accounts }) |
| 444 | }); |
| 445 | const html = await response.text(); |
| 446 | if (!response.ok) { |
| 447 | continueButton.disabled = false; |
| 448 | selectError.textContent = html || response.statusText || requiredMessage; |
| 449 | return; |
| 450 | } |
| 451 | document.open(); |
| 452 | document.write(html); |
| 453 | document.close(); |
| 454 | } |
| 455 | catch (error) { |
| 456 | continueButton.disabled = false; |
| 457 | selectError.textContent = error && error.message ? error.message : requiredMessage; |
| 458 | } |
| 459 | }); |
| 460 | |
| 461 | updateSelection(); |
| 462 | </script> |
| 463 | </body> |
| 464 | |
| 465 | </html> |
| 466 |