From 640e7a3a4fde4e3f4a1f1af7348291e642d0194d Mon Sep 17 00:00:00 2001 From: Alexander Schmidt Date: Sat, 31 Jan 2026 15:43:58 +0100 Subject: [PATCH] cleanup semicolon from js --- js/app.js | 20 +- js/components/app-footer.js | 12 +- js/components/app-header.js | 172 ++++++------ js/components/app-shell.js | 50 ++-- js/components/auth-modal.js | 170 ++++++------ js/components/chat-widget.js | 106 ++++---- js/components/listing-card.js | 96 +++---- js/components/pages/page-create.js | 181 +++++++------ js/components/pages/page-home.js | 26 +- js/components/pages/page-listing.js | 106 ++++---- js/components/pages/page-not-found.js | 18 +- js/components/pages/page-search.js | 120 ++++----- js/components/search-box.js | 368 +++++++++++++------------- js/data/mock-listings.js | 14 +- js/i18n.js | 118 ++++----- js/router.js | 130 ++++----- js/services/api.js | 80 +++--- js/services/auth.js | 96 +++---- js/services/chat.js | 128 ++++----- js/services/crypto.js | 80 +++--- js/services/currency.js | 86 +++--- js/services/directus.js | 350 ++++++++++++------------ 22 files changed, 1258 insertions(+), 1269 deletions(-) diff --git a/js/app.js b/js/app.js index 38bf68a..441d6d1 100644 --- a/js/app.js +++ b/js/app.js @@ -1,25 +1,25 @@ -import { i18n } from './i18n.js'; +import { i18n } from './i18n.js' async function initApp() { - const savedTheme = localStorage.getItem('theme'); + const savedTheme = localStorage.getItem('theme') if (savedTheme) { - document.documentElement.dataset.theme = savedTheme; + document.documentElement.dataset.theme = savedTheme } - await i18n.init(); + await i18n.init() - await import('./components/app-shell.js'); + await import('./components/app-shell.js') - document.getElementById('app').innerHTML = ''; + document.getElementById('app').innerHTML = '' if ('serviceWorker' in navigator) { try { - const registration = await navigator.serviceWorker.register('/service-worker.js'); - console.log('SW registered:', registration.scope); + const registration = await navigator.serviceWorker.register('/service-worker.js') + console.log('SW registered:', registration.scope) } catch (error) { - console.log('SW registration failed:', error); + console.log('SW registration failed:', error) } } } -initApp(); +initApp() diff --git a/js/components/app-footer.js b/js/components/app-footer.js index ee21a01..3989e16 100644 --- a/js/components/app-footer.js +++ b/js/components/app-footer.js @@ -1,12 +1,12 @@ -import { t } from '../i18n.js'; +import { t } from '../i18n.js' class AppFooter extends HTMLElement { connectedCallback() { - this.render(); + this.render() } render() { - const year = new Date().getFullYear(); + const year = new Date().getFullYear() this.innerHTML = /* html */` - `; + ` } updateTranslations() { - this.render(); + this.render() } } -customElements.define('app-footer', AppFooter); +customElements.define('app-footer', AppFooter) diff --git a/js/components/app-header.js b/js/components/app-header.js index da63bcd..2ad8b5d 100644 --- a/js/components/app-header.js +++ b/js/components/app-header.js @@ -1,80 +1,80 @@ -import { i18n, t } from '../i18n.js'; -import { router } from '../router.js'; -import { auth } from '../services/auth.js'; +import { i18n, t } from '../i18n.js' +import { router } from '../router.js' +import { auth } from '../services/auth.js' class AppHeader extends HTMLElement { constructor() { - super(); - this.langDropdownOpen = false; - this.handleOutsideClick = this.handleOutsideClick.bind(this); - this.handleKeydown = this.handleKeydown.bind(this); + super() + this.langDropdownOpen = false + this.handleOutsideClick = this.handleOutsideClick.bind(this) + this.handleKeydown = this.handleKeydown.bind(this) } connectedCallback() { - this.render(); - this.setupEventListeners(); - document.addEventListener('click', this.handleOutsideClick); - document.addEventListener('keydown', this.handleKeydown); + this.render() + this.setupEventListeners() + document.addEventListener('click', this.handleOutsideClick) + document.addEventListener('keydown', this.handleKeydown) // Subscribe to auth changes (only once!) this.authUnsubscribe = auth.subscribe(() => { - this.render(); - this.setupEventListeners(); - }); + this.render() + this.setupEventListeners() + }) } disconnectedCallback() { - document.removeEventListener('click', this.handleOutsideClick); - document.removeEventListener('keydown', this.handleKeydown); - if (this.authUnsubscribe) this.authUnsubscribe(); + document.removeEventListener('click', this.handleOutsideClick) + document.removeEventListener('keydown', this.handleKeydown) + if (this.authUnsubscribe) this.authUnsubscribe() } handleOutsideClick() { if (this.langDropdownOpen) { - this.closeDropdown(); + this.closeDropdown() } } handleKeydown(e) { - if (!this.langDropdownOpen) return; + if (!this.langDropdownOpen) return - const items = Array.from(this.querySelectorAll('.dropdown-item')); - const currentIndex = items.findIndex(item => item === document.activeElement); + const items = Array.from(this.querySelectorAll('.dropdown-item')) + const currentIndex = items.findIndex(item => item === document.activeElement) switch (e.key) { case 'Escape': - e.preventDefault(); - this.closeDropdown(); - this.querySelector('#lang-toggle')?.focus(); - break; + e.preventDefault() + this.closeDropdown() + this.querySelector('#lang-toggle')?.focus() + break case 'ArrowDown': - e.preventDefault(); - const nextIndex = currentIndex < items.length - 1 ? currentIndex + 1 : 0; - items[nextIndex]?.focus(); - break; + e.preventDefault() + const nextIndex = currentIndex < items.length - 1 ? currentIndex + 1 : 0 + items[nextIndex]?.focus() + break case 'ArrowUp': - e.preventDefault(); - const prevIndex = currentIndex > 0 ? currentIndex - 1 : items.length - 1; - items[prevIndex]?.focus(); - break; + e.preventDefault() + const prevIndex = currentIndex > 0 ? currentIndex - 1 : items.length - 1 + items[prevIndex]?.focus() + break } } closeDropdown() { - this.langDropdownOpen = false; - const langDropdown = this.querySelector('#lang-dropdown'); - const langToggle = this.querySelector('#lang-toggle'); - langDropdown?.classList.remove('open'); - langToggle?.setAttribute('aria-expanded', 'false'); + this.langDropdownOpen = false + const langDropdown = this.querySelector('#lang-dropdown') + const langToggle = this.querySelector('#lang-toggle') + langDropdown?.classList.remove('open') + langToggle?.setAttribute('aria-expanded', 'false') } openDropdown() { - this.langDropdownOpen = true; - const langDropdown = this.querySelector('#lang-dropdown'); - const langToggle = this.querySelector('#lang-toggle'); - langDropdown?.classList.add('open'); - langToggle?.setAttribute('aria-expanded', 'true'); - this.querySelector('.dropdown-item.active')?.focus(); + this.langDropdownOpen = true + const langDropdown = this.querySelector('#lang-dropdown') + const langToggle = this.querySelector('#lang-toggle') + langDropdown?.classList.add('open') + langToggle?.setAttribute('aria-expanded', 'true') + this.querySelector('.dropdown-item.active')?.focus() } render() { @@ -162,88 +162,88 @@ class AppHeader extends HTMLElement { `} - `; + ` } setupEventListeners() { - const themeToggle = this.querySelector('#theme-toggle'); - themeToggle.addEventListener('click', () => this.toggleTheme()); + const themeToggle = this.querySelector('#theme-toggle') + themeToggle.addEventListener('click', () => this.toggleTheme()) - const langDropdown = this.querySelector('#lang-dropdown'); - const langToggle = this.querySelector('#lang-toggle'); + const langDropdown = this.querySelector('#lang-dropdown') + const langToggle = this.querySelector('#lang-toggle') langToggle.addEventListener('click', (e) => { - e.stopPropagation(); + e.stopPropagation() if (this.langDropdownOpen) { - this.closeDropdown(); + this.closeDropdown() } else { - this.openDropdown(); + this.openDropdown() } - }); + }) langDropdown.querySelectorAll('[data-locale]').forEach(btn => { btn.addEventListener('click', async () => { - await i18n.setLocale(btn.dataset.locale); - this.closeDropdown(); - this.render(); - this.setupEventListeners(); - }); - }); + await i18n.setLocale(btn.dataset.locale) + this.closeDropdown() + this.render() + this.setupEventListeners() + }) + }) // Login button - const loginBtn = this.querySelector('#login-btn'); + const loginBtn = this.querySelector('#login-btn') loginBtn?.addEventListener('click', () => { - const authModal = document.querySelector('auth-modal'); + const authModal = document.querySelector('auth-modal') if (authModal) { - authModal.show('login'); + authModal.show('login') } - }); + }) // Profile button - const profileBtn = this.querySelector('#profile-btn'); + const profileBtn = this.querySelector('#profile-btn') profileBtn?.addEventListener('click', () => { - router.navigate('/profile'); - }); + router.navigate('/profile') + }) - this.updateThemeIcon(); + this.updateThemeIcon() } toggleTheme() { - const currentTheme = document.documentElement.dataset.theme; - const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; + const currentTheme = document.documentElement.dataset.theme + const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches - let newTheme; + let newTheme if (!currentTheme) { - newTheme = prefersDark ? 'light' : 'dark'; + newTheme = prefersDark ? 'light' : 'dark' } else if (currentTheme === 'dark') { - newTheme = 'light'; + newTheme = 'light' } else { - newTheme = 'dark'; + newTheme = 'dark' } - document.documentElement.dataset.theme = newTheme; - localStorage.setItem('theme', newTheme); - this.updateThemeIcon(); + document.documentElement.dataset.theme = newTheme + localStorage.setItem('theme', newTheme) + this.updateThemeIcon() } updateThemeIcon() { - const theme = document.documentElement.dataset.theme; - const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; - const isDark = theme === 'dark' || (!theme && prefersDark); + const theme = document.documentElement.dataset.theme + const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches + const isDark = theme === 'dark' || (!theme && prefersDark) - const sunIcon = this.querySelector('.icon-sun'); - const moonIcon = this.querySelector('.icon-moon'); + const sunIcon = this.querySelector('.icon-sun') + const moonIcon = this.querySelector('.icon-moon') if (sunIcon && moonIcon) { - sunIcon.style.display = isDark ? 'block' : 'none'; - moonIcon.style.display = isDark ? 'none' : 'block'; + sunIcon.style.display = isDark ? 'block' : 'none' + moonIcon.style.display = isDark ? 'none' : 'block' } } updateTranslations() { - i18n.updateDOM(); - this.querySelector('#current-lang').textContent = i18n.getLocale().toUpperCase(); + i18n.updateDOM() + this.querySelector('#current-lang').textContent = i18n.getLocale().toUpperCase() } } -customElements.define('app-header', AppHeader); +customElements.define('app-header', AppHeader) diff --git a/js/components/app-shell.js b/js/components/app-shell.js index 0730bb0..734cf28 100644 --- a/js/components/app-shell.js +++ b/js/components/app-shell.js @@ -1,29 +1,29 @@ -import { router } from '../router.js'; -import { i18n } from '../i18n.js'; -import { auth } from '../services/auth.js'; -import './app-header.js'; -import './app-footer.js'; -import './auth-modal.js'; -import './pages/page-home.js'; -import './pages/page-search.js'; -import './pages/page-listing.js'; -import './pages/page-create.js'; -import './pages/page-not-found.js'; +import { router } from '../router.js' +import { i18n } from '../i18n.js' +import { auth } from '../services/auth.js' +import './app-header.js' +import './app-footer.js' +import './auth-modal.js' +import './pages/page-home.js' +import './pages/page-search.js' +import './pages/page-listing.js' +import './pages/page-create.js' +import './pages/page-not-found.js' class AppShell extends HTMLElement { constructor() { - super(); - this.main = null; + super() + this.main = null } connectedCallback() { - this.render(); - this.setupRouter(); + this.render() + this.setupRouter() i18n.subscribe(() => { - this.querySelector('app-header').updateTranslations(); - this.querySelector('app-footer').updateTranslations(); - }); + this.querySelector('app-header').updateTranslations() + this.querySelector('app-footer').updateTranslations() + }) } render() { @@ -32,25 +32,25 @@ class AppShell extends HTMLElement {
- `; + ` - this.main = this.querySelector('#router-outlet'); + this.main = this.querySelector('#router-outlet') // Try to restore session - auth.tryRestoreSession(); + auth.tryRestoreSession() } setupRouter() { - router.setOutlet(this.main); + router.setOutlet(this.main) router .register('/', 'page-home') .register('/search', 'page-search') .register('/listing/:id', 'page-listing') - .register('/create', 'page-create'); + .register('/create', 'page-create') - router.handleRouteChange(); + router.handleRouteChange() } } -customElements.define('app-shell', AppShell); +customElements.define('app-shell', AppShell) diff --git a/js/components/auth-modal.js b/js/components/auth-modal.js index d510b75..297b765 100644 --- a/js/components/auth-modal.js +++ b/js/components/auth-modal.js @@ -2,52 +2,52 @@ * Auth Modal - Login/Register with UUID */ -import { t, i18n } from '../i18n.js'; -import { auth } from '../services/auth.js'; +import { t, i18n } from '../i18n.js' +import { auth } from '../services/auth.js' class AuthModal extends HTMLElement { constructor() { - super(); - this.mode = 'login'; // 'login' | 'register' | 'show-uuid' - this.generatedUuid = null; - this.error = null; - this.loading = false; + super() + this.mode = 'login' // 'login' | 'register' | 'show-uuid' + this.generatedUuid = null + this.error = null + this.loading = false } connectedCallback() { - this.render(); - this.unsubscribe = i18n.subscribe(() => this.render()); + this.render() + this.unsubscribe = i18n.subscribe(() => this.render()) } disconnectedCallback() { - if (this.unsubscribe) this.unsubscribe(); + if (this.unsubscribe) this.unsubscribe() } show(mode = 'login') { - this.mode = mode; - this.error = null; - this.generatedUuid = null; - this.hidden = false; - this.render(); - document.body.style.overflow = 'hidden'; + this.mode = mode + this.error = null + this.generatedUuid = null + this.hidden = false + this.render() + document.body.style.overflow = 'hidden' } hide() { - this.hidden = true; - document.body.style.overflow = ''; - this.dispatchEvent(new CustomEvent('close')); + this.hidden = true + document.body.style.overflow = '' + this.dispatchEvent(new CustomEvent('close')) } switchMode(mode) { - this.mode = mode; - this.error = null; - this.render(); + this.mode = mode + this.error = null + this.render() } render() { if (this.hidden) { - this.innerHTML = ''; - return; + this.innerHTML = '' + return } this.innerHTML = /* html */` @@ -65,13 +65,13 @@ class AuthModal extends HTMLElement { ${this.mode === 'show-uuid' ? this.renderShowUuid() : ''} - `; + ` - this.setupEventListeners(); + this.setupEventListeners() } renderLogin() { - const storedUuid = auth.getStoredUuid(); + const storedUuid = auth.getStoredUuid() return /* html */` @@ -104,7 +104,7 @@ class AuthModal extends HTMLElement {

