feat: free edit and publish/unpublish toggle for paid listings within 30-day period

This commit is contained in:
2026-02-08 10:07:47 +01:00
parent c66b77dbf8
commit e7c73f85b9
12 changed files with 152 additions and 24 deletions

View File

@@ -124,6 +124,8 @@ class ListingCard extends HTMLElement {
paymentBadge = /* html */`<span class="payment-badge payment-processing"><span class="pulse-dot"></span>${t('myListings.status.processing')}</span>`
} else if (paymentStatus === 'expired') {
paymentBadge = /* html */`<span class="payment-badge payment-expired">${t('myListings.status.expired')}</span>`
} else if (paymentStatus === 'paid' && status === 'draft') {
paymentBadge = /* html */`<span class="payment-badge payment-unpublished">${t('myListings.status.unpublished')}</span>`
} else if (paymentStatus === 'paid') {
paymentBadge = /* html */`<span class="payment-badge payment-published">${t('myListings.status.published')}</span>`
}
@@ -255,6 +257,11 @@ style.textContent = /* css */`
color: #fff;
}
listing-card .payment-unpublished {
background: rgba(120, 120, 120, 0.85);
color: #fff;
}
listing-card .pulse-dot {
width: 6px;
height: 6px;

View File

@@ -2,6 +2,7 @@ import { t, i18n } from '../../i18n.js'
import { router } from '../../router.js'
import { auth } from '../../services/auth.js'
import { directus } from '../../services/directus.js'
import { listingsService } from '../../services/listings.js'
import { categoriesService } from '../../services/categories.js'
import { SUPPORTED_CURRENCIES, getDisplayCurrency } from '../../services/currency.js'
import { createInvoice, openCheckout, getPendingInvoice, savePendingInvoice, clearPendingInvoice, getInvoiceStatus } from '../../services/btcpay.js'
@@ -17,6 +18,7 @@ class PageCreate extends HTMLElement {
super()
this.editMode = false
this.editId = null
this.editListing = null
this.existingImages = []
this.formData = this.loadDraft() || this.getEmptyFormData()
this.imageFiles = []
@@ -102,6 +104,8 @@ class PageCreate extends HTMLElement {
return
}
this.editListing = listing
this.formData = {
title: listing.title || '',
description: listing.description || '',
@@ -606,6 +610,12 @@ class PageCreate extends HTMLElement {
}
}
if (this.editListing && !listingsService.isPaidAndActive(this.editListing)) {
await directus.updateListing(this.editId, listingData)
await this.startPayment(this.editId, formElements.currency)
return
}
await directus.updateListing(this.editId, listingData)
router.navigate(`/listing/${this.editId}`)
} else {

View File

@@ -1,6 +1,7 @@
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'
@@ -112,7 +113,7 @@ class PageMyListings extends HTMLElement {
const response = await directus.getListings({
fields: [
'id', 'status', 'title', 'slug', 'price', 'currency',
'condition', 'payment_status', 'expires_at', 'date_created', 'user_created',
'condition', 'payment_status', 'paid_at', 'expires_at', 'date_created', 'user_created',
'images.directus_files_id.id',
'location.id', 'location.name'
],
@@ -142,7 +143,7 @@ class PageMyListings extends HTMLElement {
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') {
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 ''
@@ -209,12 +210,25 @@ class PageMyListings extends HTMLElement {
`
}
return this.listings.map(listing => {
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 paidActive = listingsService.isPaidAndActive(listing)
const isPublished = listing.status === 'published'
const isDraftPaid = listing.status === 'draft' && paidActive
let toggleBtn = ''
if (isPublished || isDraftPaid) {
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>
`
}
return /* html */`
<div class="listing-wrapper">
${statusBadge}
@@ -229,9 +243,38 @@ class PageMyListings extends HTMLElement {
payment-status="${listing.payment_status || ''}"
status="${listing.status || ''}"
></listing-card>
${toggleBtn}
</div>
`
}).join('')
setTimeout(() => this.setupToggleListeners(), 0)
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)
})
})
}
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)
}
}
}
@@ -283,6 +326,24 @@ style.textContent = /* css */`
text-decoration: line-through;
}
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;

View File

@@ -422,6 +422,8 @@ class DirectusService {
'shipping',
'shipping_cost',
'views',
'paid_at',
'payment_status',
'expires_at',
'date_created',
'user_created',

View File

@@ -69,6 +69,12 @@ class ListingsService {
search: filters.search
})
}
isPaidAndActive(listing) {
return listing.paid_at
&& listing.expires_at
&& new Date(listing.expires_at) > new Date()
}
}
export const listingsService = new ListingsService()