mirror of
git://f0xx.org/ac/ac-platform-web
synced 2026-07-29 04:18:31 +03:00
feat(platform-web): global form styles, register.js, footer i18n
Signed-off-by: Anton Afanasyeu <a.afanasieff@gmail.com>
This commit is contained in:
@@ -1592,20 +1592,62 @@ button.report-tag--filter:hover {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
.tag-field input[type="text"] {
|
.tag-field input[type="text"],
|
||||||
padding: 8px 10px;
|
.tag-field input[type="url"],
|
||||||
border-radius: 6px;
|
.tag-field input[type="email"],
|
||||||
|
.tag-field input[type="number"],
|
||||||
|
.tag-field input[type="password"],
|
||||||
|
.tag-field input[type="search"],
|
||||||
|
.tag-field select,
|
||||||
|
.tag-field textarea {
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
|
font-size: 14px;
|
||||||
min-width: 10rem;
|
min-width: 10rem;
|
||||||
}
|
}
|
||||||
|
.tag-field input:focus,
|
||||||
|
.tag-field select:focus,
|
||||||
|
.tag-field textarea:focus {
|
||||||
|
outline: 2px solid var(--accent);
|
||||||
|
outline-offset: 1px;
|
||||||
|
}
|
||||||
|
.tag-field select {
|
||||||
|
cursor: pointer;
|
||||||
|
appearance: auto;
|
||||||
|
}
|
||||||
.tag-editor-actions {
|
.tag-editor-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
.data-table select,
|
||||||
|
#rbac-users-table select {
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 14px;
|
||||||
|
min-width: 8rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.data-table select:focus,
|
||||||
|
#rbac-users-table select:focus {
|
||||||
|
outline: 2px solid var(--accent);
|
||||||
|
outline-offset: 1px;
|
||||||
|
}
|
||||||
|
.register-countdown-wrap {
|
||||||
|
margin: 1rem 0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.register-countdown-wrap strong {
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
margin-left: 0.35rem;
|
||||||
|
}
|
||||||
.tag-editor-status--err { color: var(--danger); }
|
.tag-editor-status--err { color: var(--danger); }
|
||||||
.tag-preset-bar {
|
.tag-preset-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -147,6 +147,10 @@
|
|||||||
"register.password": "Password",
|
"register.password": "Password",
|
||||||
"register.submit": "Register",
|
"register.submit": "Register",
|
||||||
"register.back_login": "Back to sign in",
|
"register.back_login": "Back to sign in",
|
||||||
|
"register.pending_title": "Check your email",
|
||||||
|
"register.pending_hint": "We sent a verification link. Confirm your email before signing in.",
|
||||||
|
"register.countdown_label": "Link expires in",
|
||||||
|
"register.resend": "Resend verification email",
|
||||||
"verify.title": "Email verification",
|
"verify.title": "Email verification",
|
||||||
"verify.ok": "Your email is verified. You can sign in and enroll two-factor authentication.",
|
"verify.ok": "Your email is verified. You can sign in and enroll two-factor authentication.",
|
||||||
"verify.fail": "Invalid or expired link.",
|
"verify.fail": "Invalid or expired link.",
|
||||||
@@ -173,7 +177,7 @@
|
|||||||
"security.remove_totp": "Remove authenticator",
|
"security.remove_totp": "Remove authenticator",
|
||||||
"security.back_console": "Back to console",
|
"security.back_console": "Back to console",
|
||||||
"nav.security": "Security",
|
"nav.security": "Security",
|
||||||
"footer.copyright": "© Anton Afanaasyeu, {year}",
|
"footer.copyright": "{holder}, {year}",
|
||||||
"tickets.title": "Tickets",
|
"tickets.title": "Tickets",
|
||||||
"issues.new": "New issue",
|
"issues.new": "New issue",
|
||||||
"tickets.new": "New ticket",
|
"tickets.new": "New ticket",
|
||||||
|
|||||||
@@ -156,6 +156,10 @@
|
|||||||
"register.password": "Пароль",
|
"register.password": "Пароль",
|
||||||
"register.submit": "Зарегистрироваться",
|
"register.submit": "Зарегистрироваться",
|
||||||
"register.back_login": "Назад ко входу",
|
"register.back_login": "Назад ко входу",
|
||||||
|
"register.pending_title": "Проверьте почту",
|
||||||
|
"register.pending_hint": "Мы отправили ссылку для подтверждения. Подтвердите email перед входом.",
|
||||||
|
"register.countdown_label": "Ссылка действует",
|
||||||
|
"register.resend": "Отправить письмо повторно",
|
||||||
"verify.title": "Подтверждение email",
|
"verify.title": "Подтверждение email",
|
||||||
"verify.ok": "Email подтверждён. Войдите и настройте двухфакторную аутентификацию.",
|
"verify.ok": "Email подтверждён. Войдите и настройте двухфакторную аутентификацию.",
|
||||||
"verify.fail": "Недействительная или просроченная ссылка.",
|
"verify.fail": "Недействительная или просроченная ссылка.",
|
||||||
@@ -182,7 +186,7 @@
|
|||||||
"security.remove_totp": "Отключить аутентификатор",
|
"security.remove_totp": "Отключить аутентификатор",
|
||||||
"security.back_console": "Назад в консоль",
|
"security.back_console": "Назад в консоль",
|
||||||
"nav.security": "Безопасность",
|
"nav.security": "Безопасность",
|
||||||
"footer.copyright": "© Anton Afanaasyeu, {year}",
|
"footer.copyright": "{holder}, {year}",
|
||||||
"nav.tickets": "Тикеты",
|
"nav.tickets": "Тикеты",
|
||||||
"ticket.lifecycle": "Жизненный цикл",
|
"ticket.lifecycle": "Жизненный цикл",
|
||||||
"ticket.assignees": "Исполнители",
|
"ticket.assignees": "Исполнители",
|
||||||
|
|||||||
131
assets/js/register.js
Normal file
131
assets/js/register.js
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
(function () {
|
||||||
|
function authBase() {
|
||||||
|
return document.body.getAttribute('data-auth-base') || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function apiBase() {
|
||||||
|
var bp = document.body.getAttribute('data-base-path') || '';
|
||||||
|
return bp.replace(/\/issues\/?$/, '') + '/api/auth_register.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatCountdown(totalSeconds) {
|
||||||
|
var s = Math.max(0, totalSeconds);
|
||||||
|
var m = Math.floor(s / 60);
|
||||||
|
var r = s % 60;
|
||||||
|
return m + ':' + String(r).padStart(2, '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseExpiresMs(iso) {
|
||||||
|
if (!iso) return 0;
|
||||||
|
var t = Date.parse(iso.replace(' ', 'T') + 'Z');
|
||||||
|
if (Number.isNaN(t)) {
|
||||||
|
t = Date.parse(iso);
|
||||||
|
}
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
function initCountdown(expiresTs, countdownEl, onExpired) {
|
||||||
|
var expMs = expiresTs > 0 ? expiresTs * 1000 : 0;
|
||||||
|
if (!expMs) {
|
||||||
|
countdownEl.textContent = '—';
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
function tick() {
|
||||||
|
var left = Math.ceil((expMs - Date.now()) / 1000);
|
||||||
|
if (left <= 0) {
|
||||||
|
countdownEl.textContent = '0:00';
|
||||||
|
if (onExpired) onExpired();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
countdownEl.textContent = formatCountdown(left);
|
||||||
|
}
|
||||||
|
tick();
|
||||||
|
return window.setInterval(tick, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function postJson(body) {
|
||||||
|
var res = await fetch(apiBase(), {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'same-origin',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
var data = await res.json().catch(function () { return {}; });
|
||||||
|
return { ok: res.ok && data.ok !== false, status: res.status, data: data };
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
var pending = document.getElementById('register-pending');
|
||||||
|
var form = document.getElementById('register-form');
|
||||||
|
var countdownEl = document.getElementById('register-countdown');
|
||||||
|
var resendBtn = document.getElementById('register-resend-btn');
|
||||||
|
var resendStatus = document.getElementById('register-resend-status');
|
||||||
|
var emailEl = document.getElementById('register-pending-email');
|
||||||
|
if (!pending || !countdownEl) return;
|
||||||
|
|
||||||
|
var email = document.body.getAttribute('data-register-email') || '';
|
||||||
|
var expiresTs = parseInt(document.body.getAttribute('data-register-expires-ts') || '0', 10);
|
||||||
|
var timerId = null;
|
||||||
|
|
||||||
|
function showPending(nextEmail, nextExpiresTs) {
|
||||||
|
if (form) form.hidden = true;
|
||||||
|
pending.hidden = false;
|
||||||
|
email = nextEmail || email;
|
||||||
|
expiresTs = nextExpiresTs || expiresTs;
|
||||||
|
if (emailEl && email) emailEl.textContent = email;
|
||||||
|
if (timerId) window.clearInterval(timerId);
|
||||||
|
timerId = initCountdown(expiresTs, countdownEl, function () {
|
||||||
|
if (resendStatus) {
|
||||||
|
resendStatus.textContent = 'Verification link expired — use Resend to get a new one.';
|
||||||
|
resendStatus.classList.add('error');
|
||||||
|
}
|
||||||
|
if (resendBtn) resendBtn.disabled = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.body.getAttribute('data-register-pending') === '1') {
|
||||||
|
showPending(email, expiresTs);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resendBtn) {
|
||||||
|
resendBtn.addEventListener('click', async function () {
|
||||||
|
if (!email) {
|
||||||
|
var input = form && form.querySelector('input[name="email"]');
|
||||||
|
email = input ? input.value.trim() : '';
|
||||||
|
}
|
||||||
|
if (!email) {
|
||||||
|
if (resendStatus) resendStatus.textContent = 'Enter your email on the registration form first.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resendBtn.disabled = true;
|
||||||
|
if (resendStatus) {
|
||||||
|
resendStatus.textContent = 'Sending…';
|
||||||
|
resendStatus.classList.remove('error');
|
||||||
|
}
|
||||||
|
var out = await postJson({ action: 'resend', email: email });
|
||||||
|
if (out.ok && out.data.expires_at) {
|
||||||
|
var ts = out.data.expires_at ? Math.floor(Date.parse(String(out.data.expires_at).replace(' ', 'T')) / 1000) : 0;
|
||||||
|
if (!ts && out.data.ttl_seconds) {
|
||||||
|
ts = Math.floor(Date.now() / 1000) + Number(out.data.ttl_seconds);
|
||||||
|
}
|
||||||
|
showPending(email, ts);
|
||||||
|
if (resendStatus) resendStatus.textContent = 'Verification email sent.';
|
||||||
|
resendBtn.disabled = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var err = (out.data && out.data.error) || 'resend_failed';
|
||||||
|
if (resendStatus) {
|
||||||
|
resendStatus.textContent = err === 'rate_limited'
|
||||||
|
? 'Too many attempts — wait a few minutes.'
|
||||||
|
: err === 'not_pending'
|
||||||
|
? 'No pending registration for this email.'
|
||||||
|
: err === 'mail_failed'
|
||||||
|
? 'Could not send email — contact support.'
|
||||||
|
: 'Could not resend verification email.';
|
||||||
|
resendStatus.classList.add('error');
|
||||||
|
}
|
||||||
|
resendBtn.disabled = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
Reference in New Issue
Block a user