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": "Сообщения",