返回 AiToEarn
select-accounts.ejs
根目录 / project / aitoearn-backend / apps / aitoearn-server / src / views / channels / auth / select-accounts.ejs
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">&#10003;</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
466 lines Plain Text