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 */`
${t('auth.login')}
@@ -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 */`
- `;
+ `
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