refactor: event delegation, unified subscription cleanup, centralized listing status helpers

This commit is contained in:
2026-02-08 10:50:11 +01:00
parent 088db52258
commit 45e7f9dde7
14 changed files with 101 additions and 72 deletions

View File

@@ -2,12 +2,14 @@ import { getCurrentLanguage, i18n } from '../../i18n.js'
class PageAbout extends HTMLElement { class PageAbout extends HTMLElement {
connectedCallback() { connectedCallback() {
this._unsubs = []
this.render() this.render()
this.unsubscribe = i18n.subscribe(() => this.render()) this._unsubs.push(i18n.subscribe(() => this.render()))
} }
disconnectedCallback() { disconnectedCallback() {
if (this.unsubscribe) this.unsubscribe() this._unsubs.forEach(fn => fn())
this._unsubs = []
} }
getContent(lang) { getContent(lang) {

View File

@@ -2,12 +2,14 @@ import { getCurrentLanguage, i18n } from '../../i18n.js'
class PageContact extends HTMLElement { class PageContact extends HTMLElement {
connectedCallback() { connectedCallback() {
this._unsubs = []
this.render() this.render()
this.unsubscribe = i18n.subscribe(() => this.render()) this._unsubs.push(i18n.subscribe(() => this.render()))
} }
disconnectedCallback() { disconnectedCallback() {
if (this.unsubscribe) this.unsubscribe() this._unsubs.forEach(fn => fn())
this._unsubs = []
} }
getContent(lang) { getContent(lang) {

View File

@@ -72,6 +72,8 @@ class PageCreate extends HTMLElement {
} }
async connectedCallback() { async connectedCallback() {
this._unsubs = []
// Check if logged in // Check if logged in
if (!auth.isLoggedIn()) { if (!auth.isLoggedIn()) {
this.showLoginRequired() this.showLoginRequired()
@@ -90,7 +92,7 @@ class PageCreate extends HTMLElement {
await this.loadCategories() await this.loadCategories()
await this.checkAccountStatus() await this.checkAccountStatus()
this.render() this.render()
this.unsubscribe = i18n.subscribe(() => this.render()) this._unsubs.push(i18n.subscribe(() => this.render()))
} }
async loadExistingListing() { async loadExistingListing() {
@@ -165,7 +167,7 @@ class PageCreate extends HTMLElement {
this.hasDraft = !!localStorage.getItem(STORAGE_KEY) this.hasDraft = !!localStorage.getItem(STORAGE_KEY)
await this.loadCategories() await this.loadCategories()
this.render() this.render()
this.unsubscribe = i18n.subscribe(() => this.render()) this._unsubs.push(i18n.subscribe(() => this.render()))
}, { once: true }) }, { once: true })
authModal.addEventListener('close', () => { authModal.addEventListener('close', () => {
// If closed without login, go back // If closed without login, go back
@@ -206,7 +208,8 @@ class PageCreate extends HTMLElement {
} }
disconnectedCallback() { disconnectedCallback() {
if (this.unsubscribe) this.unsubscribe() this._unsubs.forEach(fn => fn())
this._unsubs = []
} }
render() { render() {

View File

@@ -15,15 +15,16 @@ class PageFavorites extends HTMLElement {
} }
connectedCallback() { connectedCallback() {
this._unsubs = []
this.render() this.render()
this.loadFavorites() this.loadFavorites()
this.unsubscribe = i18n.subscribe(() => this.render()) this._unsubs.push(i18n.subscribe(() => this.render()))
this.favUnsubscribe = favoritesService.subscribe(() => this.loadFavorites()) this._unsubs.push(favoritesService.subscribe(() => this.loadFavorites()))
} }
disconnectedCallback() { disconnectedCallback() {
if (this.unsubscribe) this.unsubscribe() this._unsubs.forEach(fn => fn())
if (this.favUnsubscribe) this.favUnsubscribe() this._unsubs = []
} }
async loadFavorites() { async loadFavorites() {

View File

@@ -40,25 +40,26 @@ class PageHome extends HTMLElement {
this.setupEventListeners() this.setupEventListeners()
this.setupPullToRefresh() this.setupPullToRefresh()
this.loadListings() this.loadListings()
this.unsubscribe = i18n.subscribe(() => { this._unsubs = []
this._unsubs.push(i18n.subscribe(() => {
this.updateTextContent() this.updateTextContent()
}) }))
// Re-render listings on auth change to show owner badges // Re-render listings on auth change to show owner badges
this.authUnsubscribe = auth.subscribe(() => { this._unsubs.push(auth.subscribe(() => {
const container = this.querySelector('#listings-container') const container = this.querySelector('#listings-container')
if (container) { if (container) {
container.innerHTML = this.renderListings() container.innerHTML = this.renderListings()
} }
}) }))
// Listen for URL changes (back/forward navigation) // Listen for URL changes (back/forward navigation)
window.addEventListener('hashchange', this._onHashChange) window.addEventListener('hashchange', this._onHashChange)
} }
disconnectedCallback() { disconnectedCallback() {
if (this.unsubscribe) this.unsubscribe() this._unsubs.forEach(fn => fn())
if (this.authUnsubscribe) this.authUnsubscribe() this._unsubs = []
window.removeEventListener('hashchange', this._onHashChange) window.removeEventListener('hashchange', this._onHashChange)
} }

View File

@@ -23,14 +23,16 @@ class PageListing extends HTMLElement {
connectedCallback() { connectedCallback() {
this.listingId = this.dataset.id this.listingId = this.dataset.id
this._unsubs = []
this.render() this.render()
this.loadListing() this.loadListing()
this.unsubscribe = i18n.subscribe(() => this.render()) this._unsubs.push(i18n.subscribe(() => this.render()))
window.addEventListener('currency-changed', this.handleCurrencyChange) window.addEventListener('currency-changed', this.handleCurrencyChange)
} }
disconnectedCallback() { disconnectedCallback() {
if (this.unsubscribe) this.unsubscribe() this._unsubs.forEach(fn => fn())
this._unsubs = []
window.removeEventListener('currency-changed', this.handleCurrencyChange) window.removeEventListener('currency-changed', this.handleCurrencyChange)
this.resetMetaTags() this.resetMetaTags()
} }

View File

@@ -23,19 +23,20 @@ class PageMessages extends HTMLElement {
this.render() this.render()
this.loadConversations() this.loadConversations()
this.unsubscribe = i18n.subscribe(() => this.render()) this._unsubs = []
this.authUnsubscribe = auth.subscribe(() => { this._unsubs.push(i18n.subscribe(() => this.render()))
this._unsubs.push(auth.subscribe(() => {
this.isLoggedIn = auth.isLoggedIn() this.isLoggedIn = auth.isLoggedIn()
if (!this.isLoggedIn) { if (!this.isLoggedIn) {
window.location.hash = '#/' window.location.hash = '#/'
} }
}) }))
} }
disconnectedCallback() { disconnectedCallback() {
if (this.unsubscribe) this.unsubscribe() this._unsubs.forEach(fn => fn())
if (this.authUnsubscribe) this.authUnsubscribe() this._unsubs = []
} }
async loadConversations() { async loadConversations() {

View File

@@ -13,6 +13,7 @@ class PageMyListings extends HTMLElement {
this.loading = true this.loading = true
this.error = null this.error = null
this.isLoggedIn = false this.isLoggedIn = false
this._handleClick = this.handleDelegatedClick.bind(this)
} }
connectedCallback() { connectedCallback() {
@@ -25,23 +26,42 @@ class PageMyListings extends HTMLElement {
this.render() this.render()
this.loadMyListings() this.loadMyListings()
this.addEventListener('click', this._handleClick)
this.unsubscribe = i18n.subscribe(() => this.render()) this._unsubs = []
this.authUnsubscribe = auth.subscribe(() => { this._unsubs.push(i18n.subscribe(() => this.render()))
this._unsubs.push(auth.subscribe(() => {
this.isLoggedIn = auth.isLoggedIn() this.isLoggedIn = auth.isLoggedIn()
if (!this.isLoggedIn) { if (!this.isLoggedIn) {
window.location.hash = '#/' window.location.hash = '#/'
} }
}) }))
} }
disconnectedCallback() { disconnectedCallback() {
if (this.unsubscribe) this.unsubscribe() this._unsubs.forEach(fn => fn())
if (this.authUnsubscribe) this.authUnsubscribe() this._unsubs = []
this.removeEventListener('click', this._handleClick)
this.stopPolling() this.stopPolling()
} }
handleDelegatedClick(e) {
const toggleBtn = e.target.closest('.btn-toggle-status')
if (toggleBtn) {
e.preventDefault()
e.stopPropagation()
this.toggleListingStatus(toggleBtn.dataset.id, toggleBtn.dataset.status)
return
}
const deleteBtn = e.target.closest('.btn-delete-listing')
if (deleteBtn) {
e.preventDefault()
e.stopPropagation()
this.deleteListing(deleteBtn.dataset.id)
}
}
startPolling() { startPolling() {
this.stopPolling() this.stopPolling()
const hasPending = this.listings.some(l => const hasPending = this.listings.some(l =>
@@ -221,11 +241,9 @@ class PageMyListings extends HTMLElement {
const locationName = listing.location?.name || '' const locationName = listing.location?.name || ''
const statusBadge = this.getStatusBadge(listing) const statusBadge = this.getStatusBadge(listing)
const paidActive = listingsService.isPaidAndActive(listing)
const isPublished = listing.status === 'published' const isPublished = listing.status === 'published'
const isDraftPaid = listing.status === 'draft' && paidActive
let toggleBtn = '' let toggleBtn = ''
if (isPublished || isDraftPaid) { if (listingsService.canTogglePublish(listing)) {
const label = isPublished ? t('myListings.unpublish') : t('myListings.republish') const label = isPublished ? t('myListings.unpublish') : t('myListings.republish')
toggleBtn = /* html */` toggleBtn = /* html */`
<button class="btn-toggle-status" data-id="${listing.id}" data-status="${isPublished ? 'draft' : 'published'}"> <button class="btn-toggle-status" data-id="${listing.id}" data-status="${isPublished ? 'draft' : 'published'}">
@@ -235,7 +253,7 @@ class PageMyListings extends HTMLElement {
} }
let deleteBtn = '' let deleteBtn = ''
if (listing.status === 'deleted') { if (listingsService.isSoftDeleted(listing)) {
deleteBtn = /* html */` deleteBtn = /* html */`
<p class="deleted-hint">${t('myListings.deletedHint')}</p> <p class="deleted-hint">${t('myListings.deletedHint')}</p>
` `
@@ -248,7 +266,7 @@ class PageMyListings extends HTMLElement {
} }
return /* html */` return /* html */`
<div class="listing-wrapper${listing.status === 'deleted' ? ' is-deleted' : ''}"> <div class="listing-wrapper${listingsService.isSoftDeleted(listing) ? ' is-deleted' : ''}">
${statusBadge} ${statusBadge}
<listing-card <listing-card
listing-id="${listing.id}" listing-id="${listing.id}"
@@ -267,30 +285,9 @@ class PageMyListings extends HTMLElement {
` `
}).join('') }).join('')
setTimeout(() => this.setupToggleListeners(), 0)
return html return html
} }
setupToggleListeners() {
this.querySelectorAll('.btn-toggle-status').forEach(btn => {
btn.addEventListener('click', (e) => {
e.preventDefault()
e.stopPropagation()
const id = btn.dataset.id
const newStatus = btn.dataset.status
this.toggleListingStatus(id, newStatus)
})
})
this.querySelectorAll('.btn-delete-listing').forEach(btn => {
btn.addEventListener('click', (e) => {
e.preventDefault()
e.stopPropagation()
const id = btn.dataset.id
this.deleteListing(id)
})
})
}
async toggleListingStatus(id, newStatus) { async toggleListingStatus(id, newStatus) {
try { try {
await directus.updateListing(id, { status: newStatus }) await directus.updateListing(id, { status: newStatus })

View File

@@ -2,12 +2,14 @@ import { t, i18n } from '../../i18n.js'
class PageNotFound extends HTMLElement { class PageNotFound extends HTMLElement {
connectedCallback() { connectedCallback() {
this._unsubs = []
this.render() this.render()
this.unsubscribe = i18n.subscribe(() => this.render()) this._unsubs.push(i18n.subscribe(() => this.render()))
} }
disconnectedCallback() { disconnectedCallback() {
if (this.unsubscribe) this.unsubscribe() this._unsubs.forEach(fn => fn())
this._unsubs = []
} }
render() { render() {

View File

@@ -10,18 +10,19 @@ class PageNotifications extends HTMLElement {
} }
connectedCallback() { connectedCallback() {
this._unsubs = []
this.render() this.render()
this.loadNotifications() this.loadNotifications()
this.unsubscribe = i18n.subscribe(() => this.render()) this._unsubs.push(i18n.subscribe(() => this.render()))
this.notifUnsubscribe = notificationsService.subscribe(() => { this._unsubs.push(notificationsService.subscribe(() => {
this.loading = false this.loading = false
this.updateContent() this.updateContent()
}) }))
} }
disconnectedCallback() { disconnectedCallback() {
if (this.unsubscribe) this.unsubscribe() this._unsubs.forEach(fn => fn())
if (this.notifUnsubscribe) this.notifUnsubscribe() this._unsubs = []
} }
async loadNotifications() { async loadNotifications() {

View File

@@ -2,12 +2,14 @@ import { getCurrentLanguage, i18n } from '../../i18n.js'
class PagePrivacy extends HTMLElement { class PagePrivacy extends HTMLElement {
connectedCallback() { connectedCallback() {
this._unsubs = []
this.render() this.render()
this.unsubscribe = i18n.subscribe(() => this.render()) this._unsubs.push(i18n.subscribe(() => this.render()))
} }
disconnectedCallback() { disconnectedCallback() {
if (this.unsubscribe) this.unsubscribe() this._unsubs.forEach(fn => fn())
this._unsubs = []
} }
getContent(lang) { getContent(lang) {

View File

@@ -23,23 +23,24 @@ class PageSettings extends HTMLElement {
this.render() this.render()
this.setupEventListeners() this.setupEventListeners()
this.unsubscribe = i18n.subscribe(() => { this._unsubs = []
this._unsubs.push(i18n.subscribe(() => {
this.render() this.render()
this.setupEventListeners() this.setupEventListeners()
}) }))
this.authUnsubscribe = auth.subscribe(() => { this._unsubs.push(auth.subscribe(() => {
this.isLoggedIn = auth.isLoggedIn() this.isLoggedIn = auth.isLoggedIn()
if (!this.isLoggedIn) { if (!this.isLoggedIn) {
window.location.hash = '#/' window.location.hash = '#/'
} }
}) }))
} }
disconnectedCallback() { disconnectedCallback() {
if (this.unsubscribe) this.unsubscribe() this._unsubs.forEach(fn => fn())
if (this.authUnsubscribe) this.authUnsubscribe() this._unsubs = []
} }
setupEventListeners() { setupEventListeners() {

View File

@@ -2,12 +2,14 @@ import { getCurrentLanguage, i18n } from '../../i18n.js'
class PageTerms extends HTMLElement { class PageTerms extends HTMLElement {
connectedCallback() { connectedCallback() {
this._unsubs = []
this.render() this.render()
this.unsubscribe = i18n.subscribe(() => this.render()) this._unsubs.push(i18n.subscribe(() => this.render()))
} }
disconnectedCallback() { disconnectedCallback() {
if (this.unsubscribe) this.unsubscribe() this._unsubs.forEach(fn => fn())
this._unsubs = []
} }
render() { render() {

View File

@@ -75,6 +75,18 @@ class ListingsService {
&& listing.expires_at && listing.expires_at
&& new Date(listing.expires_at) > new Date() && new Date(listing.expires_at) > new Date()
} }
canTogglePublish(listing) {
return listing.status === 'published' || (listing.status === 'draft' && this.isPaidAndActive(listing))
}
isSoftDeleted(listing) {
return listing.status === 'deleted'
}
canPublicView(listing) {
return listing.status === 'published'
}
} }
export const listingsService = new ListingsService() export const listingsService = new ListingsService()