From 87b118562329b24cf7af9354b7cdd0960f04e48a Mon Sep 17 00:00:00 2001 From: Alexander Schmidt Date: Sun, 1 Feb 2026 10:36:50 +0100 Subject: [PATCH] add new page-listing --- js/components/pages/page-listing.js | 568 ++++++++++++++++++++-------- locales/de.json | 5 +- locales/en.json | 5 +- locales/fr.json | 5 +- 4 files changed, 414 insertions(+), 169 deletions(-) diff --git a/js/components/pages/page-listing.js b/js/components/pages/page-listing.js index 180ee62..f25792f 100644 --- a/js/components/pages/page-listing.js +++ b/js/components/pages/page-listing.js @@ -1,13 +1,17 @@ import { t, i18n } from '../../i18n.js' import { directus } from '../../services/directus.js' +import { listingsService } from '../../services/listings.js' import '../chat-widget.js' import '../location-map.js' +import '../listing-card.js' class PageListing extends HTMLElement { constructor() { super() this.listing = null + this.sellerListings = [] this.loading = true + this.isFavorite = false } connectedCallback() { @@ -24,6 +28,12 @@ class PageListing extends HTMLElement { async loadListing() { try { this.listing = await directus.getListing(this.listingId) + this.loadFavoriteState() + + // Load other listings from same seller + if (this.listing?.user_created) { + await this.loadSellerListings() + } } catch (e) { console.error('Failed to load listing:', e) this.listing = null @@ -31,6 +41,58 @@ class PageListing extends HTMLElement { this.loading = false this.render() + this.setupEventListeners() + } + + async loadSellerListings() { + try { + const response = await directus.getListings({ + filter: { + status: { _eq: 'published' }, + user_created: { _eq: this.listing.user_created }, + id: { _neq: this.listing.id } + }, + limit: 4 + }) + this.sellerListings = response.items || [] + } catch (e) { + console.error('Failed to load seller listings:', e) + this.sellerListings = [] + } + } + + loadFavoriteState() { + const favorites = JSON.parse(localStorage.getItem('favorites') || '[]') + this.isFavorite = favorites.includes(this.listingId) + } + + toggleFavorite() { + let favorites = JSON.parse(localStorage.getItem('favorites') || '[]') + + if (this.isFavorite) { + favorites = favorites.filter(f => f !== this.listingId) + } else { + favorites.push(this.listingId) + } + + localStorage.setItem('favorites', JSON.stringify(favorites)) + this.isFavorite = !this.isFavorite + + const btn = this.querySelector('#favorite-btn') + btn?.classList.toggle('active', this.isFavorite) + btn.innerHTML = this.getFavoriteIcon() + } + + async copyShareLink() { + const url = window.location.href + try { + await navigator.clipboard.writeText(url) + const btn = this.querySelector('#share-btn') + btn?.classList.add('copied') + setTimeout(() => btn?.classList.remove('copied'), 2000) + } catch (e) { + console.error('Failed to copy:', e) + } } render() { @@ -47,7 +109,7 @@ class PageListing extends HTMLElement { this.innerHTML = /* html */`
😕
-

${t('listing.notFound')}

+

${t('listing.notFound')}

${t('listing.backHome')}
` @@ -57,18 +119,9 @@ class PageListing extends HTMLElement { const images = (this.listing.images || []).slice(0, 5) const hasImages = images.length > 0 const firstImage = hasImages ? this.getImageUrl(images[0], 800) : null - this.currentImageIndex = 0 this.allImages = images - const placeholderSvg = /* html */` - - - - - - ` - - const categoryName = this.listing.category?.name || this.listing.category?.slug || '' + const categoryName = this.listing.category?.name || '' const price = this.formatPrice(this.listing.price, this.listing.currency) const createdDate = this.listing.date_created ? new Date(this.listing.date_created).toLocaleDateString() @@ -76,11 +129,12 @@ class PageListing extends HTMLElement { this.innerHTML = /* html */`
+ - -
-
- ${categoryName ? `${this.escapeHtml(categoryName)}` : ''} -

${this.escapeHtml(this.listing.title)}

-

${price}

- ${this.listing.condition ? `

${this.getConditionLabel(this.listing.condition)}

` : ''} -
- -
-

${t('listing.description')}

-
${this.formatDescription(this.listing.description)}
-
- -
-

${t('listing.seller')}

-
-
?
-
- ${t('listing.anonymousSeller')} - ${createdDate ? `${t('listing.postedOn')} ${createdDate}` : ''} + + +
+ +
+
+ ${categoryName ? `${this.escapeHtml(categoryName)}` : ''} +

${this.escapeHtml(this.listing.title)}

+

${price}

+
+ ${this.listing.condition ? `${this.getConditionLabel(this.listing.condition)}` : ''} + ${this.listing.shipping ? `📦 ${t('listing.shippingAvailable')}` : ''} + ${t('listing.postedOn')} ${createdDate} +
+
+ +
+

${t('listing.description')}

+
${this.formatDescription(this.listing.description)}
+
+ + ${this.listing.location ? ` +
+

${t('listing.location')}

+ +
+ ` : ''} +
+ + +
- - ${this.listing.shipping ? ` -
- 📦 ${t('listing.shippingAvailable')} -
- ` : ''} - - ${this.listing.location ? ` -
-

${t('listing.location') || 'Standort'}

- -
- ` : ''} - -
- -
+ + + +
+ + + ${this.sellerListings.length > 0 ? ` +
+

${t('listing.moreFromSeller') || 'Weitere Anzeigen des Anbieters'}

+
+ ${this.sellerListings.map(listing => this.renderListingCard(listing)).join('')} +
+
+ ` : ''}
- + ${this.renderContactDialog()} + ` + } + + renderListingCard(listing) { + const imageId = listing.images?.[0]?.directus_files_id?.id || listing.images?.[0]?.directus_files_id + const imageUrl = imageId ? directus.getThumbnailUrl(imageId, 300) : '' + const locationName = listing.location?.name || '' + + return /* html */` + + ` + } + + renderContactDialog() { + return /* html */` ` - - this.setupEventListeners() + } + + getFavoriteIcon() { + return this.isFavorite + ? ` + + ` + : ` + + ` + } + + getPlaceholderSvg() { + return /* html */` + + + + + + ` } setupEventListeners() { + // Contact dialog const contactBtn = this.querySelector('#contact-btn') const dialog = this.querySelector('#contact-dialog') const closeBtn = this.querySelector('#dialog-close') - const copyBtn = this.querySelector('#copy-btn') - const tabBtns = this.querySelectorAll('.tab-btn') - - contactBtn?.addEventListener('click', () => { - dialog?.showModal() - }) - - closeBtn?.addEventListener('click', () => { - dialog?.close() - }) + contactBtn?.addEventListener('click', () => dialog?.showModal()) + closeBtn?.addEventListener('click', () => dialog?.close()) dialog?.addEventListener('click', (e) => { - if (e.target === dialog) { - dialog.close() - } + if (e.target === dialog) dialog.close() }) - - copyBtn?.addEventListener('click', async () => { - const addr = this.querySelector('#monero-addr')?.textContent - if (addr) { - await navigator.clipboard.writeText(addr) - copyBtn.classList.add('copied') - setTimeout(() => copyBtn.classList.remove('copied'), 2000) - } - }) - - tabBtns.forEach(btn => { + + // Dialog tabs + this.querySelectorAll('.tab-btn').forEach(btn => { btn.addEventListener('click', () => { const tab = btn.dataset.tab - - tabBtns.forEach(b => b.classList.remove('active')) + this.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active')) btn.classList.add('active') - this.querySelectorAll('.tab-content').forEach(content => { content.classList.toggle('hidden', content.id !== `tab-${tab}`) }) }) }) + // Copy Monero address + this.querySelector('#copy-addr-btn')?.addEventListener('click', async () => { + const addr = this.querySelector('#monero-addr')?.textContent + if (addr) { + await navigator.clipboard.writeText(addr) + const btn = this.querySelector('#copy-addr-btn') + btn?.classList.add('copied') + setTimeout(() => btn?.classList.remove('copied'), 2000) + } + }) + + // Favorite button + this.querySelector('#favorite-btn')?.addEventListener('click', () => this.toggleFavorite()) + + // Share button + this.querySelector('#share-btn')?.addEventListener('click', () => this.copyShareLink()) + // Thumbnail gallery - const thumbnails = this.querySelectorAll('.thumbnail') - const mainImg = this.querySelector('#main-img') - - thumbnails.forEach(thumb => { + this.querySelectorAll('.thumbnail').forEach(thumb => { thumb.addEventListener('click', () => { const index = parseInt(thumb.dataset.index) + const mainImg = this.querySelector('#main-img') if (mainImg && this.allImages[index]) { mainImg.src = this.getImageUrl(this.allImages[index], 800) - thumbnails.forEach(t => t.classList.remove('active')) + this.querySelectorAll('.thumbnail').forEach(t => t.classList.remove('active')) thumb.classList.add('active') - this.currentImageIndex = index } }) }) @@ -260,23 +393,18 @@ class PageListing extends HTMLElement { } formatPrice(price, currency = 'EUR') { - if (price === null || price === undefined) return t('listing.priceOnRequest') + if (!price && price !== 0) return t('listing.priceOnRequest') if (currency === 'XMR') { return `${parseFloat(price).toFixed(4)} XMR` } - return new Intl.NumberFormat('de-DE', { + return new Intl.NumberFormat('de-CH', { style: 'currency', currency: currency }).format(price) } - formatDescription(desc) { - if (!desc) return '' - return this.escapeHtml(desc).replace(/\n/g, '
') - } - getConditionLabel(condition) { const labels = { new: t('create.conditionNew'), @@ -288,6 +416,15 @@ class PageListing extends HTMLElement { return labels[condition] || condition } + formatDescription(text) { + if (!text) return '' + return text + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/\n/g, '
') + } + escapeHtml(text) { if (!text) return '' const div = document.createElement('div') @@ -301,39 +438,31 @@ customElements.define('page-listing', PageListing) const style = document.createElement('style') style.textContent = /* css */` page-listing .listing-detail { - display: grid; - grid-template-columns: 1fr 400px; - gap: var(--space-xl); padding: var(--space-lg) 0; } - - @media (max-width: 900px) { - page-listing .listing-detail { - grid-template-columns: 1fr; - } - } - + + /* Gallery - Full Width */ page-listing .listing-gallery { background: var(--color-bg-secondary); border-radius: var(--radius-lg); overflow: hidden; - align-self: start; + margin-bottom: var(--space-xl); } page-listing .listing-image-main { - aspect-ratio: 4 / 3; - background: var(--color-bg-tertiary); + aspect-ratio: 16/9; + max-height: 500px; display: flex; align-items: center; justify-content: center; overflow: hidden; + background: var(--color-bg-tertiary); } page-listing .listing-image-main img { width: 100%; height: 100%; object-fit: contain; - background: var(--color-bg-tertiary); } page-listing .listing-image-main .placeholder-icon { @@ -343,36 +472,24 @@ style.textContent = /* css */` } page-listing .listing-thumbnails { - display: grid; - grid-template-columns: repeat(5, 1fr); + display: flex; gap: var(--space-xs); - padding: var(--space-xs); - background: var(--color-bg-secondary); - } - - @media (max-width: 480px) { - page-listing .listing-thumbnails { - grid-template-columns: repeat(5, 1fr); - gap: 4px; - padding: 4px; - } + padding: var(--space-sm); + overflow-x: auto; } page-listing .thumbnail { - aspect-ratio: 1; + flex-shrink: 0; + width: 80px; + height: 80px; border: 2px solid transparent; border-radius: var(--radius-md); overflow: hidden; cursor: pointer; - transition: border-color var(--transition-fast), opacity var(--transition-fast); padding: 0; background: var(--color-bg-tertiary); } - page-listing .thumbnail:hover { - opacity: 0.8; - } - page-listing .thumbnail.active { border-color: var(--color-primary); } @@ -382,46 +499,147 @@ style.textContent = /* css */` height: 100%; object-fit: cover; } - - page-listing .listing-info header { + + /* Content Row */ + page-listing .listing-content { + display: grid; + grid-template-columns: 1fr 320px; + gap: var(--space-xl); + align-items: start; + } + + @media (max-width: 900px) { + page-listing .listing-content { + grid-template-columns: 1fr; + } + + page-listing .listing-sidebar { + position: static; + order: -1; + } + } + + /* Main Info */ + page-listing .listing-info { + min-width: 0; + } + + page-listing .listing-header { margin-bottom: var(--space-xl); } - page-listing .listing-info h1 { + page-listing .listing-header h1 { margin: var(--space-sm) 0; + font-size: var(--font-size-2xl); } page-listing .listing-price { - font-size: var(--font-size-2xl); + font-size: var(--font-size-3xl); font-weight: var(--font-weight-bold); color: var(--color-primary); + margin: 0; } - - page-listing .listing-location { + + page-listing .listing-meta { + display: flex; + flex-wrap: wrap; + gap: var(--space-sm); + margin-top: var(--space-md); + } + + page-listing .meta-item { + font-size: var(--font-size-sm); color: var(--color-text-secondary); - margin-top: var(--space-sm); + background: var(--color-bg-secondary); + padding: var(--space-xs) var(--space-sm); + border-radius: var(--radius-sm); + } + + page-listing .meta-date { + margin-left: auto; } page-listing .listing-description, - page-listing .listing-seller, page-listing .listing-location-section { margin-bottom: var(--space-xl); } page-listing .listing-description h2, - page-listing .listing-seller h2, page-listing .listing-location-section h2 { font-size: var(--font-size-lg); margin-bottom: var(--space-md); } - - page-listing .seller-card { + + page-listing .description-text { + line-height: 1.6; + color: var(--color-text-secondary); + } + + /* Sidebar */ + page-listing .listing-sidebar { + position: sticky; + top: var(--space-lg); + display: flex; + flex-direction: column; + gap: var(--space-md); + } + + page-listing .sidebar-card { + background: var(--color-bg-secondary); + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + padding: var(--space-lg); + } + + page-listing .sidebar-btn { + width: 100%; + display: flex; + align-items: center; + justify-content: center; + gap: var(--space-sm); + } + + page-listing .sidebar-actions { + display: flex; + flex-direction: column; + gap: var(--space-xs); + margin-top: var(--space-md); + padding-top: var(--space-md); + border-top: 1px solid var(--color-border); + } + + page-listing .action-btn { + display: flex; + align-items: center; + gap: var(--space-sm); + padding: var(--space-sm) var(--space-md); + background: transparent; + border: none; + border-radius: var(--radius-md); + color: var(--color-text-secondary); + cursor: pointer; + transition: all var(--transition-fast); + font-size: var(--font-size-sm); + } + + page-listing .action-btn:hover { + background: var(--color-bg-tertiary); + color: var(--color-text); + } + + page-listing .action-btn.active { + color: var(--color-accent); + } + + page-listing .action-btn.copied { + color: var(--color-success); + } + + /* Seller Card */ + page-listing .seller-header { display: flex; align-items: center; gap: var(--space-md); - padding: var(--space-md); - background: var(--color-bg-secondary); - border-radius: var(--radius-md); } page-listing .seller-avatar { @@ -446,15 +664,18 @@ style.textContent = /* css */` font-size: var(--font-size-sm); color: var(--color-text-muted); } - - page-listing .listing-actions { - margin-top: var(--space-xl); + + /* Seller Listings */ + page-listing .seller-listings { + margin-top: var(--space-3xl); + padding-top: var(--space-xl); + border-top: 1px solid var(--color-border); } - - page-listing .listing-actions .btn { - width: 100%; + + page-listing .seller-listings h2 { + margin-bottom: var(--space-lg); } - + /* Dialog */ page-listing .contact-dialog { background: var(--color-bg); @@ -489,6 +710,9 @@ style.textContent = /* css */` color: var(--color-text-muted); border-radius: var(--radius-md); transition: all var(--transition-fast); + background: transparent; + border: none; + cursor: pointer; } page-listing .tab-btn:hover { @@ -511,17 +735,15 @@ style.textContent = /* css */` right: var(--space-md); padding: var(--space-xs); color: var(--color-text-muted); - transition: color var(--transition-fast); + background: transparent; + border: none; + cursor: pointer; } page-listing .dialog-close:hover { color: var(--color-text); } - page-listing .contact-dialog h2 { - margin-bottom: var(--space-sm); - } - page-listing .dialog-subtitle { color: var(--color-text-secondary); margin-bottom: var(--space-lg); @@ -554,13 +776,10 @@ style.textContent = /* css */` font-size: var(--font-size-xs); word-break: break-all; line-height: 1.4; - color: var(--color-text); } page-listing .btn-copy { padding: var(--space-sm); - border-color: var(--color-border); - color: var(--color-text); flex-shrink: 0; } @@ -575,5 +794,22 @@ style.textContent = /* css */` color: var(--color-text-muted); text-align: center; } + + /* Loading & Empty States */ + page-listing .loading { + display: flex; + justify-content: center; + padding: var(--space-3xl); + } + + page-listing .empty-state { + text-align: center; + padding: var(--space-3xl); + } + + page-listing .empty-state-icon { + font-size: 3rem; + margin-bottom: var(--space-md); + } ` document.head.appendChild(style) diff --git a/locales/de.json b/locales/de.json index 485b1bd..08d6438 100644 --- a/locales/de.json +++ b/locales/de.json @@ -115,7 +115,10 @@ "copyAddress": "Adresse kopieren", "contactHint": "Kopiere die Adresse und sende den Betrag über dein Monero-Wallet.", "priceOnRequest": "Preis auf Anfrage", - "shippingAvailable": "Versand verfügbar" + "shippingAvailable": "Versand verfügbar", + "share": "Teilen", + "report": "Melden", + "moreFromSeller": "Weitere Anzeigen des Anbieters" }, "chat": { "title": "Nachricht senden", diff --git a/locales/en.json b/locales/en.json index 4b22a7e..a21ec97 100644 --- a/locales/en.json +++ b/locales/en.json @@ -115,7 +115,10 @@ "copyAddress": "Copy address", "contactHint": "Copy the address and send the amount using your Monero wallet.", "priceOnRequest": "Price on request", - "shippingAvailable": "Shipping available" + "shippingAvailable": "Shipping available", + "share": "Share", + "report": "Report", + "moreFromSeller": "More from this seller" }, "chat": { "title": "Send Message", diff --git a/locales/fr.json b/locales/fr.json index 1385529..71b8565 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -115,7 +115,10 @@ "copyAddress": "Copier l'adresse", "contactHint": "Copiez l'adresse et envoyez le montant via votre portefeuille Monero.", "priceOnRequest": "Prix sur demande", - "shippingAvailable": "Livraison disponible" + "shippingAvailable": "Livraison disponible", + "share": "Partager", + "report": "Signaler", + "moreFromSeller": "Autres annonces du vendeur" }, "chat": { "title": "Envoyer un message",