feat: add reputation system with deals, ratings, level badges, and chat-widget deal confirmation
This commit is contained in:
@@ -7,6 +7,7 @@ import { t, i18n } from '../i18n.js'
|
||||
import { conversationsService } from '../services/conversations.js'
|
||||
import { cryptoService } from '../services/crypto.js'
|
||||
import { escapeHTML } from '../utils/helpers.js'
|
||||
import { reputationService } from '../services/reputation.js'
|
||||
|
||||
class ChatWidget extends HTMLElement {
|
||||
static get observedAttributes() {
|
||||
@@ -21,6 +22,8 @@ class ChatWidget extends HTMLElement {
|
||||
this.loading = true
|
||||
this.error = null
|
||||
this._initialized = false
|
||||
this.deal = null
|
||||
this.hasRated = false
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
@@ -63,6 +66,7 @@ class ChatWidget extends HTMLElement {
|
||||
try {
|
||||
this.conversation = await conversationsService.startOrFindByListing(this.listingId)
|
||||
await this.loadMessages()
|
||||
await this.loadDealState()
|
||||
} catch (e) {
|
||||
console.error('Failed to init conversation:', e)
|
||||
this.error = 'init-failed'
|
||||
@@ -86,6 +90,24 @@ class ChatWidget extends HTMLElement {
|
||||
this.scrollToBottom()
|
||||
}
|
||||
|
||||
async loadDealState() {
|
||||
if (!this.conversation) return
|
||||
try {
|
||||
const deals = await reputationService.getDealsForConversation(this.conversation.id)
|
||||
this.deal = deals[0] || null
|
||||
this._cachedUserHash = await reputationService.getUserHash()
|
||||
if (this.deal && this.deal.status === 'confirmed') {
|
||||
const userHash = this._cachedUserHash
|
||||
const ratings = await reputationService.getRatingsForUser(
|
||||
this.deal.seller_hash === userHash ? this.deal.buyer_hash : this.deal.seller_hash
|
||||
)
|
||||
this.hasRated = ratings.some(r => r.deal === this.deal.id && r.rater_hash === userHash)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load deal state:', e)
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.loading) {
|
||||
this.innerHTML = /* html */`
|
||||
@@ -124,6 +146,8 @@ class ChatWidget extends HTMLElement {
|
||||
: this.renderMessagesHtml()}
|
||||
</div>
|
||||
|
||||
${this.renderDealSection()}
|
||||
|
||||
<form class="chat-input" id="chat-form">
|
||||
<input
|
||||
type="text"
|
||||
@@ -172,9 +196,89 @@ class ChatWidget extends HTMLElement {
|
||||
}
|
||||
}
|
||||
|
||||
renderDealSection() {
|
||||
if (!this.conversation || !this.conversation.otherPublicKey) return ''
|
||||
|
||||
if (!this.deal) {
|
||||
return /* html */`
|
||||
<div class="deal-section">
|
||||
<button class="deal-btn" id="deal-create-btn">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
|
||||
<circle cx="9" cy="7" r="4"></circle>
|
||||
<path d="M23 21v-2a4 4 0 0 0-3-3.87"></path>
|
||||
<path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
|
||||
</svg>
|
||||
${t('reputation.confirmDeal')}
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
if (this.deal.status === 'pending') {
|
||||
const userHash = this._cachedUserHash
|
||||
const iAmConfirmed = (this.deal.seller_hash === userHash && this.deal.seller_confirmed)
|
||||
|| (this.deal.buyer_hash === userHash && this.deal.buyer_confirmed)
|
||||
|
||||
if (iAmConfirmed) {
|
||||
return /* html */`
|
||||
<div class="deal-section">
|
||||
<div class="deal-status deal-pending">
|
||||
<span class="pulse-dot"></span>
|
||||
${t('reputation.dealPending')}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
return /* html */`
|
||||
<div class="deal-section">
|
||||
<button class="deal-btn deal-confirm" id="deal-confirm-btn">
|
||||
${t('reputation.confirmDeal')}
|
||||
</button>
|
||||
<span class="deal-hint">${t('reputation.confirmDealHint')}</span>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
if (this.deal.status === 'confirmed') {
|
||||
if (this.hasRated) {
|
||||
return /* html */`
|
||||
<div class="deal-section">
|
||||
<div class="deal-status deal-confirmed">
|
||||
✓ ${t('reputation.dealConfirmed')} · ${t('reputation.rated')}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
return /* html */`
|
||||
<div class="deal-section">
|
||||
<div class="deal-status deal-confirmed">✓ ${t('reputation.dealConfirmed')}</div>
|
||||
<div class="deal-rating" id="deal-rating">
|
||||
<span class="rating-label">${t('reputation.rate')}</span>
|
||||
<div class="rating-stars">
|
||||
${[1,2,3,4,5].map(s => `<button class="star-btn" data-score="${s}" aria-label="${s}">★</button>`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
const form = this.querySelector('#chat-form')
|
||||
form?.addEventListener('submit', (e) => this.handleSubmit(e))
|
||||
|
||||
const dealCreateBtn = this.querySelector('#deal-create-btn')
|
||||
dealCreateBtn?.addEventListener('click', () => this.handleCreateDeal())
|
||||
|
||||
const dealConfirmBtn = this.querySelector('#deal-confirm-btn')
|
||||
dealConfirmBtn?.addEventListener('click', () => this.handleConfirmDeal())
|
||||
|
||||
this.querySelectorAll('.star-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => this.handleRate(parseInt(btn.dataset.score)))
|
||||
})
|
||||
}
|
||||
|
||||
async handleSubmit(e) {
|
||||
@@ -196,6 +300,38 @@ class ChatWidget extends HTMLElement {
|
||||
await this.refreshMessages()
|
||||
}
|
||||
|
||||
async handleCreateDeal() {
|
||||
if (!this.conversation) return
|
||||
try {
|
||||
this._cachedUserHash = await reputationService.getUserHash()
|
||||
this.deal = await reputationService.createDeal(this.conversation)
|
||||
this.render()
|
||||
} catch (e) {
|
||||
console.error('Failed to create deal:', e)
|
||||
}
|
||||
}
|
||||
|
||||
async handleConfirmDeal() {
|
||||
if (!this.deal) return
|
||||
try {
|
||||
this.deal = await reputationService.confirmDeal(this.deal.id)
|
||||
this.render()
|
||||
} catch (e) {
|
||||
console.error('Failed to confirm deal:', e)
|
||||
}
|
||||
}
|
||||
|
||||
async handleRate(score) {
|
||||
if (!this.deal) return
|
||||
try {
|
||||
await reputationService.rateDeal(this.deal.id, score)
|
||||
this.hasRated = true
|
||||
this.render()
|
||||
} catch (e) {
|
||||
console.error('Failed to rate:', e)
|
||||
}
|
||||
}
|
||||
|
||||
scrollToBottom() {
|
||||
const container = this.querySelector('#chat-messages')
|
||||
if (container) {
|
||||
@@ -357,6 +493,120 @@ style.textContent = /* css */`
|
||||
chat-widget .chat-input button:hover {
|
||||
background: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
chat-widget .deal-section {
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
border-top: 1px solid var(--color-border);
|
||||
background: var(--color-bg-secondary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
chat-widget .deal-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-xs);
|
||||
padding: var(--space-xs) var(--space-sm);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--color-bg);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
chat-widget .deal-btn:hover {
|
||||
border-color: var(--color-text-muted);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
chat-widget .deal-btn.deal-confirm {
|
||||
background: var(--color-text);
|
||||
color: var(--color-bg);
|
||||
border-color: var(--color-text);
|
||||
}
|
||||
|
||||
chat-widget .deal-btn.deal-confirm:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
chat-widget .deal-hint {
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
chat-widget .deal-status {
|
||||
font-size: var(--font-size-sm);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-xs);
|
||||
}
|
||||
|
||||
chat-widget .deal-pending {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
chat-widget .deal-confirmed {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
chat-widget .deal-rating {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
chat-widget .rating-label {
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
chat-widget .rating-stars {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
chat-widget .star-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: var(--font-size-lg);
|
||||
color: var(--color-border);
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
|
||||
chat-widget .star-btn:hover,
|
||||
chat-widget .star-btn:hover ~ .star-btn {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
chat-widget .rating-stars:hover .star-btn {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
chat-widget .rating-stars .star-btn:hover ~ .star-btn {
|
||||
color: var(--color-border);
|
||||
}
|
||||
|
||||
chat-widget .pulse-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-text-muted);
|
||||
display: inline-block;
|
||||
animation: pulse-deal 1.5s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse-deal {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.3; }
|
||||
}
|
||||
`
|
||||
document.head.appendChild(style)
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
208
js/services/reputation.js
Normal file
208
js/services/reputation.js
Normal file
@@ -0,0 +1,208 @@
|
||||
import { directus } from './directus.js'
|
||||
import { cryptoService } from './crypto.js'
|
||||
|
||||
const CACHE_TTL = 5 * 60 * 1000
|
||||
|
||||
class ReputationService {
|
||||
constructor() {
|
||||
this.cache = new Map()
|
||||
}
|
||||
|
||||
async getUserHash() {
|
||||
await cryptoService.ready
|
||||
const publicKey = cryptoService.getPublicKey()
|
||||
const encoder = new TextEncoder()
|
||||
const data = encoder.encode(publicKey)
|
||||
const hash = await crypto.subtle.digest('SHA-256', data)
|
||||
return Array.from(new Uint8Array(hash))
|
||||
.map(b => b.toString(16).padStart(2, '0'))
|
||||
.join('')
|
||||
}
|
||||
|
||||
// ── Deals ──
|
||||
|
||||
async createDeal(conversation) {
|
||||
const userHash = await this.getUserHash()
|
||||
|
||||
// participant_hash_1 = buyer (initiator), participant_hash_2 = seller
|
||||
const sellerHash = conversation.participant_hash_2
|
||||
const buyerHash = conversation.participant_hash_1
|
||||
const isSeller = sellerHash === userHash
|
||||
|
||||
const response = await directus.post('/items/deals', {
|
||||
conversation: conversation.id,
|
||||
listing: conversation.listing_id,
|
||||
seller_hash: sellerHash,
|
||||
buyer_hash: buyerHash,
|
||||
status: 'pending',
|
||||
seller_confirmed: isSeller,
|
||||
buyer_confirmed: !isSeller
|
||||
})
|
||||
|
||||
return response.data || response
|
||||
}
|
||||
|
||||
async confirmDeal(dealId) {
|
||||
const userHash = await this.getUserHash()
|
||||
const deal = await this.getDeal(dealId)
|
||||
|
||||
if (!deal) throw new Error('Deal not found')
|
||||
|
||||
const isSeller = deal.seller_hash === userHash
|
||||
const isBuyer = deal.buyer_hash === userHash
|
||||
if (!isSeller && !isBuyer) throw new Error('Not a participant of this deal')
|
||||
|
||||
const update = {}
|
||||
if (isSeller) update.seller_confirmed = true
|
||||
if (isBuyer) update.buyer_confirmed = true
|
||||
|
||||
const sellerConfirmed = isSeller ? true : deal.seller_confirmed
|
||||
const buyerConfirmed = isBuyer ? true : deal.buyer_confirmed
|
||||
|
||||
if (sellerConfirmed && buyerConfirmed) {
|
||||
update.status = 'confirmed'
|
||||
update.date_confirmed = new Date().toISOString()
|
||||
}
|
||||
|
||||
const response = await directus.patch(`/items/deals/${dealId}`, update)
|
||||
this.clearCache()
|
||||
return response.data || response
|
||||
}
|
||||
|
||||
async getDeal(dealId) {
|
||||
const response = await directus.get(`/items/deals/${dealId}`)
|
||||
return response.data || response
|
||||
}
|
||||
|
||||
async getDealsForConversation(conversationId) {
|
||||
const response = await directus.get('/items/deals', {
|
||||
filter: { conversation: { _eq: conversationId } },
|
||||
sort: ['-date_created']
|
||||
})
|
||||
return response.data || []
|
||||
}
|
||||
|
||||
async getDealsForUser(userHash) {
|
||||
const response = await directus.get('/items/deals', {
|
||||
filter: {
|
||||
status: { _eq: 'confirmed' },
|
||||
_or: [
|
||||
{ seller_hash: { _eq: userHash } },
|
||||
{ buyer_hash: { _eq: userHash } }
|
||||
]
|
||||
},
|
||||
sort: ['-date_confirmed']
|
||||
})
|
||||
return response.data || []
|
||||
}
|
||||
|
||||
// ── Ratings ──
|
||||
|
||||
async rateDeal(dealId, score) {
|
||||
if (score < 1 || score > 5) throw new Error('Score must be between 1 and 5')
|
||||
|
||||
const deal = await this.getDeal(dealId)
|
||||
if (!deal) throw new Error('Deal not found')
|
||||
if (deal.status !== 'confirmed') throw new Error('Deal must be confirmed before rating')
|
||||
|
||||
const userHash = await this.getUserHash()
|
||||
const isSeller = deal.seller_hash === userHash
|
||||
const isBuyer = deal.buyer_hash === userHash
|
||||
if (!isSeller && !isBuyer) throw new Error('Not a participant of this deal')
|
||||
|
||||
const ratedUserHash = isSeller ? deal.buyer_hash : deal.seller_hash
|
||||
|
||||
const existing = await directus.get('/items/ratings', {
|
||||
filter: {
|
||||
deal: { _eq: dealId },
|
||||
rater_hash: { _eq: userHash }
|
||||
},
|
||||
limit: 1
|
||||
})
|
||||
|
||||
if (existing.data?.length > 0) throw new Error('Already rated this deal')
|
||||
|
||||
const response = await directus.post('/items/ratings', {
|
||||
deal: dealId,
|
||||
rater_hash: userHash,
|
||||
rated_hash: ratedUserHash,
|
||||
score,
|
||||
date_created: new Date().toISOString()
|
||||
})
|
||||
|
||||
this.clearCache()
|
||||
return response.data || response
|
||||
}
|
||||
|
||||
async getRatingsForUser(userHash) {
|
||||
const response = await directus.get('/items/ratings', {
|
||||
filter: { rated_hash: { _eq: userHash } },
|
||||
sort: ['-date_created']
|
||||
})
|
||||
return response.data || []
|
||||
}
|
||||
|
||||
// ── Reputation ──
|
||||
|
||||
async getReputation(userHash) {
|
||||
const cached = this.cache.get(userHash)
|
||||
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
|
||||
return cached.value
|
||||
}
|
||||
|
||||
const [deals, ratings] = await Promise.all([
|
||||
this.getDealsForUser(userHash),
|
||||
this.getRatingsForUser(userHash)
|
||||
])
|
||||
|
||||
const dealsCompleted = deals.length
|
||||
const avgRating = ratings.length > 0
|
||||
? ratings.reduce((sum, r) => sum + r.score, 0) / ratings.length
|
||||
: 0
|
||||
|
||||
let accountAgeDays = 0
|
||||
const dates = deals.map(d => d.date_confirmed || d.date_created).filter(Boolean)
|
||||
|
||||
if (dates.length > 0) {
|
||||
const oldest = dates
|
||||
.map(d => new Date(d).getTime())
|
||||
.reduce((min, t) => Math.min(min, t), Infinity)
|
||||
accountAgeDays = Math.floor((Date.now() - oldest) / (1000 * 60 * 60 * 24))
|
||||
}
|
||||
|
||||
let level = 'new'
|
||||
if (dealsCompleted >= 30 && avgRating >= 4.5) {
|
||||
level = 'power'
|
||||
} else if (dealsCompleted >= 10 && avgRating >= 4.0) {
|
||||
level = 'trusted'
|
||||
} else if (dealsCompleted >= 3 && accountAgeDays >= 30) {
|
||||
level = 'active'
|
||||
}
|
||||
|
||||
const reputation = {
|
||||
deals_completed: dealsCompleted,
|
||||
avg_rating: Math.round(avgRating * 100) / 100,
|
||||
account_age_days: accountAgeDays,
|
||||
level
|
||||
}
|
||||
|
||||
this.cache.set(userHash, { value: reputation, timestamp: Date.now() })
|
||||
return reputation
|
||||
}
|
||||
|
||||
getLevelInfo(level) {
|
||||
const levels = {
|
||||
new: { badge: '⚪', i18nKey: 'reputation.level.new' },
|
||||
active: { badge: '🔵', i18nKey: 'reputation.level.active' },
|
||||
trusted: { badge: '🟢', i18nKey: 'reputation.level.trusted' },
|
||||
power: { badge: '🟣', i18nKey: 'reputation.level.power' }
|
||||
}
|
||||
return levels[level] || levels.new
|
||||
}
|
||||
|
||||
clearCache() {
|
||||
this.cache.clear()
|
||||
}
|
||||
}
|
||||
|
||||
export const reputationService = new ReputationService()
|
||||
Reference in New Issue
Block a user