439 lines
14 KiB
JavaScript
439 lines
14 KiB
JavaScript
import { t, i18n } from '../../i18n.js'
|
|
import { directus } from '../../services/directus.js'
|
|
import { auth } from '../../services/auth.js'
|
|
import { listingsService } from '../../services/listings.js'
|
|
import { escapeHTML } from '../../utils/helpers.js'
|
|
import '../listing-card.js'
|
|
import '../skeleton-card.js'
|
|
|
|
class PageMyListings extends HTMLElement {
|
|
constructor() {
|
|
super()
|
|
this.listings = []
|
|
this.loading = true
|
|
this.error = null
|
|
this.isLoggedIn = false
|
|
this._handleClick = this.handleDelegatedClick.bind(this)
|
|
}
|
|
|
|
connectedCallback() {
|
|
this.isLoggedIn = auth.isLoggedIn()
|
|
|
|
if (!this.isLoggedIn) {
|
|
window.location.hash = '#/'
|
|
return
|
|
}
|
|
|
|
this.render()
|
|
this.loadMyListings()
|
|
this.addEventListener('click', this._handleClick)
|
|
|
|
this._unsubs = []
|
|
this._unsubs.push(i18n.subscribe(() => this.render()))
|
|
this._unsubs.push(auth.subscribe(() => {
|
|
this.isLoggedIn = auth.isLoggedIn()
|
|
|
|
if (!this.isLoggedIn) {
|
|
window.location.hash = '#/'
|
|
}
|
|
}))
|
|
}
|
|
|
|
disconnectedCallback() {
|
|
this._unsubs.forEach(fn => fn())
|
|
this._unsubs = []
|
|
this.removeEventListener('click', this._handleClick)
|
|
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() {
|
|
this.stopPolling()
|
|
const hasPending = this.listings.some(l =>
|
|
l.payment_status === 'processing' || l.payment_status === 'pending'
|
|
)
|
|
if (hasPending) {
|
|
this.pollInterval = setInterval(() => this.pollListings(), 15000)
|
|
}
|
|
}
|
|
|
|
stopPolling() {
|
|
if (this.pollInterval) {
|
|
clearInterval(this.pollInterval)
|
|
this.pollInterval = null
|
|
}
|
|
}
|
|
|
|
async pollListings() {
|
|
try {
|
|
const user = await auth.getUser()
|
|
if (!user) return
|
|
|
|
const pendingIds = this.listings
|
|
.filter(l => l.payment_status === 'processing' || l.payment_status === 'pending')
|
|
.map(l => l.id)
|
|
|
|
if (pendingIds.length === 0) {
|
|
this.stopPolling()
|
|
return
|
|
}
|
|
|
|
const response = await directus.getListings({
|
|
fields: ['id', 'status', 'payment_status'],
|
|
filter: {
|
|
id: { _in: pendingIds }
|
|
},
|
|
limit: pendingIds.length
|
|
})
|
|
|
|
const updated = response.items || []
|
|
let changed = false
|
|
for (const item of updated) {
|
|
const existing = this.listings.find(l => l.id === item.id)
|
|
if (existing && (existing.payment_status !== item.payment_status || existing.status !== item.status)) {
|
|
existing.payment_status = item.payment_status
|
|
existing.status = item.status
|
|
changed = true
|
|
}
|
|
}
|
|
|
|
if (changed) {
|
|
this.updateContent()
|
|
}
|
|
this.startPolling()
|
|
} catch (err) {
|
|
console.error('Polling failed:', err)
|
|
}
|
|
}
|
|
|
|
async loadMyListings() {
|
|
try {
|
|
const user = await auth.getUser()
|
|
if (!user) {
|
|
this.loading = false
|
|
this.updateContent()
|
|
return
|
|
}
|
|
|
|
const response = await directus.getListings({
|
|
fields: [
|
|
'id', 'status', 'title', 'slug', 'price', 'currency',
|
|
'condition', 'payment_status', 'paid_at', 'expires_at', 'date_created', 'date_updated', 'user_created',
|
|
'images.directus_files_id.id',
|
|
'location.id', 'location.name'
|
|
],
|
|
filter: {
|
|
user_created: { _eq: user.id }
|
|
},
|
|
sort: ['-date_created'],
|
|
limit: 50
|
|
})
|
|
const thirtyDaysAgo = Date.now() - 30 * 24 * 60 * 60 * 1000
|
|
this.listings = (response.items || []).filter(l => {
|
|
if (l.status !== 'deleted') return true
|
|
const updated = new Date(l.date_updated || l.date_created).getTime()
|
|
return updated > thirtyDaysAgo
|
|
})
|
|
this.loading = false
|
|
this.updateContent()
|
|
this.startPolling()
|
|
} catch (err) {
|
|
console.error('Failed to load my listings:', err)
|
|
this.error = err.message
|
|
this.loading = false
|
|
this.updateContent()
|
|
}
|
|
}
|
|
|
|
showAuthModal() {
|
|
document.querySelector('auth-modal')?.show()
|
|
}
|
|
|
|
getStatusBadge(listing) {
|
|
if (listing.status === 'archived') {
|
|
return `<span class="status-badge status-archived">${t('myListings.status.archived')}</span>`
|
|
}
|
|
if (listing.status === 'draft' && listing.payment_status !== 'processing' && listing.payment_status !== 'pending' && !listingsService.isPaidAndActive(listing)) {
|
|
return `<span class="status-badge status-draft">${t('myListings.status.draft')}</span>`
|
|
}
|
|
return ''
|
|
}
|
|
|
|
render() {
|
|
this.innerHTML = /* html */`
|
|
<div class="my-listings-page">
|
|
<header class="page-header">
|
|
<h1>${t('myListings.title')}</h1>
|
|
<p class="page-subtitle">${t('myListings.subtitle')}</p>
|
|
</header>
|
|
|
|
<div id="my-listings-content" class="listings-grid">
|
|
${this.renderContent()}
|
|
</div>
|
|
</div>
|
|
`
|
|
|
|
this.querySelector('#login-btn')?.addEventListener('click', () => this.showAuthModal())
|
|
}
|
|
|
|
updateContent() {
|
|
const container = this.querySelector('#my-listings-content')
|
|
if (container) {
|
|
container.innerHTML = this.renderContent()
|
|
this.querySelector('#login-btn')?.addEventListener('click', () => this.showAuthModal())
|
|
}
|
|
}
|
|
|
|
renderContent() {
|
|
if (!this.isLoggedIn) {
|
|
return /* html */`
|
|
<div class="empty-state">
|
|
<div class="empty-icon">🔐</div>
|
|
<h3>${t('myListings.loginRequired')}</h3>
|
|
<p>${t('myListings.loginHint')}</p>
|
|
<button id="login-btn" class="btn btn-primary">${t('myListings.login')}</button>
|
|
</div>
|
|
`
|
|
}
|
|
|
|
if (this.loading) {
|
|
return Array(4).fill(0).map(() => '<skeleton-card></skeleton-card>').join('')
|
|
}
|
|
|
|
if (this.error) {
|
|
return /* html */`
|
|
<div class="empty-state">
|
|
<div class="empty-icon">⚠️</div>
|
|
<p>${t('common.error')}</p>
|
|
</div>
|
|
`
|
|
}
|
|
|
|
if (this.listings.length === 0) {
|
|
return /* html */`
|
|
<div class="empty-state">
|
|
<div class="empty-icon">📦</div>
|
|
<h3>${t('myListings.empty')}</h3>
|
|
<p>${t('myListings.emptyHint')}</p>
|
|
<a href="#/create" class="btn btn-primary">${t('myListings.create')}</a>
|
|
</div>
|
|
`
|
|
}
|
|
|
|
const html = this.listings.map(listing => {
|
|
const imageId = listing.images?.[0]?.directus_files_id?.id || listing.images?.[0]?.directus_files_id
|
|
const imageUrl = imageId ? directus.getThumbnailUrl(imageId, 300) : ''
|
|
const locationName = listing.location?.name || ''
|
|
const statusBadge = this.getStatusBadge(listing)
|
|
|
|
const isPublished = listing.status === 'published'
|
|
let toggleBtn = ''
|
|
if (listingsService.canTogglePublish(listing)) {
|
|
const label = isPublished ? t('myListings.unpublish') : t('myListings.republish')
|
|
toggleBtn = /* html */`
|
|
<button class="btn-toggle-status" data-id="${listing.id}" data-status="${isPublished ? 'draft' : 'published'}">
|
|
${label}
|
|
</button>
|
|
`
|
|
}
|
|
|
|
let deleteBtn = ''
|
|
if (listingsService.isSoftDeleted(listing)) {
|
|
deleteBtn = /* html */`
|
|
<p class="deleted-hint">${t('myListings.deletedHint')}</p>
|
|
`
|
|
} else if (listing.status !== 'archived') {
|
|
deleteBtn = /* html */`
|
|
<button class="btn-delete-listing" data-id="${listing.id}">
|
|
${t('myListings.delete')}
|
|
</button>
|
|
`
|
|
}
|
|
|
|
return /* html */`
|
|
<div class="listing-wrapper${listingsService.isSoftDeleted(listing) ? ' is-deleted' : ''}">
|
|
${statusBadge}
|
|
<listing-card
|
|
listing-id="${listing.id}"
|
|
title="${escapeHTML(listing.title || '')}"
|
|
price="${listing.price || ''}"
|
|
currency="${listing.currency || 'EUR'}"
|
|
location="${escapeHTML(locationName)}"
|
|
image="${imageUrl}"
|
|
owner-id="${listing.user_created || ''}"
|
|
payment-status="${listing.payment_status || ''}"
|
|
status="${listing.status || ''}"
|
|
></listing-card>
|
|
${toggleBtn}
|
|
${deleteBtn}
|
|
</div>
|
|
`
|
|
}).join('')
|
|
|
|
return html
|
|
}
|
|
|
|
async toggleListingStatus(id, newStatus) {
|
|
try {
|
|
await directus.updateListing(id, { status: newStatus })
|
|
const listing = this.listings.find(l => l.id === id)
|
|
if (listing) {
|
|
listing.status = newStatus
|
|
this.updateContent()
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to toggle listing status:', err)
|
|
}
|
|
}
|
|
|
|
async deleteListing(id) {
|
|
if (!confirm(t('myListings.deleteConfirm'))) return
|
|
try {
|
|
await directus.updateListing(id, { status: 'deleted' })
|
|
const listing = this.listings.find(l => l.id === id)
|
|
if (listing) {
|
|
listing.status = 'deleted'
|
|
this.updateContent()
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to delete listing:', err)
|
|
}
|
|
}
|
|
}
|
|
|
|
customElements.define('page-my-listings', PageMyListings)
|
|
|
|
const style = document.createElement('style')
|
|
style.textContent = /* css */`
|
|
page-my-listings .my-listings-page {
|
|
padding: var(--space-lg) 0;
|
|
}
|
|
|
|
page-my-listings .page-header {
|
|
margin-bottom: var(--space-xl);
|
|
}
|
|
|
|
page-my-listings .page-header h1 {
|
|
margin: 0 0 var(--space-xs);
|
|
}
|
|
|
|
page-my-listings .page-subtitle {
|
|
color: var(--color-text-muted);
|
|
margin: 0;
|
|
}
|
|
|
|
page-my-listings .listing-wrapper {
|
|
position: relative;
|
|
min-width: 0;
|
|
}
|
|
|
|
page-my-listings .listing-wrapper.is-deleted {
|
|
opacity: 0.5;
|
|
filter: grayscale(1);
|
|
pointer-events: none;
|
|
}
|
|
|
|
page-my-listings .deleted-hint {
|
|
margin: var(--space-xs) 0 0;
|
|
font-size: var(--font-size-xs);
|
|
color: var(--color-text-muted);
|
|
text-align: center;
|
|
}
|
|
|
|
page-my-listings .status-badge {
|
|
position: absolute;
|
|
top: var(--space-sm);
|
|
left: var(--space-sm);
|
|
padding: var(--space-xs) var(--space-sm);
|
|
font-size: var(--font-size-xs);
|
|
font-weight: var(--font-weight-medium);
|
|
border-radius: var(--radius-sm);
|
|
z-index: 2;
|
|
}
|
|
|
|
page-my-listings .status-draft {
|
|
background: var(--color-bg-tertiary);
|
|
color: var(--color-text-muted);
|
|
}
|
|
|
|
page-my-listings .status-archived {
|
|
background: var(--color-bg-tertiary);
|
|
color: var(--color-text-muted);
|
|
text-decoration: line-through;
|
|
}
|
|
|
|
page-my-listings .btn-delete-listing {
|
|
display: block;
|
|
width: 100%;
|
|
padding: var(--space-xs) var(--space-sm);
|
|
margin-top: var(--space-xs);
|
|
background: none;
|
|
border: 1px solid var(--color-border);
|
|
border-radius: var(--radius-sm);
|
|
color: var(--color-text-muted);
|
|
font-size: var(--font-size-sm);
|
|
cursor: pointer;
|
|
transition: all var(--transition-fast);
|
|
}
|
|
|
|
page-my-listings .btn-delete-listing:hover {
|
|
border-color: var(--color-error, #b43c3c);
|
|
color: var(--color-error, #b43c3c);
|
|
}
|
|
|
|
page-my-listings .btn-toggle-status {
|
|
display: block;
|
|
width: 100%;
|
|
padding: var(--space-xs) var(--space-sm);
|
|
margin-top: var(--space-xs);
|
|
background: var(--color-bg-secondary);
|
|
border: 1px solid var(--color-border);
|
|
border-radius: var(--radius-sm);
|
|
color: var(--color-text-secondary);
|
|
font-size: var(--font-size-sm);
|
|
cursor: pointer;
|
|
transition: background-color var(--transition-fast);
|
|
}
|
|
|
|
page-my-listings .btn-toggle-status:hover {
|
|
background: var(--color-bg-tertiary);
|
|
}
|
|
|
|
page-my-listings .empty-state {
|
|
grid-column: 1 / -1;
|
|
text-align: center;
|
|
padding: var(--space-3xl);
|
|
}
|
|
|
|
page-my-listings .empty-icon {
|
|
font-size: 4rem;
|
|
margin-bottom: var(--space-md);
|
|
filter: grayscale(1);
|
|
}
|
|
|
|
page-my-listings .empty-state h3 {
|
|
margin: 0 0 var(--space-sm);
|
|
}
|
|
|
|
page-my-listings .empty-state p {
|
|
color: var(--color-text-muted);
|
|
margin: 0 0 var(--space-lg);
|
|
}
|
|
`
|
|
document.head.appendChild(style)
|