From af25be449dc0093c90f3b23f7aa531eb401904e6 Mon Sep 17 00:00:00 2001 From: Alexander Schmidt Date: Sun, 8 Feb 2026 10:25:06 +0100 Subject: [PATCH] feat: soft-delete listings with visual dimming, auto-remove hint, and 30-day expiry --- js/components/listing-card.js | 22 ++++--- js/components/pages/page-listing.js | 11 ++++ js/components/pages/page-messages.js | 3 +- js/components/pages/page-my-listings.js | 79 ++++++++++++++++++++++++- locales/de.json | 12 +++- locales/en.json | 12 +++- locales/es.json | 12 +++- locales/fr.json | 12 +++- locales/it.json | 12 +++- locales/pt.json | 12 +++- locales/ru.json | 12 +++- 11 files changed, 167 insertions(+), 32 deletions(-) diff --git a/js/components/listing-card.js b/js/components/listing-card.js index aa9254b..1d2a018 100644 --- a/js/components/listing-card.js +++ b/js/components/listing-card.js @@ -105,7 +105,11 @@ class ListingCard extends HTMLElement { ` - const ownerBadge = this.isOwner ? /* html */` + const paymentStatus = this.getAttribute('payment-status') + const status = this.getAttribute('status') + const isDeleted = status === 'deleted' + + const ownerBadge = (this.isOwner && !isDeleted) ? /* html */` @@ -113,12 +117,11 @@ class ListingCard extends HTMLElement { ` : '' - - const paymentStatus = this.getAttribute('payment-status') - const status = this.getAttribute('status') let paymentBadge = '' - if (status === 'archived') { + if (status === 'deleted') { + paymentBadge = /* html */`${t('myListings.status.deleted')}` + } else if (status === 'archived') { paymentBadge = /* html */`${t('myListings.status.expired')}` } else if (paymentStatus === 'processing' || paymentStatus === 'pending') { paymentBadge = /* html */`${t('myListings.status.processing')}` @@ -130,9 +133,12 @@ class ListingCard extends HTMLElement { paymentBadge = /* html */`${t('myListings.status.published')}` } + const linkTag = isDeleted ? 'div' : 'a' + const linkAttr = isDeleted ? '' : `href="#/listing/${escapeHTML(id)}"` + this.innerHTML = /* html */` ${ownerBadge} - + <${linkTag} ${linkAttr} class="listing-link">
${image ? `${escapeHTML(title)}` @@ -147,7 +153,8 @@ class ListingCard extends HTMLElement {

${escapeHTML(location)}

-
+ + ${!isDeleted ? /* html */` + ` : ''} ` } diff --git a/js/components/pages/page-listing.js b/js/components/pages/page-listing.js index 6157f9d..82ecf92 100644 --- a/js/components/pages/page-listing.js +++ b/js/components/pages/page-listing.js @@ -199,6 +199,17 @@ class PageListing extends HTMLElement { return } + if (this.listing.status === 'deleted' && !this.isOwner) { + this.innerHTML = /* html */` +
+
🗑️
+

${t('messages.listingRemoved')}

+ ${t('listing.backHome')} +
+ ` + return + } + const images = (this.listing.images || []).slice(0, 5) const hasImages = images.length > 0 const firstImage = hasImages ? this.getImageUrl(images[0]) : null diff --git a/js/components/pages/page-messages.js b/js/components/pages/page-messages.js index e24ebce..703d369 100644 --- a/js/components/pages/page-messages.js +++ b/js/components/pages/page-messages.js @@ -57,6 +57,7 @@ class PageMessages extends HTMLElement { 'date_updated', 'listing_id.id', 'listing_id.title', + 'listing_id.status', 'listing_id.images.directus_files_id.id', 'status' ], @@ -155,7 +156,7 @@ class PageMessages extends HTMLElement { const listing = conv.listing_id const imageId = listing?.images?.[0]?.directus_files_id?.id const imageUrl = imageId ? directus.getThumbnailUrl(imageId, 80) : '' - const title = listing?.title || t('messages.unknownListing') + const title = listing?.status === 'deleted' ? t('messages.listingRemoved') : (listing?.title || t('messages.unknownListing')) const dateStr = this.formatDate(conv.date_updated || conv.date_created) return /* html */` diff --git a/js/components/pages/page-my-listings.js b/js/components/pages/page-my-listings.js index 712c5ab..7871c83 100644 --- a/js/components/pages/page-my-listings.js +++ b/js/components/pages/page-my-listings.js @@ -113,7 +113,7 @@ class PageMyListings extends HTMLElement { const response = await directus.getListings({ fields: [ 'id', 'status', 'title', 'slug', 'price', 'currency', - 'condition', 'payment_status', 'paid_at', 'expires_at', 'date_created', 'user_created', + 'condition', 'payment_status', 'paid_at', 'expires_at', 'date_created', 'date_updated', 'user_created', 'images.directus_files_id.id', 'location.id', 'location.name' ], @@ -123,7 +123,12 @@ class PageMyListings extends HTMLElement { sort: ['-date_created'], limit: 50 }) - this.listings = response.items || [] + 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() @@ -229,8 +234,21 @@ class PageMyListings extends HTMLElement { ` } + let deleteBtn = '' + if (listing.status === 'deleted') { + deleteBtn = /* html */` +

${t('myListings.deletedHint')}

+ ` + } else if (listing.status !== 'archived') { + deleteBtn = /* html */` + + ` + } + return /* html */` -
+
${statusBadge} ${toggleBtn} + ${deleteBtn}
` }).join('') @@ -262,6 +281,14 @@ class PageMyListings extends HTMLElement { 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) { @@ -276,6 +303,20 @@ class PageMyListings extends HTMLElement { 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) @@ -304,6 +345,19 @@ style.textContent = /* css */` 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); @@ -326,6 +380,25 @@ style.textContent = /* css */` 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%; diff --git a/locales/de.json b/locales/de.json index 124177a..5097b32 100644 --- a/locales/de.json +++ b/locales/de.json @@ -222,12 +222,17 @@ "processing": "Ausstehend", "published": "Veröffentlicht", "expired": "Abgelaufen", - "unpublished": "Deaktiviert" + "unpublished": "Deaktiviert", + "deleted": "Gelöscht" }, "unpublish": "Deaktivieren", "republish": "Aktivieren", "unpublished": "Anzeige deaktiviert", - "republished": "Anzeige wieder aktiviert" + "republished": "Anzeige wieder aktiviert", + "delete": "Löschen", + "deleteConfirm": "Anzeige wirklich löschen? Dies kann nicht rückgängig gemacht werden.", + "deleted": "Anzeige gelöscht", + "deletedHint": "Wird in 30 Tagen automatisch entfernt" }, "messages": { "title": "Nachrichten", @@ -241,7 +246,8 @@ "unknownListing": "Unbekannte Anzeige", "today": "Heute", "yesterday": "Gestern", - "daysAgo": "Vor {{days}} Tagen" + "daysAgo": "Vor {{days}} Tagen", + "listingRemoved": "Anzeige entfernt" }, "settings": { "title": "Einstellungen", diff --git a/locales/en.json b/locales/en.json index 49e7aec..b22c986 100644 --- a/locales/en.json +++ b/locales/en.json @@ -222,12 +222,17 @@ "processing": "Pending", "published": "Published", "expired": "Expired", - "unpublished": "Unpublished" + "unpublished": "Unpublished", + "deleted": "Deleted" }, "unpublish": "Unpublish", "republish": "Republish", "unpublished": "Listing unpublished", - "republished": "Listing republished" + "republished": "Listing republished", + "delete": "Delete", + "deleteConfirm": "Really delete this listing? This cannot be undone.", + "deleted": "Listing deleted", + "deletedHint": "Will be automatically removed in 30 days" }, "messages": { "title": "Messages", @@ -241,7 +246,8 @@ "unknownListing": "Unknown listing", "today": "Today", "yesterday": "Yesterday", - "daysAgo": "{{days}} days ago" + "daysAgo": "{{days}} days ago", + "listingRemoved": "Listing removed" }, "settings": { "title": "Settings", diff --git a/locales/es.json b/locales/es.json index 48fe789..842eb10 100644 --- a/locales/es.json +++ b/locales/es.json @@ -222,12 +222,17 @@ "processing": "Pendiente", "published": "Publicado", "expired": "Caducado", - "unpublished": "Desactivado" + "unpublished": "Desactivado", + "deleted": "Eliminado" }, "unpublish": "Desactivar", "republish": "Reactivar", "unpublished": "Anuncio desactivado", - "republished": "Anuncio reactivado" + "republished": "Anuncio reactivado", + "delete": "Eliminar", + "deleteConfirm": "¿Realmente eliminar este anuncio? No se puede deshacer.", + "deleted": "Anuncio eliminado", + "deletedHint": "Se eliminará automáticamente en 30 días" }, "messages": { "title": "Mensajes", @@ -241,7 +246,8 @@ "unknownListing": "Anuncio desconocido", "today": "Hoy", "yesterday": "Ayer", - "daysAgo": "Hace {{days}} días" + "daysAgo": "Hace {{days}} días", + "listingRemoved": "Anuncio eliminado" }, "settings": { "title": "Ajustes", diff --git a/locales/fr.json b/locales/fr.json index ab8c5ef..a8a17d2 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -222,12 +222,17 @@ "processing": "En attente", "published": "Publié", "expired": "Expiré", - "unpublished": "Désactivé" + "unpublished": "Désactivé", + "deleted": "Supprimé" }, "unpublish": "Désactiver", "republish": "Réactiver", "unpublished": "Annonce désactivée", - "republished": "Annonce réactivée" + "republished": "Annonce réactivée", + "delete": "Supprimer", + "deleteConfirm": "Vraiment supprimer cette annonce ? Cette action est irréversible.", + "deleted": "Annonce supprimée", + "deletedHint": "Sera automatiquement supprimée dans 30 jours" }, "messages": { "title": "Messages", @@ -241,7 +246,8 @@ "unknownListing": "Annonce inconnue", "today": "Aujourd'hui", "yesterday": "Hier", - "daysAgo": "Il y a {{days}} jours" + "daysAgo": "Il y a {{days}} jours", + "listingRemoved": "Annonce supprimée" }, "settings": { "title": "Paramètres", diff --git a/locales/it.json b/locales/it.json index cae44c2..e74a757 100644 --- a/locales/it.json +++ b/locales/it.json @@ -222,12 +222,17 @@ "processing": "In attesa", "published": "Pubblicato", "expired": "Scaduto", - "unpublished": "Disattivato" + "unpublished": "Disattivato", + "deleted": "Eliminato" }, "unpublish": "Disattiva", "republish": "Riattiva", "unpublished": "Annuncio disattivato", - "republished": "Annuncio riattivato" + "republished": "Annuncio riattivato", + "delete": "Elimina", + "deleteConfirm": "Eliminare davvero questo annuncio? Non può essere annullato.", + "deleted": "Annuncio eliminato", + "deletedHint": "Verrà rimosso automaticamente tra 30 giorni" }, "messages": { "title": "Messaggi", @@ -241,7 +246,8 @@ "unknownListing": "Annuncio sconosciuto", "today": "Oggi", "yesterday": "Ieri", - "daysAgo": "{{days}} giorni fa" + "daysAgo": "{{days}} giorni fa", + "listingRemoved": "Annuncio rimosso" }, "settings": { "title": "Impostazioni", diff --git a/locales/pt.json b/locales/pt.json index 8d2b841..56ab85c 100644 --- a/locales/pt.json +++ b/locales/pt.json @@ -222,12 +222,17 @@ "processing": "Pendente", "published": "Publicado", "expired": "Expirado", - "unpublished": "Desativado" + "unpublished": "Desativado", + "deleted": "Excluído" }, "unpublish": "Desativar", "republish": "Reativar", "unpublished": "Anúncio desativado", - "republished": "Anúncio reativado" + "republished": "Anúncio reativado", + "delete": "Excluir", + "deleteConfirm": "Realmente excluir este anúncio? Isso não pode ser desfeito.", + "deleted": "Anúncio excluído", + "deletedHint": "Será removido automaticamente em 30 dias" }, "messages": { "title": "Mensagens", @@ -241,7 +246,8 @@ "unknownListing": "Anúncio desconhecido", "today": "Hoje", "yesterday": "Ontem", - "daysAgo": "{{days}} dias atrás" + "daysAgo": "{{days}} dias atrás", + "listingRemoved": "Anúncio removido" }, "settings": { "title": "Configurações", diff --git a/locales/ru.json b/locales/ru.json index d72211f..3a070de 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -222,12 +222,17 @@ "processing": "На рассмотрении", "published": "Опубликовано", "expired": "Истекло", - "unpublished": "Снято" + "unpublished": "Снято", + "deleted": "Удалено" }, "unpublish": "Снять с публикации", "republish": "Опубликовать снова", "unpublished": "Объявление снято с публикации", - "republished": "Объявление опубликовано снова" + "republished": "Объявление опубликовано снова", + "delete": "Удалить", + "deleteConfirm": "Действительно удалить это объявление? Это нельзя отменить.", + "deleted": "Объявление удалено", + "deletedHint": "Будет автоматически удалено через 30 дней" }, "messages": { "title": "Сообщения", @@ -241,7 +246,8 @@ "unknownListing": "Неизвестное объявление", "today": "Сегодня", "yesterday": "Вчера", - "daysAgo": "{{days}} дн. назад" + "daysAgo": "{{days}} дн. назад", + "listingRemoved": "Объявление удалено" }, "settings": { "title": "Настройки",