improve listing and search on home
This commit is contained in:
13
css/base.css
13
css/base.css
@@ -42,6 +42,19 @@ button, input, textarea, select {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* Override browser autofill styles */
|
||||
input:-webkit-autofill,
|
||||
input:-webkit-autofill:hover,
|
||||
input:-webkit-autofill:focus,
|
||||
input:-webkit-autofill:active,
|
||||
textarea:-webkit-autofill,
|
||||
select:-webkit-autofill {
|
||||
-webkit-box-shadow: 0 0 0 1000px var(--color-bg) inset !important;
|
||||
-webkit-text-fill-color: var(--color-text) !important;
|
||||
box-shadow: 0 0 0 1000px var(--color-bg) inset !important;
|
||||
transition: background-color 5000s ease-in-out 0s;
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
|
||||
@@ -10,44 +10,95 @@ class PageHome extends HTMLElement {
|
||||
this.listings = []
|
||||
this.loading = true
|
||||
this.error = null
|
||||
this.currentFilters = {}
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.render()
|
||||
this.setupEventListeners()
|
||||
this.loadListings()
|
||||
this.unsubscribe = i18n.subscribe(() => this.render())
|
||||
this.unsubscribe = i18n.subscribe(() => {
|
||||
this.render()
|
||||
this.setupEventListeners()
|
||||
})
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
if (this.unsubscribe) this.unsubscribe()
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
const searchBox = this.querySelector('search-box')
|
||||
|
||||
// Listen for filter changes (live update)
|
||||
searchBox?.addEventListener('filter-change', (e) => {
|
||||
this.currentFilters = e.detail
|
||||
this.loadListings()
|
||||
})
|
||||
|
||||
// Listen for search submit (with query)
|
||||
searchBox?.addEventListener('search', (e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
this.currentFilters = e.detail
|
||||
this.loadListings()
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
async loadListings() {
|
||||
try {
|
||||
this.loading = true
|
||||
this.error = null
|
||||
this.render()
|
||||
this.updateListingsSection()
|
||||
|
||||
const response = await listingsService.getRecentListings(12)
|
||||
const filters = {
|
||||
search: this.currentFilters.query || undefined,
|
||||
category: this.currentFilters.category || undefined,
|
||||
limit: 20
|
||||
}
|
||||
|
||||
const response = await listingsService.getListingsWithFilters(filters)
|
||||
this.listings = response.items || []
|
||||
this.loading = false
|
||||
this.render()
|
||||
this.updateListingsSection()
|
||||
} catch (err) {
|
||||
console.error('Failed to load listings:', err)
|
||||
this.error = err.message
|
||||
this.loading = false
|
||||
this.render()
|
||||
this.updateListingsSection()
|
||||
}
|
||||
}
|
||||
|
||||
updateListingsSection() {
|
||||
const listingsGrid = this.querySelector('.listings-grid')
|
||||
const sectionTitle = this.querySelector('.listings-title')
|
||||
|
||||
if (listingsGrid) {
|
||||
listingsGrid.innerHTML = this.renderListings()
|
||||
}
|
||||
|
||||
if (sectionTitle) {
|
||||
sectionTitle.textContent = this.getListingsTitle()
|
||||
}
|
||||
}
|
||||
|
||||
getListingsTitle() {
|
||||
if (this.currentFilters.query || this.currentFilters.category) {
|
||||
const count = this.listings.length
|
||||
return t('search.resultsCount', { count }) || `${count} Ergebnisse`
|
||||
}
|
||||
return t('home.recentListings')
|
||||
}
|
||||
|
||||
render() {
|
||||
this.innerHTML = /* html */`
|
||||
<section class="search-section">
|
||||
<search-box></search-box>
|
||||
<search-box no-navigate></search-box>
|
||||
</section>
|
||||
|
||||
<section class="recent-listings">
|
||||
<h2>${t('home.recentListings')}</h2>
|
||||
<h2 class="listings-title">${this.getListingsTitle()}</h2>
|
||||
<div class="listings-grid">
|
||||
${this.renderListings()}
|
||||
</div>
|
||||
@@ -57,21 +108,21 @@ class PageHome extends HTMLElement {
|
||||
|
||||
renderListings() {
|
||||
if (this.loading) {
|
||||
return /* html */`<p class="loading-text">${t('common.loading') || 'Laden...'}</p>`
|
||||
return /* html */`<p class="loading-text">${t('common.loading')}</p>`
|
||||
}
|
||||
|
||||
if (this.error) {
|
||||
return /* html */`<p class="error-text">${t('common.error') || 'Fehler beim Laden'}</p>`
|
||||
return /* html */`<p class="error-text">${t('common.error')}</p>`
|
||||
}
|
||||
|
||||
if (this.listings.length === 0) {
|
||||
return /* html */`<p class="empty-text">${t('home.noListings') || 'Keine Anzeigen gefunden'}</p>`
|
||||
return /* html */`<p class="empty-text">${t('home.noListings')}</p>`
|
||||
}
|
||||
|
||||
return this.listings.map(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 || listing.location || ''
|
||||
const locationName = listing.location?.name || ''
|
||||
|
||||
return /* html */`
|
||||
<listing-card
|
||||
@@ -97,12 +148,21 @@ style.textContent = /* css */`
|
||||
}
|
||||
|
||||
/* Listings */
|
||||
page-home section {
|
||||
page-home .recent-listings {
|
||||
margin-bottom: var(--space-xl);
|
||||
}
|
||||
|
||||
page-home section h2 {
|
||||
page-home .listings-title {
|
||||
margin-bottom: var(--space-lg);
|
||||
}
|
||||
|
||||
page-home .loading-text,
|
||||
page-home .error-text,
|
||||
page-home .empty-text {
|
||||
text-align: center;
|
||||
padding: var(--space-xl);
|
||||
color: var(--color-text-muted);
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
`
|
||||
document.head.appendChild(style)
|
||||
|
||||
@@ -30,6 +30,9 @@ class PageListing extends HTMLElement {
|
||||
this.listing = await directus.getListing(this.listingId)
|
||||
this.loadFavoriteState()
|
||||
|
||||
// Increment view counter (async, don't wait)
|
||||
directus.incrementListingViews(this.listingId)
|
||||
|
||||
// Load other listings from same seller
|
||||
if (this.listing?.user_created) {
|
||||
await this.loadSellerListings()
|
||||
@@ -159,6 +162,7 @@ class PageListing extends HTMLElement {
|
||||
<div class="listing-meta">
|
||||
${this.listing.condition ? `<span class="meta-item">${this.getConditionLabel(this.listing.condition)}</span>` : ''}
|
||||
${this.listing.shipping ? `<span class="meta-item">📦 ${t('listing.shippingAvailable')}</span>` : ''}
|
||||
<span class="meta-item views-item"><span class="views-icon">👁</span> ${this.formatViews(this.listing.views || 0)}</span>
|
||||
<span class="meta-item meta-date">${t('listing.postedOn')} ${createdDate}</span>
|
||||
</div>
|
||||
</header>
|
||||
@@ -442,6 +446,13 @@ class PageListing extends HTMLElement {
|
||||
return labels[condition] || condition
|
||||
}
|
||||
|
||||
formatViews(count) {
|
||||
if (count === 1) {
|
||||
return `1 ${t('listing.viewSingular')}`
|
||||
}
|
||||
return `${count} ${t('listing.viewPlural')}`
|
||||
}
|
||||
|
||||
formatDescription(text) {
|
||||
if (!text) return ''
|
||||
return text
|
||||
@@ -605,6 +616,10 @@ style.textContent = /* css */`
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
page-listing .views-icon {
|
||||
filter: grayscale(1);
|
||||
}
|
||||
|
||||
page-listing .listing-description,
|
||||
page-listing .listing-location-section {
|
||||
margin-bottom: var(--space-xl);
|
||||
|
||||
@@ -337,8 +337,8 @@ class SearchBox extends HTMLElement {
|
||||
this.selectedCategory = ''
|
||||
this.selectedSubcategory = ''
|
||||
this._expandedCategory = ''
|
||||
this.saveFiltersToStorage()
|
||||
categoryMenu?.classList.remove('open')
|
||||
this.emitFilterChange()
|
||||
this.render()
|
||||
this.setupEventListeners()
|
||||
})
|
||||
@@ -350,8 +350,8 @@ class SearchBox extends HTMLElement {
|
||||
this.selectedCategory = item.dataset.category
|
||||
this.selectedSubcategory = item.dataset.subcategory
|
||||
this._expandedCategory = ''
|
||||
this.saveFiltersToStorage()
|
||||
categoryMenu?.classList.remove('open')
|
||||
this.emitFilterChange()
|
||||
this.render()
|
||||
this.setupEventListeners()
|
||||
})
|
||||
@@ -366,7 +366,7 @@ class SearchBox extends HTMLElement {
|
||||
this.useCurrentLocation = false
|
||||
this.selectedCountry = e.target.value
|
||||
}
|
||||
this.saveFiltersToStorage()
|
||||
this.emitFilterChange()
|
||||
this.render()
|
||||
this.setupEventListeners()
|
||||
}
|
||||
@@ -377,7 +377,7 @@ class SearchBox extends HTMLElement {
|
||||
// Radius select handler (both desktop and mobile)
|
||||
const handleRadiusChange = (e) => {
|
||||
this.selectedRadius = parseInt(e.target.value)
|
||||
this.saveFiltersToStorage()
|
||||
this.emitFilterChange()
|
||||
}
|
||||
|
||||
radiusSelect?.addEventListener('change', handleRadiusChange)
|
||||
@@ -410,6 +410,28 @@ class SearchBox extends HTMLElement {
|
||||
measurer.remove()
|
||||
}
|
||||
|
||||
getFilterDetails() {
|
||||
return {
|
||||
query: this.searchQuery,
|
||||
category: this.selectedCategory,
|
||||
subcategory: this.selectedSubcategory,
|
||||
country: this.selectedCountry,
|
||||
useCurrentLocation: this.useCurrentLocation,
|
||||
lat: this.currentLat,
|
||||
lng: this.currentLng,
|
||||
radius: this.selectedRadius
|
||||
}
|
||||
}
|
||||
|
||||
emitFilterChange() {
|
||||
this.saveFiltersToStorage()
|
||||
|
||||
this.dispatchEvent(new CustomEvent('filter-change', {
|
||||
bubbles: true,
|
||||
detail: this.getFilterDetails()
|
||||
}))
|
||||
}
|
||||
|
||||
handleSearch() {
|
||||
const params = new URLSearchParams()
|
||||
|
||||
@@ -432,14 +454,7 @@ class SearchBox extends HTMLElement {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
detail: {
|
||||
query: this.searchQuery,
|
||||
category: this.selectedCategory,
|
||||
subcategory: this.selectedSubcategory,
|
||||
country: this.selectedCountry,
|
||||
useCurrentLocation: this.useCurrentLocation,
|
||||
lat: this.currentLat,
|
||||
lng: this.currentLng,
|
||||
radius: this.selectedRadius,
|
||||
...this.getFilterDetails(),
|
||||
params: params.toString()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -331,6 +331,34 @@ class DirectusService {
|
||||
return response.data
|
||||
}
|
||||
|
||||
async incrementListingViews(id) {
|
||||
// Check if user already viewed this listing (session-based)
|
||||
const viewedKey = `dgray_viewed_${id}`
|
||||
if (sessionStorage.getItem(viewedKey)) {
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
// Get current views
|
||||
const listing = await this.get(`/items/listings/${id}`, {
|
||||
fields: ['views']
|
||||
})
|
||||
const currentViews = listing.data?.views || 0
|
||||
|
||||
// Increment views
|
||||
await this.patch(`/items/listings/${id}`, {
|
||||
views: currentViews + 1
|
||||
})
|
||||
|
||||
// Mark as viewed for this session
|
||||
sessionStorage.setItem(viewedKey, 'true')
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Failed to increment views:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async deleteListing(id) {
|
||||
return this.delete(`/items/listings/${id}`)
|
||||
}
|
||||
|
||||
@@ -116,6 +116,8 @@
|
||||
"contactHint": "Kopiere die Adresse und sende den Betrag über dein Monero-Wallet.",
|
||||
"priceOnRequest": "Preis auf Anfrage",
|
||||
"shippingAvailable": "Versand verfügbar",
|
||||
"viewSingular": "Aufruf",
|
||||
"viewPlural": "Aufrufe",
|
||||
"share": "Teilen",
|
||||
"report": "Melden",
|
||||
"moreFromSeller": "Weitere Anzeigen des Anbieters"
|
||||
|
||||
@@ -116,6 +116,8 @@
|
||||
"contactHint": "Copy the address and send the amount using your Monero wallet.",
|
||||
"priceOnRequest": "Price on request",
|
||||
"shippingAvailable": "Shipping available",
|
||||
"viewSingular": "view",
|
||||
"viewPlural": "views",
|
||||
"share": "Share",
|
||||
"report": "Report",
|
||||
"moreFromSeller": "More from this seller"
|
||||
|
||||
@@ -116,6 +116,8 @@
|
||||
"contactHint": "Copiez l'adresse et envoyez le montant via votre portefeuille Monero.",
|
||||
"priceOnRequest": "Prix sur demande",
|
||||
"shippingAvailable": "Livraison disponible",
|
||||
"viewSingular": "vue",
|
||||
"viewPlural": "vues",
|
||||
"share": "Partager",
|
||||
"report": "Signaler",
|
||||
"moreFromSeller": "Autres annonces du vendeur"
|
||||
|
||||
Reference in New Issue
Block a user