add image placeholder; add dialog in listing
This commit is contained in:
@@ -58,9 +58,19 @@ class ListingCard extends HTMLElement {
|
||||
const priceDisplay = price ? formatPrice(parseFloat(price), currency) : '–';
|
||||
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>
|
||||
`;
|
||||
|
||||
this.innerHTML = /* html */`
|
||||
<a href="#/listing/${escapeHTML(id)}" class="listing-link">
|
||||
<div class="listing-image" ${image ? `style="background-image: url('${escapeHTML(image)}')"` : ''}></div>
|
||||
<div class="listing-image" ${image ? `style="background-image: url('${escapeHTML(image)}')"` : ''}>
|
||||
${!image ? placeholderSvg : ''}
|
||||
</div>
|
||||
<div class="listing-info">
|
||||
<h3 class="listing-title">${escapeHTML(title)}</h3>
|
||||
<p class="listing-price">${priceDisplay}</p>
|
||||
@@ -138,6 +148,15 @@ style.textContent = /* css */`
|
||||
background: var(--color-bg-tertiary);
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
listing-card .listing-image .placeholder-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
color: var(--color-border);
|
||||
}
|
||||
|
||||
listing-card .listing-info {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { t, i18n } from '../../i18n.js';
|
||||
import { mockListings } from '../../data/mock-listings.js';
|
||||
import '../listing-card.js';
|
||||
import '../search-box.js';
|
||||
|
||||
@@ -21,20 +22,19 @@ class PageHome extends HTMLElement {
|
||||
<section class="recent-listings">
|
||||
<h2>${t('home.recentListings')}</h2>
|
||||
<div class="listings-grid">
|
||||
${this.renderPlaceholderListings()}
|
||||
${this.renderListings()}
|
||||
</div>
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
|
||||
renderPlaceholderListings() {
|
||||
const placeholders = Array(10).fill(null);
|
||||
return placeholders.map((_, i) => /* html */`
|
||||
renderListings() {
|
||||
return mockListings.map(listing => /* html */`
|
||||
<listing-card
|
||||
listing-id="${i + 1}"
|
||||
title="${t('home.placeholderTitle')}"
|
||||
price="699"
|
||||
location="${t('home.placeholderLocation')}"
|
||||
listing-id="${listing.id}"
|
||||
title="${listing.title}"
|
||||
price="${listing.price}"
|
||||
location="${listing.location}"
|
||||
></listing-card>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { t, i18n } from '../../i18n.js';
|
||||
import { getListingById } from '../../data/mock-listings.js';
|
||||
|
||||
class PageListing extends HTMLElement {
|
||||
constructor() {
|
||||
@@ -21,20 +22,7 @@ class PageListing extends HTMLElement {
|
||||
async loadListing() {
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
|
||||
this.listing = {
|
||||
id: this.listingId,
|
||||
title: 'iPhone 13 Pro - Sehr guter Zustand',
|
||||
description: 'Verkaufe mein iPhone 13 Pro in sehr gutem Zustand. Das Gerät hat keine Kratzer und funktioniert einwandfrei. Originalverpackung und Ladekabel sind dabei.',
|
||||
price: 699,
|
||||
location: 'Berlin, Mitte',
|
||||
category: 'electronics',
|
||||
createdAt: new Date().toISOString(),
|
||||
seller: {
|
||||
name: 'Max M.',
|
||||
memberSince: '2023'
|
||||
},
|
||||
images: []
|
||||
};
|
||||
this.listing = getListingById(this.listingId);
|
||||
|
||||
this.loading = false;
|
||||
this.render();
|
||||
@@ -61,10 +49,21 @@ class PageListing extends HTMLElement {
|
||||
return;
|
||||
}
|
||||
|
||||
const hasImages = this.listing.images && this.listing.images.length > 0;
|
||||
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>
|
||||
`;
|
||||
|
||||
this.innerHTML = /* html */`
|
||||
<article class="listing-detail">
|
||||
<div class="listing-gallery">
|
||||
<div class="listing-image-main"></div>
|
||||
<div class="listing-image-main">
|
||||
${!hasImages ? placeholderSvg : ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="listing-info">
|
||||
@@ -92,13 +91,72 @@ class PageListing extends HTMLElement {
|
||||
</section>
|
||||
|
||||
<div class="listing-actions">
|
||||
<button class="btn btn-primary btn-lg">
|
||||
<button class="btn btn-primary btn-lg" id="contact-btn">
|
||||
${t('listing.contactSeller')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<!-- Contact Dialog -->
|
||||
<dialog class="contact-dialog" id="contact-dialog">
|
||||
<button class="dialog-close" id="dialog-close" aria-label="Close">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
<h2>${t('listing.contactSeller')}</h2>
|
||||
<p class="dialog-subtitle">${t('listing.paymentInfo')}</p>
|
||||
|
||||
<div class="monero-section">
|
||||
<label>${t('listing.moneroAddress')}</label>
|
||||
<div class="monero-address">
|
||||
<code id="monero-addr">${this.listing.seller.moneroAddress || '888tNkZrPN6JsEgekjMnABU4TBzc2Dt29EPAvkRxbANsAnjyPbb3iQ1YBRk1UXcdRsiKc9dhwMVgN5S9cQUiyoogDavup3H'}</code>
|
||||
<button class="btn btn-outline btn-copy" id="copy-btn" title="${t('listing.copyAddress')}">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
||||
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="dialog-hint">${t('listing.contactHint')}</p>
|
||||
</dialog>
|
||||
`;
|
||||
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
const contactBtn = this.querySelector('#contact-btn');
|
||||
const dialog = this.querySelector('#contact-dialog');
|
||||
const closeBtn = this.querySelector('#dialog-close');
|
||||
const copyBtn = this.querySelector('#copy-btn');
|
||||
|
||||
contactBtn?.addEventListener('click', () => {
|
||||
dialog?.showModal();
|
||||
});
|
||||
|
||||
closeBtn?.addEventListener('click', () => {
|
||||
dialog?.close();
|
||||
});
|
||||
|
||||
dialog?.addEventListener('click', (e) => {
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
escapeHtml(text) {
|
||||
@@ -134,6 +192,15 @@ style.textContent = /* css */`
|
||||
page-listing .listing-image-main {
|
||||
aspect-ratio: 4 / 3;
|
||||
background: var(--color-bg-tertiary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
page-listing .listing-image-main .placeholder-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
color: var(--color-border);
|
||||
}
|
||||
|
||||
page-listing .listing-info header {
|
||||
@@ -205,5 +272,92 @@ style.textContent = /* css */`
|
||||
page-listing .listing-actions .btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Dialog */
|
||||
page-listing .contact-dialog {
|
||||
background: var(--color-bg);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--space-xl);
|
||||
max-width: 500px;
|
||||
width: calc(100% - 2 * var(--space-md));
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
page-listing .contact-dialog::backdrop {
|
||||
background: var(--color-overlay);
|
||||
}
|
||||
|
||||
page-listing .dialog-close {
|
||||
position: absolute;
|
||||
top: var(--space-md);
|
||||
right: var(--space-md);
|
||||
padding: var(--space-xs);
|
||||
color: var(--color-text-muted);
|
||||
transition: color var(--transition-fast);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
page-listing .monero-section {
|
||||
margin-bottom: var(--space-lg);
|
||||
}
|
||||
|
||||
page-listing .monero-section label {
|
||||
display: block;
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-medium);
|
||||
margin-bottom: var(--space-sm);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
page-listing .monero-address {
|
||||
display: flex;
|
||||
gap: var(--space-sm);
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
page-listing .monero-address code {
|
||||
flex: 1;
|
||||
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);
|
||||
word-break: break-all;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
page-listing .btn-copy {
|
||||
padding: var(--space-sm);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
page-listing .btn-copy.copied {
|
||||
background: var(--color-success);
|
||||
border-color: var(--color-success);
|
||||
color: white;
|
||||
}
|
||||
|
||||
page-listing .dialog-hint {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-text-muted);
|
||||
text-align: center;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { t, i18n } from '../../i18n.js';
|
||||
import { router } from '../../router.js';
|
||||
import { searchListings } from '../../data/mock-listings.js';
|
||||
import '../search-box.js';
|
||||
import '../listing-card.js';
|
||||
|
||||
@@ -101,14 +102,7 @@ class PageSearch extends HTMLElement {
|
||||
}
|
||||
|
||||
getMockResults() {
|
||||
return [
|
||||
{ id: 1, title: 'iPhone 13 Pro', price: 699, location: 'Berlin' },
|
||||
{ id: 2, title: 'Vintage Sofa', price: 250, location: 'München' },
|
||||
{ id: 3, title: 'Mountain Bike', price: 450, location: 'Hamburg' },
|
||||
{ id: 4, title: 'Gaming PC', price: 1200, location: 'Köln' },
|
||||
{ id: 5, title: 'Schreibtisch', price: 80, location: 'Zürich' },
|
||||
{ id: 6, title: 'Winterjacke', price: 45, location: 'Wien' },
|
||||
];
|
||||
return searchListings(this.query, this.category, this.subcategory);
|
||||
}
|
||||
|
||||
updateResults() {
|
||||
|
||||
167
js/data/mock-listings.js
Normal file
167
js/data/mock-listings.js
Normal file
@@ -0,0 +1,167 @@
|
||||
export const mockListings = [
|
||||
{
|
||||
id: '1',
|
||||
title: 'iPhone 13 Pro - Sehr guter Zustand',
|
||||
description: 'Verkaufe mein iPhone 13 Pro in sehr gutem Zustand. Das Gerät hat keine Kratzer und funktioniert einwandfrei. Originalverpackung und Ladekabel sind dabei.',
|
||||
price: 699,
|
||||
location: 'Berlin, Mitte',
|
||||
category: 'electronics',
|
||||
subcategory: 'phones',
|
||||
createdAt: '2026-01-27T10:00:00Z',
|
||||
seller: {
|
||||
name: 'Max M.',
|
||||
memberSince: '2023',
|
||||
moneroAddress: '888tNkZrPN6JsEgekjMnABU4TBzc2Dt29EPAvkRxbANsAnjyPbb3iQ1YBRk1UXcdRsiKc9dhwMVgN5S9cQUiyoogDavup3H'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'Vintage Ledersofa 3-Sitzer',
|
||||
description: 'Wunderschönes Vintage-Ledersofa aus den 70er Jahren. Cognacfarben, leichte Patina die dem Stück Charakter verleiht. Sehr bequem.',
|
||||
price: 450,
|
||||
location: 'München, Schwabing',
|
||||
category: 'furniture',
|
||||
subcategory: 'living',
|
||||
createdAt: '2026-01-26T14:30:00Z',
|
||||
seller: {
|
||||
name: 'Anna K.',
|
||||
memberSince: '2024',
|
||||
moneroAddress: '47sghzufGhJJDQEbScMCwVBimTuq6L5JiRixD8VeGbpjCTA12GwZVPWzjmpfLDJNDAWvuNDAWvuNDAWvuNDAWvuN'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
title: 'Canyon Mountainbike 29 Zoll',
|
||||
description: 'Canyon Spectral AL 6.0 in Größe L. Carbon-Rahmen, Shimano XT Schaltung, frisch gewartet. Ideal für Trails und Touren.',
|
||||
price: 1200,
|
||||
location: 'Zürich',
|
||||
category: 'sports',
|
||||
subcategory: 'outdoor',
|
||||
createdAt: '2026-01-25T09:15:00Z',
|
||||
seller: {
|
||||
name: 'Thomas B.',
|
||||
memberSince: '2022',
|
||||
moneroAddress: '44AFFq5kSiGBoZ4NMDwYtN18obc8AemS33DBLWs3H7otXft3XjrpDtQGv7SqSsaBYBb98uNbr2VBBEt7f2wfn3RVGQBEP3A'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
title: 'Gaming PC - RTX 4070, Ryzen 7',
|
||||
description: 'Selbstgebauter Gaming-PC: Ryzen 7 5800X, RTX 4070, 32GB RAM, 1TB NVMe SSD. Perfekt für 1440p Gaming. RGB-Beleuchtung.',
|
||||
price: 1450,
|
||||
location: 'Hamburg',
|
||||
category: 'electronics',
|
||||
subcategory: 'gaming',
|
||||
createdAt: '2026-01-24T16:45:00Z',
|
||||
seller: {
|
||||
name: 'Felix R.',
|
||||
memberSince: '2023',
|
||||
moneroAddress: '48iWMy1PH6VGBJVvHDg9mY7mJ6vBDWVHpGgXEtCGp99kT4Xk5QfN3v7nqMrqGpvU'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
title: 'Ikea MALM Schreibtisch weiß',
|
||||
description: 'Ikea MALM Schreibtisch in weiß, 140x65cm. Minimale Gebrauchsspuren, Kabelmanagement integriert. Selbstabholung.',
|
||||
price: 80,
|
||||
location: 'Wien, 1050',
|
||||
category: 'furniture',
|
||||
subcategory: 'office',
|
||||
createdAt: '2026-01-23T11:20:00Z',
|
||||
seller: {
|
||||
name: 'Lisa S.',
|
||||
memberSince: '2025',
|
||||
moneroAddress: '45dEQp8dFKrMXKvFWJFmZKCZhH3ARYMW4MJYM9FJcPuNT5Kek9R'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
title: 'Canada Goose Winterjacke M',
|
||||
description: 'Original Canada Goose Expedition Parka in Schwarz, Größe M. Sehr warm, perfekt für extreme Kälte. NP 1200€.',
|
||||
price: 550,
|
||||
location: 'Köln',
|
||||
category: 'clothing',
|
||||
subcategory: 'men',
|
||||
createdAt: '2026-01-22T08:00:00Z',
|
||||
seller: {
|
||||
name: 'Jan P.',
|
||||
memberSince: '2024',
|
||||
moneroAddress: '42nTNQp8dFKrMXKvFWJFmZKCZhH3ARYMW4MJYM9FJcPuNT5Kek9R'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '7',
|
||||
title: 'Sony A7 III Kamera + 24-70mm',
|
||||
description: 'Sony A7 III Vollformat-Kamera mit Sony 24-70mm f/2.8 GM Objektiv. 15.000 Auslösungen, einwandfreier Zustand.',
|
||||
price: 2200,
|
||||
location: 'Frankfurt',
|
||||
category: 'electronics',
|
||||
subcategory: 'tv_audio',
|
||||
createdAt: '2026-01-21T13:10:00Z',
|
||||
seller: {
|
||||
name: 'Sarah M.',
|
||||
memberSince: '2021',
|
||||
moneroAddress: '46BeWrHpwXmHDpDEUmZBWZfoQpdc6HaERCNmx1pEYL2rAcuwufPN9rXHHtyUA4QVy24G3euoGWpzR7T3'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '8',
|
||||
title: 'Elektrische Gitarre Fender Strat',
|
||||
description: 'Fender Stratocaster Player Series in Sunburst. Ahorn-Hals, 3 Single-Coil Pickups. Inkl. Gigbag.',
|
||||
price: 650,
|
||||
location: 'Stuttgart',
|
||||
category: 'other',
|
||||
subcategory: 'art',
|
||||
createdAt: '2026-01-20T17:30:00Z',
|
||||
seller: {
|
||||
name: 'Michael W.',
|
||||
memberSince: '2022',
|
||||
moneroAddress: '43gFrHpwXmHDpDEUmZBWZfoQpdc6HaERCNmx1pEYL2rAcuwufPN9rXHHtyUA4QVy24G3euoGWpzR7T3'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '9',
|
||||
title: 'Weber Gasgrill Genesis II',
|
||||
description: 'Weber Genesis II E-310 Gasgrill in Schwarz. 3 Brenner, Sear Station, iGrill kompatibel. Wenig benutzt.',
|
||||
price: 480,
|
||||
location: 'Düsseldorf',
|
||||
category: 'garden',
|
||||
subcategory: 'outdoor_living',
|
||||
createdAt: '2026-01-19T10:45:00Z',
|
||||
seller: {
|
||||
name: 'Klaus H.',
|
||||
memberSince: '2023',
|
||||
moneroAddress: '47sghzufGhJJDQEbScMCwVBimTuq6L5JiRixD8VeGbpjCTA12GwZVPWzjmpfLDJNDAWvuNDAWvuNDAWvuNDAWvuN'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '10',
|
||||
title: 'Adidas Ultra Boost 22 Gr. 43',
|
||||
description: 'Adidas Ultra Boost 22 Laufschuhe in Core Black, Größe 43. Nur wenige Male getragen, wie neu.',
|
||||
price: 95,
|
||||
location: 'Bern',
|
||||
category: 'clothing',
|
||||
subcategory: 'shoes',
|
||||
createdAt: '2026-01-18T15:20:00Z',
|
||||
seller: {
|
||||
name: 'Nina L.',
|
||||
memberSince: '2024',
|
||||
moneroAddress: '44AFFq5kSiGBoZ4NMDwYtN18obc8AemS33DBLWs3H7otXft3XjrpDtQGv7SqSsaBYBb98uNbr2VBBEt7f2wfn3RVGQBEP3A'
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
export function getListingById(id) {
|
||||
return mockListings.find(l => l.id === id) || null;
|
||||
}
|
||||
|
||||
export function searchListings(query = '', category = '', subcategory = '') {
|
||||
return mockListings.filter(listing => {
|
||||
const matchesQuery = !query ||
|
||||
listing.title.toLowerCase().includes(query.toLowerCase()) ||
|
||||
listing.description.toLowerCase().includes(query.toLowerCase());
|
||||
const matchesCategory = !category || listing.category === category;
|
||||
const matchesSubcategory = !subcategory || listing.subcategory === subcategory;
|
||||
return matchesQuery && matchesCategory && matchesSubcategory;
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user