feat: soft-delete listings with visual dimming, auto-remove hint, and 30-day expiry

This commit is contained in:
2026-02-08 10:25:06 +01:00
parent e7c73f85b9
commit af25be449d
11 changed files with 167 additions and 32 deletions

View File

@@ -105,7 +105,11 @@ class ListingCard extends HTMLElement {
</svg> </svg>
` `
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 */`
<a href="#/edit/${escapeHTML(id)}" class="owner-badge" title="${t('listing.edit')}"> <a href="#/edit/${escapeHTML(id)}" class="owner-badge" title="${t('listing.edit')}">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path> <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
@@ -113,12 +117,11 @@ class ListingCard extends HTMLElement {
</svg> </svg>
</a> </a>
` : '' ` : ''
const paymentStatus = this.getAttribute('payment-status')
const status = this.getAttribute('status')
let paymentBadge = '' let paymentBadge = ''
if (status === 'archived') { if (status === 'deleted') {
paymentBadge = /* html */`<span class="payment-badge payment-expired">${t('myListings.status.deleted')}</span>`
} else if (status === 'archived') {
paymentBadge = /* html */`<span class="payment-badge payment-expired">${t('myListings.status.expired')}</span>` paymentBadge = /* html */`<span class="payment-badge payment-expired">${t('myListings.status.expired')}</span>`
} else if (paymentStatus === 'processing' || paymentStatus === 'pending') { } else if (paymentStatus === 'processing' || paymentStatus === 'pending') {
paymentBadge = /* html */`<span class="payment-badge payment-processing"><span class="pulse-dot"></span>${t('myListings.status.processing')}</span>` paymentBadge = /* html */`<span class="payment-badge payment-processing"><span class="pulse-dot"></span>${t('myListings.status.processing')}</span>`
@@ -130,9 +133,12 @@ class ListingCard extends HTMLElement {
paymentBadge = /* html */`<span class="payment-badge payment-published">${t('myListings.status.published')}</span>` paymentBadge = /* html */`<span class="payment-badge payment-published">${t('myListings.status.published')}</span>`
} }
const linkTag = isDeleted ? 'div' : 'a'
const linkAttr = isDeleted ? '' : `href="#/listing/${escapeHTML(id)}"`
this.innerHTML = /* html */` this.innerHTML = /* html */`
${ownerBadge} ${ownerBadge}
<a href="#/listing/${escapeHTML(id)}" class="listing-link"> <${linkTag} ${linkAttr} class="listing-link">
<div class="listing-image"> <div class="listing-image">
${image ${image
? `<img src="${escapeHTML(image)}" alt="${escapeHTML(title)}" loading="lazy">` ? `<img src="${escapeHTML(image)}" alt="${escapeHTML(title)}" loading="lazy">`
@@ -147,7 +153,8 @@ class ListingCard extends HTMLElement {
</div> </div>
<p class="listing-location">${escapeHTML(location)}</p> <p class="listing-location">${escapeHTML(location)}</p>
</div> </div>
</a> </${linkTag}>
${!isDeleted ? /* html */`
<button <button
class="favorite-btn ${this.isFavorite ? 'active' : ''}" class="favorite-btn ${this.isFavorite ? 'active' : ''}"
aria-label="${favoriteLabel}" aria-label="${favoriteLabel}"
@@ -157,6 +164,7 @@ class ListingCard extends HTMLElement {
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path> <path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path>
</svg> </svg>
</button> </button>
` : ''}
` `
} }

View File

@@ -199,6 +199,17 @@ class PageListing extends HTMLElement {
return return
} }
if (this.listing.status === 'deleted' && !this.isOwner) {
this.innerHTML = /* html */`
<div class="empty-state">
<div class="empty-state-icon">🗑️</div>
<p>${t('messages.listingRemoved')}</p>
<a href="#/" class="btn btn-primary">${t('listing.backHome')}</a>
</div>
`
return
}
const images = (this.listing.images || []).slice(0, 5) const images = (this.listing.images || []).slice(0, 5)
const hasImages = images.length > 0 const hasImages = images.length > 0
const firstImage = hasImages ? this.getImageUrl(images[0]) : null const firstImage = hasImages ? this.getImageUrl(images[0]) : null

View File

@@ -57,6 +57,7 @@ class PageMessages extends HTMLElement {
'date_updated', 'date_updated',
'listing_id.id', 'listing_id.id',
'listing_id.title', 'listing_id.title',
'listing_id.status',
'listing_id.images.directus_files_id.id', 'listing_id.images.directus_files_id.id',
'status' 'status'
], ],
@@ -155,7 +156,7 @@ class PageMessages extends HTMLElement {
const listing = conv.listing_id const listing = conv.listing_id
const imageId = listing?.images?.[0]?.directus_files_id?.id const imageId = listing?.images?.[0]?.directus_files_id?.id
const imageUrl = imageId ? directus.getThumbnailUrl(imageId, 80) : '' 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) const dateStr = this.formatDate(conv.date_updated || conv.date_created)
return /* html */` return /* html */`

View File

@@ -113,7 +113,7 @@ class PageMyListings extends HTMLElement {
const response = await directus.getListings({ const response = await directus.getListings({
fields: [ fields: [
'id', 'status', 'title', 'slug', 'price', 'currency', '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', 'images.directus_files_id.id',
'location.id', 'location.name' 'location.id', 'location.name'
], ],
@@ -123,7 +123,12 @@ class PageMyListings extends HTMLElement {
sort: ['-date_created'], sort: ['-date_created'],
limit: 50 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.loading = false
this.updateContent() this.updateContent()
this.startPolling() this.startPolling()
@@ -229,8 +234,21 @@ class PageMyListings extends HTMLElement {
` `
} }
let deleteBtn = ''
if (listing.status === 'deleted') {
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 */` return /* html */`
<div class="listing-wrapper"> <div class="listing-wrapper${listing.status === 'deleted' ? ' is-deleted' : ''}">
${statusBadge} ${statusBadge}
<listing-card <listing-card
listing-id="${listing.id}" listing-id="${listing.id}"
@@ -244,6 +262,7 @@ class PageMyListings extends HTMLElement {
status="${listing.status || ''}" status="${listing.status || ''}"
></listing-card> ></listing-card>
${toggleBtn} ${toggleBtn}
${deleteBtn}
</div> </div>
` `
}).join('') }).join('')
@@ -262,6 +281,14 @@ class PageMyListings extends HTMLElement {
this.toggleListingStatus(id, newStatus) 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) { async toggleListingStatus(id, newStatus) {
@@ -276,6 +303,20 @@ class PageMyListings extends HTMLElement {
console.error('Failed to toggle listing status:', 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) customElements.define('page-my-listings', PageMyListings)
@@ -304,6 +345,19 @@ style.textContent = /* css */`
min-width: 0; 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 { page-my-listings .status-badge {
position: absolute; position: absolute;
top: var(--space-sm); top: var(--space-sm);
@@ -326,6 +380,25 @@ style.textContent = /* css */`
text-decoration: line-through; 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 { page-my-listings .btn-toggle-status {
display: block; display: block;
width: 100%; width: 100%;

View File

@@ -222,12 +222,17 @@
"processing": "Ausstehend", "processing": "Ausstehend",
"published": "Veröffentlicht", "published": "Veröffentlicht",
"expired": "Abgelaufen", "expired": "Abgelaufen",
"unpublished": "Deaktiviert" "unpublished": "Deaktiviert",
"deleted": "Gelöscht"
}, },
"unpublish": "Deaktivieren", "unpublish": "Deaktivieren",
"republish": "Aktivieren", "republish": "Aktivieren",
"unpublished": "Anzeige deaktiviert", "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": { "messages": {
"title": "Nachrichten", "title": "Nachrichten",
@@ -241,7 +246,8 @@
"unknownListing": "Unbekannte Anzeige", "unknownListing": "Unbekannte Anzeige",
"today": "Heute", "today": "Heute",
"yesterday": "Gestern", "yesterday": "Gestern",
"daysAgo": "Vor {{days}} Tagen" "daysAgo": "Vor {{days}} Tagen",
"listingRemoved": "Anzeige entfernt"
}, },
"settings": { "settings": {
"title": "Einstellungen", "title": "Einstellungen",

View File

@@ -222,12 +222,17 @@
"processing": "Pending", "processing": "Pending",
"published": "Published", "published": "Published",
"expired": "Expired", "expired": "Expired",
"unpublished": "Unpublished" "unpublished": "Unpublished",
"deleted": "Deleted"
}, },
"unpublish": "Unpublish", "unpublish": "Unpublish",
"republish": "Republish", "republish": "Republish",
"unpublished": "Listing unpublished", "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": { "messages": {
"title": "Messages", "title": "Messages",
@@ -241,7 +246,8 @@
"unknownListing": "Unknown listing", "unknownListing": "Unknown listing",
"today": "Today", "today": "Today",
"yesterday": "Yesterday", "yesterday": "Yesterday",
"daysAgo": "{{days}} days ago" "daysAgo": "{{days}} days ago",
"listingRemoved": "Listing removed"
}, },
"settings": { "settings": {
"title": "Settings", "title": "Settings",

View File

@@ -222,12 +222,17 @@
"processing": "Pendiente", "processing": "Pendiente",
"published": "Publicado", "published": "Publicado",
"expired": "Caducado", "expired": "Caducado",
"unpublished": "Desactivado" "unpublished": "Desactivado",
"deleted": "Eliminado"
}, },
"unpublish": "Desactivar", "unpublish": "Desactivar",
"republish": "Reactivar", "republish": "Reactivar",
"unpublished": "Anuncio desactivado", "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": { "messages": {
"title": "Mensajes", "title": "Mensajes",
@@ -241,7 +246,8 @@
"unknownListing": "Anuncio desconocido", "unknownListing": "Anuncio desconocido",
"today": "Hoy", "today": "Hoy",
"yesterday": "Ayer", "yesterday": "Ayer",
"daysAgo": "Hace {{days}} días" "daysAgo": "Hace {{days}} días",
"listingRemoved": "Anuncio eliminado"
}, },
"settings": { "settings": {
"title": "Ajustes", "title": "Ajustes",

View File

@@ -222,12 +222,17 @@
"processing": "En attente", "processing": "En attente",
"published": "Publié", "published": "Publié",
"expired": "Expiré", "expired": "Expiré",
"unpublished": "Désactivé" "unpublished": "Désactivé",
"deleted": "Supprimé"
}, },
"unpublish": "Désactiver", "unpublish": "Désactiver",
"republish": "Réactiver", "republish": "Réactiver",
"unpublished": "Annonce désactivée", "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": { "messages": {
"title": "Messages", "title": "Messages",
@@ -241,7 +246,8 @@
"unknownListing": "Annonce inconnue", "unknownListing": "Annonce inconnue",
"today": "Aujourd'hui", "today": "Aujourd'hui",
"yesterday": "Hier", "yesterday": "Hier",
"daysAgo": "Il y a {{days}} jours" "daysAgo": "Il y a {{days}} jours",
"listingRemoved": "Annonce supprimée"
}, },
"settings": { "settings": {
"title": "Paramètres", "title": "Paramètres",

View File

@@ -222,12 +222,17 @@
"processing": "In attesa", "processing": "In attesa",
"published": "Pubblicato", "published": "Pubblicato",
"expired": "Scaduto", "expired": "Scaduto",
"unpublished": "Disattivato" "unpublished": "Disattivato",
"deleted": "Eliminato"
}, },
"unpublish": "Disattiva", "unpublish": "Disattiva",
"republish": "Riattiva", "republish": "Riattiva",
"unpublished": "Annuncio disattivato", "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": { "messages": {
"title": "Messaggi", "title": "Messaggi",
@@ -241,7 +246,8 @@
"unknownListing": "Annuncio sconosciuto", "unknownListing": "Annuncio sconosciuto",
"today": "Oggi", "today": "Oggi",
"yesterday": "Ieri", "yesterday": "Ieri",
"daysAgo": "{{days}} giorni fa" "daysAgo": "{{days}} giorni fa",
"listingRemoved": "Annuncio rimosso"
}, },
"settings": { "settings": {
"title": "Impostazioni", "title": "Impostazioni",

View File

@@ -222,12 +222,17 @@
"processing": "Pendente", "processing": "Pendente",
"published": "Publicado", "published": "Publicado",
"expired": "Expirado", "expired": "Expirado",
"unpublished": "Desativado" "unpublished": "Desativado",
"deleted": "Excluído"
}, },
"unpublish": "Desativar", "unpublish": "Desativar",
"republish": "Reativar", "republish": "Reativar",
"unpublished": "Anúncio desativado", "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": { "messages": {
"title": "Mensagens", "title": "Mensagens",
@@ -241,7 +246,8 @@
"unknownListing": "Anúncio desconhecido", "unknownListing": "Anúncio desconhecido",
"today": "Hoje", "today": "Hoje",
"yesterday": "Ontem", "yesterday": "Ontem",
"daysAgo": "{{days}} dias atrás" "daysAgo": "{{days}} dias atrás",
"listingRemoved": "Anúncio removido"
}, },
"settings": { "settings": {
"title": "Configurações", "title": "Configurações",

View File

@@ -222,12 +222,17 @@
"processing": "На рассмотрении", "processing": "На рассмотрении",
"published": "Опубликовано", "published": "Опубликовано",
"expired": "Истекло", "expired": "Истекло",
"unpublished": "Снято" "unpublished": "Снято",
"deleted": "Удалено"
}, },
"unpublish": "Снять с публикации", "unpublish": "Снять с публикации",
"republish": "Опубликовать снова", "republish": "Опубликовать снова",
"unpublished": "Объявление снято с публикации", "unpublished": "Объявление снято с публикации",
"republished": "Объявление опубликовано снова" "republished": "Объявление опубликовано снова",
"delete": "Удалить",
"deleteConfirm": "Действительно удалить это объявление? Это нельзя отменить.",
"deleted": "Объявление удалено",
"deletedHint": "Будет автоматически удалено через 30 дней"
}, },
"messages": { "messages": {
"title": "Сообщения", "title": "Сообщения",
@@ -241,7 +246,8 @@
"unknownListing": "Неизвестное объявление", "unknownListing": "Неизвестное объявление",
"today": "Сегодня", "today": "Сегодня",
"yesterday": "Вчера", "yesterday": "Вчера",
"daysAgo": "{{days}} дн. назад" "daysAgo": "{{days}} дн. назад",
"listingRemoved": "Объявление удалено"
}, },
"settings": { "settings": {
"title": "Настройки", "title": "Настройки",