- `; + ` } renderRegister() { @@ -130,7 +130,7 @@ class AuthModal extends HTMLElement {

- `; + ` } renderShowUuid() { @@ -161,100 +161,100 @@ class AuthModal extends HTMLElement { - `; + ` } setupEventListeners() { // Close modal this.querySelector('#modal-overlay')?.addEventListener('click', (e) => { - if (e.target.id === 'modal-overlay') this.hide(); - }); - this.querySelector('#modal-close')?.addEventListener('click', () => this.hide()); + if (e.target.id === 'modal-overlay') this.hide() + }) + this.querySelector('#modal-close')?.addEventListener('click', () => this.hide()) // Switch modes - this.querySelector('#switch-register')?.addEventListener('click', () => this.switchMode('register')); - this.querySelector('#switch-login')?.addEventListener('click', () => this.switchMode('login')); + this.querySelector('#switch-register')?.addEventListener('click', () => this.switchMode('register')) + this.querySelector('#switch-login')?.addEventListener('click', () => this.switchMode('login')) // Login form - this.querySelector('#login-form')?.addEventListener('submit', (e) => this.handleLogin(e)); + this.querySelector('#login-form')?.addEventListener('submit', (e) => this.handleLogin(e)) // Generate UUID - this.querySelector('#generate-uuid')?.addEventListener('click', () => this.handleRegister()); + this.querySelector('#generate-uuid')?.addEventListener('click', () => this.handleRegister()) // Copy UUID - this.querySelector('#copy-uuid')?.addEventListener('click', () => this.copyUuid()); + this.querySelector('#copy-uuid')?.addEventListener('click', () => this.copyUuid()) // Download backup - this.querySelector('#download-uuid')?.addEventListener('click', () => this.downloadBackup()); + this.querySelector('#download-uuid')?.addEventListener('click', () => this.downloadBackup()) // Confirm saved - this.querySelector('#confirm-saved')?.addEventListener('click', () => this.hide()); + this.querySelector('#confirm-saved')?.addEventListener('click', () => this.hide()) // Escape key - document.addEventListener('keydown', this.handleKeydown.bind(this)); + document.addEventListener('keydown', this.handleKeydown.bind(this)) } handleKeydown(e) { if (e.key === 'Escape' && !this.hidden) { - this.hide(); + this.hide() } } async handleLogin(e) { - e.preventDefault(); - const uuid = this.querySelector('#uuid').value.trim(); + e.preventDefault() + const uuid = this.querySelector('#uuid').value.trim() if (!uuid) { - this.error = t('auth.enterUuid'); - this.render(); - return; + this.error = t('auth.enterUuid') + this.render() + return } - this.loading = true; - this.error = null; - this.render(); + this.loading = true + this.error = null + this.render() - const result = await auth.login(uuid); + const result = await auth.login(uuid) - this.loading = false; + this.loading = false if (result.success) { - this.hide(); - this.dispatchEvent(new CustomEvent('login', { detail: { success: true } })); + this.hide() + this.dispatchEvent(new CustomEvent('login', { detail: { success: true } })) } else { - this.error = result.error || t('auth.invalidUuid'); - this.render(); + this.error = result.error || t('auth.invalidUuid') + this.render() } } async handleRegister() { - this.loading = true; - this.error = null; - this.render(); + this.loading = true + this.error = null + this.render() - const result = await auth.createAccount(); + const result = await auth.createAccount() - this.loading = false; + this.loading = false if (result.success) { - this.generatedUuid = result.uuid; - this.mode = 'show-uuid'; - this.render(); - this.dispatchEvent(new CustomEvent('register', { detail: { uuid: result.uuid } })); + this.generatedUuid = result.uuid + this.mode = 'show-uuid' + this.render() + this.dispatchEvent(new CustomEvent('register', { detail: { uuid: result.uuid } })) } else { - this.error = result.error || t('auth.registrationFailed'); - this.render(); + this.error = result.error || t('auth.registrationFailed') + this.render() } } async copyUuid() { try { - await navigator.clipboard.writeText(this.generatedUuid); - const btn = this.querySelector('#copy-uuid'); - btn.innerHTML = ''; - setTimeout(() => this.render(), 2000); + await navigator.clipboard.writeText(this.generatedUuid) + const btn = this.querySelector('#copy-uuid') + btn.innerHTML = '' + setTimeout(() => this.render(), 2000) } catch (e) { - console.error('Copy failed:', e); + console.error('Copy failed:', e) } } @@ -271,21 +271,21 @@ https://dgray.io/#/login Created: ${new Date().toISOString()} WARNING: If you lose this UUID, you cannot recover your account! -`; +` - const blob = new Blob([content], { type: 'text/plain' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = `dgray-backup-${this.generatedUuid.slice(0, 8)}.txt`; - a.click(); - URL.revokeObjectURL(url); + const blob = new Blob([content], { type: 'text/plain' }) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = `dgray-backup-${this.generatedUuid.slice(0, 8)}.txt` + a.click() + URL.revokeObjectURL(url) } } -customElements.define('auth-modal', AuthModal); +customElements.define('auth-modal', AuthModal) -const style = document.createElement('style'); +const style = document.createElement('style') style.textContent = /* css */` auth-modal { position: fixed; @@ -420,7 +420,7 @@ style.textContent = /* css */` auth-modal .link-btn:hover { color: var(--color-accent-hover); } -`; -document.head.appendChild(style); +` +document.head.appendChild(style) -export default AuthModal; +export default AuthModal diff --git a/js/components/chat-widget.js b/js/components/chat-widget.js index 9595c06..922e179 100644 --- a/js/components/chat-widget.js +++ b/js/components/chat-widget.js @@ -3,60 +3,60 @@ * Embedded chat for buyer-seller communication */ -import { t, i18n } from '../i18n.js'; -import { chatService } from '../services/chat.js'; -import { cryptoService } from '../services/crypto.js'; +import { t, i18n } from '../i18n.js' +import { chatService } from '../services/chat.js' +import { cryptoService } from '../services/crypto.js' class ChatWidget extends HTMLElement { static get observedAttributes() { - return ['listing-id', 'recipient-id', 'recipient-key', 'recipient-name']; + return ['listing-id', 'recipient-id', 'recipient-key', 'recipient-name'] } constructor() { - super(); - this.chat = null; - this.messages = []; - this.unsubscribe = null; + super() + this.chat = null + this.messages = [] + this.unsubscribe = null } async connectedCallback() { - await cryptoService.ready; + await cryptoService.ready - this.listingId = this.getAttribute('listing-id'); - this.recipientId = this.getAttribute('recipient-id'); - this.recipientKey = this.getAttribute('recipient-key'); - this.recipientName = this.getAttribute('recipient-name') || 'Seller'; + this.listingId = this.getAttribute('listing-id') + this.recipientId = this.getAttribute('recipient-id') + this.recipientKey = this.getAttribute('recipient-key') + this.recipientName = this.getAttribute('recipient-name') || 'Seller' if (this.listingId && this.recipientId && this.recipientKey) { this.chat = chatService.getOrCreateChat( this.recipientId, this.recipientKey, this.listingId - ); - await this.loadMessages(); + ) + await this.loadMessages() } - this.render(); - this.setupEventListeners(); + this.render() + this.setupEventListeners() - this.unsubscribe = chatService.subscribe(() => this.refreshMessages()); - this.i18nUnsubscribe = i18n.subscribe(() => this.render()); + this.unsubscribe = chatService.subscribe(() => this.refreshMessages()) + this.i18nUnsubscribe = i18n.subscribe(() => this.render()) } disconnectedCallback() { - if (this.unsubscribe) this.unsubscribe(); - if (this.i18nUnsubscribe) this.i18nUnsubscribe(); + if (this.unsubscribe) this.unsubscribe() + if (this.i18nUnsubscribe) this.i18nUnsubscribe() } async loadMessages() { - if (!this.chat) return; - this.messages = await chatService.getMessages(this.chat.id, this.recipientKey); + if (!this.chat) return + this.messages = await chatService.getMessages(this.chat.id, this.recipientKey) } async refreshMessages() { - await this.loadMessages(); - this.renderMessages(); - this.scrollToBottom(); + await this.loadMessages() + this.renderMessages() + this.scrollToBottom() } render() { @@ -86,10 +86,10 @@ class ChatWidget extends HTMLElement { - `; + ` - this.setupEventListeners(); - this.scrollToBottom(); + this.setupEventListeners() + this.scrollToBottom() } renderMessagesHtml() { @@ -98,7 +98,7 @@ class ChatWidget extends HTMLElement {

${t('chat.startConversation')}

- `; + ` } return this.messages.map(msg => /* html */` @@ -108,60 +108,60 @@ class ChatWidget extends HTMLElement { ${this.formatTime(msg.timestamp)} - `).join(''); + `).join('') } renderMessages() { - const container = this.querySelector('#chat-messages'); + const container = this.querySelector('#chat-messages') if (container) { - container.innerHTML = this.renderMessagesHtml(); + container.innerHTML = this.renderMessagesHtml() } } setupEventListeners() { - const form = this.querySelector('#chat-form'); - form?.addEventListener('submit', (e) => this.handleSubmit(e)); + const form = this.querySelector('#chat-form') + form?.addEventListener('submit', (e) => this.handleSubmit(e)) } async handleSubmit(e) { - e.preventDefault(); + e.preventDefault() - const input = this.querySelector('#message-input'); - const text = input?.value.trim(); + const input = this.querySelector('#message-input') + const text = input?.value.trim() - if (!text || !this.chat) return; + if (!text || !this.chat) return - input.value = ''; + input.value = '' await chatService.sendMessage( this.chat.id, this.recipientKey, text - ); + ) } scrollToBottom() { - const container = this.querySelector('#chat-messages'); + const container = this.querySelector('#chat-messages') if (container) { - container.scrollTop = container.scrollHeight; + container.scrollTop = container.scrollHeight } } formatTime(timestamp) { - const date = new Date(timestamp); - return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + const date = new Date(timestamp) + return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) } escapeHtml(text) { - const div = document.createElement('div'); - div.textContent = text; - return div.innerHTML; + const div = document.createElement('div') + div.textContent = text + return div.innerHTML } } -customElements.define('chat-widget', ChatWidget); +customElements.define('chat-widget', ChatWidget) -const style = document.createElement('style'); +const style = document.createElement('style') style.textContent = /* css */` chat-widget { display: block; @@ -295,7 +295,7 @@ style.textContent = /* css */` chat-widget .chat-input button:hover { background: var(--color-text-secondary); } -`; -document.head.appendChild(style); +` +document.head.appendChild(style) -export { ChatWidget }; +export { ChatWidget } diff --git a/js/components/listing-card.js b/js/components/listing-card.js index 04644e0..b5896e4 100644 --- a/js/components/listing-card.js +++ b/js/components/listing-card.js @@ -1,62 +1,62 @@ -import { t, i18n } from '../i18n.js'; -import { escapeHTML, formatPrice } from '../utils/helpers.js'; +import { t, i18n } from '../i18n.js' +import { escapeHTML, formatPrice } from '../utils/helpers.js' class ListingCard extends HTMLElement { static get observedAttributes() { - return ['listing-id', 'title', 'price', 'currency', 'location', 'image']; + return ['listing-id', 'title', 'price', 'currency', 'location', 'image'] } constructor() { - super(); - this.isFavorite = false; + super() + this.isFavorite = false } connectedCallback() { - this.loadFavoriteState(); - this.render(); - this.setupEventListeners(); + this.loadFavoriteState() + this.render() + this.setupEventListeners() } attributeChangedCallback() { if (this.isConnected) { - this.render(); - this.setupEventListeners(); + this.render() + this.setupEventListeners() } } loadFavoriteState() { - const id = this.getAttribute('listing-id'); + const id = this.getAttribute('listing-id') if (id) { - const favorites = JSON.parse(localStorage.getItem('favorites') || '[]'); - this.isFavorite = favorites.includes(id); + const favorites = JSON.parse(localStorage.getItem('favorites') || '[]') + this.isFavorite = favorites.includes(id) } } saveFavoriteState() { - const id = this.getAttribute('listing-id'); - if (!id) return; + const id = this.getAttribute('listing-id') + if (!id) return - let favorites = JSON.parse(localStorage.getItem('favorites') || '[]'); + let favorites = JSON.parse(localStorage.getItem('favorites') || '[]') if (this.isFavorite) { - if (!favorites.includes(id)) favorites.push(id); + if (!favorites.includes(id)) favorites.push(id) } else { - favorites = favorites.filter(f => f !== id); + favorites = favorites.filter(f => f !== id) } - localStorage.setItem('favorites', JSON.stringify(favorites)); + localStorage.setItem('favorites', JSON.stringify(favorites)) } render() { - const id = this.getAttribute('listing-id') || ''; - const title = this.getAttribute('title') || t('home.placeholderTitle'); - const price = this.getAttribute('price'); - const currency = this.getAttribute('currency') || 'EUR'; - const location = this.getAttribute('location') || t('home.placeholderLocation'); - const image = this.getAttribute('image'); + const id = this.getAttribute('listing-id') || '' + const title = this.getAttribute('title') || t('home.placeholderTitle') + const price = this.getAttribute('price') + const currency = this.getAttribute('currency') || 'EUR' + const location = this.getAttribute('location') || t('home.placeholderLocation') + const image = this.getAttribute('image') - const priceDisplay = price ? formatPrice(parseFloat(price), currency) : '–'; - const favoriteLabel = this.isFavorite ? t('home.removeFavorite') : t('home.addFavorite'); + const priceDisplay = price ? formatPrice(parseFloat(price), currency) : '–' + const favoriteLabel = this.isFavorite ? t('home.removeFavorite') : t('home.addFavorite') const placeholderSvg = /* html */` @@ -64,7 +64,7 @@ class ListingCard extends HTMLElement { - `; + ` this.innerHTML = /* html */` @@ -86,42 +86,42 @@ class ListingCard extends HTMLElement { - `; + ` } setupEventListeners() { - const btn = this.querySelector('.favorite-btn'); + const btn = this.querySelector('.favorite-btn') btn?.addEventListener('click', (e) => { - e.preventDefault(); - e.stopPropagation(); - this.toggleFavorite(); - }); + e.preventDefault() + e.stopPropagation() + this.toggleFavorite() + }) } toggleFavorite() { - this.isFavorite = !this.isFavorite; - this.saveFavoriteState(); + this.isFavorite = !this.isFavorite + this.saveFavoriteState() - const btn = this.querySelector('.favorite-btn'); - btn?.classList.toggle('active', this.isFavorite); - btn?.setAttribute('aria-pressed', this.isFavorite); - btn?.setAttribute('aria-label', this.isFavorite ? t('home.removeFavorite') : t('home.addFavorite')); + const btn = this.querySelector('.favorite-btn') + btn?.classList.toggle('active', this.isFavorite) + btn?.setAttribute('aria-pressed', this.isFavorite) + btn?.setAttribute('aria-label', this.isFavorite ? t('home.removeFavorite') : t('home.addFavorite')) - btn?.classList.add('animate__animated', 'animate__heartBeat'); + btn?.classList.add('animate__animated', 'animate__heartBeat') btn?.addEventListener('animationend', () => { - btn?.classList.remove('animate__animated', 'animate__heartBeat'); - }, { once: true }); + btn?.classList.remove('animate__animated', 'animate__heartBeat') + }, { once: true }) this.dispatchEvent(new CustomEvent('favorite-toggle', { bubbles: true, detail: { id: this.getAttribute('listing-id'), isFavorite: this.isFavorite } - })); + })) } } -customElements.define('listing-card', ListingCard); +customElements.define('listing-card', ListingCard) -const style = document.createElement('style'); +const style = document.createElement('style') style.textContent = /* css */` listing-card { display: block; @@ -221,5 +221,5 @@ style.textContent = /* css */` listing-card .favorite-btn:hover .heart-icon { color: var(--color-accent); } -`; -document.head.appendChild(style); +` +document.head.appendChild(style) diff --git a/js/components/pages/page-create.js b/js/components/pages/page-create.js index 898c5b8..fd12648 100644 --- a/js/components/pages/page-create.js +++ b/js/components/pages/page-create.js @@ -1,12 +1,12 @@ -import { t, i18n } from '../../i18n.js'; -import { router } from '../../router.js'; -import { auth } from '../../services/auth.js'; -import { directus } from '../../services/directus.js'; -import { SUPPORTED_CURRENCIES } from '../../services/currency.js'; +import { t, i18n } from '../../i18n.js' +import { router } from '../../router.js' +import { auth } from '../../services/auth.js' +import { directus } from '../../services/directus.js' +import { SUPPORTED_CURRENCIES } from '../../services/currency.js' class PageCreate extends HTMLElement { constructor() { - super(); + super() this.formData = { title: '', description: '', @@ -19,23 +19,23 @@ class PageCreate extends HTMLElement { location: '', shipping: false, moneroAddress: '' - }; - this.imageFiles = []; - this.imagePreviews = []; - this.categories = []; - this.submitting = false; + } + this.imageFiles = [] + this.imagePreviews = [] + this.categories = [] + this.submitting = false } async connectedCallback() { // Check if logged in if (!auth.isLoggedIn()) { - this.showLoginRequired(); - return; + this.showLoginRequired() + return } - await this.loadCategories(); - this.render(); - this.unsubscribe = i18n.subscribe(() => this.render()); + await this.loadCategories() + this.render() + this.unsubscribe = i18n.subscribe(() => this.render()) } showLoginRequired() { @@ -46,29 +46,29 @@ class PageCreate extends HTMLElement { - `; + ` this.querySelector('#login-btn')?.addEventListener('click', () => { - const authModal = document.querySelector('auth-modal'); + const authModal = document.querySelector('auth-modal') if (authModal) { - authModal.show('login'); + authModal.show('login') authModal.addEventListener('login', () => { - this.connectedCallback(); - }, { once: true }); + this.connectedCallback() + }, { once: true }) } - }); + }) } async loadCategories() { try { - this.categories = await directus.getCategories(); + this.categories = await directus.getCategories() } catch (e) { - console.error('Failed to load categories:', e); - this.categories = []; + console.error('Failed to load categories:', e) + this.categories = [] } } disconnectedCallback() { - if (this.unsubscribe) this.unsubscribe(); + if (this.unsubscribe) this.unsubscribe() } render() { @@ -220,69 +220,68 @@ class PageCreate extends HTMLElement { - `; + ` - this.setupEventListeners(); + this.setupEventListeners() } setupEventListeners() { - const form = this.querySelector('#create-form'); - const cancelBtn = this.querySelector('#cancel-btn'); - const imageInput = this.querySelector('#images'); + const form = this.querySelector('#create-form') + const cancelBtn = this.querySelector('#cancel-btn') + const imageInput = this.querySelector('#images') - form.addEventListener('submit', (e) => this.handleSubmit(e)); - cancelBtn.addEventListener('click', () => router.back()); + form.addEventListener('submit', (e) => this.handleSubmit(e)) + cancelBtn.addEventListener('click', () => router.back()) form.querySelectorAll('input:not([type="checkbox"]), textarea, select').forEach(input => { input.addEventListener('input', (e) => { if (e.target.name) { - this.formData[e.target.name] = e.target.value; + this.formData[e.target.name] = e.target.value } - }); - }); + }) + }) // Checkbox handler - const shippingCheckbox = this.querySelector('#shipping'); + const shippingCheckbox = this.querySelector('#shipping') shippingCheckbox?.addEventListener('change', (e) => { - this.formData.shipping = e.target.checked; - }); + this.formData.shipping = e.target.checked + }) - imageInput?.addEventListener('change', (e) => this.handleImageSelect(e)); + imageInput?.addEventListener('change', (e) => this.handleImageSelect(e)) } - + handleImageSelect(e) { - const files = Array.from(e.target.files); + const files = Array.from(e.target.files) files.forEach(file => { - if (this.imageFiles.length >= 5) return; + if (this.imageFiles.length >= 5) return + this.imageFiles.push(file) - this.imageFiles.push(file); - - const reader = new FileReader(); - reader.onload = (event) => { - this.imagePreviews.push(event.target.result); - this.updateImagePreviews(); - }; - reader.readAsDataURL(file); - }); + const reader = new FileReader() + reader.onload = (e) => { + this.imagePreviews.push(e.target.result) + this.updateImagePreviews() + } + reader.readAsDataURL(file) + }) } updateImagePreviews() { - const container = this.querySelector('#image-previews'); - const uploadArea = this.querySelector('#upload-area'); + const container = this.querySelector('#image-previews') + const uploadArea = this.querySelector('#upload-area') if (container) { - container.innerHTML = this.renderImagePreviews(); - this.setupRemoveListeners(); + container.innerHTML = this.renderImagePreviews() + this.setupRemoveListeners() } if (uploadArea) { - uploadArea.style.display = this.imageFiles.length >= 5 ? 'none' : 'flex'; + uploadArea.style.display = this.imageFiles.length >= 5 ? 'none' : 'flex' } } renderImagePreviews() { - if (this.imagePreviews.length === 0) return ''; + if (this.imagePreviews.length === 0) return '' return this.imagePreviews.map((src, index) => /* html */`
@@ -294,37 +293,37 @@ class PageCreate extends HTMLElement {
- `).join(''); + `).join('') } setupRemoveListeners() { this.querySelectorAll('.remove-image').forEach(btn => { btn.addEventListener('click', (e) => { - const index = parseInt(e.currentTarget.dataset.index); - this.imageFiles.splice(index, 1); - this.imagePreviews.splice(index, 1); - this.updateImagePreviews(); - }); - }); + const index = parseInt(e.currentTarget.dataset.index) + this.imageFiles.splice(index, 1) + this.imagePreviews.splice(index, 1) + this.updateImagePreviews() + }) + }) } async handleSubmit(e) { - e.preventDefault(); + e.preventDefault() - if (this.submitting) return; - this.submitting = true; + if (this.submitting) return + this.submitting = true - const form = e.target; - const submitBtn = form.querySelector('[type="submit"]'); - submitBtn.disabled = true; - submitBtn.textContent = t('create.publishing'); + const form = e.target + const submitBtn = form.querySelector('[type="submit"]') + submitBtn.disabled = true + submitBtn.textContent = t('create.publishing') try { // Upload images first - let imageIds = []; + let imageIds = [] if (this.imageFiles.length > 0) { - const uploadedFiles = await directus.uploadMultipleFiles(this.imageFiles); - imageIds = uploadedFiles.map(f => f.id); + const uploadedFiles = await directus.uploadMultipleFiles(this.imageFiles) + imageIds = uploadedFiles.map(f => f.id) } // Create listing @@ -340,38 +339,38 @@ class PageCreate extends HTMLElement { shipping: this.formData.shipping, monero_address: this.formData.moneroAddress, status: 'published' - }; + } // Add images if uploaded if (imageIds.length > 0) { listingData.images = imageIds.map((id, index) => ({ directus_files_id: id, sort: index - })); + })) } - const listing = await directus.createListing(listingData); + const listing = await directus.createListing(listingData) - router.navigate(`/listing/${listing.id}`); + router.navigate(`/listing/${listing.id}`) } catch (error) { - console.error('Failed to create listing:', error); - submitBtn.disabled = false; - submitBtn.textContent = t('create.publish'); - this.submitting = false; - alert(error.message || 'Failed to create listing'); + console.error('Failed to create listing:', error) + submitBtn.disabled = false + submitBtn.textContent = t('create.publish') + this.submitting = false + alert(error.message || 'Failed to create listing') } } escapeHtml(text) { - const div = document.createElement('div'); - div.textContent = text; - return div.innerHTML; + const div = document.createElement('div') + div.textContent = text + return div.innerHTML } } -customElements.define('page-create', PageCreate); +customElements.define('page-create', PageCreate) -const style = document.createElement('style'); +const style = document.createElement('style') style.textContent = /* css */` page-create .create-page { max-width: 600px; @@ -509,5 +508,5 @@ style.textContent = /* css */` justify-content: flex-end; margin-top: var(--space-xl); } -`; -document.head.appendChild(style); +` +document.head.appendChild(style) diff --git a/js/components/pages/page-home.js b/js/components/pages/page-home.js index d7ec658..4a4f3a3 100644 --- a/js/components/pages/page-home.js +++ b/js/components/pages/page-home.js @@ -1,16 +1,16 @@ -import { t, i18n } from '../../i18n.js'; -import { mockListings } from '../../data/mock-listings.js'; -import '../listing-card.js'; -import '../search-box.js'; +import { t, i18n } from '../../i18n.js' +import { mockListings } from '../../data/mock-listings.js' +import '../listing-card.js' +import '../search-box.js' class PageHome extends HTMLElement { connectedCallback() { - this.render(); - this.unsubscribe = i18n.subscribe(() => this.render()); + this.render() + this.unsubscribe = i18n.subscribe(() => this.render()) } disconnectedCallback() { - if (this.unsubscribe) this.unsubscribe(); + if (this.unsubscribe) this.unsubscribe() } render() { @@ -25,7 +25,7 @@ class PageHome extends HTMLElement { ${this.renderListings()} - `; + ` } renderListings() { @@ -36,13 +36,13 @@ class PageHome extends HTMLElement { price="${listing.price}" location="${listing.location}" > - `).join(''); + `).join('') } } -customElements.define('page-home', PageHome); +customElements.define('page-home', PageHome) -const style = document.createElement('style'); +const style = document.createElement('style') style.textContent = /* css */` /* Search Section */ page-home .search-section { @@ -57,5 +57,5 @@ style.textContent = /* css */` page-home section h2 { margin-bottom: var(--space-lg); } -`; -document.head.appendChild(style); +` +document.head.appendChild(style) diff --git a/js/components/pages/page-listing.js b/js/components/pages/page-listing.js index 0a9b7c0..a3e82ea 100644 --- a/js/components/pages/page-listing.js +++ b/js/components/pages/page-listing.js @@ -1,32 +1,32 @@ -import { t, i18n } from '../../i18n.js'; -import { getListingById } from '../../data/mock-listings.js'; -import '../chat-widget.js'; +import { t, i18n } from '../../i18n.js' +import { getListingById } from '../../data/mock-listings.js' +import '../chat-widget.js' class PageListing extends HTMLElement { constructor() { - super(); - this.listing = null; - this.loading = true; + super() + this.listing = null + this.loading = true } connectedCallback() { - this.listingId = this.dataset.id; - this.render(); - this.loadListing(); - this.unsubscribe = i18n.subscribe(() => this.render()); + this.listingId = this.dataset.id + this.render() + this.loadListing() + this.unsubscribe = i18n.subscribe(() => this.render()) } disconnectedCallback() { - if (this.unsubscribe) this.unsubscribe(); + if (this.unsubscribe) this.unsubscribe() } async loadListing() { - await new Promise(resolve => setTimeout(resolve, 300)); + await new Promise(resolve => setTimeout(resolve, 300)) - this.listing = getListingById(this.listingId); + this.listing = getListingById(this.listingId) - this.loading = false; - this.render(); + this.loading = false + this.render() } render() { @@ -35,8 +35,8 @@ class PageListing extends HTMLElement {
- `; - return; + ` + return } if (!this.listing) { @@ -46,18 +46,18 @@ class PageListing extends HTMLElement {

${t('listing.notFound')}

${t('listing.backHome')} - `; - return; + ` + return } - const hasImages = this.listing.images && this.listing.images.length > 0; + const hasImages = this.listing.images && this.listing.images.length > 0 const placeholderSvg = /* html */` - `; + ` this.innerHTML = /* html */`
@@ -141,65 +141,65 @@ class PageListing extends HTMLElement {

${t('listing.contactHint')}

- `; + ` - this.setupEventListeners(); + this.setupEventListeners() } setupEventListeners() { - const contactBtn = this.querySelector('#contact-btn'); - const dialog = this.querySelector('#contact-dialog'); - const closeBtn = this.querySelector('#dialog-close'); - const copyBtn = this.querySelector('#copy-btn'); - const tabBtns = this.querySelectorAll('.tab-btn'); + const contactBtn = this.querySelector('#contact-btn') + const dialog = this.querySelector('#contact-dialog') + const closeBtn = this.querySelector('#dialog-close') + const copyBtn = this.querySelector('#copy-btn') + const tabBtns = this.querySelectorAll('.tab-btn') contactBtn?.addEventListener('click', () => { - dialog?.showModal(); - }); + dialog?.showModal() + }) closeBtn?.addEventListener('click', () => { - dialog?.close(); - }); + dialog?.close() + }) dialog?.addEventListener('click', (e) => { if (e.target === dialog) { - dialog.close(); + dialog.close() } - }); + }) copyBtn?.addEventListener('click', async () => { - const addr = this.querySelector('#monero-addr')?.textContent; + const addr = this.querySelector('#monero-addr')?.textContent if (addr) { - await navigator.clipboard.writeText(addr); - copyBtn.classList.add('copied'); - setTimeout(() => copyBtn.classList.remove('copied'), 2000); + await navigator.clipboard.writeText(addr) + copyBtn.classList.add('copied') + setTimeout(() => copyBtn.classList.remove('copied'), 2000) } - }); + }) tabBtns.forEach(btn => { btn.addEventListener('click', () => { - const tab = btn.dataset.tab; + const tab = btn.dataset.tab - tabBtns.forEach(b => b.classList.remove('active')); - btn.classList.add('active'); + tabBtns.forEach(b => b.classList.remove('active')) + btn.classList.add('active') this.querySelectorAll('.tab-content').forEach(content => { - content.classList.toggle('hidden', content.id !== `tab-${tab}`); - }); - }); - }); + content.classList.toggle('hidden', content.id !== `tab-${tab}`) + }) + }) + }) } escapeHtml(text) { - const div = document.createElement('div'); - div.textContent = text; - return div.innerHTML; + const div = document.createElement('div') + div.textContent = text + return div.innerHTML } } -customElements.define('page-listing', PageListing); +customElements.define('page-listing', PageListing) -const style = document.createElement('style'); +const style = document.createElement('style') style.textContent = /* css */` page-listing .listing-detail { display: grid; @@ -425,5 +425,5 @@ style.textContent = /* css */` color: var(--color-text-muted); text-align: center; } -`; -document.head.appendChild(style); +` +document.head.appendChild(style) diff --git a/js/components/pages/page-not-found.js b/js/components/pages/page-not-found.js index 2795816..81342dc 100644 --- a/js/components/pages/page-not-found.js +++ b/js/components/pages/page-not-found.js @@ -1,13 +1,13 @@ -import { t, i18n } from '../../i18n.js'; +import { t, i18n } from '../../i18n.js' class PageNotFound extends HTMLElement { connectedCallback() { - this.render(); - this.unsubscribe = i18n.subscribe(() => this.render()); + this.render() + this.unsubscribe = i18n.subscribe(() => this.render()) } disconnectedCallback() { - if (this.unsubscribe) this.unsubscribe(); + if (this.unsubscribe) this.unsubscribe() } render() { @@ -20,13 +20,13 @@ class PageNotFound extends HTMLElement { ${t('notFound.backHome')} - `; + ` } } -customElements.define('page-not-found', PageNotFound); +customElements.define('page-not-found', PageNotFound) -const style = document.createElement('style'); +const style = document.createElement('style') style.textContent = /* css */` page-not-found .not-found { text-align: center; @@ -48,5 +48,5 @@ style.textContent = /* css */` color: var(--color-text-secondary); margin-bottom: var(--space-xl); } -`; -document.head.appendChild(style); +` +document.head.appendChild(style) diff --git a/js/components/pages/page-search.js b/js/components/pages/page-search.js index d5ce584..bcb0162 100644 --- a/js/components/pages/page-search.js +++ b/js/components/pages/page-search.js @@ -1,52 +1,52 @@ -import { t, i18n } from '../../i18n.js'; -import { router } from '../../router.js'; -import { searchListings } from '../../data/mock-listings.js'; -import '../search-box.js'; -import '../listing-card.js'; +import { t, i18n } from '../../i18n.js' +import { router } from '../../router.js' +import { searchListings } from '../../data/mock-listings.js' +import '../search-box.js' +import '../listing-card.js' class PageSearch extends HTMLElement { constructor() { - super(); - this.results = []; - this.loading = false; + super() + this.results = [] + this.loading = false } connectedCallback() { - this.parseUrlParams(); - this.render(); - this.afterRender(); + this.parseUrlParams() + this.render() + this.afterRender() this.unsubscribe = i18n.subscribe(() => { - this.render(); - this.afterRender(); - }); + this.render() + this.afterRender() + }) if (this.hasFilters()) { - this.performSearch(); + this.performSearch() } } disconnectedCallback() { - if (this.unsubscribe) this.unsubscribe(); + if (this.unsubscribe) this.unsubscribe() } parseUrlParams() { - const params = new URLSearchParams(window.location.hash.split('?')[1] || ''); - this.query = params.get('q') || ''; - this.category = params.get('category') || ''; - this.subcategory = params.get('sub') || ''; - this.country = params.get('country') || 'ch'; - this.useCurrentLocation = params.get('location') === 'current'; - this.radius = parseInt(params.get('radius')) || 50; - this.lat = params.get('lat') ? parseFloat(params.get('lat')) : null; - this.lng = params.get('lng') ? parseFloat(params.get('lng')) : null; + const params = new URLSearchParams(window.location.hash.split('?')[1] || '') + this.query = params.get('q') || '' + this.category = params.get('category') || '' + this.subcategory = params.get('sub') || '' + this.country = params.get('country') || 'ch' + this.useCurrentLocation = params.get('location') === 'current' + this.radius = parseInt(params.get('radius')) || 50 + this.lat = params.get('lat') ? parseFloat(params.get('lat')) : null + this.lng = params.get('lng') ? parseFloat(params.get('lng')) : null } hasFilters() { - return this.query || this.category || this.subcategory; + return this.query || this.category || this.subcategory } afterRender() { - const searchBox = this.querySelector('search-box'); + const searchBox = this.querySelector('search-box') if (searchBox) { searchBox.setFilters({ query: this.query, @@ -55,25 +55,25 @@ class PageSearch extends HTMLElement { country: this.country, useCurrentLocation: this.useCurrentLocation, radius: this.radius - }); + }) searchBox.addEventListener('search', (e) => { - this.handleSearch(e.detail); - }); + this.handleSearch(e.detail) + }) } } handleSearch(filters) { - this.query = filters.query; - this.category = filters.category; - this.subcategory = filters.subcategory; - this.country = filters.country; - this.useCurrentLocation = filters.useCurrentLocation; - this.radius = filters.radius; - this.lat = filters.lat; - this.lng = filters.lng; + this.query = filters.query + this.category = filters.category + this.subcategory = filters.subcategory + this.country = filters.country + this.useCurrentLocation = filters.useCurrentLocation + this.radius = filters.radius + this.lat = filters.lat + this.lng = filters.lng - this.performSearch(); + this.performSearch() } render() { @@ -87,28 +87,28 @@ class PageSearch extends HTMLElement { ${this.renderResults()} - `; + ` } async performSearch() { - this.loading = true; - this.updateResults(); + this.loading = true + this.updateResults() - await new Promise(resolve => setTimeout(resolve, 500)); + await new Promise(resolve => setTimeout(resolve, 500)) - this.results = this.getMockResults(); - this.loading = false; - this.updateResults(); + this.results = this.getMockResults() + this.loading = false + this.updateResults() } getMockResults() { - return searchListings(this.query, this.category, this.subcategory); + return searchListings(this.query, this.category, this.subcategory) } updateResults() { - const resultsContainer = this.querySelector('#results'); + const resultsContainer = this.querySelector('#results') if (resultsContainer) { - resultsContainer.innerHTML = this.renderResults(); + resultsContainer.innerHTML = this.renderResults() } } @@ -119,7 +119,7 @@ class PageSearch extends HTMLElement {

${t('search.loading')}

- `; + ` } if (!this.hasFilters() && this.results.length === 0) { @@ -128,7 +128,7 @@ class PageSearch extends HTMLElement {
🔍

${t('search.enterQuery')}

- `; + ` } if (this.results.length === 0) { @@ -137,7 +137,7 @@ class PageSearch extends HTMLElement {
😕

${t('search.noResults')}

- `; + ` } return /* html */` @@ -152,19 +152,19 @@ class PageSearch extends HTMLElement { > `).join('')} - `; + ` } escapeHtml(text) { - const div = document.createElement('div'); - div.textContent = text; - return div.innerHTML; + const div = document.createElement('div') + div.textContent = text + return div.innerHTML } } -customElements.define('page-search', PageSearch); +customElements.define('page-search', PageSearch) -const style = document.createElement('style'); +const style = document.createElement('style') style.textContent = /* css */` page-search .search-page { padding: var(--space-lg) 0; @@ -212,5 +212,5 @@ style.textContent = /* css */` font-size: 3rem; margin-bottom: var(--space-md); } -`; -document.head.appendChild(style); +` +document.head.appendChild(style) diff --git a/js/components/search-box.js b/js/components/search-box.js index 8a59121..a2043a2 100644 --- a/js/components/search-box.js +++ b/js/components/search-box.js @@ -1,4 +1,4 @@ -import { t, i18n } from '../i18n.js'; +import { t, i18n } from '../i18n.js' const CATEGORIES = { electronics: ['phones', 'computers', 'tv_audio', 'gaming', 'appliances'], @@ -9,50 +9,50 @@ const CATEGORIES = { books: ['fiction', 'nonfiction', 'textbooks', 'music_movies'], garden: ['plants', 'tools', 'outdoor_living', 'decoration'], other: ['collectibles', 'art', 'handmade', 'services'] -}; +} -const COUNTRIES = ['ch', 'de', 'at', 'fr', 'it', 'li']; -const RADIUS_OPTIONS = [5, 10, 20, 50, 100, 200]; +const COUNTRIES = ['ch', 'de', 'at', 'fr', 'it', 'li'] +const RADIUS_OPTIONS = [5, 10, 20, 50, 100, 200] class SearchBox extends HTMLElement { static get observedAttributes() { - return ['category', 'subcategory', 'country', 'query']; + return ['category', 'subcategory', 'country', 'query'] } constructor() { - super(); - this.loadFiltersFromStorage(); + super() + this.loadFiltersFromStorage() } loadFiltersFromStorage() { - const saved = localStorage.getItem('searchFilters'); + const saved = localStorage.getItem('searchFilters') if (saved) { try { - const filters = JSON.parse(saved); - this.selectedCategory = filters.category || ''; - this.selectedSubcategory = filters.subcategory || ''; - this.selectedCountry = filters.country || 'ch'; - this.selectedRadius = filters.radius || 50; - this.useCurrentLocation = filters.useCurrentLocation || false; - this.searchQuery = filters.query || ''; + const filters = JSON.parse(saved) + this.selectedCategory = filters.category || '' + this.selectedSubcategory = filters.subcategory || '' + this.selectedCountry = filters.country || 'ch' + this.selectedRadius = filters.radius || 50 + this.useCurrentLocation = filters.useCurrentLocation || false + this.searchQuery = filters.query || '' } catch (e) { - this.resetFilters(); + this.resetFilters() } } else { - this.resetFilters(); + this.resetFilters() } } resetFilters() { - this.selectedCategory = ''; - this.selectedSubcategory = ''; - this.selectedCountry = 'ch'; - this.selectedRadius = 50; - this.useCurrentLocation = false; - this.searchQuery = ''; - this.geoLoading = false; - this.currentLat = null; - this.currentLng = null; + this.selectedCategory = '' + this.selectedSubcategory = '' + this.selectedCountry = 'ch' + this.selectedRadius = 50 + this.useCurrentLocation = false + this.searchQuery = '' + this.geoLoading = false + this.currentLat = null + this.currentLng = null } saveFiltersToStorage() { @@ -63,61 +63,61 @@ class SearchBox extends HTMLElement { radius: this.selectedRadius, useCurrentLocation: this.useCurrentLocation, query: this.searchQuery - }; - localStorage.setItem('searchFilters', JSON.stringify(filters)); + } + localStorage.setItem('searchFilters', JSON.stringify(filters)) } connectedCallback() { // Override from attributes if provided if (this.hasAttribute('category')) { - this.selectedCategory = this.getAttribute('category'); + this.selectedCategory = this.getAttribute('category') } if (this.hasAttribute('subcategory')) { - this.selectedSubcategory = this.getAttribute('subcategory'); + this.selectedSubcategory = this.getAttribute('subcategory') } if (this.hasAttribute('country')) { - this.selectedCountry = this.getAttribute('country'); + this.selectedCountry = this.getAttribute('country') } if (this.hasAttribute('query')) { - this.searchQuery = this.getAttribute('query'); + this.searchQuery = this.getAttribute('query') } - this.render(); - this.setupEventListeners(); + this.render() + this.setupEventListeners() this.unsubscribe = i18n.subscribe(() => { - this.render(); - this.setupEventListeners(); - }); + this.render() + this.setupEventListeners() + }) } disconnectedCallback() { - if (this.unsubscribe) this.unsubscribe(); + if (this.unsubscribe) this.unsubscribe() if (this._closeDropdown) { - document.removeEventListener('click', this._closeDropdown); + document.removeEventListener('click', this._closeDropdown) } } attributeChangedCallback(name, oldValue, newValue) { - if (oldValue === newValue) return; + if (oldValue === newValue) return switch (name) { case 'category': - this.selectedCategory = newValue || ''; - break; + this.selectedCategory = newValue || '' + break case 'subcategory': - this.selectedSubcategory = newValue || ''; - break; + this.selectedSubcategory = newValue || '' + break case 'country': - this.selectedCountry = newValue || 'ch'; - break; + this.selectedCountry = newValue || 'ch' + break case 'query': - this.searchQuery = newValue || ''; - break; + this.searchQuery = newValue || '' + break } if (this.isConnected) { - this.render(); - this.setupEventListeners(); + this.render() + this.setupEventListeners() } } @@ -146,12 +146,12 @@ class SearchBox extends HTMLElement { - `; + ` } renderFilters() { // Track which category accordion is expanded - this._expandedCategory = this._expandedCategory || ''; + this._expandedCategory = this._expandedCategory || '' return /* html */` @@ -263,169 +263,169 @@ class SearchBox extends HTMLElement { - `; + ` } setupEventListeners() { - const form = this.querySelector('#search-form'); - const queryInput = this.querySelector('#search-query'); + const form = this.querySelector('#search-form') + const queryInput = this.querySelector('#search-query') // Desktop selects - const countrySelect = this.querySelector('#country-select'); - const radiusSelect = this.querySelector('#radius-select'); + const countrySelect = this.querySelector('#country-select') + const radiusSelect = this.querySelector('#radius-select') // Mobile selects - const countrySelectMobile = this.querySelector('#country-select-mobile'); - const radiusSelectMobile = this.querySelector('#radius-select-mobile'); + const countrySelectMobile = this.querySelector('#country-select-mobile') + const radiusSelectMobile = this.querySelector('#radius-select-mobile') // Accordion dropdown - const categoryTrigger = this.querySelector('#category-trigger'); - const categoryMenu = this.querySelector('#category-menu'); + const categoryTrigger = this.querySelector('#category-trigger') + const categoryMenu = this.querySelector('#category-menu') form?.addEventListener('submit', (e) => { - e.preventDefault(); - this.handleSearch(); - }); + e.preventDefault() + this.handleSearch() + }) queryInput?.addEventListener('input', (e) => { - this.searchQuery = e.target.value; - }); + this.searchQuery = e.target.value + }) // Toggle dropdown if (categoryTrigger && categoryMenu) { categoryTrigger.addEventListener('click', (e) => { - e.preventDefault(); - e.stopPropagation(); - categoryMenu.classList.toggle('open'); - }); + e.preventDefault() + e.stopPropagation() + categoryMenu.classList.toggle('open') + }) } // Close dropdown on outside click this._closeDropdown = (e) => { if (!this.contains(e.target)) { - categoryMenu?.classList.remove('open'); + categoryMenu?.classList.remove('open') } - }; - document.addEventListener('click', this._closeDropdown); + } + document.addEventListener('click', this._closeDropdown) // Category accordion headers - toggle expand this.querySelectorAll('.category-accordion > .category-item').forEach(item => { item.addEventListener('click', (e) => { - e.preventDefault(); - e.stopPropagation(); - const cat = item.dataset.category; - const accordion = item.closest('.category-accordion'); + e.preventDefault() + e.stopPropagation() + const cat = item.dataset.category + const accordion = item.closest('.category-accordion') // Toggle this accordion if (this._expandedCategory === cat) { - this._expandedCategory = ''; - accordion?.classList.remove('expanded'); + this._expandedCategory = '' + accordion?.classList.remove('expanded') } else { // Close other accordions this.querySelectorAll('.category-accordion.expanded').forEach(el => { - el.classList.remove('expanded'); - }); - this._expandedCategory = cat; - accordion?.classList.add('expanded'); + el.classList.remove('expanded') + }) + this._expandedCategory = cat + accordion?.classList.add('expanded') } - }); - }); + }) + }) // "All categories" button this.querySelector('.category-item--all')?.addEventListener('click', (e) => { - e.stopPropagation(); - this.selectedCategory = ''; - this.selectedSubcategory = ''; - this._expandedCategory = ''; - this.saveFiltersToStorage(); - categoryMenu?.classList.remove('open'); - this.render(); - this.setupEventListeners(); - }); + e.stopPropagation() + this.selectedCategory = '' + this.selectedSubcategory = '' + this._expandedCategory = '' + this.saveFiltersToStorage() + categoryMenu?.classList.remove('open') + this.render() + this.setupEventListeners() + }) // Subcategory items - select category + subcategory this.querySelectorAll('.subcategory-item').forEach(item => { item.addEventListener('click', (e) => { - e.stopPropagation(); - this.selectedCategory = item.dataset.category; - this.selectedSubcategory = item.dataset.subcategory; - this._expandedCategory = ''; - this.saveFiltersToStorage(); - categoryMenu?.classList.remove('open'); - this.render(); - this.setupEventListeners(); - }); - }); + e.stopPropagation() + this.selectedCategory = item.dataset.category + this.selectedSubcategory = item.dataset.subcategory + this._expandedCategory = '' + this.saveFiltersToStorage() + categoryMenu?.classList.remove('open') + this.render() + this.setupEventListeners() + }) + }) // Country select handler (both desktop and mobile) const handleCountryChange = (e) => { if (e.target.value === 'current') { - this.useCurrentLocation = true; - this.requestGeolocation(); + this.useCurrentLocation = true + this.requestGeolocation() } else { - this.useCurrentLocation = false; - this.selectedCountry = e.target.value; + this.useCurrentLocation = false + this.selectedCountry = e.target.value } - this.saveFiltersToStorage(); - this.render(); - this.setupEventListeners(); - }; + this.saveFiltersToStorage() + this.render() + this.setupEventListeners() + } - countrySelect?.addEventListener('change', handleCountryChange); - countrySelectMobile?.addEventListener('change', handleCountryChange); + countrySelect?.addEventListener('change', handleCountryChange) + countrySelectMobile?.addEventListener('change', handleCountryChange) // Radius select handler (both desktop and mobile) const handleRadiusChange = (e) => { - this.selectedRadius = parseInt(e.target.value); - this.saveFiltersToStorage(); - }; + this.selectedRadius = parseInt(e.target.value) + this.saveFiltersToStorage() + } - radiusSelect?.addEventListener('change', handleRadiusChange); - radiusSelectMobile?.addEventListener('change', handleRadiusChange); + radiusSelect?.addEventListener('change', handleRadiusChange) + radiusSelectMobile?.addEventListener('change', handleRadiusChange) // Adjust select width to selected option (desktop only) - this.adjustSelectWidth(countrySelect); - this.adjustSelectWidth(radiusSelect); + this.adjustSelectWidth(countrySelect) + this.adjustSelectWidth(radiusSelect) } adjustSelectWidth(select) { - if (!select) return; + if (!select) return // Only apply fixed width on desktop (768px+) if (window.innerWidth < 768) { - select.style.width = ''; - return; + select.style.width = '' + return } // Create hidden span to measure text width - const measurer = document.createElement('span'); - measurer.style.cssText = 'position:absolute;visibility:hidden;white-space:nowrap;font:inherit;'; - select.parentElement.appendChild(measurer); + const measurer = document.createElement('span') + measurer.style.cssText = 'position:absolute;visibility:hidden;white-space:nowrap;font:inherit;' + select.parentElement.appendChild(measurer) - const selectedOption = select.options[select.selectedIndex]; - measurer.textContent = selectedOption ? selectedOption.textContent : ''; + const selectedOption = select.options[select.selectedIndex] + measurer.textContent = selectedOption ? selectedOption.textContent : '' // Add padding for arrow, icon and buffer - select.style.width = (measurer.offsetWidth + 90) + 'px'; - measurer.remove(); + select.style.width = (measurer.offsetWidth + 90) + 'px' + measurer.remove() } handleSearch() { - const params = new URLSearchParams(); + const params = new URLSearchParams() - if (this.searchQuery) params.set('q', this.searchQuery); - if (this.selectedCategory) params.set('category', this.selectedCategory); - if (this.selectedSubcategory) params.set('sub', this.selectedSubcategory); + if (this.searchQuery) params.set('q', this.searchQuery) + if (this.selectedCategory) params.set('category', this.selectedCategory) + if (this.selectedSubcategory) params.set('sub', this.selectedSubcategory) if (this.useCurrentLocation && this.currentLat && this.currentLng) { - params.set('lat', this.currentLat); - params.set('lng', this.currentLng); - params.set('radius', this.selectedRadius); + params.set('lat', this.currentLat) + params.set('lng', this.currentLng) + params.set('radius', this.selectedRadius) } else if (!this.useCurrentLocation) { - params.set('country', this.selectedCountry); + params.set('country', this.selectedCountry) } - this.saveFiltersToStorage(); + this.saveFiltersToStorage() // Emit custom event const event = new CustomEvent('search', { @@ -442,63 +442,63 @@ class SearchBox extends HTMLElement { radius: this.selectedRadius, params: params.toString() } - }); + }) - const cancelled = !this.dispatchEvent(event); + const cancelled = !this.dispatchEvent(event) // Navigate to search page unless event was cancelled if (!cancelled && !this.hasAttribute('no-navigate')) { - const url = '#/search' + (params.toString() ? '?' + params.toString() : ''); - window.location.hash = url; + const url = '#/search' + (params.toString() ? '?' + params.toString() : '') + window.location.hash = url } } requestGeolocation() { if (!('geolocation' in navigator)) { - this.handleGeoError(); - return; + this.handleGeoError() + return } - this.geoLoading = true; - this.updateGeoButton(); + this.geoLoading = true + this.updateGeoButton() navigator.geolocation.getCurrentPosition( (position) => { - this.currentLat = position.coords.latitude; - this.currentLng = position.coords.longitude; - this.geoLoading = false; - this.updateGeoButton(); + this.currentLat = position.coords.latitude + this.currentLng = position.coords.longitude + this.geoLoading = false + this.updateGeoButton() }, (error) => { - console.warn('Geolocation error:', error); - this.handleGeoError(); + console.warn('Geolocation error:', error) + this.handleGeoError() }, { timeout: 10000, enableHighAccuracy: false } - ); + ) } handleGeoError() { // Keep useCurrentLocation = true, just stop loading indicator // User can still search by current location (backend will handle it) - this.geoLoading = false; - this.updateGeoButton(); + this.geoLoading = false + this.updateGeoButton() } updateGeoButton() { - const countrySelect = this.querySelector('#country-select'); - if (!countrySelect) return; + const countrySelect = this.querySelector('#country-select') + if (!countrySelect) return if (this.geoLoading) { - countrySelect.disabled = true; - const currentOption = countrySelect.querySelector('option[value="current"]'); + countrySelect.disabled = true + const currentOption = countrySelect.querySelector('option[value="current"]') if (currentOption) { - currentOption.textContent = `⏳ ${t('search.locating')}`; + currentOption.textContent = `⏳ ${t('search.locating')}` } } else { - countrySelect.disabled = false; - const currentOption = countrySelect.querySelector('option[value="current"]'); + countrySelect.disabled = false + const currentOption = countrySelect.querySelector('option[value="current"]') if (currentOption) { - currentOption.textContent = `📍 ${t('search.currentLocation')}`; + currentOption.textContent = `📍 ${t('search.currentLocation')}` } } } @@ -514,33 +514,33 @@ class SearchBox extends HTMLElement { lat: this.currentLat, lng: this.currentLng, radius: this.selectedRadius - }; + } } setFilters(filters) { - if (filters.query !== undefined) this.searchQuery = filters.query; - if (filters.category !== undefined) this.selectedCategory = filters.category; - if (filters.subcategory !== undefined) this.selectedSubcategory = filters.subcategory; - if (filters.country !== undefined) this.selectedCountry = filters.country; - if (filters.radius !== undefined) this.selectedRadius = filters.radius; - if (filters.useCurrentLocation !== undefined) this.useCurrentLocation = filters.useCurrentLocation; + if (filters.query !== undefined) this.searchQuery = filters.query + if (filters.category !== undefined) this.selectedCategory = filters.category + if (filters.subcategory !== undefined) this.selectedSubcategory = filters.subcategory + if (filters.country !== undefined) this.selectedCountry = filters.country + if (filters.radius !== undefined) this.selectedRadius = filters.radius + if (filters.useCurrentLocation !== undefined) this.useCurrentLocation = filters.useCurrentLocation - this.saveFiltersToStorage(); - this.render(); - this.setupEventListeners(); + this.saveFiltersToStorage() + this.render() + this.setupEventListeners() } clearFilters() { - this.resetFilters(); - localStorage.removeItem('searchFilters'); - this.render(); - this.setupEventListeners(); + this.resetFilters() + localStorage.removeItem('searchFilters') + this.render() + this.setupEventListeners() } } -customElements.define('search-box', SearchBox); +customElements.define('search-box', SearchBox) -const style = document.createElement('style'); +const style = document.createElement('style') style.textContent = /* css */` search-box { display: block; @@ -905,7 +905,7 @@ style.textContent = /* css */` background: var(--color-primary-light); color: var(--color-primary); } -`; -document.head.appendChild(style); +` +document.head.appendChild(style) -export { SearchBox, CATEGORIES, COUNTRIES, RADIUS_OPTIONS }; +export { SearchBox, CATEGORIES, COUNTRIES, RADIUS_OPTIONS } diff --git a/js/data/mock-listings.js b/js/data/mock-listings.js index b03bf98..fdcc213 100644 --- a/js/data/mock-listings.js +++ b/js/data/mock-listings.js @@ -159,19 +159,19 @@ export const mockListings = [ publicKey: 'dGVzdC1wdWJsaWMta2V5LW5pbmEtbC0xMjM0NTY3ODkw' } } -]; +] export function getListingById(id) { - return mockListings.find(l => l.id === id) || null; + return mockListings.find(l => l.id === id) || null } export function searchListings(query = '', category = '', subcategory = '') { return mockListings.filter(listing => { const matchesQuery = !query || listing.title.toLowerCase().includes(query.toLowerCase()) || - listing.description.toLowerCase().includes(query.toLowerCase()); - const matchesCategory = !category || listing.category === category; - const matchesSubcategory = !subcategory || listing.subcategory === subcategory; - return matchesQuery && matchesCategory && matchesSubcategory; - }); + listing.description.toLowerCase().includes(query.toLowerCase()) + const matchesCategory = !category || listing.category === category + const matchesSubcategory = !subcategory || listing.subcategory === subcategory + return matchesQuery && matchesCategory && matchesSubcategory + }) } diff --git a/js/i18n.js b/js/i18n.js index 45a7154..21028c5 100644 --- a/js/i18n.js +++ b/js/i18n.js @@ -1,125 +1,125 @@ class I18n { constructor() { - this.translations = {}; - this.currentLocale = 'de'; - this.fallbackLocale = 'de'; - this.supportedLocales = ['de', 'en', 'fr']; - this.subscribers = new Set(); - this.loaded = false; + this.translations = {} + this.currentLocale = 'de' + this.fallbackLocale = 'de' + this.supportedLocales = ['de', 'en', 'fr'] + this.subscribers = new Set() + this.loaded = false } async init() { - const savedLocale = localStorage.getItem('locale'); - const browserLocale = navigator.language.split('-')[0]; + const savedLocale = localStorage.getItem('locale') + const browserLocale = navigator.language.split('-')[0] this.currentLocale = savedLocale - || (this.supportedLocales.includes(browserLocale) ? browserLocale : this.fallbackLocale); + || (this.supportedLocales.includes(browserLocale) ? browserLocale : this.fallbackLocale) - await this.loadTranslations(this.currentLocale); - this.loaded = true; - this.updateDOM(); + await this.loadTranslations(this.currentLocale) + this.loaded = true + this.updateDOM() } async loadTranslations(locale) { - if (this.translations[locale]) return; + if (this.translations[locale]) return try { - const response = await fetch(`/locales/${locale}.json`); - if (!response.ok) throw new Error(`Failed to load ${locale}`); - this.translations[locale] = await response.json(); + const response = await fetch(`/locales/${locale}.json`) + if (!response.ok) throw new Error(`Failed to load ${locale}`) + this.translations[locale] = await response.json() } catch (error) { - console.error(`Failed to load translations for ${locale}:`, error); + console.error(`Failed to load translations for ${locale}:`, error) if (locale !== this.fallbackLocale) { - await this.loadTranslations(this.fallbackLocale); + await this.loadTranslations(this.fallbackLocale) } } } async setLocale(locale) { if (!this.supportedLocales.includes(locale)) { - console.warn(`Locale ${locale} is not supported`); - return; + console.warn(`Locale ${locale} is not supported`) + return } - await this.loadTranslations(locale); - this.currentLocale = locale; - localStorage.setItem('locale', locale); - document.documentElement.lang = locale; + await this.loadTranslations(locale) + this.currentLocale = locale + localStorage.setItem('locale', locale) + document.documentElement.lang = locale - this.updateDOM(); - this.notifySubscribers(); + this.updateDOM() + this.notifySubscribers() } getLocale() { - return this.currentLocale; + return this.currentLocale } t(key, params = {}) { const translations = this.translations[this.currentLocale] || this.translations[this.fallbackLocale] - || {}; + || {} - let text = this.getNestedValue(translations, key); + let text = this.getNestedValue(translations, key) if (text === undefined) { - console.warn(`Missing translation: ${key}`); - return key; + console.warn(`Missing translation: ${key}`) + return key } Object.entries(params).forEach(([param, value]) => { - text = text.replace(new RegExp(`{{${param}}}`, 'g'), value); - }); + text = text.replace(new RegExp(`{{${param}}}`, 'g'), value) + }) - return text; + return text } getNestedValue(obj, path) { return path.split('.').reduce((current, key) => { - return current && current[key] !== undefined ? current[key] : undefined; - }, obj); + return current && current[key] !== undefined ? current[key] : undefined + }, obj) } updateDOM() { document.querySelectorAll('[data-i18n]').forEach((el) => { - const key = el.getAttribute('data-i18n'); - let params = {}; + const key = el.getAttribute('data-i18n') + let params = {} if (el.dataset.i18nParams) { try { - params = JSON.parse(el.dataset.i18nParams); + params = JSON.parse(el.dataset.i18nParams) } catch (e) { - console.warn(`Invalid i18n params for key "${key}":`, e); + console.warn(`Invalid i18n params for key "${key}":`, e) } } - el.textContent = this.t(key, params); - }); + el.textContent = this.t(key, params) + }) document.querySelectorAll('[data-i18n-placeholder]').forEach((el) => { - const key = el.getAttribute('data-i18n-placeholder'); - el.placeholder = this.t(key); - }); + const key = el.getAttribute('data-i18n-placeholder') + el.placeholder = this.t(key) + }) document.querySelectorAll('[data-i18n-title]').forEach((el) => { - const key = el.getAttribute('data-i18n-title'); - el.title = this.t(key); - }); + const key = el.getAttribute('data-i18n-title') + el.title = this.t(key) + }) document.querySelectorAll('[data-i18n-aria]').forEach((el) => { - const key = el.getAttribute('data-i18n-aria'); - el.setAttribute('aria-label', this.t(key)); - }); + const key = el.getAttribute('data-i18n-aria') + el.setAttribute('aria-label', this.t(key)) + }) } subscribe(callback) { - this.subscribers.add(callback); - return () => this.subscribers.delete(callback); + this.subscribers.add(callback) + return () => this.subscribers.delete(callback) } notifySubscribers() { - this.subscribers.forEach(callback => callback(this.currentLocale)); + this.subscribers.forEach(callback => callback(this.currentLocale)) } getSupportedLocales() { - return this.supportedLocales; + return this.supportedLocales } getLocaleDisplayName(locale) { @@ -127,10 +127,10 @@ class I18n { de: 'Deutsch', en: 'English', fr: 'Français' - }; - return names[locale] || locale; + } + return names[locale] || locale } } -export const i18n = new I18n(); -export const t = (key, params) => i18n.t(key, params); +export const i18n = new I18n() +export const t = (key, params) => i18n.t(key, params) diff --git a/js/router.js b/js/router.js index b7840a4..f74316f 100644 --- a/js/router.js +++ b/js/router.js @@ -1,155 +1,155 @@ class Router { constructor() { - this.routes = new Map(); - this.currentRoute = null; - this.outlet = null; - this.beforeNavigate = null; - this.afterNavigate = null; + this.routes = new Map() + this.currentRoute = null + this.outlet = null + this.beforeNavigate = null + this.afterNavigate = null - window.addEventListener('hashchange', () => this.handleRouteChange()); + window.addEventListener('hashchange', () => this.handleRouteChange()) } setOutlet(element) { - this.outlet = element; + this.outlet = element } register(path, componentTag) { - this.routes.set(path, componentTag); - return this; + this.routes.set(path, componentTag) + return this } parseHash() { - const hash = window.location.hash.slice(1) || '/'; - const [path, queryString] = hash.split('?'); - const params = new URLSearchParams(queryString || ''); + const hash = window.location.hash.slice(1) || '/' + const [path, queryString] = hash.split('?') + const params = new URLSearchParams(queryString || '') - return { path, params: Object.fromEntries(params) }; + return { path, params: Object.fromEntries(params) } } matchRoute(path) { if (this.routes.has(path)) { - return { componentTag: this.routes.get(path), params: {} }; + return { componentTag: this.routes.get(path), params: {} } } for (const [routePath, componentTag] of this.routes) { - const routeParts = routePath.split('/'); - const pathParts = path.split('/'); + const routeParts = routePath.split('/') + const pathParts = path.split('/') - if (routeParts.length !== pathParts.length) continue; + if (routeParts.length !== pathParts.length) continue - const params = {}; - let match = true; + const params = {} + let match = true for (let i = 0; i < routeParts.length; i++) { if (routeParts[i].startsWith(':')) { - params[routeParts[i].slice(1)] = pathParts[i]; + params[routeParts[i].slice(1)] = pathParts[i] } else if (routeParts[i] !== pathParts[i]) { - match = false; - break; + match = false + break } } if (match) { - return { componentTag, params }; + return { componentTag, params } } } - return null; + return null } async handleRouteChange() { - const { path, params: queryParams } = this.parseHash(); - const match = this.matchRoute(path); + const { path, params: queryParams } = this.parseHash() + const match = this.matchRoute(path) if (this.beforeNavigate) { - const shouldContinue = await this.beforeNavigate(path); - if (!shouldContinue) return; + const shouldContinue = await this.beforeNavigate(path) + if (!shouldContinue) return } if (!match) { - this.renderNotFound(); - return; + this.renderNotFound() + return } - const { componentTag, params: routeParams } = match; + const { componentTag, params: routeParams } = match this.currentRoute = { path, params: { ...routeParams, ...queryParams }, componentTag - }; + } - this.render(); + this.render() if (this.afterNavigate) { - this.afterNavigate(this.currentRoute); + this.afterNavigate(this.currentRoute) } } render() { - if (!this.outlet || !this.currentRoute) return; + if (!this.outlet || !this.currentRoute) return - const { componentTag, params } = this.currentRoute; - const oldComponent = this.outlet.firstElementChild; - const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; + const { componentTag, params } = this.currentRoute + const oldComponent = this.outlet.firstElementChild + const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches - const component = document.createElement(componentTag); + const component = document.createElement(componentTag) Object.entries(params).forEach(([key, value]) => { - component.setAttribute(`data-${key}`, value); - }); + component.setAttribute(`data-${key}`, value) + }) if (prefersReducedMotion || !oldComponent) { - if (oldComponent) oldComponent.remove(); - this.outlet.appendChild(component); - return; + if (oldComponent) oldComponent.remove() + this.outlet.appendChild(component) + return } - component.classList.add('animate__animated', 'animate__fadeIn', 'animate__faster'); - oldComponent.classList.add('animate__animated', 'animate__fadeOut', 'animate__faster'); + component.classList.add('animate__animated', 'animate__fadeIn', 'animate__faster') + oldComponent.classList.add('animate__animated', 'animate__fadeOut', 'animate__faster') const fallbackTimer = setTimeout(() => { - oldComponent.remove(); - this.outlet.appendChild(component); - }, 300); + oldComponent.remove() + this.outlet.appendChild(component) + }, 300) oldComponent.addEventListener('animationend', () => { - clearTimeout(fallbackTimer); - oldComponent.remove(); - this.outlet.appendChild(component); - }, { once: true }); + clearTimeout(fallbackTimer) + oldComponent.remove() + this.outlet.appendChild(component) + }, { once: true }) } renderNotFound() { - if (!this.outlet) return; + if (!this.outlet) return - this.outlet.innerHTML = ''; - const notFound = document.createElement('page-not-found'); - this.outlet.appendChild(notFound); + this.outlet.innerHTML = '' + const notFound = document.createElement('page-not-found') + this.outlet.appendChild(notFound) } navigate(path, params = {}) { - let url = `#${path}`; + let url = `#${path}` if (Object.keys(params).length > 0) { - const queryString = new URLSearchParams(params).toString(); - url += `?${queryString}`; + const queryString = new URLSearchParams(params).toString() + url += `?${queryString}` } - window.location.hash = url.slice(1); + window.location.hash = url.slice(1) } back() { - window.history.back(); + window.history.back() } forward() { - window.history.forward(); + window.history.forward() } getCurrentRoute() { - return this.currentRoute; + return this.currentRoute } } -export const router = new Router(); +export const router = new Router() diff --git a/js/services/api.js b/js/services/api.js index 09a7c9c..08cd47f 100644 --- a/js/services/api.js +++ b/js/services/api.js @@ -1,138 +1,138 @@ -const API_BASE_URL = '/api'; +const API_BASE_URL = '/api' class ApiService { constructor() { - this.baseUrl = API_BASE_URL; - this.token = null; + this.baseUrl = API_BASE_URL + this.token = null } setToken(token) { - this.token = token; + this.token = token } clearToken() { - this.token = null; + this.token = null } async request(endpoint, options = {}) { - const url = `${this.baseUrl}${endpoint}`; + const url = `${this.baseUrl}${endpoint}` const headers = { 'Content-Type': 'application/json', ...options.headers - }; + } if (this.token) { - headers['Authorization'] = `Bearer ${this.token}`; + headers['Authorization'] = `Bearer ${this.token}` } try { const response = await fetch(url, { ...options, headers - }); + }) if (!response.ok) { - const error = await response.json().catch(() => ({})); - throw new ApiError(response.status, error.message || 'Request failed', error); + const error = await response.json().catch(() => ({})) + throw new ApiError(response.status, error.message || 'Request failed', error) } if (response.status === 204) { - return null; + return null } - return await response.json(); + return await response.json() } catch (error) { - if (error instanceof ApiError) throw error; - throw new ApiError(0, 'Network error', { originalError: error }); + if (error instanceof ApiError) throw error + throw new ApiError(0, 'Network error', { originalError: error }) } } async get(endpoint, params = {}) { - const queryString = new URLSearchParams(params).toString(); - const url = queryString ? `${endpoint}?${queryString}` : endpoint; - return this.request(url, { method: 'GET' }); + const queryString = new URLSearchParams(params).toString() + const url = queryString ? `${endpoint}?${queryString}` : endpoint + return this.request(url, { method: 'GET' }) } async post(endpoint, data) { return this.request(endpoint, { method: 'POST', body: JSON.stringify(data) - }); + }) } async put(endpoint, data) { return this.request(endpoint, { method: 'PUT', body: JSON.stringify(data) - }); + }) } async patch(endpoint, data) { return this.request(endpoint, { method: 'PATCH', body: JSON.stringify(data) - }); + }) } async delete(endpoint) { - return this.request(endpoint, { method: 'DELETE' }); + return this.request(endpoint, { method: 'DELETE' }) } async getListings(params = {}) { - return this.get('/items/listings', params); + return this.get('/items/listings', params) } async getListing(id) { - return this.get(`/items/listings/${id}`); + return this.get(`/items/listings/${id}`) } async createListing(data) { - return this.post('/items/listings', data); + return this.post('/items/listings', data) } async updateListing(id, data) { - return this.patch(`/items/listings/${id}`, data); + return this.patch(`/items/listings/${id}`, data) } async deleteListing(id) { - return this.delete(`/items/listings/${id}`); + return this.delete(`/items/listings/${id}`) } async searchListings(query, params = {}) { - return this.get('/items/listings', { search: query, ...params }); + return this.get('/items/listings', { search: query, ...params }) } async getCategories() { - return this.get('/items/categories'); + return this.get('/items/categories') } async uploadFile(file) { - const formData = new FormData(); - formData.append('file', file); + const formData = new FormData() + formData.append('file', file) const response = await fetch(`${this.baseUrl}/files`, { method: 'POST', headers: this.token ? { 'Authorization': `Bearer ${this.token}` } : {}, body: formData - }); + }) if (!response.ok) { - throw new ApiError(response.status, 'Upload failed'); + throw new ApiError(response.status, 'Upload failed') } - return response.json(); + return response.json() } } class ApiError extends Error { constructor(status, message, data = {}) { - super(message); - this.name = 'ApiError'; - this.status = status; - this.data = data; + super(message) + this.name = 'ApiError' + this.status = status + this.data = data } } -export const api = new ApiService(); -export { ApiError }; +export const api = new ApiService() +export { ApiError } diff --git a/js/services/auth.js b/js/services/auth.js index 40b4e2d..8febe87 100644 --- a/js/services/auth.js +++ b/js/services/auth.js @@ -5,14 +5,14 @@ * User remembers only their UUID */ -import { directus } from './directus.js'; +import { directus } from './directus.js' -const AUTH_DOMAIN = 'dgray.io'; +const AUTH_DOMAIN = 'dgray.io' class AuthService { constructor() { - this.currentUser = null; - this.listeners = new Set(); + this.currentUser = null + this.listeners = new Set() } /** @@ -22,15 +22,15 @@ class AuthService { generateUuid() { // Use native if available (secure contexts) if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') { - return crypto.randomUUID(); + return crypto.randomUUID() } // Fallback for non-secure contexts return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { - const r = Math.random() * 16 | 0; - const v = c === 'x' ? r : (r & 0x3 | 0x8); - return v.toString(16); - }); + const r = Math.random() * 16 | 0 + const v = c === 'x' ? r : (r & 0x3 | 0x8) + return v.toString(16) + }) } /** @@ -39,7 +39,7 @@ class AuthService { * @returns {string} Fake email address */ uuidToEmail(uuid) { - return `${uuid}@${AUTH_DOMAIN}`; + return `${uuid}@${AUTH_DOMAIN}` } /** @@ -47,14 +47,14 @@ class AuthService { * @returns {Promise<{uuid: string, success: boolean, error?: string}>} */ async createAccount() { - const uuid = this.generateUuid(); - const email = this.uuidToEmail(uuid); + const uuid = this.generateUuid() + const email = this.uuidToEmail(uuid) try { - await directus.register(email, uuid); + await directus.register(email, uuid) // Try auto-login (may fail if verification required) - const loginResult = await this.login(uuid); + const loginResult = await this.login(uuid) if (!loginResult.success) { // Registration worked but login failed (verification pending) @@ -63,17 +63,17 @@ class AuthService { success: true, pendingVerification: true, message: 'Account created. Login may require activation.' - }; + } } - return { uuid, success: true }; + return { uuid, success: true } } catch (error) { - console.error('Registration failed:', error); + console.error('Registration failed:', error) return { uuid: null, success: false, error: error.message || 'Registration failed' - }; + } } } @@ -83,21 +83,21 @@ class AuthService { * @returns {Promise<{success: boolean, error?: string}>} */ async login(uuid) { - const email = this.uuidToEmail(uuid); + const email = this.uuidToEmail(uuid) try { - await directus.login(email, uuid); - this.currentUser = await directus.getCurrentUser(); - this.notifyListeners(); - this.storeUuid(uuid); + await directus.login(email, uuid) + this.currentUser = await directus.getCurrentUser() + this.notifyListeners() + this.storeUuid(uuid) - return { success: true }; + return { success: true } } catch (error) { - console.error('Login failed:', error); + console.error('Login failed:', error) return { success: false, error: error.message || 'Invalid UUID' - }; + } } } @@ -106,14 +106,14 @@ class AuthService { */ async logout() { try { - await directus.logout(); + await directus.logout() } catch (e) { // Ignore errors } - this.currentUser = null; - this.clearStoredUuid(); - this.notifyListeners(); + this.currentUser = null + this.clearStoredUuid() + this.notifyListeners() } /** @@ -121,7 +121,7 @@ class AuthService { * @returns {boolean} */ isLoggedIn() { - return directus.isAuthenticated(); + return directus.isAuthenticated() } /** @@ -129,17 +129,17 @@ class AuthService { * @returns {Promise} */ async getUser() { - if (!this.isLoggedIn()) return null; + if (!this.isLoggedIn()) return null if (!this.currentUser) { try { - this.currentUser = await directus.getCurrentUser(); + this.currentUser = await directus.getCurrentUser() } catch (e) { - return null; + return null } } - return this.currentUser; + return this.currentUser } /** @@ -148,7 +148,7 @@ class AuthService { * @param {string} uuid */ storeUuid(uuid) { - localStorage.setItem('dgray_uuid', uuid); + localStorage.setItem('dgray_uuid', uuid) } /** @@ -156,14 +156,14 @@ class AuthService { * @returns {string|null} */ getStoredUuid() { - return localStorage.getItem('dgray_uuid'); + return localStorage.getItem('dgray_uuid') } /** * Clears stored UUID */ clearStoredUuid() { - localStorage.removeItem('dgray_uuid'); + localStorage.removeItem('dgray_uuid') } /** @@ -172,15 +172,15 @@ class AuthService { * @returns {Function} Unsubscribe function */ subscribe(callback) { - this.listeners.add(callback); - return () => this.listeners.delete(callback); + this.listeners.add(callback) + return () => this.listeners.delete(callback) } /** * Notifies all listeners of auth state change */ notifyListeners() { - this.listeners.forEach(cb => cb(this.isLoggedIn(), this.currentUser)); + this.listeners.forEach(cb => cb(this.isLoggedIn(), this.currentUser)) } /** @@ -189,16 +189,16 @@ class AuthService { async tryRestoreSession() { if (directus.isAuthenticated()) { try { - this.currentUser = await directus.getCurrentUser(); - this.notifyListeners(); - return true; + this.currentUser = await directus.getCurrentUser() + this.notifyListeners() + return true } catch (e) { - return false; + return false } } - return false; + return false } } -export const auth = new AuthService(); -export default auth; +export const auth = new AuthService() +export default auth diff --git a/js/services/chat.js b/js/services/chat.js index 93c9614..aa044b2 100644 --- a/js/services/chat.js +++ b/js/services/chat.js @@ -3,14 +3,14 @@ * Uses LocalStorage as mock backend until Directus is ready */ -import { cryptoService } from './crypto.js'; +import { cryptoService } from './crypto.js' -const CHATS_STORAGE_KEY = 'dgray_chats'; -const MESSAGES_STORAGE_KEY = 'dgray_messages'; +const CHATS_STORAGE_KEY = 'dgray_chats' +const MESSAGES_STORAGE_KEY = 'dgray_messages' class ChatService { constructor() { - this.subscribers = new Set(); + this.subscribers = new Set() } /** @@ -21,14 +21,14 @@ class ChatService { * @returns {object} - Chat object */ getOrCreateChat(recipientId, recipientPublicKey, listingId) { - const chats = this.getAllChats(); - const myPublicKey = cryptoService.getPublicKey(); + const chats = this.getAllChats() + const myPublicKey = cryptoService.getPublicKey() // Find existing chat for this listing + recipient let chat = chats.find(c => c.listingId === listingId && c.recipientId === recipientId - ); + ) if (!chat) { chat = { @@ -39,24 +39,24 @@ class ChatService { myPublicKey, createdAt: new Date().toISOString(), lastMessageAt: null - }; - chats.push(chat); - this.saveChats(chats); + } + chats.push(chat) + this.saveChats(chats) } - return chat; + return chat } getAllChats() { try { - return JSON.parse(localStorage.getItem(CHATS_STORAGE_KEY) || '[]'); + return JSON.parse(localStorage.getItem(CHATS_STORAGE_KEY) || '[]') } catch { - return []; + return [] } } saveChats(chats) { - localStorage.setItem(CHATS_STORAGE_KEY, JSON.stringify(chats)); + localStorage.setItem(CHATS_STORAGE_KEY, JSON.stringify(chats)) } /** @@ -67,9 +67,9 @@ class ChatService { * @returns {object} - The saved message */ async sendMessage(chatId, recipientPublicKey, plainText) { - await cryptoService.ready; + await cryptoService.ready - const { nonce, ciphertext } = cryptoService.encrypt(plainText, recipientPublicKey); + const { nonce, ciphertext } = cryptoService.encrypt(plainText, recipientPublicKey) const message = { id: this.generateId(), @@ -80,23 +80,23 @@ class ChatService { timestamp: new Date().toISOString(), // Store plain text for own messages (we can't decrypt our own box messages) _plainText: plainText - }; - - const messages = this.getAllMessages(); - messages.push(message); - this.saveMessages(messages); - - // Update chat's lastMessageAt - const chats = this.getAllChats(); - const chat = chats.find(c => c.id === chatId); - if (chat) { - chat.lastMessageAt = message.timestamp; - this.saveChats(chats); } - this.notifySubscribers(); + const messages = this.getAllMessages() + messages.push(message) + this.saveMessages(messages) - return message; + // Update chat's lastMessageAt + const chats = this.getAllChats() + const chat = chats.find(c => c.id === chatId) + if (chat) { + chat.lastMessageAt = message.timestamp + this.saveChats(chats) + } + + this.notifySubscribers() + + return message } /** @@ -106,22 +106,22 @@ class ChatService { * @returns {Array} - Decrypted messages */ async getMessages(chatId, otherPublicKey) { - await cryptoService.ready; + await cryptoService.ready - const messages = this.getAllMessages().filter(m => m.chatId === chatId); - const myPublicKey = cryptoService.getPublicKey(); + const messages = this.getAllMessages().filter(m => m.chatId === chatId) + const myPublicKey = cryptoService.getPublicKey() return messages.map(msg => { - const isOwn = msg.senderPublicKey === myPublicKey; + const isOwn = msg.senderPublicKey === myPublicKey - let text; + let text if (isOwn) { // Use stored plain text for own messages - text = msg._plainText || '[Encrypted]'; + text = msg._plainText || '[Encrypted]' } else { // Decrypt messages from others - text = cryptoService.decrypt(msg.ciphertext, msg.nonce, msg.senderPublicKey); - if (!text) text = '[Decryption failed]'; + text = cryptoService.decrypt(msg.ciphertext, msg.nonce, msg.senderPublicKey) + if (!text) text = '[Decryption failed]' } return { @@ -129,33 +129,33 @@ class ChatService { text, isOwn, timestamp: msg.timestamp - }; - }); + } + }) } getAllMessages() { try { - return JSON.parse(localStorage.getItem(MESSAGES_STORAGE_KEY) || '[]'); + return JSON.parse(localStorage.getItem(MESSAGES_STORAGE_KEY) || '[]') } catch { - return []; + return [] } } saveMessages(messages) { - localStorage.setItem(MESSAGES_STORAGE_KEY, JSON.stringify(messages)); + localStorage.setItem(MESSAGES_STORAGE_KEY, JSON.stringify(messages)) } generateId() { - return 'msg_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); + return 'msg_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9) } subscribe(callback) { - this.subscribers.add(callback); - return () => this.subscribers.delete(callback); + this.subscribers.add(callback) + return () => this.subscribers.delete(callback) } notifySubscribers() { - this.subscribers.forEach(cb => cb()); + this.subscribers.forEach(cb => cb()) } /** @@ -163,10 +163,10 @@ class ChatService { * In production, this would come from Directus/WebSocket */ async simulateIncomingMessage(chatId, senderPublicKey, plainText) { - await cryptoService.ready; + await cryptoService.ready - const myPublicKey = cryptoService.getPublicKey(); - const { nonce, ciphertext } = this.encryptForRecipient(plainText, myPublicKey, senderPublicKey); + const myPublicKey = cryptoService.getPublicKey() + const { nonce, ciphertext } = this.encryptForRecipient(plainText, myPublicKey, senderPublicKey) const message = { id: this.generateId(), @@ -175,43 +175,43 @@ class ChatService { nonce, ciphertext, timestamp: new Date().toISOString() - }; + } - const messages = this.getAllMessages(); - messages.push(message); - this.saveMessages(messages); + const messages = this.getAllMessages() + messages.push(message) + this.saveMessages(messages) - this.notifySubscribers(); + this.notifySubscribers() - return message; + return message } // Helper for simulation - encrypt as if from another user encryptForRecipient(message, recipientPublicKey, senderSecretKey) { // This is a simplified simulation - in reality the sender would have their own keypair - const nacl = window.nacl; - const naclUtil = window.nacl.util; + const nacl = window.nacl + const naclUtil = window.nacl.util - const nonce = nacl.randomBytes(nacl.box.nonceLength); - const messageUint8 = naclUtil.decodeUTF8(message); + const nonce = nacl.randomBytes(nacl.box.nonceLength) + const messageUint8 = naclUtil.decodeUTF8(message) // For simulation, we create a temporary keypair for the "sender" const senderKeyPair = nacl.box.keyPair.fromSecretKey( naclUtil.decodeBase64(senderSecretKey || naclUtil.encodeBase64(nacl.randomBytes(32))) - ); + ) const encrypted = nacl.box( messageUint8, nonce, naclUtil.decodeBase64(recipientPublicKey), senderKeyPair.secretKey - ); + ) return { nonce: naclUtil.encodeBase64(nonce), ciphertext: naclUtil.encodeBase64(encrypted) - }; + } } } -export const chatService = new ChatService(); +export const chatService = new ChatService() diff --git a/js/services/crypto.js b/js/services/crypto.js index 74742fb..a67727d 100644 --- a/js/services/crypto.js +++ b/js/services/crypto.js @@ -3,76 +3,76 @@ * https://tweetnacl.js.org/ */ -const STORAGE_KEY = 'dgray_keypair'; +const STORAGE_KEY = 'dgray_keypair' class CryptoService { constructor() { - this.nacl = null; - this.naclUtil = null; - this.keyPair = null; - this.ready = this.init(); + this.nacl = null + this.naclUtil = null + this.keyPair = null + this.ready = this.init() } async init() { // Dynamically import TweetNaCl from CDN if (!window.nacl) { - await this.loadScript('https://cdn.jsdelivr.net/npm/tweetnacl@1.0.3/nacl-fast.min.js'); - await this.loadScript('https://cdn.jsdelivr.net/npm/tweetnacl-util@0.15.1/nacl-util.min.js'); + await this.loadScript('https://cdn.jsdelivr.net/npm/tweetnacl@1.0.3/nacl-fast.min.js') + await this.loadScript('https://cdn.jsdelivr.net/npm/tweetnacl-util@0.15.1/nacl-util.min.js') } - this.nacl = window.nacl; - this.naclUtil = window.nacl.util; + this.nacl = window.nacl + this.naclUtil = window.nacl.util - this.loadOrCreateKeyPair(); + this.loadOrCreateKeyPair() } loadScript(src) { return new Promise((resolve, reject) => { if (document.querySelector(`script[src="${src}"]`)) { - resolve(); - return; + resolve() + return } - const script = document.createElement('script'); - script.src = src; - script.onload = resolve; - script.onerror = reject; - document.head.appendChild(script); - }); + const script = document.createElement('script') + script.src = src + script.onload = resolve + script.onerror = reject + document.head.appendChild(script) + }) } loadOrCreateKeyPair() { - const stored = localStorage.getItem(STORAGE_KEY); + const stored = localStorage.getItem(STORAGE_KEY) if (stored) { try { - const parsed = JSON.parse(stored); + const parsed = JSON.parse(stored) this.keyPair = { publicKey: this.naclUtil.decodeBase64(parsed.publicKey), secretKey: this.naclUtil.decodeBase64(parsed.secretKey) - }; - return; + } + return } catch (e) { - console.warn('Failed to load keypair, generating new one'); + console.warn('Failed to load keypair, generating new one') } } - this.generateKeyPair(); + this.generateKeyPair() } generateKeyPair() { - this.keyPair = this.nacl.box.keyPair(); + this.keyPair = this.nacl.box.keyPair() const toStore = { publicKey: this.naclUtil.encodeBase64(this.keyPair.publicKey), secretKey: this.naclUtil.encodeBase64(this.keyPair.secretKey) - }; + } - localStorage.setItem(STORAGE_KEY, JSON.stringify(toStore)); + localStorage.setItem(STORAGE_KEY, JSON.stringify(toStore)) } getPublicKey() { - if (!this.keyPair) return null; - return this.naclUtil.encodeBase64(this.keyPair.publicKey); + if (!this.keyPair) return null + return this.naclUtil.encodeBase64(this.keyPair.publicKey) } /** @@ -82,21 +82,21 @@ class CryptoService { * @returns {object} - { nonce, ciphertext } both base64 encoded */ encrypt(message, recipientPublicKey) { - const nonce = this.nacl.randomBytes(this.nacl.box.nonceLength); - const messageUint8 = this.naclUtil.decodeUTF8(message); - const recipientKey = this.naclUtil.decodeBase64(recipientPublicKey); + const nonce = this.nacl.randomBytes(this.nacl.box.nonceLength) + const messageUint8 = this.naclUtil.decodeUTF8(message) + const recipientKey = this.naclUtil.decodeBase64(recipientPublicKey) const encrypted = this.nacl.box( messageUint8, nonce, recipientKey, this.keyPair.secretKey - ); + ) return { nonce: this.naclUtil.encodeBase64(nonce), ciphertext: this.naclUtil.encodeBase64(encrypted) - }; + } } /** @@ -113,15 +113,15 @@ class CryptoService { this.naclUtil.decodeBase64(nonce), this.naclUtil.decodeBase64(senderPublicKey), this.keyPair.secretKey - ); + ) - if (!decrypted) return null; - return this.naclUtil.encodeUTF8(decrypted); + if (!decrypted) return null + return this.naclUtil.encodeUTF8(decrypted) } catch (e) { - console.error('Decryption failed:', e); - return null; + console.error('Decryption failed:', e) + return null } } } -export const cryptoService = new CryptoService(); +export const cryptoService = new CryptoService() diff --git a/js/services/currency.js b/js/services/currency.js index 156572d..b1624cf 100644 --- a/js/services/currency.js +++ b/js/services/currency.js @@ -5,7 +5,7 @@ * Supports two modes: fiat-fix and xmr-fix */ -const KRAKEN_API = 'https://api.kraken.com/0/public/Ticker'; +const KRAKEN_API = 'https://api.kraken.com/0/public/Ticker' const PAIRS = { USD: 'XXMRZUSD', @@ -13,7 +13,7 @@ const PAIRS = { GBP: 'XXMRZGBP', CHF: 'XMRCHF', JPY: 'XMRJPY' -}; +} const CURRENCY_SYMBOLS = { XMR: 'ɱ', @@ -22,12 +22,12 @@ const CURRENCY_SYMBOLS = { GBP: '£', CHF: 'CHF', JPY: '¥' -}; +} -const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes +const CACHE_DURATION = 5 * 60 * 1000 // 5 minutes -let cachedRates = null; -let cacheTimestamp = 0; +let cachedRates = null +let cacheTimestamp = 0 /** * Fetches current XMR rates from Kraken @@ -36,35 +36,35 @@ let cacheTimestamp = 0; export async function getXmrRates() { // Check cache if (cachedRates && Date.now() - cacheTimestamp < CACHE_DURATION) { - return cachedRates; + return cachedRates } try { - const pairs = Object.values(PAIRS).join(','); - const response = await fetch(`${KRAKEN_API}?pair=${pairs}`); - const data = await response.json(); + const pairs = Object.values(PAIRS).join(',') + const response = await fetch(`${KRAKEN_API}?pair=${pairs}`) + const data = await response.json() if (data.error && data.error.length > 0) { - console.error('Kraken API Error:', data.error); - return cachedRates || getDefaultRates(); + console.error('Kraken API Error:', data.error) + return cachedRates || getDefaultRates() } - const rates = {}; + const rates = {} for (const [currency, pair] of Object.entries(PAIRS)) { - const ticker = data.result[pair]; + const ticker = data.result[pair] if (ticker) { - rates[currency] = parseFloat(ticker.c[0]); // Last trade price + rates[currency] = parseFloat(ticker.c[0]) // Last trade price } } // Update cache - cachedRates = rates; - cacheTimestamp = Date.now(); + cachedRates = rates + cacheTimestamp = Date.now() - return rates; + return rates } catch (error) { - console.error('Failed to fetch XMR rates:', error); - return cachedRates || getDefaultRates(); + console.error('Failed to fetch XMR rates:', error) + return cachedRates || getDefaultRates() } } @@ -78,7 +78,7 @@ function getDefaultRates() { GBP: 130, CHF: 145, JPY: 24000 - }; + } } /** @@ -89,9 +89,9 @@ function getDefaultRates() { * @returns {number} Amount in XMR */ export function convertToXmr(amount, currency, rates) { - if (currency === 'XMR') return amount; - if (!rates[currency]) return amount; - return amount / rates[currency]; + if (currency === 'XMR') return amount + if (!rates[currency]) return amount + return amount / rates[currency] } /** @@ -102,9 +102,9 @@ export function convertToXmr(amount, currency, rates) { * @returns {number} Amount in target currency */ export function convertFromXmr(xmrAmount, currency, rates) { - if (currency === 'XMR') return xmrAmount; - if (!rates[currency]) return xmrAmount; - return xmrAmount * rates[currency]; + if (currency === 'XMR') return xmrAmount + if (!rates[currency]) return xmrAmount + return xmrAmount * rates[currency] } /** @@ -114,36 +114,36 @@ export function convertFromXmr(xmrAmount, currency, rates) { * @returns {Object} { primary, secondary, xmrAmount } */ export function formatPrice(listing, rates) { - const { price, currency, price_mode } = listing; + const { price, currency, price_mode } = listing if (!price || price === 0) { return { primary: 'Free', secondary: null, xmrAmount: 0 - }; + } } // XMR mode: XMR is the reference price if (price_mode === 'xmr' || currency === 'XMR') { - const xmrPrice = currency === 'XMR' ? price : convertToXmr(price, currency, rates); - const fiatEquivalent = currency !== 'XMR' ? price : convertFromXmr(price, 'EUR', rates); + const xmrPrice = currency === 'XMR' ? price : convertToXmr(price, currency, rates) + const fiatEquivalent = currency !== 'XMR' ? price : convertFromXmr(price, 'EUR', rates) return { primary: formatXmr(xmrPrice), secondary: currency !== 'XMR' ? `≈ ${formatFiat(price, currency)}` : `≈ ${formatFiat(fiatEquivalent, 'EUR')}`, xmrAmount: xmrPrice - }; + } } // Fiat mode: Fiat is the reference price - const xmrEquivalent = convertToXmr(price, currency, rates); + const xmrEquivalent = convertToXmr(price, currency, rates) return { primary: formatFiat(price, currency), secondary: `≈ ${formatXmr(xmrEquivalent)}`, xmrAmount: xmrEquivalent - }; + } } /** @@ -153,9 +153,9 @@ export function formatPrice(listing, rates) { */ export function formatXmr(amount) { if (amount >= 1) { - return `${amount.toFixed(4)} XMR`; + return `${amount.toFixed(4)} XMR` } - return `${amount.toFixed(6)} XMR`; + return `${amount.toFixed(6)} XMR` } /** @@ -165,18 +165,18 @@ export function formatXmr(amount) { * @returns {string} Formatted string (e.g. "€ 150,00") */ export function formatFiat(amount, currency) { - const symbol = CURRENCY_SYMBOLS[currency] || currency; + const symbol = CURRENCY_SYMBOLS[currency] || currency const formatted = new Intl.NumberFormat('de-DE', { minimumFractionDigits: 2, maximumFractionDigits: 2 - }).format(amount); + }).format(amount) // Symbol before or after amount if (['EUR', 'GBP', 'USD'].includes(currency)) { - return `${symbol} ${formatted}`; + return `${symbol} ${formatted}` } - return `${formatted} ${symbol}`; + return `${formatted} ${symbol}` } /** @@ -185,13 +185,13 @@ export function formatFiat(amount, currency) { * @returns {string} Symbol */ export function getCurrencySymbol(currency) { - return CURRENCY_SYMBOLS[currency] || currency; + return CURRENCY_SYMBOLS[currency] || currency } /** * List of supported currencies */ -export const SUPPORTED_CURRENCIES = ['XMR', 'EUR', 'CHF', 'USD', 'GBP', 'JPY']; +export const SUPPORTED_CURRENCIES = ['XMR', 'EUR', 'CHF', 'USD', 'GBP', 'JPY'] export default { getXmrRates, @@ -202,4 +202,4 @@ export default { formatFiat, getCurrencySymbol, SUPPORTED_CURRENCIES -}; +} diff --git a/js/services/directus.js b/js/services/directus.js index bea1317..3a576ca 100644 --- a/js/services/directus.js +++ b/js/services/directus.js @@ -1,247 +1,247 @@ /** - * Directus API Service für dgray.io - * Verbindet sich mit https://api.dgray.io/ + * Directus API Service for dgray.io + * Connects to https://api.dgray.io/ */ -const DIRECTUS_URL = 'https://api.dgray.io'; +const DIRECTUS_URL = 'https://api.dgray.io' class DirectusService { constructor() { - this.baseUrl = DIRECTUS_URL; - this.accessToken = null; - this.refreshToken = null; - this.tokenExpiry = null; - this.refreshTimeout = null; + this.baseUrl = DIRECTUS_URL + this.accessToken = null + this.refreshToken = null + this.tokenExpiry = null + this.refreshTimeout = null - this.loadTokens(); + this.loadTokens() } // ==================== Token Management ==================== loadTokens() { - const stored = localStorage.getItem('dgray_auth'); + const stored = localStorage.getItem('dgray_auth') if (stored) { try { - const { accessToken, refreshToken, expiry } = JSON.parse(stored); - this.accessToken = accessToken; - this.refreshToken = refreshToken; - this.tokenExpiry = expiry; - this.scheduleTokenRefresh(); + const { accessToken, refreshToken, expiry } = JSON.parse(stored) + this.accessToken = accessToken + this.refreshToken = refreshToken + this.tokenExpiry = expiry + this.scheduleTokenRefresh() } catch (e) { - this.clearTokens(); + this.clearTokens() } } } saveTokens(accessToken, refreshToken, expiresIn) { - this.accessToken = accessToken; - this.refreshToken = refreshToken; - this.tokenExpiry = Date.now() + (expiresIn * 1000); + this.accessToken = accessToken + this.refreshToken = refreshToken + this.tokenExpiry = Date.now() + (expiresIn * 1000) localStorage.setItem('dgray_auth', JSON.stringify({ accessToken: this.accessToken, refreshToken: this.refreshToken, expiry: this.tokenExpiry - })); + })) - this.scheduleTokenRefresh(); + this.scheduleTokenRefresh() } clearTokens() { - this.accessToken = null; - this.refreshToken = null; - this.tokenExpiry = null; - localStorage.removeItem('dgray_auth'); + this.accessToken = null + this.refreshToken = null + this.tokenExpiry = null + localStorage.removeItem('dgray_auth') if (this.refreshTimeout) { - clearTimeout(this.refreshTimeout); - this.refreshTimeout = null; + clearTimeout(this.refreshTimeout) + this.refreshTimeout = null } } scheduleTokenRefresh() { if (this.refreshTimeout) { - clearTimeout(this.refreshTimeout); + clearTimeout(this.refreshTimeout) } - if (!this.tokenExpiry || !this.refreshToken) return; + if (!this.tokenExpiry || !this.refreshToken) return - // Refresh 1 Minute vor Ablauf - const refreshIn = this.tokenExpiry - Date.now() - 60000; + // Refresh 1 minute before expiry + const refreshIn = this.tokenExpiry - Date.now() - 60000 if (refreshIn > 0) { - this.refreshTimeout = setTimeout(() => this.refreshSession(), refreshIn); + this.refreshTimeout = setTimeout(() => this.refreshSession(), refreshIn) } else if (this.refreshToken) { - this.refreshSession(); + this.refreshSession() } } isAuthenticated() { - return !!this.accessToken && (!this.tokenExpiry || Date.now() < this.tokenExpiry); + return !!this.accessToken && (!this.tokenExpiry || Date.now() < this.tokenExpiry) } // ==================== HTTP Methods ==================== async request(endpoint, options = {}) { - const url = `${this.baseUrl}${endpoint}`; + const url = `${this.baseUrl}${endpoint}` const headers = { 'Content-Type': 'application/json', ...options.headers - }; + } if (this.accessToken) { - headers['Authorization'] = `Bearer ${this.accessToken}`; + headers['Authorization'] = `Bearer ${this.accessToken}` } try { const response = await fetch(url, { ...options, headers - }); + }) - // Token abgelaufen - versuche Refresh (aber nicht für auth-Endpoints) + // Token expired - try refresh (but not for auth endpoints) if (response.status === 401 && this.refreshToken && !endpoint.startsWith('/auth/')) { - const refreshed = await this.refreshSession(); + const refreshed = await this.refreshSession() if (refreshed) { - headers['Authorization'] = `Bearer ${this.accessToken}`; - return this.request(endpoint, options); + headers['Authorization'] = `Bearer ${this.accessToken}` + return this.request(endpoint, options) } else { // Refresh failed - clear tokens to prevent loops - this.clearTokens(); + this.clearTokens() } } if (!response.ok) { - const error = await response.json().catch(() => ({})); - throw new DirectusError(response.status, error.errors?.[0]?.message || 'Request failed', error); + const error = await response.json().catch(() => ({})) + throw new DirectusError(response.status, error.errors?.[0]?.message || 'Request failed', error) } if (response.status === 204) { - return null; + return null } - return await response.json(); + return await response.json() } catch (error) { - if (error instanceof DirectusError) throw error; - throw new DirectusError(0, 'Network error', { originalError: error }); + if (error instanceof DirectusError) throw error + throw new DirectusError(0, 'Network error', { originalError: error }) } } async get(endpoint, params = {}) { - const queryString = this.buildQueryString(params); - const url = queryString ? `${endpoint}?${queryString}` : endpoint; - return this.request(url, { method: 'GET' }); + const queryString = this.buildQueryString(params) + const url = queryString ? `${endpoint}?${queryString}` : endpoint + return this.request(url, { method: 'GET' }) } async post(endpoint, data) { return this.request(endpoint, { method: 'POST', body: JSON.stringify(data) - }); + }) } async patch(endpoint, data) { return this.request(endpoint, { method: 'PATCH', body: JSON.stringify(data) - }); + }) } async delete(endpoint) { - return this.request(endpoint, { method: 'DELETE' }); + return this.request(endpoint, { method: 'DELETE' }) } buildQueryString(params) { - const searchParams = new URLSearchParams(); + const searchParams = new URLSearchParams() for (const [key, value] of Object.entries(params)) { - if (value === undefined || value === null) continue; + if (value === undefined || value === null) continue if (typeof value === 'object') { - searchParams.set(key, JSON.stringify(value)); + searchParams.set(key, JSON.stringify(value)) } else { - searchParams.set(key, value); + searchParams.set(key, value) } } - return searchParams.toString(); + return searchParams.toString() } // ==================== Authentication ==================== async login(email, password) { - const response = await this.post('/auth/login', { email, password }); + const response = await this.post('/auth/login', { email, password }) if (response.data) { this.saveTokens( response.data.access_token, response.data.refresh_token, response.data.expires - ); + ) } - return response.data; + return response.data } async logout() { if (this.refreshToken) { try { - await this.post('/auth/logout', { refresh_token: this.refreshToken }); + await this.post('/auth/logout', { refresh_token: this.refreshToken }) } catch (e) { - // Ignorieren - Token wird trotzdem gelöscht + // Ignore errors - tokens will be cleared anyway } } - this.clearTokens(); + this.clearTokens() } async refreshSession() { - if (!this.refreshToken) return false; + if (!this.refreshToken) return false try { const response = await this.post('/auth/refresh', { refresh_token: this.refreshToken, mode: 'json' - }); + }) if (response.data) { this.saveTokens( response.data.access_token, response.data.refresh_token, response.data.expires - ); - return true; + ) + return true } } catch (e) { - this.clearTokens(); + this.clearTokens() } - return false; + return false } async register(email, password) { // Public registration (no verification required) - return this.post('/users/register', { email, password }); + return this.post('/users/register', { email, password }) } async requestPasswordReset(email) { - return this.post('/auth/password/request', { email }); + return this.post('/auth/password/request', { email }) } async resetPassword(token, password) { - return this.post('/auth/password/reset', { token, password }); + return this.post('/auth/password/reset', { token, password }) } async getCurrentUser() { const response = await this.get('/users/me', { fields: ['id', 'email', 'first_name', 'last_name', 'avatar', 'role.name', 'status'] - }); - return response.data; + }) + return response.data } async updateCurrentUser(data) { - const response = await this.patch('/users/me', data); - return response.data; + const response = await this.patch('/users/me', data) + return response.data } // ==================== Listings (Anzeigen) ==================== @@ -250,99 +250,89 @@ class DirectusService { const params = { fields: options.fields || [ '*', - 'user_created.id', - 'user_created.first_name', + 'images.directus_files_id.id', + 'images.directus_files_id.title', 'category.id', 'category.name', - 'category.translations.*', - 'images.directus_files_id.*' + 'user_created.id', + 'user_created.first_name' ], filter: options.filter || { status: { _eq: 'published' } }, sort: options.sort || ['-date_created'], limit: options.limit || 20, - page: options.page || 1, - meta: 'total_count,filter_count' - }; - - if (options.search) { - params.search = options.search; + page: options.page || 1 } - const response = await this.get('/items/listings', params); + if (options.search) { + params.search = options.search + } + + const response = await this.get('/items/listings', params) return { items: response.data, meta: response.meta - }; + } } async getListing(id) { const response = await this.get(`/items/listings/${id}`, { fields: [ '*', + 'images.directus_files_id.*', + 'category.*', 'user_created.id', 'user_created.first_name', 'user_created.avatar', - 'category.*', - 'category.translations.*', - 'images.directus_files_id.*', - 'location.*' + 'user_created.date_created' ] - }); - return response.data; + }) + return response.data } async createListing(data) { - const response = await this.post('/items/listings', data); - return response.data; + const response = await this.post('/items/listings', data) + return response.data } async updateListing(id, data) { - const response = await this.patch(`/items/listings/${id}`, data); - return response.data; + const response = await this.patch(`/items/listings/${id}`, data) + return response.data } async deleteListing(id) { - return this.delete(`/items/listings/${id}`); + return this.delete(`/items/listings/${id}`) } - async getMyListings(options = {}) { - return this.getListings({ - ...options, - filter: { user_created: { _eq: '$CURRENT_USER' } } - }); + async getMyListings() { + const response = await this.get('/items/listings', { + fields: ['*', 'images.directus_files_id.id'], + filter: { user_created: { _eq: '$CURRENT_USER' } }, + sort: ['-date_created'] + }) + return response.data } async searchListings(query, options = {}) { return this.getListings({ - ...options, - search: query - }); - } - - async getListingsByCategory(categoryId, options = {}) { - return this.getListings({ - ...options, - filter: { - status: { _eq: 'published' }, - category: { _eq: categoryId } - } - }); + search: query, + ...options + }) } // ==================== Categories (Kategorien) ==================== async getCategories() { const response = await this.get('/items/categories', { - fields: ['*', 'translations.*', 'parent.*'], - filter: { status: { _eq: 'published' } }, - sort: ['sort', 'name'] - }); - return response.data; + fields: ['id', 'name', 'slug', 'icon', 'parent.id', 'parent.name'], + sort: ['sort', 'name'], + limit: -1 + }) + return response.data } async getCategoryTree() { - const categories = await this.getCategories(); - return this.buildCategoryTree(categories); + const categories = await this.getCategories() + return this.buildCategoryTree(categories) } buildCategoryTree(categories, parentId = null) { @@ -351,7 +341,7 @@ class DirectusService { .map(cat => ({ ...cat, children: this.buildCategoryTree(categories, cat.id) - })); + })) } // ==================== Messages (Nachrichten) ==================== @@ -376,8 +366,8 @@ class DirectusService { ] }, sort: ['-date_updated'] - }); - return response.data; + }) + return response.data } async getConversation(id) { @@ -391,16 +381,16 @@ class DirectusService { 'messages.*', 'messages.sender.*' ] - }); - return response.data; + }) + return response.data } async sendMessage(conversationId, content) { const response = await this.post('/items/messages', { conversation: conversationId, content - }); - return response.data; + }) + return response.data } async startConversation(listingId, message) { @@ -409,8 +399,8 @@ class DirectusService { messages: { create: [{ content: message }] } - }); - return response.data; + }) + return response.data } // ==================== Favorites (Favoriten) ==================== @@ -419,19 +409,19 @@ class DirectusService { const response = await this.get('/items/favorites', { fields: ['*', 'listing.*', 'listing.images.directus_files_id.*'], filter: { user: { _eq: '$CURRENT_USER' } } - }); - return response.data; + }) + return response.data } async addFavorite(listingId) { const response = await this.post('/items/favorites', { listing: listingId - }); - return response.data; + }) + return response.data } async removeFavorite(favoriteId) { - return this.delete(`/items/favorites/${favoriteId}`); + return this.delete(`/items/favorites/${favoriteId}`) } async isFavorite(listingId) { @@ -441,8 +431,8 @@ class DirectusService { listing: { _eq: listingId } }, limit: 1 - }); - return response.data.length > 0 ? response.data[0] : null; + }) + return response.data.length > 0 ? response.data[0] : null } // ==================== Reports (Meldungen) ==================== @@ -452,8 +442,8 @@ class DirectusService { listing: listingId, reason, details - }); - return response.data; + }) + return response.data } async reportUser(userId, reason, details = '') { @@ -461,57 +451,57 @@ class DirectusService { reported_user: userId, reason, details - }); - return response.data; + }) + return response.data } // ==================== Files (Dateien/Bilder) ==================== async uploadFile(file, options = {}) { - const formData = new FormData(); - formData.append('file', file); + const formData = new FormData() + formData.append('file', file) if (options.folder) { - formData.append('folder', options.folder); + formData.append('folder', options.folder) } const response = await fetch(`${this.baseUrl}/files`, { method: 'POST', headers: this.accessToken ? { 'Authorization': `Bearer ${this.accessToken}` } : {}, body: formData - }); + }) if (!response.ok) { - const error = await response.json().catch(() => ({})); - throw new DirectusError(response.status, 'Upload failed', error); + const error = await response.json().catch(() => ({})) + throw new DirectusError(response.status, 'Upload failed', error) } - const result = await response.json(); - return result.data; + const result = await response.json() + return result.data } async uploadMultipleFiles(files, options = {}) { - const uploads = Array.from(files).map(file => this.uploadFile(file, options)); - return Promise.all(uploads); + const uploads = Array.from(files).map(file => this.uploadFile(file, options)) + return Promise.all(uploads) } getFileUrl(fileId, options = {}) { - if (!fileId) return null; + if (!fileId) return null - const params = new URLSearchParams(); + const params = new URLSearchParams() - if (options.width) params.set('width', options.width); - if (options.height) params.set('height', options.height); - if (options.fit) params.set('fit', options.fit); - if (options.quality) params.set('quality', options.quality); - if (options.format) params.set('format', options.format); + if (options.width) params.set('width', options.width) + if (options.height) params.set('height', options.height) + if (options.fit) params.set('fit', options.fit) + if (options.quality) params.set('quality', options.quality) + if (options.format) params.set('format', options.format) - const queryString = params.toString(); - return `${this.baseUrl}/assets/${fileId}${queryString ? '?' + queryString : ''}`; + const queryString = params.toString() + return `${this.baseUrl}/assets/${fileId}${queryString ? '?' + queryString : ''}` } getThumbnailUrl(fileId, size = 300) { - return this.getFileUrl(fileId, { width: size, height: size, fit: 'cover' }); + return this.getFileUrl(fileId, { width: size, height: size, fit: 'cover' }) } // ==================== Search ==================== @@ -523,12 +513,12 @@ class DirectusService { search: query, limit: options.categoryLimit || 5 }) - ]); + ]) return { listings: listings.items, categories: categories.data - }; + } } // ==================== Stats / Dashboard ==================== @@ -552,37 +542,37 @@ class DirectusService { }, aggregate: { count: '*' } }) - ]); + ]) return { listingsCount: listings.data?.[0]?.count || 0, favoritesCount: favorites.data?.[0]?.count || 0, conversationsCount: conversations.data?.[0]?.count || 0 - }; + } } } class DirectusError extends Error { constructor(status, message, data = {}) { - super(message); - this.name = 'DirectusError'; - this.status = status; - this.data = data; + super(message) + this.name = 'DirectusError' + this.status = status + this.data = data } isAuthError() { - return this.status === 401 || this.status === 403; + return this.status === 401 || this.status === 403 } isNotFound() { - return this.status === 404; + return this.status === 404 } isValidationError() { - return this.status === 400; + return this.status === 400 } } // Singleton Export -export const directus = new DirectusService(); -export { DirectusError }; +export const directus = new DirectusService() +export { DirectusError }