diff --git a/js/components/listing-card.js b/js/components/listing-card.js
index 3d593e2..aa9254b 100644
--- a/js/components/listing-card.js
+++ b/js/components/listing-card.js
@@ -124,6 +124,8 @@ class ListingCard extends HTMLElement {
paymentBadge = /* html */`${t('myListings.status.processing')}`
} else if (paymentStatus === 'expired') {
paymentBadge = /* html */`${t('myListings.status.expired')}`
+ } else if (paymentStatus === 'paid' && status === 'draft') {
+ paymentBadge = /* html */`${t('myListings.status.unpublished')}`
} else if (paymentStatus === 'paid') {
paymentBadge = /* html */`${t('myListings.status.published')}`
}
@@ -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;
diff --git a/js/components/pages/page-create.js b/js/components/pages/page-create.js
index 84cb5f3..527d842 100644
--- a/js/components/pages/page-create.js
+++ b/js/components/pages/page-create.js
@@ -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 {
diff --git a/js/components/pages/page-my-listings.js b/js/components/pages/page-my-listings.js
index 25e93a3..712c5ab 100644
--- a/js/components/pages/page-my-listings.js
+++ b/js/components/pages/page-my-listings.js
@@ -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 `${t('myListings.status.archived')}`
}
- 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 `${t('myListings.status.draft')}`
}
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 */`
+
+ `
+ }
+
return /* html */`
${statusBadge}
@@ -229,9 +243,38 @@ class PageMyListings extends HTMLElement {
payment-status="${listing.payment_status || ''}"
status="${listing.status || ''}"
>
+ ${toggleBtn}
`
}).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;
diff --git a/js/services/directus.js b/js/services/directus.js
index c944e74..2e1ba88 100644
--- a/js/services/directus.js
+++ b/js/services/directus.js
@@ -422,6 +422,8 @@ class DirectusService {
'shipping',
'shipping_cost',
'views',
+ 'paid_at',
+ 'payment_status',
'expires_at',
'date_created',
'user_created',
diff --git a/js/services/listings.js b/js/services/listings.js
index 6f65078..720478b 100644
--- a/js/services/listings.js
+++ b/js/services/listings.js
@@ -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()
diff --git a/locales/de.json b/locales/de.json
index d437076..124177a 100644
--- a/locales/de.json
+++ b/locales/de.json
@@ -149,7 +149,8 @@
"publishFailed": "Veröffentlichung fehlgeschlagen. Bitte versuche es erneut.",
"invalidMoneroAddress": "Ungültige Monero-Adresse. Bitte prüfe das Format.",
"draftRestored": "Entwurf wiederhergestellt",
- "clearDraft": "Verwerfen"
+ "clearDraft": "Verwerfen",
+ "paymentExpired": "Die bezahlte Laufzeit ist abgelaufen. Bitte erneut bezahlen."
},
"notFound": {
"title": "Seite nicht gefunden",
@@ -220,8 +221,13 @@
"archived": "Archiviert",
"processing": "Ausstehend",
"published": "Veröffentlicht",
- "expired": "Abgelaufen"
- }
+ "expired": "Abgelaufen",
+ "unpublished": "Deaktiviert"
+ },
+ "unpublish": "Deaktivieren",
+ "republish": "Aktivieren",
+ "unpublished": "Anzeige deaktiviert",
+ "republished": "Anzeige wieder aktiviert"
},
"messages": {
"title": "Nachrichten",
diff --git a/locales/en.json b/locales/en.json
index 21583b5..49e7aec 100644
--- a/locales/en.json
+++ b/locales/en.json
@@ -149,7 +149,8 @@
"publishFailed": "Publishing failed. Please try again.",
"invalidMoneroAddress": "Invalid Monero address. Please check the format.",
"draftRestored": "Draft restored",
- "clearDraft": "Discard"
+ "clearDraft": "Discard",
+ "paymentExpired": "The paid period has expired. Please pay again."
},
"notFound": {
"title": "Page Not Found",
@@ -220,8 +221,13 @@
"archived": "Archived",
"processing": "Pending",
"published": "Published",
- "expired": "Expired"
- }
+ "expired": "Expired",
+ "unpublished": "Unpublished"
+ },
+ "unpublish": "Unpublish",
+ "republish": "Republish",
+ "unpublished": "Listing unpublished",
+ "republished": "Listing republished"
},
"messages": {
"title": "Messages",
diff --git a/locales/es.json b/locales/es.json
index e2c0719..48fe789 100644
--- a/locales/es.json
+++ b/locales/es.json
@@ -149,7 +149,8 @@
"publishFailed": "Error al publicar. Inténtalo de nuevo.",
"invalidMoneroAddress": "Dirección Monero no válida. Comprueba el formato.",
"draftRestored": "Borrador restaurado",
- "clearDraft": "Descartar"
+ "clearDraft": "Descartar",
+ "paymentExpired": "El período pagado ha expirado. Por favor, pague de nuevo."
},
"notFound": {
"title": "Página no encontrada",
@@ -220,8 +221,13 @@
"archived": "Archivado",
"processing": "Pendiente",
"published": "Publicado",
- "expired": "Caducado"
- }
+ "expired": "Caducado",
+ "unpublished": "Desactivado"
+ },
+ "unpublish": "Desactivar",
+ "republish": "Reactivar",
+ "unpublished": "Anuncio desactivado",
+ "republished": "Anuncio reactivado"
},
"messages": {
"title": "Mensajes",
diff --git a/locales/fr.json b/locales/fr.json
index 7fa6843..ab8c5ef 100644
--- a/locales/fr.json
+++ b/locales/fr.json
@@ -149,7 +149,8 @@
"publishFailed": "La publication a échoué. Veuillez réessayer.",
"invalidMoneroAddress": "Adresse Monero invalide. Veuillez vérifier le format.",
"draftRestored": "Brouillon restauré",
- "clearDraft": "Supprimer"
+ "clearDraft": "Supprimer",
+ "paymentExpired": "La période payée a expiré. Veuillez payer à nouveau."
},
"notFound": {
"title": "Page non trouvée",
@@ -220,8 +221,13 @@
"archived": "Archivé",
"processing": "En attente",
"published": "Publié",
- "expired": "Expiré"
- }
+ "expired": "Expiré",
+ "unpublished": "Désactivé"
+ },
+ "unpublish": "Désactiver",
+ "republish": "Réactiver",
+ "unpublished": "Annonce désactivée",
+ "republished": "Annonce réactivée"
},
"messages": {
"title": "Messages",
diff --git a/locales/it.json b/locales/it.json
index 8e9c542..cae44c2 100644
--- a/locales/it.json
+++ b/locales/it.json
@@ -149,7 +149,8 @@
"publishFailed": "Pubblicazione fallita. Riprova.",
"invalidMoneroAddress": "Indirizzo Monero non valido. Controlla il formato.",
"draftRestored": "Bozza ripristinata",
- "clearDraft": "Elimina"
+ "clearDraft": "Elimina",
+ "paymentExpired": "Il periodo pagato è scaduto. Si prega di pagare di nuovo."
},
"notFound": {
"title": "Pagina non trovata",
@@ -220,8 +221,13 @@
"archived": "Archiviato",
"processing": "In attesa",
"published": "Pubblicato",
- "expired": "Scaduto"
- }
+ "expired": "Scaduto",
+ "unpublished": "Disattivato"
+ },
+ "unpublish": "Disattiva",
+ "republish": "Riattiva",
+ "unpublished": "Annuncio disattivato",
+ "republished": "Annuncio riattivato"
},
"messages": {
"title": "Messaggi",
diff --git a/locales/pt.json b/locales/pt.json
index 90cb03f..8d2b841 100644
--- a/locales/pt.json
+++ b/locales/pt.json
@@ -149,7 +149,8 @@
"publishFailed": "Falha ao publicar. Por favor, tente novamente.",
"invalidMoneroAddress": "Endereço Monero inválido. Verifique o formato.",
"draftRestored": "Rascunho restaurado",
- "clearDraft": "Descartar"
+ "clearDraft": "Descartar",
+ "paymentExpired": "O período pago expirou. Por favor, pague novamente."
},
"notFound": {
"title": "Página Não Encontrada",
@@ -220,8 +221,13 @@
"archived": "Arquivado",
"processing": "Pendente",
"published": "Publicado",
- "expired": "Expirado"
- }
+ "expired": "Expirado",
+ "unpublished": "Desativado"
+ },
+ "unpublish": "Desativar",
+ "republish": "Reativar",
+ "unpublished": "Anúncio desativado",
+ "republished": "Anúncio reativado"
},
"messages": {
"title": "Mensagens",
diff --git a/locales/ru.json b/locales/ru.json
index 6f13ca1..d72211f 100644
--- a/locales/ru.json
+++ b/locales/ru.json
@@ -149,7 +149,8 @@
"publishFailed": "Публикация не удалась. Попробуйте снова.",
"invalidMoneroAddress": "Неверный Monero-адрес. Проверьте формат.",
"draftRestored": "Черновик восстановлен",
- "clearDraft": "Удалить черновик"
+ "clearDraft": "Удалить черновик",
+ "paymentExpired": "Оплаченный период истёк. Пожалуйста, оплатите снова."
},
"notFound": {
"title": "Страница не найдена",
@@ -220,8 +221,13 @@
"archived": "В архиве",
"processing": "На рассмотрении",
"published": "Опубликовано",
- "expired": "Истекло"
- }
+ "expired": "Истекло",
+ "unpublished": "Снято"
+ },
+ "unpublish": "Снять с публикации",
+ "republish": "Опубликовать снова",
+ "unpublished": "Объявление снято с публикации",
+ "republished": "Объявление опубликовано снова"
},
"messages": {
"title": "Сообщения",