Files
kashilo/js/components/pages/page-settings.js

525 lines
19 KiB
JavaScript

import { t, i18n } from '../../i18n.js'
import { auth } from '../../services/auth.js'
import { favoritesService } from '../../services/favorites.js'
class PageSettings extends HTMLElement {
constructor() {
super()
this.isLoggedIn = false
this.user = null
this.uidVisible = false
}
async connectedCallback() {
this.isLoggedIn = auth.isLoggedIn()
if (!this.isLoggedIn) {
window.location.hash = '#/'
return
}
this.user = await auth.getUser()
this.render()
this.setupEventListeners()
this.unsubscribe = i18n.subscribe(() => {
this.render()
this.setupEventListeners()
})
this.authUnsubscribe = auth.subscribe(() => {
this.isLoggedIn = auth.isLoggedIn()
if (!this.isLoggedIn) {
window.location.hash = '#/'
}
})
}
disconnectedCallback() {
if (this.unsubscribe) this.unsubscribe()
if (this.authUnsubscribe) this.authUnsubscribe()
}
setupEventListeners() {
// Theme toggle
this.querySelectorAll('input[name="theme"]').forEach(input => {
input.addEventListener('change', (e) => {
this.setTheme(e.target.value)
})
})
// Language select
this.querySelector('#lang-select')?.addEventListener('change', async (e) => {
i18n.setLocale(e.target.value)
// Save to user profile if logged in
if (this.isLoggedIn) {
await auth.updatePreferences({ preferred_locale: e.target.value })
}
})
// Currency select
this.querySelector('#currency-select')?.addEventListener('change', async (e) => {
await this.setCurrency(e.target.value)
this.showToast(t('settings.currencyChanged'))
})
// Clear favorites
this.querySelector('#clear-favorites')?.addEventListener('click', async () => {
if (confirm(t('settings.confirmClearFavorites'))) {
const ids = favoritesService.getAll()
for (const id of ids) {
await favoritesService.toggle(id)
}
this.showToast(t('settings.favoritesCleared'))
}
})
// Clear search history
this.querySelector('#clear-search')?.addEventListener('click', () => {
if (confirm(t('settings.confirmClearSearch'))) {
localStorage.removeItem('searchFilters')
this.showToast(t('settings.searchCleared'))
}
})
// Toggle UUID visibility
this.querySelector('#toggle-uid-btn')?.addEventListener('click', () => {
this.uidVisible = !this.uidVisible
const display = this.querySelector('#user-id-display')
const eyeIcon = this.querySelector('#toggle-uid-btn .icon-eye')
const eyeOffIcon = this.querySelector('#toggle-uid-btn .icon-eye-off')
if (display) {
display.textContent = this.uidVisible
? this.user?.id
: '••••••••-••••-••••-••••-••••••••••••'
}
if (eyeIcon) eyeIcon.style.display = this.uidVisible ? 'none' : 'block'
if (eyeOffIcon) eyeOffIcon.style.display = this.uidVisible ? 'block' : 'none'
})
// Copy UUID
this.querySelector('#copy-uid-btn')?.addEventListener('click', async () => {
if (this.user?.id) {
await navigator.clipboard.writeText(this.user.id)
this.showToast(t('auth.copy') + ' ✓')
}
})
// Logout
this.querySelector('#logout-btn')?.addEventListener('click', () => {
auth.logout()
this.showToast(t('settings.loggedOut'))
})
// Login
this.querySelector('#login-btn')?.addEventListener('click', () => {
document.querySelector('auth-modal')?.show()
})
}
setTheme(theme) {
if (theme === 'system') {
localStorage.removeItem('theme')
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
document.documentElement.dataset.theme = prefersDark ? 'dark' : 'light'
} else {
localStorage.setItem('theme', theme)
document.documentElement.dataset.theme = theme
}
}
getCurrentTheme() {
return localStorage.getItem('theme') || 'system'
}
getCurrentCurrency() {
return localStorage.getItem('dgray_currency') || 'USD'
}
async setCurrency(currency) {
localStorage.setItem('dgray_currency', currency)
window.dispatchEvent(new CustomEvent('currency-changed', { detail: { currency } }))
// Save to user profile if logged in
if (auth.isLoggedIn()) {
await auth.updatePreferences({ preferred_currency: currency })
}
}
showToast(message) {
const existing = document.querySelector('.settings-toast')
existing?.remove()
const toast = document.createElement('div')
toast.className = 'settings-toast'
toast.textContent = message
document.body.appendChild(toast)
requestAnimationFrame(() => toast.classList.add('visible'))
setTimeout(() => {
toast.classList.remove('visible')
setTimeout(() => toast.remove(), 300)
}, 2000)
}
render() {
const currentTheme = this.getCurrentTheme()
const currentLang = i18n.getLocale()
const user = this.user
this.innerHTML = /* html */`
<div class="settings-page">
<header class="page-header">
<h1>${t('settings.title')}</h1>
</header>
<div class="settings-sections">
<!-- Appearance -->
<section class="settings-section">
<h2>${t('settings.appearance')}</h2>
<div class="setting-item">
<label>${t('settings.theme')}</label>
<div class="theme-options">
<label class="theme-option">
<input type="radio" name="theme" value="light" ${currentTheme === 'light' ? 'checked' : ''}>
<span>☀️ ${t('settings.themeLight')}</span>
</label>
<label class="theme-option">
<input type="radio" name="theme" value="dark" ${currentTheme === 'dark' ? 'checked' : ''}>
<span>🌙 ${t('settings.themeDark')}</span>
</label>
<label class="theme-option">
<input type="radio" name="theme" value="system" ${currentTheme === 'system' ? 'checked' : ''}>
<span>💻 ${t('settings.themeSystem')}</span>
</label>
</div>
</div>
<div class="setting-item">
<label for="lang-select">${t('settings.language')}</label>
<select id="lang-select">
<option value="de" ${currentLang === 'de' ? 'selected' : ''}>Deutsch</option>
<option value="en" ${currentLang === 'en' ? 'selected' : ''}>English</option>
<option value="fr" ${currentLang === 'fr' ? 'selected' : ''}>Français</option>
</select>
</div>
<div class="setting-item">
<label for="currency-select">${t('settings.currency')}</label>
<select id="currency-select">
<option value="USD" ${this.getCurrentCurrency() === 'USD' ? 'selected' : ''}>USD ($)</option>
<option value="EUR" ${this.getCurrentCurrency() === 'EUR' ? 'selected' : ''}>EUR (€)</option>
<option value="CHF" ${this.getCurrentCurrency() === 'CHF' ? 'selected' : ''}>CHF</option>
</select>
</div>
</section>
<!-- Account -->
<section class="settings-section">
<h2>${t('settings.account')}</h2>
${this.isLoggedIn ? /* html */`
<div class="setting-item">
<label>${t('settings.userId')}</label>
<div class="user-id-row">
<code class="user-id" id="user-id-display">${'•'.repeat(8)}-••••-••••-••••-${'•'.repeat(12)}</code>
<div class="user-id-actions">
<button class="btn btn-icon btn-icon-sm btn-ghost" id="toggle-uid-btn" title="${t('auth.yourUuid')}">
<svg class="icon-eye" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
<circle cx="12" cy="12" r="3"></circle>
</svg>
<svg class="icon-eye-off" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display:none">
<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"></path>
<line x1="1" y1="1" x2="23" y2="23"></line>
</svg>
</button>
<button class="btn btn-icon btn-icon-sm btn-ghost" id="copy-uid-btn" title="${t('auth.copy')}">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
</button>
</div>
</div>
</div>
<div class="setting-item">
<button id="logout-btn" class="btn btn-outline btn-danger">
${t('settings.logout')}
</button>
</div>
` : /* html */`
<div class="setting-item">
<p class="setting-hint">${t('settings.notLoggedIn')}</p>
<button id="login-btn" class="btn btn-primary">
${t('settings.login')}
</button>
</div>
`}
</section>
<!-- Data -->
<section class="settings-section">
<h2>${t('settings.data')}</h2>
<div class="setting-item">
<div>
<label>${t('settings.favorites')}</label>
<p class="setting-hint">${t('settings.favoritesHint')}</p>
</div>
<button id="clear-favorites" class="btn btn-outline btn-sm">
${t('settings.clear')}
</button>
</div>
<div class="setting-item">
<div>
<label>${t('settings.searchHistory')}</label>
<p class="setting-hint">${t('settings.searchHistoryHint')}</p>
</div>
<button id="clear-search" class="btn btn-outline btn-sm">
${t('settings.clear')}
</button>
</div>
</section>
<!-- About -->
<section class="settings-section">
<h2>${t('settings.about')}</h2>
<div class="about-links">
<a href="#/about">${t('footer.about')}</a>
<a href="#/privacy">${t('footer.privacy')}</a>
<a href="#/terms">${t('footer.terms')}</a>
<a href="#/contact">${t('footer.contact')}</a>
</div>
<p class="version">dgray.io v1.0.0</p>
</section>
</div>
</div>
`
}
}
customElements.define('page-settings', PageSettings)
const style = document.createElement('style')
style.textContent = /* css */`
page-settings .settings-page {
padding: var(--space-lg) 0;
max-width: 600px;
}
page-settings .page-header {
margin-bottom: var(--space-xl);
}
page-settings .page-header h1 {
margin: 0;
}
page-settings .settings-sections {
display: flex;
flex-direction: column;
gap: var(--space-xl);
}
page-settings .settings-section {
background: var(--color-bg-secondary);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
padding: var(--space-lg);
}
page-settings .settings-section h2 {
font-size: var(--font-size-lg);
margin: 0 0 var(--space-lg);
padding-bottom: var(--space-sm);
border-bottom: 1px solid var(--color-border);
}
page-settings .setting-item {
display: flex;
justify-content: space-between;
align-items: center;
gap: var(--space-md);
padding: var(--space-sm) 0;
}
page-settings .setting-item + .setting-item {
border-top: 1px solid var(--color-border);
margin-top: var(--space-sm);
padding-top: var(--space-md);
}
page-settings .setting-item > label {
font-weight: var(--font-weight-medium);
}
page-settings .setting-hint {
font-size: var(--font-size-sm);
color: var(--color-text-muted);
margin: var(--space-xs) 0 0;
}
page-settings .theme-options {
display: flex;
gap: var(--space-sm);
}
page-settings .theme-option {
display: flex;
align-items: center;
gap: var(--space-xs);
padding: var(--space-xs) var(--space-sm);
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
cursor: pointer;
font-size: var(--font-size-sm);
transition: all var(--transition-fast);
}
page-settings .theme-option:has(input:checked) {
border-color: var(--color-primary);
background: var(--color-primary-light);
}
page-settings .theme-option input {
display: none;
}
page-settings select {
padding: var(--space-xs) var(--space-sm);
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
background: var(--color-bg);
color: var(--color-text);
font-size: var(--font-size-sm);
}
page-settings .user-id-row {
display: flex;
align-items: center;
gap: var(--space-xs);
flex-wrap: wrap;
}
page-settings .user-id-actions {
display: flex;
gap: var(--space-xs);
}
@media (max-width: 768px) {
page-settings .user-id-row {
flex-direction: column;
align-items: stretch;
}
page-settings .user-id-actions {
justify-content: flex-end;
}
}
page-settings .user-id {
flex: 1;
min-width: 0;
padding: var(--space-xs) var(--space-sm);
background: var(--color-bg-tertiary);
border-radius: var(--radius-sm);
font-size: var(--font-size-sm);
word-break: break-all;
letter-spacing: 0.05em;
}
page-settings .btn-icon-sm {
width: 32px;
height: 32px;
padding: 0;
flex-shrink: 0;
}
page-settings .btn-ghost {
background: transparent;
border: none;
color: var(--color-text-muted);
cursor: pointer;
border-radius: var(--radius-sm);
display: flex;
align-items: center;
justify-content: center;
transition: color var(--transition-fast), background var(--transition-fast);
}
page-settings .btn-ghost:hover {
color: var(--color-text);
background: var(--color-bg-secondary);
}
page-settings .btn-danger {
color: var(--color-error, #dc2626);
border-color: var(--color-error, #dc2626);
}
page-settings .btn-danger:hover {
background: var(--color-error, #dc2626);
color: white;
}
page-settings .about-links {
display: flex;
flex-wrap: wrap;
gap: var(--space-md);
margin-bottom: var(--space-md);
}
page-settings .about-links a {
color: var(--color-text-secondary);
text-decoration: none;
}
page-settings .about-links a:hover {
color: var(--color-primary);
}
page-settings .version {
font-size: var(--font-size-sm);
color: var(--color-text-muted);
margin: 0;
}
@media (max-width: 768px) {
page-settings .setting-item {
flex-direction: column;
align-items: flex-start;
}
page-settings .theme-options {
flex-wrap: wrap;
}
}
/* Toast */
.settings-toast {
position: fixed;
bottom: var(--space-lg);
left: 50%;
transform: translateX(-50%) translateY(100px);
padding: var(--space-sm) var(--space-lg);
background: var(--color-bg-secondary);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
box-shadow: var(--shadow-lg);
opacity: 0;
transition: all 0.3s ease;
z-index: 1000;
}
.settings-toast.visible {
transform: translateX(-50%) translateY(0);
opacity: 1;
}
`
document.head.appendChild(style)