409 lines
13 KiB
JavaScript
409 lines
13 KiB
JavaScript
import { t, i18n } from '../i18n.js'
|
||
import { escapeHTML } from '../utils/helpers.js'
|
||
import { getXmrRates, formatPrice as formatCurrencyPrice } from '../services/currency.js'
|
||
import { auth } from '../services/auth.js'
|
||
|
||
let cachedRates = null
|
||
|
||
class ListingCard extends HTMLElement {
|
||
static get observedAttributes() {
|
||
return ['listing-id', 'title', 'price', 'currency', 'location', 'image', 'owner-id', 'payment-status', 'status']
|
||
}
|
||
|
||
constructor() {
|
||
super()
|
||
this.isFavorite = false
|
||
this.rates = null
|
||
this.isOwner = false
|
||
this.handleCurrencyChange = this.handleCurrencyChange.bind(this)
|
||
}
|
||
|
||
async connectedCallback() {
|
||
this.loadFavoriteState()
|
||
await this.loadRates()
|
||
await this.checkOwnership()
|
||
this.render()
|
||
this.setupEventListeners()
|
||
window.addEventListener('currency-changed', this.handleCurrencyChange)
|
||
}
|
||
|
||
disconnectedCallback() {
|
||
window.removeEventListener('currency-changed', this.handleCurrencyChange)
|
||
}
|
||
|
||
handleCurrencyChange() {
|
||
this.render()
|
||
this.setupEventListeners()
|
||
}
|
||
|
||
async checkOwnership() {
|
||
const ownerId = this.getAttribute('owner-id')
|
||
if (!ownerId || !auth.isLoggedIn()) {
|
||
this.isOwner = false
|
||
return
|
||
}
|
||
|
||
try {
|
||
const user = await auth.getUser()
|
||
this.isOwner = user?.id === ownerId
|
||
} catch {
|
||
this.isOwner = false
|
||
}
|
||
}
|
||
|
||
async loadRates() {
|
||
if (!cachedRates) {
|
||
cachedRates = await getXmrRates()
|
||
}
|
||
this.rates = cachedRates
|
||
}
|
||
|
||
attributeChangedCallback() {
|
||
if (this.isConnected) {
|
||
this.render()
|
||
this.setupEventListeners()
|
||
}
|
||
}
|
||
|
||
loadFavoriteState() {
|
||
const id = this.getAttribute('listing-id')
|
||
if (id) {
|
||
const favorites = JSON.parse(localStorage.getItem('favorites') || '[]')
|
||
this.isFavorite = favorites.includes(id)
|
||
}
|
||
}
|
||
|
||
saveFavoriteState() {
|
||
const id = this.getAttribute('listing-id')
|
||
if (!id) return
|
||
|
||
let favorites = JSON.parse(localStorage.getItem('favorites') || '[]')
|
||
|
||
if (this.isFavorite) {
|
||
if (!favorites.includes(id)) favorites.push(id)
|
||
} else {
|
||
favorites = favorites.filter(f => f !== id)
|
||
}
|
||
|
||
localStorage.setItem('favorites', JSON.stringify(favorites))
|
||
}
|
||
|
||
render() {
|
||
const id = this.getAttribute('listing-id') || ''
|
||
const title = this.getAttribute('title') || t('home.placeholderTitle')
|
||
const price = this.getAttribute('price')
|
||
const currency = this.getAttribute('currency') || 'EUR'
|
||
const location = this.getAttribute('location') || t('home.placeholderLocation')
|
||
const image = this.getAttribute('image')
|
||
|
||
let priceDisplay = '–'
|
||
let secondaryPrice = null
|
||
|
||
if (price && this.rates) {
|
||
const listing = { price: parseFloat(price), currency, price_mode: currency === 'XMR' ? 'xmr' : 'fiat' }
|
||
const formatted = formatCurrencyPrice(listing, this.rates)
|
||
priceDisplay = formatted.primary
|
||
secondaryPrice = formatted.secondary
|
||
} else if (price) {
|
||
priceDisplay = currency === 'XMR'
|
||
? `${parseFloat(price).toFixed(4)} XMR`
|
||
: `€ ${parseFloat(price).toFixed(2)}`
|
||
}
|
||
|
||
const favoriteLabel = this.isFavorite ? t('home.removeFavorite') : t('home.addFavorite')
|
||
|
||
const placeholderSvg = /* html */`
|
||
<svg class="placeholder-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
||
<circle cx="8.5" cy="8.5" r="1.5"></circle>
|
||
<polyline points="21 15 16 10 5 21"></polyline>
|
||
</svg>
|
||
`
|
||
|
||
const ownerBadge = this.isOwner ? /* html */`
|
||
<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">
|
||
<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="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>
|
||
</svg>
|
||
</a>
|
||
` : ''
|
||
|
||
const paymentStatus = this.getAttribute('payment-status')
|
||
const status = this.getAttribute('status')
|
||
|
||
let paymentBadge = ''
|
||
if (status === 'archived') {
|
||
paymentBadge = /* html */`<span class="payment-badge payment-expired">${t('myListings.status.expired')}</span>`
|
||
} else if (paymentStatus === 'processing' || paymentStatus === 'pending') {
|
||
paymentBadge = /* html */`<span class="payment-badge payment-processing"><span class="pulse-dot"></span>${t('myListings.status.processing')}</span>`
|
||
} else if (paymentStatus === 'expired') {
|
||
paymentBadge = /* html */`<span class="payment-badge payment-expired">${t('myListings.status.expired')}</span>`
|
||
} else if (paymentStatus === 'paid') {
|
||
paymentBadge = /* html */`<span class="payment-badge payment-published">${t('myListings.status.published')}</span>`
|
||
}
|
||
|
||
this.innerHTML = /* html */`
|
||
${ownerBadge}
|
||
<a href="#/listing/${escapeHTML(id)}" class="listing-link">
|
||
<div class="listing-image">
|
||
${image
|
||
? `<img src="${escapeHTML(image)}" alt="${escapeHTML(title)}" loading="lazy">`
|
||
: placeholderSvg}
|
||
${paymentBadge}
|
||
</div>
|
||
<div class="listing-info">
|
||
<h3 class="listing-title">${escapeHTML(title)}</h3>
|
||
<div class="listing-price-wrapper">
|
||
<p class="listing-price">${priceDisplay}</p>
|
||
${secondaryPrice ? `<p class="listing-price-secondary">${secondaryPrice}</p>` : ''}
|
||
</div>
|
||
<p class="listing-location">${escapeHTML(location)}</p>
|
||
</div>
|
||
</a>
|
||
<button
|
||
class="favorite-btn ${this.isFavorite ? 'active' : ''}"
|
||
aria-label="${favoriteLabel}"
|
||
aria-pressed="${this.isFavorite}"
|
||
>
|
||
<svg class="heart-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<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>
|
||
</button>
|
||
`
|
||
}
|
||
|
||
setupEventListeners() {
|
||
const btn = this.querySelector('.favorite-btn')
|
||
btn?.addEventListener('click', (e) => {
|
||
e.preventDefault()
|
||
e.stopPropagation()
|
||
this.toggleFavorite()
|
||
})
|
||
}
|
||
|
||
toggleFavorite() {
|
||
this.isFavorite = !this.isFavorite
|
||
this.saveFavoriteState()
|
||
|
||
const btn = this.querySelector('.favorite-btn')
|
||
btn?.classList.toggle('active', this.isFavorite)
|
||
btn?.setAttribute('aria-pressed', this.isFavorite)
|
||
btn?.setAttribute('aria-label', this.isFavorite ? t('home.removeFavorite') : t('home.addFavorite'))
|
||
|
||
btn?.classList.add('animate__animated', 'animate__heartBeat')
|
||
btn?.addEventListener('animationend', () => {
|
||
btn?.classList.remove('animate__animated', 'animate__heartBeat')
|
||
}, { once: true })
|
||
|
||
this.dispatchEvent(new CustomEvent('favorite-toggle', {
|
||
bubbles: true,
|
||
detail: { id: this.getAttribute('listing-id'), isFavorite: this.isFavorite }
|
||
}))
|
||
}
|
||
}
|
||
|
||
customElements.define('listing-card', ListingCard)
|
||
|
||
const style = document.createElement('style')
|
||
style.textContent = /* css */`
|
||
listing-card {
|
||
display: block;
|
||
position: relative;
|
||
background: var(--color-bg-secondary);
|
||
border: 1px solid var(--color-border);
|
||
border-radius: var(--radius-md);
|
||
overflow: hidden;
|
||
transition: box-shadow var(--transition-fast);
|
||
min-width: 0;
|
||
}
|
||
|
||
listing-card:hover {
|
||
box-shadow: var(--shadow-md);
|
||
}
|
||
|
||
listing-card .listing-link {
|
||
display: block;
|
||
text-decoration: none;
|
||
color: inherit;
|
||
}
|
||
|
||
listing-card .listing-image {
|
||
aspect-ratio: 1;
|
||
background: var(--color-bg-tertiary);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
overflow: hidden;
|
||
position: relative;
|
||
}
|
||
|
||
listing-card .payment-badge {
|
||
position: absolute;
|
||
bottom: 0;
|
||
left: 0;
|
||
right: 0;
|
||
padding: var(--space-xs) var(--space-sm);
|
||
font-size: var(--font-size-xs);
|
||
font-weight: var(--font-weight-medium);
|
||
text-align: center;
|
||
}
|
||
|
||
listing-card .payment-processing {
|
||
background: rgba(230, 167, 0, 0.9);
|
||
color: #fff;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 6px;
|
||
}
|
||
|
||
listing-card .payment-published {
|
||
background: rgba(40, 167, 69, 0.9);
|
||
color: #fff;
|
||
}
|
||
|
||
listing-card .payment-expired {
|
||
background: rgba(180, 60, 60, 0.85);
|
||
color: #fff;
|
||
}
|
||
|
||
listing-card .pulse-dot {
|
||
width: 6px;
|
||
height: 6px;
|
||
border-radius: var(--radius-full);
|
||
background: #fff;
|
||
animation: card-pulse 1.5s ease-in-out infinite;
|
||
}
|
||
|
||
@keyframes card-pulse {
|
||
0%, 100% { opacity: 1; }
|
||
50% { opacity: 0.3; }
|
||
}
|
||
|
||
listing-card .listing-image img {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
transition: transform 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||
}
|
||
|
||
listing-card:hover .listing-image img {
|
||
transform: scale(1.08);
|
||
}
|
||
|
||
listing-card .listing-image .placeholder-icon {
|
||
width: 48px;
|
||
height: 48px;
|
||
color: var(--color-border);
|
||
}
|
||
|
||
listing-card .listing-info {
|
||
padding: var(--space-sm);
|
||
}
|
||
|
||
listing-card .listing-title {
|
||
font-size: var(--font-size-sm);
|
||
font-weight: var(--font-weight-medium);
|
||
margin: 0 0 var(--space-xs);
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
listing-card .listing-price-wrapper {
|
||
margin: 0 0 var(--space-xs);
|
||
}
|
||
|
||
listing-card .listing-price {
|
||
font-size: var(--font-size-sm);
|
||
font-weight: var(--font-weight-bold);
|
||
color: var(--color-primary);
|
||
margin: 0;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
listing-card .listing-price-secondary {
|
||
font-size: var(--font-size-xs);
|
||
color: var(--color-text-muted);
|
||
margin: 0;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
listing-card .listing-location {
|
||
font-size: var(--font-size-xs);
|
||
color: var(--color-text-muted);
|
||
margin: 0;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
listing-card .favorite-btn {
|
||
position: absolute;
|
||
top: var(--space-sm);
|
||
right: var(--space-sm);
|
||
width: 36px;
|
||
height: 36px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: var(--color-bg);
|
||
border: none;
|
||
border-radius: var(--radius-full);
|
||
cursor: pointer;
|
||
box-shadow: var(--shadow-sm);
|
||
transition: all var(--transition-fast);
|
||
z-index: 1;
|
||
}
|
||
|
||
listing-card .favorite-btn:hover {
|
||
transform: scale(1.1);
|
||
}
|
||
|
||
listing-card .favorite-btn .heart-icon {
|
||
color: var(--color-text-muted);
|
||
transition: all var(--transition-fast);
|
||
}
|
||
|
||
listing-card .favorite-btn.active .heart-icon {
|
||
fill: var(--color-accent);
|
||
stroke: var(--color-accent);
|
||
color: var(--color-accent);
|
||
}
|
||
|
||
listing-card .favorite-btn:hover .heart-icon {
|
||
color: var(--color-accent);
|
||
}
|
||
|
||
listing-card .owner-badge {
|
||
position: absolute;
|
||
top: var(--space-sm);
|
||
left: var(--space-sm);
|
||
width: 36px;
|
||
height: 36px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: var(--color-bg);
|
||
border: none;
|
||
border-radius: var(--radius-full);
|
||
box-shadow: var(--shadow-sm);
|
||
z-index: 2;
|
||
color: var(--color-text-muted);
|
||
transition: all var(--transition-fast);
|
||
text-decoration: none;
|
||
}
|
||
|
||
listing-card .owner-badge:hover {
|
||
background: var(--color-primary);
|
||
color: white;
|
||
transform: scale(1.1);
|
||
}
|
||
`
|
||
document.head.appendChild(style)
|