Lucii1 commited on
Commit
c569d1a
·
1 Parent(s): a4fd9d1

add login page

Browse files
Files changed (7) hide show
  1. auth.js +80 -0
  2. i18n.js +42 -12
  3. index.html +11 -8
  4. login.html +62 -0
  5. login.js +57 -0
  6. main.js +33 -4
  7. styles.css +146 -0
auth.js ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export const AUTH_STORAGE_KEY = 'authUser';
2
+
3
+ const FAKE_ACCOUNTS = [
4
+ { username: 'admin', password: 'admin123', displayName: 'Admin' },
5
+ {
6
+ username: 'editor',
7
+ password: 'editor123',
8
+ displayName: 'Editor',
9
+ },
10
+ {
11
+ username: 'viewer',
12
+ password: 'viewer123',
13
+ displayName: 'Viewer',
14
+ },
15
+ ];
16
+
17
+ function normalizeIdentifier(identifier = '') {
18
+ return String(identifier || '')
19
+ .trim()
20
+ .toLowerCase();
21
+ }
22
+
23
+ export function getFakeAccounts() {
24
+ return [...FAKE_ACCOUNTS];
25
+ }
26
+
27
+ export function findAccount(username, password) {
28
+ const normalizedUser = normalizeIdentifier(username);
29
+ return FAKE_ACCOUNTS.find(
30
+ (account) =>
31
+ normalizeIdentifier(account.username) === normalizedUser &&
32
+ account.password === String(password || '')
33
+ );
34
+ }
35
+
36
+ export function getAuthUser() {
37
+ try {
38
+ const raw = localStorage.getItem(AUTH_STORAGE_KEY);
39
+ if (!raw) return null;
40
+ const parsed = JSON.parse(raw);
41
+ if (!parsed?.username) return null;
42
+ return parsed;
43
+ } catch (error) {
44
+ console.error('Failed to read auth user', error);
45
+ return null;
46
+ }
47
+ }
48
+
49
+ export function saveAuthUser(account) {
50
+ if (!account) return null;
51
+ const payload = {
52
+ username: account.username,
53
+ displayName: account.displayName || account.username,
54
+ loggedInAt: Date.now(),
55
+ };
56
+ localStorage.setItem(AUTH_STORAGE_KEY, JSON.stringify(payload));
57
+ return payload;
58
+ }
59
+
60
+ export function clearAuthUser() {
61
+ localStorage.removeItem(AUTH_STORAGE_KEY);
62
+ }
63
+
64
+ export function ensureAuthenticated(redirectOnMissing = true) {
65
+ const user = getAuthUser();
66
+ if (!user && redirectOnMissing && typeof window !== 'undefined') {
67
+ window.location.href = 'login.html';
68
+ }
69
+ return user;
70
+ }
71
+
72
+ export function startAuthWatcher(intervalMs = 5000) {
73
+ if (typeof window === 'undefined') return () => {};
74
+ const timerId = window.setInterval(() => {
75
+ if (!getAuthUser()) {
76
+ window.location.href = 'login.html';
77
+ }
78
+ }, intervalMs);
79
+ return () => window.clearInterval(timerId);
80
+ }
i18n.js CHANGED
@@ -1,6 +1,16 @@
1
  const translations = {
2
  en: {
3
  pageTitle: 'News Verification',
 
 
 
 
 
 
 
 
 
 
4
  languageLabel: 'Language',
5
  languageJapanese: 'Japanese',
6
  languageEnglish: 'English',
@@ -44,7 +54,8 @@ const translations = {
44
  noDataToSave: 'No data to save.',
45
  downloadPrepareFailedNetwork:
46
  'Unable to prepare the download. Please check your connection and try again.',
47
- downloadPrepareFailed: 'Unable to prepare the download. Please try again later.',
 
48
  notificationTitle: 'Notification',
49
  movNotSupported: 'The system does not support preview for .mov files.',
50
  videoTagNotSupported: 'Your browser does not support the video tag.',
@@ -124,6 +135,16 @@ const translations = {
124
  },
125
  ja: {
126
  pageTitle: 'ニュース検証',
 
 
 
 
 
 
 
 
 
 
127
  languageLabel: '言語',
128
  languageJapanese: '日本語',
129
  languageEnglish: '英語',
@@ -131,8 +152,7 @@ const translations = {
131
  uploadMediaLabel: 'メディアファイルをアップロード',
132
  uploadHint:
133
  '<span class="primary-1">ここをクリック</span>してファイルをアップロードするか、ドラッグしてください。',
134
- supportedFormats:
135
- '対応形式: PNG, JPG, JPEG, GIF, WEBP, SVG, MOV, MP4',
136
  additionalInformation: '追加情報',
137
  socialMediaPostUrl: 'ソーシャルメディア投稿のURL',
138
  titleNewsArticle: '記事のタイトル',
@@ -167,10 +187,12 @@ const translations = {
167
  noDataToSave: '保存するデータがありません。',
168
  downloadPrepareFailedNetwork:
169
  'ダウンロードの準備に失敗しました。接続を確認して再試行してください。',
170
- downloadPrepareFailed: 'ダウンロードの準備に失敗しました。後でもう一度お試しください。',
 
171
  notificationTitle: '通知',
172
  movNotSupported: '.mov ファイルのプレビューには対応していません。',
173
- videoTagNotSupported: 'お使いのブラウザは video タグをサポートしていません。',
 
174
  previewUnavailable: 'このファイルタイプはプレビューできません。',
175
  imageLabel: '画像',
176
  feedbackHeader: '改善にご協力ください!',
@@ -207,12 +229,10 @@ const translations = {
207
  moderatelyClear: 'やや明確',
208
  slightlyClear: '少し明確',
209
  notClear: '不明瞭',
210
- q6Title:
211
- '2.2 ストーリーの中心となる主張や行動を正しく特定していましたか?',
212
  partially: '部分的に',
213
  noOption: 'いいえ',
214
- q7Title:
215
- '2.3 元の情報がいつ公開・発生したかを明確に示していましたか?',
216
  q8Title: '2.4 イベントの場所(「どこ」)は特定・検証されていましたか?',
217
  notApplicable: '該当なし',
218
  q9Title:
@@ -245,6 +265,16 @@ const translations = {
245
  },
246
  vi: {
247
  pageTitle: 'Kiểm chứng tin tức',
 
 
 
 
 
 
 
 
 
 
248
  languageLabel: 'Ngôn ngữ',
249
  languageJapanese: 'Tiếng Nhật',
250
  languageEnglish: 'Tiếng Anh',
@@ -288,7 +318,8 @@ const translations = {
288
  noDataToSave: 'Không có dữ liệu để lưu.',
289
  downloadPrepareFailedNetwork:
290
  'Không thể chuẩn bị tệp tải xuống. Vui lòng kiểm tra kết nối và thử lại.',
291
- downloadPrepareFailed: 'Không thể chuẩn bị tệp tải xuống. Vui lòng thử lại sau.',
 
292
  notificationTitle: 'Thông báo',
293
  movNotSupported: 'Hệ thống chưa hỗ trợ xem trước tệp .mov.',
294
  videoTagNotSupported: 'Trình duyệt của bạn không hỗ trợ thẻ video.',
@@ -334,8 +365,7 @@ const translations = {
334
  noOption: 'Không',
335
  q7Title:
336
  '2.3 Ứng dụng có hiển thị rõ thời điểm sự kiện/thông tin gốc được công bố không?',
337
- q8Title:
338
- '2.4 Địa điểm sự kiện ("Ở đâu") có được chỉ rõ và xác minh không?',
339
  notApplicable: 'Không áp dụng',
340
  q9Title:
341
  '2.5 Ứng dụng giải thích động cơ/mục đích đằng sau tuyên bố gốc tốt thế nào?',
 
1
  const translations = {
2
  en: {
3
  pageTitle: 'News Verification',
4
+ loginTitle: 'Sign in',
5
+ loginSubtitle: 'Use one of the sample accounts to continue.',
6
+ usernameLabel: 'Username or email',
7
+ passwordLabel: 'Password',
8
+ usernamePlaceholder: 'Enter your username or email',
9
+ passwordPlaceholder: 'Enter your password',
10
+ loginButton: 'Continue',
11
+ loginError: 'Incorrect credentials. Please try again.',
12
+ fakeAccountsLabel: 'Sample accounts',
13
+ logoutButton: 'Log out',
14
  languageLabel: 'Language',
15
  languageJapanese: 'Japanese',
16
  languageEnglish: 'English',
 
54
  noDataToSave: 'No data to save.',
55
  downloadPrepareFailedNetwork:
56
  'Unable to prepare the download. Please check your connection and try again.',
57
+ downloadPrepareFailed:
58
+ 'Unable to prepare the download. Please try again later.',
59
  notificationTitle: 'Notification',
60
  movNotSupported: 'The system does not support preview for .mov files.',
61
  videoTagNotSupported: 'Your browser does not support the video tag.',
 
135
  },
136
  ja: {
137
  pageTitle: 'ニュース検証',
138
+ loginTitle: 'サインイン',
139
+ loginSubtitle: '以下のサンプルアカウントのいずれかでログインしてください。',
140
+ usernameLabel: 'ユーザー名またはメール',
141
+ passwordLabel: 'パスワード',
142
+ usernamePlaceholder: 'ユーザー名またはメールを入力',
143
+ passwordPlaceholder: 'パスワードを入力',
144
+ loginButton: '続行',
145
+ loginError: '認証情報が正しくありません。もう一度お試しください。',
146
+ fakeAccountsLabel: 'サンプルアカウント',
147
+ logoutButton: 'ログアウト',
148
  languageLabel: '言語',
149
  languageJapanese: '日本語',
150
  languageEnglish: '英語',
 
152
  uploadMediaLabel: 'メディアファイルをアップロード',
153
  uploadHint:
154
  '<span class="primary-1">ここをクリック</span>してファイルをアップロードするか、ドラッグしてください。',
155
+ supportedFormats: '対応形式: PNG, JPG, JPEG, GIF, WEBP, SVG, MOV, MP4',
 
156
  additionalInformation: '追加情報',
157
  socialMediaPostUrl: 'ソーシャルメディア投稿のURL',
158
  titleNewsArticle: '記事のタイトル',
 
187
  noDataToSave: '保存するデータがありません。',
188
  downloadPrepareFailedNetwork:
189
  'ダウンロードの準備に失敗しました。接続を確認して再試行してください。',
190
+ downloadPrepareFailed:
191
+ 'ダウンロードの準備に失敗しました。後でもう一度お試しください。',
192
  notificationTitle: '通知',
193
  movNotSupported: '.mov ファイルのプレビューには対応していません。',
194
+ videoTagNotSupported:
195
+ 'お使いのブラウザは video タグをサポートしていません。',
196
  previewUnavailable: 'このファイルタイプはプレビューできません。',
197
  imageLabel: '画像',
198
  feedbackHeader: '改善にご協力ください!',
 
229
  moderatelyClear: 'やや明確',
230
  slightlyClear: '少し明確',
231
  notClear: '不明瞭',
232
+ q6Title: '2.2 ストーリーの中心となる主張や行動を正しく特定していましたか?',
 
233
  partially: '部分的に',
234
  noOption: 'いいえ',
235
+ q7Title: '2.3 元の情報がいつ公開・発生したかを明確に示していましたか?',
 
236
  q8Title: '2.4 イベントの場所(「どこ」)は特定・検証されていましたか?',
237
  notApplicable: '該当なし',
238
  q9Title:
 
265
  },
266
  vi: {
267
  pageTitle: 'Kiểm chứng tin tức',
268
+ loginTitle: 'Đăng nhập',
269
+ loginSubtitle: 'Dùng một trong các tài khoản mẫu để tiếp tục.',
270
+ usernameLabel: 'Tên đăng nhập hoặc email',
271
+ passwordLabel: 'Mật khẩu',
272
+ usernamePlaceholder: 'Nhập tên đăng nhập hoặc email',
273
+ passwordPlaceholder: 'Nhập mật khẩu',
274
+ loginButton: 'Tiếp tục',
275
+ loginError: 'Sai thông tin đăng nhập, vui lòng thử lại.',
276
+ fakeAccountsLabel: 'Tài khoản mẫu',
277
+ logoutButton: 'Đăng xuất',
278
  languageLabel: 'Ngôn ngữ',
279
  languageJapanese: 'Tiếng Nhật',
280
  languageEnglish: 'Tiếng Anh',
 
318
  noDataToSave: 'Không có dữ liệu để lưu.',
319
  downloadPrepareFailedNetwork:
320
  'Không thể chuẩn bị tệp tải xuống. Vui lòng kiểm tra kết nối và thử lại.',
321
+ downloadPrepareFailed:
322
+ 'Không thể chuẩn bị tệp tải xuống. Vui lòng thử lại sau.',
323
  notificationTitle: 'Thông báo',
324
  movNotSupported: 'Hệ thống chưa hỗ trợ xem trước tệp .mov.',
325
  videoTagNotSupported: 'Trình duyệt của bạn không hỗ trợ thẻ video.',
 
365
  noOption: 'Không',
366
  q7Title:
367
  '2.3 Ứng dụng có hiển thị rõ thời điểm sự kiện/thông tin gốc được công bố không?',
368
+ q8Title: '2.4 Địa điểm sự kiện ("Ở đâu") có được chỉ rõ và xác minh không?',
 
369
  notApplicable: 'Không áp dụng',
370
  q9Title:
371
  '2.5 Ứng dụng giải thích động cơ/mục đích đằng sau tuyên bố gốc tốt thế nào?',
index.html CHANGED
@@ -16,15 +16,18 @@
16
  <body>
17
  <div class="container">
18
  <header class="header-face-check">
19
- <div></div>
20
  <h1 class="title-header-face-check" data-i18n="pageTitle">News Verification</h1>
21
- <div class="language-switcher">
22
- <label class="language-label" for="language-select" data-i18n="languageLabel">Language</label>
23
- <select id="language-select" class="language-select">
24
- <option value="ja" data-i18n="languageJapanese">Japanese</option>
25
- <option value="en" data-i18n="languageEnglish" selected>English</option>
26
- <option value="vi" data-i18n="languageVietnamese">Vietnamese</option>
27
- </select>
 
 
 
28
  </div>
29
  </header>
30
  <main class="main-content">
 
16
  <body>
17
  <div class="container">
18
  <header class="header-face-check">
19
+ <div class="header-placeholder"></div>
20
  <h1 class="title-header-face-check" data-i18n="pageTitle">News Verification</h1>
21
+ <div class="header-actions">
22
+ <div class="language-switcher">
23
+ <label class="language-label" for="language-select" data-i18n="languageLabel">Language</label>
24
+ <select id="language-select" class="language-select">
25
+ <option value="ja" data-i18n="languageJapanese">Japanese</option>
26
+ <option value="en" data-i18n="languageEnglish" selected>English</option>
27
+ <option value="vi" data-i18n="languageVietnamese">Vietnamese</option>
28
+ </select>
29
+ </div>
30
+ <button id="btn-logout" class="btn btn-logout" data-i18n="logoutButton">Log out</button>
31
  </div>
32
  </header>
33
  <main class="main-content">
login.html ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en" translate="no">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Login</title>
7
+ <link rel="stylesheet" href="styles.css" />
8
+ </head>
9
+ <body>
10
+ <div class="login-layout">
11
+ <div class="login-language-switcher">
12
+ <label class="language-label" for="language-select" data-i18n="languageLabel">Language</label>
13
+ <select id="language-select" class="language-select">
14
+ <option value="ja" data-i18n="languageJapanese">Japanese</option>
15
+ <option value="en" data-i18n="languageEnglish" selected>English</option>
16
+ <option value="vi" data-i18n="languageVietnamese">Vietnamese</option>
17
+ </select>
18
+ </div>
19
+ <div class="login-card">
20
+ <div class="login-header">
21
+ <h1 data-i18n="loginTitle">Sign in</h1>
22
+ <p class="login-subtitle" data-i18n="loginSubtitle">
23
+ Use one of the sample accounts to continue.
24
+ </p>
25
+ </div>
26
+ <form id="login-form" class="login-form">
27
+ <label class="login-label" for="username" data-i18n="usernameLabel">Username or email</label>
28
+ <input
29
+ id="username"
30
+ name="username"
31
+ class="login-input"
32
+ type="text"
33
+ autocomplete="username"
34
+ required
35
+ data-i18n-placeholder="usernamePlaceholder"
36
+ placeholder="Enter your username or email"
37
+ />
38
+
39
+ <label class="login-label" for="password" data-i18n="passwordLabel">Password</label>
40
+ <input
41
+ id="password"
42
+ name="password"
43
+ class="login-input"
44
+ type="password"
45
+ autocomplete="current-password"
46
+ required
47
+ data-i18n-placeholder="passwordPlaceholder"
48
+ placeholder="Enter your password"
49
+ />
50
+
51
+ <div id="login-error" class="login-error" role="alert" aria-live="assertive"></div>
52
+
53
+ <button id="login-submit" class="btn login-submit" type="submit" data-i18n="loginButton">
54
+ Continue
55
+ </button>
56
+ </form>
57
+ </div>
58
+ </div>
59
+
60
+ <script type="module" src="login.js"></script>
61
+ </body>
62
+ </html>
login.js ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { initI18n, bindLanguageSelector, t, onLanguageChange } from './i18n.js';
2
+ import {
3
+ findAccount,
4
+ getAuthUser,
5
+ getFakeAccounts,
6
+ saveAuthUser,
7
+ } from './auth.js';
8
+
9
+ const form = document.getElementById('login-form');
10
+ const usernameInput = document.getElementById('username');
11
+ const passwordInput = document.getElementById('password');
12
+ const errorElement = document.getElementById('login-error');
13
+
14
+ initI18n();
15
+ bindLanguageSelector('#language-select');
16
+
17
+ function redirectToApp() {
18
+ window.location.href = 'index.html';
19
+ }
20
+
21
+ function setError(message) {
22
+ errorElement.textContent = message || '';
23
+ }
24
+
25
+ function applyLoginTitle() {
26
+ document.title = t('loginTitle');
27
+ }
28
+
29
+ const existingUser = getAuthUser();
30
+ if (existingUser) {
31
+ redirectToApp();
32
+ }
33
+
34
+ applyLoginTitle();
35
+
36
+ onLanguageChange(() => {
37
+ applyLoginTitle();
38
+ if (errorElement.textContent) {
39
+ setError(t('loginError'));
40
+ }
41
+ });
42
+
43
+ form?.addEventListener('submit', (event) => {
44
+ event.preventDefault();
45
+ const username = usernameInput?.value;
46
+ const password = passwordInput?.value;
47
+ const account = findAccount(username, password);
48
+
49
+ if (!account) {
50
+ setError(t('loginError'));
51
+ return;
52
+ }
53
+
54
+ saveAuthUser(account);
55
+ setError('');
56
+ redirectToApp();
57
+ });
main.js CHANGED
@@ -1,5 +1,27 @@
1
- import { t, initI18n, bindLanguageSelector, onLanguageChange, getCurrentLanguage } from './i18n.js';
 
 
 
 
 
 
2
  import { closeIcon, eyeIcon, trashIcon } from './constants.js';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
 
4
  const inputElement = document.getElementById('file-input');
5
  const textInputElement = document.getElementById('text-input-additional');
@@ -288,7 +310,9 @@ Object.defineProperty(selectedTab, 'value', {
288
  if (data.value) {
289
  const menuItems =
290
  newValue === 'verified_evidence'
291
- ? buildVerifiedEvidenceMenu(selectedTabChildren.value || 'source_details')
 
 
292
  : '';
293
 
294
  outputElement.innerHTML =
@@ -561,7 +585,10 @@ async function handleSubmit() {
561
 
562
  const formData = new FormData();
563
  formData.append('metadata_file', metadataFile);
564
- formData.append('additional_text', sanitizeTextareaValue(textInputElement?.value));
 
 
 
565
  const selectedLanguage = getCurrentLanguage();
566
  formData.append('language', languageApiMap[selectedLanguage] || 'English');
567
 
@@ -615,7 +642,9 @@ async function handleSubmit() {
615
 
616
  const menuItems =
617
  selectedTab.value === 'verified_evidence'
618
- ? buildVerifiedEvidenceMenu(selectedTabChildren.value || 'source_details')
 
 
619
  : '';
620
 
621
  outputElement.innerHTML =
 
1
+ import {
2
+ t,
3
+ initI18n,
4
+ bindLanguageSelector,
5
+ onLanguageChange,
6
+ getCurrentLanguage,
7
+ } from './i18n.js';
8
  import { closeIcon, eyeIcon, trashIcon } from './constants.js';
9
+ import {
10
+ ensureAuthenticated,
11
+ startAuthWatcher,
12
+ clearAuthUser,
13
+ } from './auth.js';
14
+
15
+ ensureAuthenticated();
16
+ startAuthWatcher();
17
+
18
+ const logoutButton = document.getElementById('btn-logout');
19
+ if (logoutButton) {
20
+ logoutButton.addEventListener('click', () => {
21
+ clearAuthUser();
22
+ window.location.href = 'login.html';
23
+ });
24
+ }
25
 
26
  const inputElement = document.getElementById('file-input');
27
  const textInputElement = document.getElementById('text-input-additional');
 
310
  if (data.value) {
311
  const menuItems =
312
  newValue === 'verified_evidence'
313
+ ? buildVerifiedEvidenceMenu(
314
+ selectedTabChildren.value || 'source_details'
315
+ )
316
  : '';
317
 
318
  outputElement.innerHTML =
 
585
 
586
  const formData = new FormData();
587
  formData.append('metadata_file', metadataFile);
588
+ formData.append(
589
+ 'additional_text',
590
+ sanitizeTextareaValue(textInputElement?.value)
591
+ );
592
  const selectedLanguage = getCurrentLanguage();
593
  formData.append('language', languageApiMap[selectedLanguage] || 'English');
594
 
 
642
 
643
  const menuItems =
644
  selectedTab.value === 'verified_evidence'
645
+ ? buildVerifiedEvidenceMenu(
646
+ selectedTabChildren.value || 'source_details'
647
+ )
648
  : '';
649
 
650
  outputElement.innerHTML =
styles.css CHANGED
@@ -72,6 +72,29 @@ body {
72
  padding: 15px 24px;
73
  }
74
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  .title-header-face-check {
76
  font-size: 28px;
77
  font-weight: 600;
@@ -87,6 +110,7 @@ body {
87
  font-size: 14px;
88
  color: #454545;
89
  font-weight: 600;
 
90
  }
91
 
92
  .language-select {
@@ -1041,3 +1065,125 @@ textarea:focus {
1041
  margin-bottom: 12px;
1042
  text-align: left;
1043
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  padding: 15px 24px;
73
  }
74
 
75
+ .header-placeholder {
76
+ width: 120px;
77
+ }
78
+
79
+ .header-actions {
80
+ display: flex;
81
+ align-items: center;
82
+ gap: 12px;
83
+ }
84
+
85
+ .btn-logout {
86
+ width: auto;
87
+ padding: 8px 14px;
88
+ background-color: #f1f3f5;
89
+ color: #374151;
90
+ border: 1px solid #d6d9de;
91
+ border-radius: 8px;
92
+ }
93
+
94
+ .btn-logout:hover {
95
+ background-color: #e9ecef;
96
+ }
97
+
98
  .title-header-face-check {
99
  font-size: 28px;
100
  font-weight: 600;
 
110
  font-size: 14px;
111
  color: #454545;
112
  font-weight: 600;
113
+ white-space: nowrap;
114
  }
115
 
116
  .language-select {
 
1065
  margin-bottom: 12px;
1066
  text-align: left;
1067
  }
1068
+
1069
+ /* Login Page */
1070
+ .login-layout {
1071
+ position: relative;
1072
+ min-height: 100vh;
1073
+ width: 100%;
1074
+ display: flex;
1075
+ align-items: center;
1076
+ justify-content: center;
1077
+ padding: 24px;
1078
+ background: radial-gradient(circle at 20% 20%, #fff4e6, #f8f9fa 35%),
1079
+ radial-gradient(circle at 80% 0%, #e7f5ff, #f8f9fa 35%),
1080
+ #f8f9fa;
1081
+ }
1082
+
1083
+ .login-language-switcher {
1084
+ position: absolute;
1085
+ top: 20px;
1086
+ right: 20px;
1087
+ display: flex;
1088
+ align-items: center;
1089
+ gap: 8px;
1090
+ }
1091
+
1092
+ .login-card {
1093
+ width: min(420px, 100%);
1094
+ background: #ffffff;
1095
+ border-radius: 16px;
1096
+ box-shadow: 0 12px 40px rgba(0, 0, 0, 0.08);
1097
+ padding: 28px;
1098
+ display: flex;
1099
+ flex-direction: column;
1100
+ gap: 18px;
1101
+ }
1102
+
1103
+ .login-header h1 {
1104
+ font-size: 26px;
1105
+ font-weight: 700;
1106
+ color: #1f2933;
1107
+ }
1108
+
1109
+ .login-subtitle {
1110
+ margin-top: 6px;
1111
+ color: #52606d;
1112
+ line-height: 1.4;
1113
+ }
1114
+
1115
+ .login-form {
1116
+ display: flex;
1117
+ flex-direction: column;
1118
+ gap: 10px;
1119
+ }
1120
+
1121
+ .login-label {
1122
+ font-weight: 600;
1123
+ color: #374151;
1124
+ }
1125
+
1126
+ .login-input {
1127
+ width: 100%;
1128
+ padding: 12px 14px;
1129
+ border: 1px solid #d1d5db;
1130
+ border-radius: 10px;
1131
+ background: #f9fafb;
1132
+ transition: border-color 0.2s ease, box-shadow 0.2s ease;
1133
+ }
1134
+
1135
+ .login-input:focus {
1136
+ outline: none;
1137
+ border-color: #fd7e14;
1138
+ box-shadow: 0 0 0 3px rgba(253, 126, 20, 0.15);
1139
+ background: #ffffff;
1140
+ }
1141
+
1142
+ .login-error {
1143
+ min-height: 20px;
1144
+ color: #d14343;
1145
+ font-weight: 600;
1146
+ }
1147
+
1148
+ .login-submit {
1149
+ width: 100%;
1150
+ background: linear-gradient(90deg, #fd7e14, #ff9f43);
1151
+ color: #ffffff;
1152
+ border: none;
1153
+ border-radius: 12px;
1154
+ padding: 12px 16px;
1155
+ font-weight: 700;
1156
+ cursor: pointer;
1157
+ transition: transform 0.1s ease, box-shadow 0.2s ease;
1158
+ }
1159
+
1160
+ .login-submit:hover {
1161
+ box-shadow: 0 10px 25px rgba(253, 126, 20, 0.28);
1162
+ }
1163
+
1164
+ .login-submit:active {
1165
+ transform: translateY(1px);
1166
+ }
1167
+
1168
+ .login-fake-accounts {
1169
+ background: #f9fafb;
1170
+ border: 1px solid #e5e7eb;
1171
+ border-radius: 12px;
1172
+ padding: 14px;
1173
+ }
1174
+
1175
+ .login-fake-accounts-title {
1176
+ font-weight: 700;
1177
+ margin-bottom: 8px;
1178
+ color: #1f2937;
1179
+ }
1180
+
1181
+ .login-fake-accounts-list {
1182
+ display: flex;
1183
+ flex-direction: column;
1184
+ gap: 6px;
1185
+ font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo,
1186
+ monospace;
1187
+ color: #334155;
1188
+ font-size: 14px;
1189
+ }