From c1144139b5ae007d1bfbd8c578a804438d437dbe Mon Sep 17 00:00:00 2001 From: Alexander Schmidt Date: Sat, 31 Jan 2026 17:11:05 +0100 Subject: [PATCH] improve page create and page listing --- js/components/pages/page-create.js | 12 ++- js/components/pages/page-listing.js | 116 ++++++++++++++++++++++------ js/services/directus.js | 21 ++--- locales/de.json | 7 +- locales/en.json | 7 +- locales/fr.json | 7 +- 6 files changed, 130 insertions(+), 40 deletions(-) diff --git a/js/components/pages/page-create.js b/js/components/pages/page-create.js index e08f158..81ab82f 100644 --- a/js/components/pages/page-create.js +++ b/js/components/pages/page-create.js @@ -205,10 +205,10 @@ class PageCreate extends HTMLElement { id="location" name="location" value="${this.escapeHtml(this.formData.location)}" - required data-i18n-placeholder="create.locationPlaceholder" placeholder="${t('create.locationPlaceholder')}" > +

${t('create.locationHint') || 'Stadt oder PLZ eingeben'}

@@ -413,13 +413,17 @@ class PageCreate extends HTMLElement { if (this.formData.price_mode) listingData.price_mode = this.formData.price_mode if (this.formData.category) listingData.category = this.formData.category if (this.formData.condition) listingData.condition = this.formData.condition - if (this.formData.location) listingData.location = this.formData.location if (this.formData.shipping) listingData.shipping = this.formData.shipping if (this.formData.moneroAddress) listingData.monero_address = this.formData.moneroAddress - // Add images if uploaded + // Add images in junction table format if (imageIds.length > 0) { - listingData.images = imageIds + listingData.images = { + create: imageIds.map((id, index) => ({ + directus_files_id: id, + sort: index + })) + } } console.log('Creating listing:', listingData) diff --git a/js/components/pages/page-listing.js b/js/components/pages/page-listing.js index a3e82ea..954753e 100644 --- a/js/components/pages/page-listing.js +++ b/js/components/pages/page-listing.js @@ -1,5 +1,5 @@ import { t, i18n } from '../../i18n.js' -import { getListingById } from '../../data/mock-listings.js' +import { directus } from '../../services/directus.js' import '../chat-widget.js' class PageListing extends HTMLElement { @@ -21,9 +21,12 @@ class PageListing extends HTMLElement { } async loadListing() { - await new Promise(resolve => setTimeout(resolve, 300)) - - this.listing = getListingById(this.listingId) + try { + this.listing = await directus.getListing(this.listingId) + } catch (e) { + console.error('Failed to load listing:', e) + this.listing = null + } this.loading = false this.render() @@ -50,7 +53,9 @@ class PageListing extends HTMLElement { return } - const hasImages = this.listing.images && this.listing.images.length > 0 + const images = this.listing.images || [] + const hasImages = images.length > 0 + const firstImage = hasImages ? this.getImageUrl(images[0]) : null const placeholderSvg = /* html */` @@ -59,38 +64,63 @@ class PageListing extends HTMLElement { ` + const categoryName = this.listing.category?.name || this.listing.category?.slug || '' + const locationName = this.listing.location?.name || '' + const price = this.formatPrice(this.listing.price, this.listing.currency) + const createdDate = this.listing.date_created + ? new Date(this.listing.date_created).toLocaleDateString() + : '' + this.innerHTML = /* html */`
- ${t(`categories.${this.listing.category}`)} + ${categoryName ? `${this.escapeHtml(categoryName)}` : ''}

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

-

€ ${this.listing.price}

-

📍 ${this.escapeHtml(this.listing.location)}

+

${price}

+ ${locationName ? `

📍 ${this.escapeHtml(locationName)}

` : ''} + ${this.listing.condition ? `

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

` : ''}

${t('listing.description')}

-

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

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

${t('listing.seller')}

-
${this.listing.seller.name.charAt(0)}
+
?
- ${this.escapeHtml(this.listing.seller.name)} - ${t('listing.memberSince')} ${this.listing.seller.memberSince} + ${t('listing.anonymousSeller')} + ${createdDate ? `${t('listing.postedOn')} ${createdDate}` : ''}
+ ${this.listing.shipping ? ` +
+ 📦 ${t('listing.shippingAvailable')} +
+ ` : ''} +
+ ${this.listing.monero_address || t('listing.noMoneroAddress')} + ${this.listing.monero_address ? ` + + ` : ''}
@@ -190,7 +222,45 @@ class PageListing extends HTMLElement { }) } + getImageUrl(image, size = null) { + const fileId = image?.directus_files_id?.id || image?.directus_files_id || image + if (!fileId) return null + return size + ? directus.getThumbnailUrl(fileId, size) + : directus.getFileUrl(fileId) + } + + formatPrice(price, currency = 'EUR') { + if (price === null || price === undefined) return t('listing.priceOnRequest') + + if (currency === 'XMR') { + return `${parseFloat(price).toFixed(4)} XMR` + } + + return new Intl.NumberFormat('de-DE', { + 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'), + like_new: t('create.conditionLikeNew'), + good: t('create.conditionGood'), + fair: t('create.conditionFair'), + poor: t('create.conditionPoor') + } + return labels[condition] || condition + } + escapeHtml(text) { + if (!text) return '' const div = document.createElement('div') div.textContent = text return div.innerHTML diff --git a/js/services/directus.js b/js/services/directus.js index cb1d8ed..1aac6ef 100644 --- a/js/services/directus.js +++ b/js/services/directus.js @@ -158,7 +158,11 @@ class DirectusService { for (const [key, value] of Object.entries(params)) { if (value === undefined || value === null) continue - if (typeof value === 'object') { + if (key === 'sort' && Array.isArray(value)) { + searchParams.set(key, value.join(',')) + } else if (key === 'fields' && Array.isArray(value)) { + searchParams.set(key, value.join(',')) + } else if (typeof value === 'object') { searchParams.set(key, JSON.stringify(value)) } else { searchParams.set(key, value) @@ -258,8 +262,7 @@ class DirectusService { 'category.icon', 'location.id', 'location.name', - 'location.region', - 'user_created.id' + 'location.region' ], filter: options.filter || { status: { _eq: 'published' } }, sort: options.sort || ['-date_created'], @@ -285,9 +288,7 @@ class DirectusService { 'images.directus_files_id.*', 'category.*', 'category.translations.*', - 'location.*', - 'user_created.id', - 'user_created.date_created' + 'location.*' ] }) return response.data @@ -309,11 +310,11 @@ class DirectusService { async getMyListings() { const response = await this.get('/items/listings', { - fields: ['*', 'images.directus_files_id.id', 'category.name', 'location.name'], + fields: ['*', 'images.directus_files_id.id', 'category.id', 'category.name', 'location.name'], filter: { user_created: { _eq: '$CURRENT_USER' } }, sort: ['-date_created'] }) - return response.data + return response.data || [] } async searchListings(query, options = {}) { @@ -356,12 +357,12 @@ class DirectusService { async getCategories() { const response = await this.get('/items/categories', { - fields: ['*', 'translations.*', 'parent.id', 'parent.name'], + fields: ['*', 'translations.*'], filter: { status: { _eq: 'published' } }, sort: ['sort', 'name'], limit: -1 }) - return response.data + return response.data || [] } async getCategory(idOrSlug) { diff --git a/locales/de.json b/locales/de.json index 2d7206a..39f78b0 100644 --- a/locales/de.json +++ b/locales/de.json @@ -99,12 +99,17 @@ "backHome": "Zur Startseite", "description": "Beschreibung", "seller": "Anbieter", + "anonymousSeller": "Anonymer Anbieter", "memberSince": "Mitglied seit", + "postedOn": "Eingestellt am", "contactSeller": "Anbieter kontaktieren", "paymentInfo": "Bezahlung erfolgt direkt über Monero (XMR).", "moneroAddress": "Monero-Adresse des Anbieters", + "noMoneroAddress": "Keine Monero-Adresse angegeben", "copyAddress": "Adresse kopieren", - "contactHint": "Kopiere die Adresse und sende den Betrag über dein Monero-Wallet." + "contactHint": "Kopiere die Adresse und sende den Betrag über dein Monero-Wallet.", + "priceOnRequest": "Preis auf Anfrage", + "shippingAvailable": "Versand verfügbar" }, "chat": { "title": "Nachricht senden", diff --git a/locales/en.json b/locales/en.json index bab0e2d..0022345 100644 --- a/locales/en.json +++ b/locales/en.json @@ -99,12 +99,17 @@ "backHome": "Back to Home", "description": "Description", "seller": "Seller", + "anonymousSeller": "Anonymous Seller", "memberSince": "Member since", + "postedOn": "Posted on", "contactSeller": "Contact Seller", "paymentInfo": "Payment is made directly via Monero (XMR).", "moneroAddress": "Seller's Monero Address", + "noMoneroAddress": "No Monero address provided", "copyAddress": "Copy address", - "contactHint": "Copy the address and send the amount using your Monero wallet." + "contactHint": "Copy the address and send the amount using your Monero wallet.", + "priceOnRequest": "Price on request", + "shippingAvailable": "Shipping available" }, "chat": { "title": "Send Message", diff --git a/locales/fr.json b/locales/fr.json index ff35090..2e4672f 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -99,12 +99,17 @@ "backHome": "Retour à l'accueil", "description": "Description", "seller": "Vendeur", + "anonymousSeller": "Vendeur anonyme", "memberSince": "Membre depuis", + "postedOn": "Publié le", "contactSeller": "Contacter le vendeur", "paymentInfo": "Le paiement s'effectue directement via Monero (XMR).", "moneroAddress": "Adresse Monero du vendeur", + "noMoneroAddress": "Aucune adresse Monero fournie", "copyAddress": "Copier l'adresse", - "contactHint": "Copiez l'adresse et envoyez le montant via votre portefeuille Monero." + "contactHint": "Copiez l'adresse et envoyez le montant via votre portefeuille Monero.", + "priceOnRequest": "Prix sur demande", + "shippingAvailable": "Livraison disponible" }, "chat": { "title": "Envoyer un message",