feat: add reputation system with deals, ratings, level badges, and chat-widget deal confirmation

This commit is contained in:
2026-02-09 17:46:49 +01:00
parent 2db3e56f00
commit 83f1303d13
11 changed files with 1072 additions and 0 deletions

View File

@@ -9,6 +9,7 @@ import '../chat-widget.js'
import '../location-map.js'
import '../listing-card.js'
import { categoriesService } from '../../services/categories.js'
import { reputationService } from '../../services/reputation.js'
class PageListing extends HTMLElement {
constructor() {
@@ -20,6 +21,7 @@ class PageListing extends HTMLElement {
this.rates = null
this.isOwner = false
this.hasPendingChats = false
this.sellerReputation = null
this.handleCurrencyChange = this.handleCurrencyChange.bind(this)
}
@@ -69,6 +71,8 @@ class PageListing extends HTMLElement {
if (this.listing?.user_created) {
await this.loadSellerListings()
}
await this.loadSellerReputation()
} catch (e) {
console.error('Failed to load listing:', e)
this.listing = null
@@ -179,6 +183,23 @@ class PageListing extends HTMLElement {
}
}
async loadSellerReputation() {
if (!this.listing?.id) return
try {
const convsResponse = await directus.get('/items/conversations', {
filter: { listing_id: { _eq: this.listing.id } },
fields: ['participant_hash_2'],
limit: 1
})
const conv = (convsResponse.data || [])[0]
if (conv?.participant_hash_2) {
this.sellerReputation = await reputationService.getReputation(conv.participant_hash_2)
}
} catch (e) {
// No conversations yet = no reputation to show
}
}
loadFavoriteState() {
this.isFavorite = favoritesService.isFavorite(this.listingId)
}
@@ -443,10 +464,44 @@ class PageListing extends HTMLElement {
<span>${t('listing.memberSince')} 2024</span>
</div>
</div>
${this.renderSellerReputation()}
</div>
`
}
renderSellerReputation() {
if (!this.sellerReputation) return ''
const rep = this.sellerReputation
const levelInfo = reputationService.getLevelInfo(rep.level)
const dealCount = rep.deals_completed
const dealText = dealCount === 1 ? t('reputation.dealsSingular') : t('reputation.deals', { count: dealCount })
let ratingHtml = ''
if (rep.avg_rating > 0) {
const stars = '★'.repeat(Math.round(rep.avg_rating)) + '☆'.repeat(5 - Math.round(rep.avg_rating))
ratingHtml = /* html */`<span class="seller-rating" title="${t('reputation.avgRating', { rating: rep.avg_rating.toFixed(1) })}">${stars}</span>`
}
const showWarning = rep.level === 'new'
return /* html */`
<div class="seller-reputation">
<div class="seller-level">
<span class="level-badge">${levelInfo.badge}</span>
<span class="level-name">${t(levelInfo.i18nKey)}</span>
${dealCount > 0 ? `<span class="deal-count">· ${dealText}</span>` : ''}
</div>
${ratingHtml}
</div>
${showWarning ? /* html */`
<div class="seller-warning">
${t('reputation.newWarning')}
</div>
` : ''}
`
}
renderListingCard(listing) {
const imageId = listing.images?.[0]?.directus_files_id?.id || listing.images?.[0]?.directus_files_id
const imageUrl = imageId ? directus.getThumbnailUrl(imageId, 180) : ''
@@ -961,6 +1016,52 @@ style.textContent = /* css */`
color: var(--color-text-muted);
}
page-listing .seller-reputation {
padding: var(--space-sm) 0 0;
margin-top: var(--space-sm);
border-top: 1px solid var(--color-border);
display: flex;
flex-direction: column;
gap: var(--space-xs);
}
page-listing .seller-level {
display: flex;
align-items: center;
gap: var(--space-xs);
font-size: var(--font-size-sm);
}
page-listing .level-badge {
font-size: var(--font-size-base);
}
page-listing .level-name {
color: var(--color-text-secondary);
font-weight: var(--font-weight-medium);
}
page-listing .deal-count {
color: var(--color-text-muted);
}
page-listing .seller-rating {
font-size: var(--font-size-sm);
color: var(--color-text-muted);
letter-spacing: 1px;
}
page-listing .seller-warning {
margin-top: var(--space-sm);
padding: var(--space-sm) var(--space-md);
background: var(--color-bg-secondary);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
font-size: var(--font-size-xs);
color: var(--color-text-muted);
line-height: 1.5;
}
/* Seller Listings */
page-listing .seller-listings {
margin-top: var(--space-3xl);