Spaces:
Running
Running
add login page
Browse files- auth.js +80 -0
- i18n.js +42 -12
- index.html +11 -8
- login.html +62 -0
- login.js +57 -0
- main.js +33 -4
- 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:
|
|
|
|
| 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:
|
|
|
|
| 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:
|
|
|
|
| 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="
|
| 22 |
-
<
|
| 23 |
-
|
| 24 |
-
<
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
|
|
|
|
|
|
|
|
|
| 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 {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
| 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 |
+
}
|