UNPKG

gentelella

Version:

Gentelella v4 — free admin template. 60 pages, 20 chart variants, fully interactive inbox & kanban, live theme generator, component playground, PWA-ready. Vite 8, vanilla JS, no Bootstrap, no jQuery.

420 lines (386 loc) 15.3 kB
// Settings page interactivity: // - Persist every toggle / radio / form input to localStorage by stable key // - Restore values on next visit // - Save / Cancel buttons reflect dirty state and roll back on Cancel // - Integration "Connect" buttons toggle to "Disconnect" with persistence // - Revoke session removes the row // - Danger-zone actions open confirm modals import { showToast } from './toast.js'; import { showModal } from './modal.js'; const STORAGE_KEY = 'gentelella:settings'; function load() { try { const raw = localStorage.getItem(STORAGE_KEY); return raw ? JSON.parse(raw) : {}; } catch (_e) { return {}; } } function save(data) { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); } catch (_e) { /* private mode */ } } // ──────────────────────── // TOGGLES — persist by label text // ──────────────────────── function toggleKey(toggle) { const row = toggle.closest('.settings-toggle-row'); const label = row?.querySelector('.label')?.textContent.trim(); if (!label) {return null;} const section = toggle.closest('.settings-section')?.id || 'general'; return `toggle:${section}:${label}`; } function initToggles() { const stored = load(); document.querySelectorAll('.settings-toggle-list .toggle').forEach((t) => { const k = toggleKey(t); if (!k) {return;} if (Object.prototype.hasOwnProperty.call(stored, k)) { t.classList.toggle('on', !!stored[k]); } }); // Click is already handled globally; we listen for the change to persist. document.addEventListener('click', (e) => { const t = e.target.closest('.settings-toggle-list .toggle'); if (!t) {return;} // Wait one tick so the global handler updates the class first. setTimeout(() => { const k = toggleKey(t); if (!k) {return;} const data = load(); data[k] = t.classList.contains('on'); save(data); }, 0); }); } // ──────────────────────── // RADIO + INPUT persistence (theme, density) // ──────────────────────── function initRadios() { const stored = load(); document.querySelectorAll('.theme-options input[type="radio"]').forEach((r) => { const k = `radio:${r.name}`; if (stored[k] === r.value) {r.checked = true;} r.addEventListener('change', () => { if (!r.checked) {return;} const data = load(); data[k] = r.value; save(data); // Side-effect: theme radio actually applies the theme. if (r.name === 'theme') {applyThemeChoice(r.value);} }); }); } function applyThemeChoice(choice) { if (choice === 'system') { try { localStorage.removeItem('theme'); } catch (_e) { /* ignore */ } const dark = window.matchMedia('(prefers-color-scheme: dark)').matches; document.documentElement.setAttribute('data-theme', dark ? 'dark' : 'light'); } else { try { localStorage.setItem('theme', choice); } catch (_e) { /* ignore */ } document.documentElement.setAttribute('data-theme', choice); } const btn = document.querySelector('.theme-toggle'); if (btn) {btn.setAttribute('aria-pressed', choice === 'dark' ? 'true' : 'false');} } // ──────────────────────── // PROFILE FORM — Save / Cancel // ──────────────────────── function initProfileForm() { // The settings page wraps profile fields in a <form>. Find the first form // inside the settings-content area. const profileForm = document.querySelector('.settings-content form'); if (!profileForm) {return;} const inputs = [...profileForm.querySelectorAll('input, textarea, select')]; const stored = load(); inputs.forEach((el) => { const k = `field:${el.id || el.name}`; if (!k.endsWith(':') && Object.prototype.hasOwnProperty.call(stored, k)) { el.value = stored[k]; } }); const initial = inputs.map((el) => el.value); let dirty = false; const saveBtn = profileForm.querySelector('button[type="submit"]'); const cancelBtn = profileForm.querySelector('button[type="reset"]'); if (saveBtn) {saveBtn.disabled = true;} if (cancelBtn) {cancelBtn.disabled = true;} const checkDirty = () => { const current = inputs.map((el) => el.value); dirty = current.some((v, i) => v !== initial[i]); if (saveBtn) {saveBtn.disabled = !dirty;} if (cancelBtn) {cancelBtn.disabled = !dirty;} }; inputs.forEach((el) => el.addEventListener('input', checkDirty)); profileForm.addEventListener('submit', (e) => { e.preventDefault(); e.stopPropagation(); if (!dirty) {return;} const data = load(); inputs.forEach((el) => { const k = `field:${el.id || el.name}`; if (!k.endsWith(':')) {data[k] = el.value;} }); save(data); inputs.forEach((el, i) => { initial[i] = el.value; }); dirty = false; if (saveBtn) {saveBtn.disabled = true;} if (cancelBtn) {cancelBtn.disabled = true;} showToast('Profile saved', { variant: 'success' }); }); if (cancelBtn) { cancelBtn.addEventListener('click', (e) => { e.preventDefault(); if (!dirty) {return;} inputs.forEach((el, i) => { el.value = initial[i]; }); checkDirty(); showToast('Changes discarded'); }); } } // ──────────────────────── // INTEGRATIONS — connect/disconnect // ──────────────────────── function initIntegrations() { const stored = load(); document.querySelectorAll('.integration').forEach((card) => { const titleEl = card.querySelector('.title'); const btn = card.querySelector('.btn'); if (!titleEl || !btn) {return;} const key = `integration:${titleEl.textContent.trim()}`; const isConnected = () => { if (Object.prototype.hasOwnProperty.call(stored, key)) {return stored[key];} // Default state from existing markup label return /connected/i.test(btn.textContent); }; const paint = () => { const connected = isConnected(); btn.textContent = connected ? 'Connected ✓' : 'Connect'; btn.classList.toggle('btn-primary', !connected); btn.classList.toggle('btn-outline', connected); }; paint(); btn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); const data = load(); data[key] = !isConnected(); Object.assign(stored, data); save(data); paint(); showToast(data[key] ? `${titleEl.textContent.trim()} connected` : `${titleEl.textContent.trim()} disconnected`, { variant: data[key] ? 'success' : 'default' }); }); }); } // ──────────────────────── // SESSIONS — revoke // ──────────────────────── function initSessions() { document.querySelectorAll('.session-row').forEach((row) => { const btn = row.querySelector('.btn'); if (!btn || btn.textContent.trim().toLowerCase() !== 'revoke') {return;} btn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); const device = row.querySelector('.device')?.textContent.trim() || 'session'; showModal({ title: 'Revoke session?', body: `<p style="font-size:13px;line-height:1.6;color:var(--text-secondary)">This will sign out <strong>${device}</strong> immediately. The user will need to sign in again.</p>`, actions: [ { label: 'Cancel', variant: 'ghost' }, { label: 'Revoke', variant: 'danger', action: () => { row.style.transition = 'opacity 200ms, transform 200ms'; row.style.opacity = '0'; row.style.transform = 'translateX(8px)'; setTimeout(() => row.remove(), 220); showToast(`Revoked: ${device}`, { variant: 'success' }); } } ] }); }); }); } // ──────────────────────── // DANGER ZONE // ──────────────────────── function initDanger() { document.querySelectorAll('.danger-row .btn').forEach((btn) => { const label = btn.textContent.trim().toLowerCase(); if (label === 'export') { btn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); const data = load(); const blob = new Blob([JSON.stringify({ exportedAt: new Date().toISOString(), settings: data }, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'gentelella-export.json'; document.body.appendChild(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(url), 0); showToast('Workspace export ready', { variant: 'success' }); }); } else if (label === 'transfer') { btn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); showModal({ title: 'Transfer workspace ownership', body: ` <p style="font-size:13px;line-height:1.6;color:var(--text-secondary);margin-bottom:14px">Transfer ownership to another team member. You'll keep your account but lose admin privileges.</p> <div class="form-group"> <label class="form-label" for="transfer-to">Transfer to</label> <select id="transfer-to" class="form-control"> <option>Sarah Kowalski (sarah@example.com)</option> <option>Michael Reyes (michael@example.com)</option> <option>Emily Wang (emily@example.com)</option> </select> </div> `, actions: [ { label: 'Cancel', variant: 'ghost' }, { label: 'Transfer', variant: 'danger', action: (ctx) => { const to = ctx.body.querySelector('#transfer-to').value; showToast(`Transfer initiated to ${to}`, { variant: 'success' }); } } ] }); }); } else if (label === 'delete') { btn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); showModal({ title: 'Delete account permanently?', body: ` <p style="font-size:13px;line-height:1.6;color:var(--text-secondary);margin-bottom:14px">This will <strong>permanently delete</strong> your account, all projects, and all associated data. This action cannot be undone.</p> <div class="form-group"> <label class="form-label" for="confirm-delete">Type <code style="background:var(--bg-surface-secondary);padding:1px 4px;border-radius:3px">DELETE</code> to confirm</label> <input id="confirm-delete" class="form-control" autocomplete="off"> </div> `, actions: [ { label: 'Cancel', variant: 'ghost' }, { label: 'Delete account', variant: 'danger', action: (ctx) => { const v = ctx.body.querySelector('#confirm-delete').value; if (v !== 'DELETE') { showToast('Type DELETE to confirm', { variant: 'error' }); return false; } showToast('Account deletion initiated', { variant: 'error' }); } } ] }); }); } }); } // ──────────────────────── // TEAM — Invite + Manage // ──────────────────────── function initTeam() { const inviteBtn = [...document.querySelectorAll('#team .btn-primary')] .find((b) => b.textContent.trim().toLowerCase().includes('invite')); if (inviteBtn) { inviteBtn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); showModal({ title: 'Invite team member', body: ` <div class="form-group"> <label class="form-label" for="invite-email">Email</label> <input type="email" id="invite-email" class="form-control" placeholder="colleague@example.com" autocomplete="off"> </div> <div class="form-group"> <label class="form-label" for="invite-role">Role</label> <select id="invite-role" class="form-control"> <option>Member</option> <option>Admin</option> <option>Designer</option> <option>Engineer</option> <option>PM</option> </select> </div> `, actions: [ { label: 'Cancel', variant: 'ghost' }, { label: 'Send invite', variant: 'primary', action: (ctx) => { const email = ctx.body.querySelector('#invite-email').value.trim(); if (!email) { showToast('Add an email address', { variant: 'error' }); return false; } showToast(`Invite sent to ${email}`, { variant: 'success' }); } } ] }); }); } document.querySelectorAll('#team .session-row .btn').forEach((btn) => { if (btn.textContent.trim().toLowerCase() !== 'manage') {return;} btn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); const row = btn.closest('.session-row'); const name = row?.querySelector('.device')?.textContent.trim() || 'Member'; const meta = row?.querySelector('.meta')?.textContent.trim() || ''; showModal({ title: `Manage ${name}`, body: ` <p style="font-size:13px;line-height:1.6;color:var(--text-secondary);margin-bottom:14px">${meta}</p> <div class="form-group"> <label class="form-label">Role</label> <select class="form-control" id="manage-role"> <option>Member</option><option>Admin</option><option>Designer</option><option>Engineer</option><option>PM</option> </select> </div> `, actions: [ { label: 'Remove', variant: 'danger', action: () => { row.remove(); showToast(`${name} removed from team`); } }, { label: 'Cancel', variant: 'ghost' }, { label: 'Save', variant: 'primary', action: (ctx) => { const role = ctx.body.querySelector('#manage-role').value; showToast(`${name}${role}`, { variant: 'success' }); } } ] }); }); }); } /** * Wire up all settings interactions. Idempotent on a single page load. */ export function initSettings() { if (initSettings._wired) {return;} initSettings._wired = true; initToggles(); initRadios(); initProfileForm(); initIntegrations(); initSessions(); initDanger(); initTeam(); }