427 lines
14 KiB
JavaScript
427 lines
14 KiB
JavaScript
import { t, i18n } from '../../i18n.js';
|
|
import { getListingById } from '../../data/mock-listings.js';
|
|
import '../chat-widget.js';
|
|
|
|
class PageListing extends HTMLElement {
|
|
constructor() {
|
|
super();
|
|
this.listing = null;
|
|
this.loading = true;
|
|
}
|
|
|
|
connectedCallback() {
|
|
this.listingId = this.dataset.id;
|
|
this.render();
|
|
this.loadListing();
|
|
this.unsubscribe = i18n.subscribe(() => this.render());
|
|
}
|
|
|
|
disconnectedCallback() {
|
|
if (this.unsubscribe) this.unsubscribe();
|
|
}
|
|
|
|
async loadListing() {
|
|
await new Promise(resolve => setTimeout(resolve, 300));
|
|
|
|
this.listing = getListingById(this.listingId);
|
|
|
|
this.loading = false;
|
|
this.render();
|
|
}
|
|
|
|
render() {
|
|
if (this.loading) {
|
|
this.innerHTML = /* html */`
|
|
<div class="loading">
|
|
<div class="spinner"></div>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
if (!this.listing) {
|
|
this.innerHTML = /* html */`
|
|
<div class="empty-state">
|
|
<div class="empty-state-icon">😕</div>
|
|
<p data-i18n="listing.notFound">${t('listing.notFound')}</p>
|
|
<a href="#/" class="btn btn-primary">${t('listing.backHome')}</a>
|
|
</div>
|
|
`;
|
|
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">
|
|
${!hasImages ? placeholderSvg : ''}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="listing-info">
|
|
<header>
|
|
<span class="badge badge-primary">${t(`categories.${this.listing.category}`)}</span>
|
|
<h1>${this.escapeHtml(this.listing.title)}</h1>
|
|
<p class="listing-price">€ ${this.listing.price}</p>
|
|
<p class="listing-location">📍 ${this.escapeHtml(this.listing.location)}</p>
|
|
</header>
|
|
|
|
<section class="listing-description">
|
|
<h2 data-i18n="listing.description">${t('listing.description')}</h2>
|
|
<p>${this.escapeHtml(this.listing.description)}</p>
|
|
</section>
|
|
|
|
<section class="listing-seller">
|
|
<h2 data-i18n="listing.seller">${t('listing.seller')}</h2>
|
|
<div class="seller-card">
|
|
<div class="seller-avatar">${this.listing.seller.name.charAt(0)}</div>
|
|
<div class="seller-info">
|
|
<strong>${this.escapeHtml(this.listing.seller.name)}</strong>
|
|
<span>${t('listing.memberSince')} ${this.listing.seller.memberSince}</span>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<div class="listing-actions">
|
|
<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>
|
|
|
|
<div class="dialog-tabs">
|
|
<button class="tab-btn active" data-tab="chat">${t('chat.title')}</button>
|
|
<button class="tab-btn" data-tab="payment">${t('listing.moneroAddress')}</button>
|
|
</div>
|
|
|
|
<div class="tab-content" id="tab-chat">
|
|
<chat-widget
|
|
listing-id="${this.listing.id}"
|
|
recipient-id="${this.listing.seller.name}"
|
|
recipient-key="${this.listing.seller.publicKey || 'demo-key-' + this.listing.id}"
|
|
recipient-name="${this.escapeHtml(this.listing.seller.name)}"
|
|
></chat-widget>
|
|
</div>
|
|
|
|
<div class="tab-content hidden" id="tab-payment">
|
|
<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>
|
|
</div>
|
|
</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');
|
|
const tabBtns = this.querySelectorAll('.tab-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);
|
|
}
|
|
});
|
|
|
|
tabBtns.forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
const tab = btn.dataset.tab;
|
|
|
|
tabBtns.forEach(b => b.classList.remove('active'));
|
|
btn.classList.add('active');
|
|
|
|
this.querySelectorAll('.tab-content').forEach(content => {
|
|
content.classList.toggle('hidden', content.id !== `tab-${tab}`);
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
page-listing .listing-gallery {
|
|
background: var(--color-bg-secondary);
|
|
border-radius: var(--radius-lg);
|
|
overflow: hidden;
|
|
align-self: start;
|
|
}
|
|
|
|
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 {
|
|
margin-bottom: var(--space-xl);
|
|
}
|
|
|
|
page-listing .listing-info h1 {
|
|
margin: var(--space-sm) 0;
|
|
}
|
|
|
|
page-listing .listing-price {
|
|
font-size: var(--font-size-2xl);
|
|
font-weight: var(--font-weight-bold);
|
|
color: var(--color-primary);
|
|
}
|
|
|
|
page-listing .listing-location {
|
|
color: var(--color-text-secondary);
|
|
margin-top: var(--space-sm);
|
|
}
|
|
|
|
page-listing .listing-description,
|
|
page-listing .listing-seller {
|
|
margin-bottom: var(--space-xl);
|
|
}
|
|
|
|
page-listing .listing-description h2,
|
|
page-listing .listing-seller h2 {
|
|
font-size: var(--font-size-lg);
|
|
margin-bottom: var(--space-md);
|
|
}
|
|
|
|
page-listing .seller-card {
|
|
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 {
|
|
width: 48px;
|
|
height: 48px;
|
|
border-radius: 50%;
|
|
background: var(--color-primary);
|
|
color: white;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-weight: var(--font-weight-bold);
|
|
font-size: var(--font-size-lg);
|
|
}
|
|
|
|
page-listing .seller-info {
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
page-listing .seller-info span {
|
|
font-size: var(--font-size-sm);
|
|
color: var(--color-text-muted);
|
|
}
|
|
|
|
page-listing .listing-actions {
|
|
margin-top: var(--space-xl);
|
|
}
|
|
|
|
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-tabs {
|
|
display: flex;
|
|
gap: var(--space-sm);
|
|
margin-bottom: var(--space-lg);
|
|
border-bottom: 1px solid var(--color-border);
|
|
padding-bottom: var(--space-sm);
|
|
}
|
|
|
|
page-listing .tab-btn {
|
|
padding: var(--space-sm) var(--space-md);
|
|
font-size: var(--font-size-sm);
|
|
font-weight: var(--font-weight-medium);
|
|
color: var(--color-text-muted);
|
|
border-radius: var(--radius-md);
|
|
transition: all var(--transition-fast);
|
|
}
|
|
|
|
page-listing .tab-btn:hover {
|
|
color: var(--color-text);
|
|
background: var(--color-bg-secondary);
|
|
}
|
|
|
|
page-listing .tab-btn.active {
|
|
color: var(--color-primary);
|
|
background: var(--color-primary-light);
|
|
}
|
|
|
|
page-listing .tab-content.hidden {
|
|
display: none;
|
|
}
|
|
|
|
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);
|