speechify-clone/static/settings.html
ifedan-ed ee3ffcfe90 Add email verification, user Nextcloud settings, Groq TTS, favicon, and settings page
- Email verification: send link on register via SMTP2GO (noreply@pedshub.com), verify endpoint, beautiful HTML email
- Unverified users see yellow banner in app with resend button; also shown on settings page
- User settings page (/settings): personal Nextcloud credentials (override admin-wide config)
- Admin and per-user Nextcloud config merged (user settings take priority)
- Groq PlayAI TTS: 20 English voices + 2 Arabic voices via groq-playai-tts / groq-playai-tts-arabic
- Added groq-playai-tts and groq-playai-tts-arabic to litellm config
- Favicon SVG (purple play button) across all pages
- Settings link in user dropdown menu
- verify-ok.html and verify-fail.html result pages
- DB migration: added email_verified column, verification_tokens and user_settings tables

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-11 04:12:25 +02:00

204 lines
8.3 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Settings — Speechify</title>
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg"/>
<link rel="preconnect" href="https://fonts.googleapis.com"/>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet"/>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg:#0d0d0d; --bg2:#161616; --bg3:#212121; --bg4:#2a2a2a;
--accent:#7c3aed; --accent-h:#9d6fff;
--text:#e8e8e8; --text-m:#888; --border:#232323; --success:#4ade80;
}
body { font-family:'Inter',sans-serif; background:var(--bg); color:var(--text); min-height:100vh; }
header {
height:54px; background:var(--bg2); border-bottom:1px solid var(--border);
display:flex; align-items:center; justify-content:space-between; padding:0 24px;
}
.logo { display:flex; align-items:center; gap:10px; font-size:17px; font-weight:700; text-decoration:none; color:var(--text); }
.header-right { display:flex; align-items:center; gap:12px; }
.back-btn, .logout-btn {
background:var(--bg3); border:1px solid var(--border); color:var(--text-m);
border-radius:7px; padding:6px 14px; font-size:13px; font-family:inherit;
cursor:pointer; text-decoration:none; display:inline-block; transition:all .15s;
}
.back-btn:hover, .logout-btn:hover { border-color:var(--accent); color:var(--text); }
.container { max-width:640px; margin:0 auto; padding:40px 24px; }
h2 { font-size:20px; font-weight:700; margin-bottom:6px; }
.subtitle { font-size:13px; color:var(--text-m); margin-bottom:32px; }
h3 { font-size:11px; font-weight:600; color:var(--text-m); text-transform:uppercase;
letter-spacing:.5px; margin-bottom:16px; }
.card { background:var(--bg2); border:1px solid var(--border); border-radius:12px;
padding:24px; margin-bottom:20px; }
.form-row { display:flex; flex-direction:column; gap:6px; margin-bottom:16px; }
.form-row:last-child { margin-bottom:0; }
label { font-size:12px; font-weight:600; color:var(--text-m); text-transform:uppercase; letter-spacing:.5px; }
input {
background:var(--bg3); border:1px solid var(--border); border-radius:8px;
color:var(--text); font-family:inherit; font-size:14px; padding:9px 12px; outline:none; width:100%;
}
input:focus { border-color:var(--accent); }
.btn-primary {
background:var(--accent); color:#fff; border:none; border-radius:8px;
padding:10px 20px; font-family:inherit; font-size:14px; font-weight:600; cursor:pointer;
}
.btn-primary:hover { background:var(--accent-h); }
.btn-secondary {
background:var(--bg3); color:var(--text-m); border:1px solid var(--border); border-radius:8px;
padding:10px 20px; font-family:inherit; font-size:14px; font-weight:500; cursor:pointer;
}
.btn-secondary:hover { border-color:var(--accent); color:var(--text); }
.msg { font-size:13px; margin-top:12px; min-height:18px; }
.msg.ok { color:var(--success); }
.msg.err { color:#f87171; }
.verify-banner {
display:none; background:rgba(245,158,11,.1); border:1px solid rgba(245,158,11,.25);
border-radius:10px; padding:14px 18px; margin-bottom:24px;
font-size:13px; color:#fbbf24; line-height:1.6;
}
.verify-banner.show { display:flex; align-items:center; justify-content:space-between; gap:12px; }
.verify-banner button {
background:rgba(245,158,11,.2); border:1px solid rgba(245,158,11,.3); color:#fbbf24;
border-radius:6px; padding:5px 12px; font-size:12px; font-family:inherit; cursor:pointer; white-space:nowrap;
}
.verified-badge {
display:inline-flex; align-items:center; gap:6px; background:rgba(74,222,128,.1);
border:1px solid rgba(74,222,128,.2); color:#4ade80; border-radius:6px;
padding:4px 10px; font-size:12px; font-weight:600; margin-top:8px;
}
</style>
</head>
<body>
<header>
<a href="/" class="logo">
<svg width="28" height="28" viewBox="0 0 30 30" fill="none">
<rect width="30" height="30" rx="9" fill="#7c3aed"/>
<polygon points="11,8 23,15 11,22" fill="white"/>
</svg>
Speechify
</a>
<div class="header-right">
<a href="/" class="back-btn">← Back to App</a>
<button class="logout-btn" onclick="logout()">Sign Out</button>
</div>
</header>
<div class="container">
<h2>Account Settings</h2>
<p class="subtitle" id="subtitle">Manage your personal preferences.</p>
<!-- Email verification banner -->
<div class="verify-banner" id="verify-banner">
<span>⚠️ Your email address is not verified. Check your inbox or resend the link.</span>
<button onclick="resendVerification()">Resend Email</button>
</div>
<!-- Email / account info -->
<div class="card">
<h3>Account</h3>
<div class="form-row">
<label>Username</label>
<input id="acc-username" type="text" readonly style="opacity:.5;cursor:default"/>
</div>
<div class="form-row">
<label>Email</label>
<input id="acc-email" type="email" readonly style="opacity:.5;cursor:default"/>
<div id="email-status"></div>
</div>
<div class="msg" id="acc-msg"></div>
</div>
<!-- Nextcloud -->
<div class="card">
<h3>Nextcloud Integration</h3>
<p style="font-size:13px;color:var(--text-m);margin-bottom:16px;line-height:1.6;">
Connect your personal Nextcloud to browse and read PDFs from the Cloud tab.
Leave blank to use the server-wide default configured by the admin.
</p>
<div class="form-row">
<label>Nextcloud URL</label>
<input id="nc-url" type="url" placeholder="https://cloud.example.com"/>
</div>
<div class="form-row">
<label>Username</label>
<input id="nc-user" type="text"/>
</div>
<div class="form-row">
<label>App Password</label>
<input id="nc-pass" type="password" placeholder="Leave blank to keep existing"/>
</div>
<button class="btn-primary" onclick="saveNextcloud()">Save</button>
<div class="msg" id="nc-msg"></div>
</div>
</div>
<script>
let me = null;
async function init() {
const resp = await fetch('/api/auth/me');
if (resp.status === 401) { window.location.href = '/login'; return; }
me = await resp.json();
document.getElementById('acc-username').value = me.username;
document.getElementById('acc-email').value = me.email;
document.getElementById('subtitle').textContent = `Signed in as ${me.username}`;
if (!me.email_verified) {
document.getElementById('verify-banner').classList.add('show');
document.getElementById('email-status').innerHTML =
'<span style="font-size:12px;color:#f87171;">Not verified</span>';
} else {
document.getElementById('email-status').innerHTML =
'<div class="verified-badge"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><polyline points="20 6 9 17 4 12"/></svg> Verified</div>';
}
// Load Nextcloud settings
const s = await fetch('/api/user/settings').then(r => r.json());
document.getElementById('nc-url').value = s.nc_url || '';
document.getElementById('nc-user').value = s.nc_user || '';
}
async function resendVerification() {
const resp = await fetch('/api/auth/resend-verification', { method: 'POST' });
if (resp.ok) {
document.getElementById('verify-banner').innerHTML =
'<span>✓ Verification email sent — check your inbox.</span>';
}
}
async function saveNextcloud() {
const msg = document.getElementById('nc-msg');
const pass = document.getElementById('nc-pass').value;
const body = {
nc_url: document.getElementById('nc-url').value,
nc_user: document.getElementById('nc-user').value,
};
if (pass) body.nc_pass = pass;
const resp = await fetch('/api/user/settings', {
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify(body),
});
if (resp.ok) {
msg.textContent = '✓ Saved'; msg.className = 'msg ok';
document.getElementById('nc-pass').value = '';
} else {
msg.textContent = '✗ Error saving'; msg.className = 'msg err';
}
setTimeout(() => msg.textContent = '', 3000);
}
function logout() {
fetch('/api/auth/logout', { method: 'POST' }).finally(() => window.location.href = '/login');
}
init();
</script>
</body>
</html>