diff --git a/js/components/pages/page-create.js b/js/components/pages/page-create.js index 6765f2e..2a1b942 100644 --- a/js/components/pages/page-create.js +++ b/js/components/pages/page-create.js @@ -1,5 +1,6 @@ import { t, i18n } from '../../i18n.js'; import { router } from '../../router.js'; +import { cryptoService } from '../../services/crypto.js'; class PageCreate extends HTMLElement { constructor() { @@ -9,8 +10,11 @@ class PageCreate extends HTMLElement { description: '', price: '', category: '', - location: '' + location: '', + moneroAddress: '' }; + this.imageFiles = []; + this.imagePreviews = []; } connectedCallback() { @@ -103,13 +107,30 @@ class PageCreate extends HTMLElement {
-
+
+ + +

${t('create.moneroHint')}

+
+
+
+ `).join(''); + } + + setupRemoveListeners() { + this.querySelectorAll('.remove-image').forEach(btn => { + btn.addEventListener('click', (e) => { + const index = parseInt(e.currentTarget.dataset.index); + this.imageFiles.splice(index, 1); + this.imagePreviews.splice(index, 1); + this.updateImagePreviews(); }); }); } @@ -209,6 +293,57 @@ style.textContent = /* css */` margin-bottom: var(--space-sm); } + page-create .image-previews { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); + gap: var(--space-sm); + padding: var(--space-sm); + } + + page-create .image-previews:empty { + display: none; + } + + page-create .image-preview { + position: relative; + aspect-ratio: 1; + border-radius: var(--radius-md); + overflow: hidden; + } + + page-create .image-preview img { + width: 100%; + height: 100%; + object-fit: cover; + } + + page-create .remove-image { + position: absolute; + top: var(--space-xs); + right: var(--space-xs); + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + background: var(--color-error); + color: white; + border-radius: var(--radius-full); + cursor: pointer; + opacity: 0; + transition: opacity var(--transition-fast); + } + + page-create .image-preview:hover .remove-image { + opacity: 1; + } + + page-create .field-hint { + font-size: var(--font-size-sm); + color: var(--color-text-muted); + margin-top: var(--space-xs); + } + page-create .form-actions { display: flex; gap: var(--space-md); diff --git a/locales/de.json b/locales/de.json index d960801..95d9048 100644 --- a/locales/de.json +++ b/locales/de.json @@ -124,7 +124,10 @@ "description": "Beschreibung", "descriptionPlaceholder": "Beschreibe deinen Artikel ausführlich...", "images": "Bilder", - "uploadImages": "Bilder hochladen", + "uploadImages": "Bilder hochladen (max. 5)", + "moneroAddress": "Deine Monero-Adresse", + "moneroPlaceholder": "4... oder 8...", + "moneroHint": "Käufer senden die Zahlung direkt an diese Adresse.", "cancel": "Abbrechen", "publish": "Veröffentlichen", "publishing": "Wird veröffentlicht..." diff --git a/locales/en.json b/locales/en.json index da53ada..7a0d8fe 100644 --- a/locales/en.json +++ b/locales/en.json @@ -124,7 +124,10 @@ "description": "Description", "descriptionPlaceholder": "Describe your item in detail...", "images": "Images", - "uploadImages": "Upload images", + "uploadImages": "Upload images (max. 5)", + "moneroAddress": "Your Monero Address", + "moneroPlaceholder": "4... or 8...", + "moneroHint": "Buyers will send payment directly to this address.", "cancel": "Cancel", "publish": "Publish", "publishing": "Publishing..." diff --git a/locales/fr.json b/locales/fr.json index ec17573..860fc68 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -124,7 +124,10 @@ "description": "Description", "descriptionPlaceholder": "Décrivez votre article en détail...", "images": "Images", - "uploadImages": "Télécharger des images", + "uploadImages": "Télécharger des images (max. 5)", + "moneroAddress": "Votre adresse Monero", + "moneroPlaceholder": "4... ou 8...", + "moneroHint": "Les acheteurs envoient le paiement directement à cette adresse.", "cancel": "Annuler", "publish": "Publier", "publishing": "Publication en cours..." diff --git a/manifest.json b/manifest.json index 96e4448..36e643b 100644 --- a/manifest.json +++ b/manifest.json @@ -1,11 +1,11 @@ { - "name": "dgray - Kleinanzeigen", + "name": "dgray.io - Marktplatz", "short_name": "dgray", - "description": "Deine lokale Kleinanzeigen-Plattform zum Tauschen und Handeln", + "description": "Anonymer Marktplatz mit Monero-Bezahlung", "start_url": "/", "display": "standalone", - "background_color": "#ffffff", - "theme_color": "#2563eb", + "background_color": "#F8F7F5", + "theme_color": "#6B7B8C", "orientation": "portrait-primary", "scope": "/", "lang": "de", diff --git a/service-worker.js b/service-worker.js index e7cf273..f49264f 100644 --- a/service-worker.js +++ b/service-worker.js @@ -1,4 +1,4 @@ -const CACHE_NAME = 'dgray-v19'; +const CACHE_NAME = 'dgray-v20'; const STATIC_ASSETS = [ '/', '/index.html', @@ -10,6 +10,12 @@ const STATIC_ASSETS = [ '/js/router.js', '/js/i18n.js', '/js/services/api.js', + '/js/services/crypto.js', + '/js/services/chat.js', + '/js/data/mock-listings.js', + '/js/components/chat-widget.js', + '/js/components/listing-card.js', + '/js/components/search-box.js', '/locales/de.json', '/locales/en.json', '/locales/fr.json', @@ -46,14 +52,24 @@ self.addEventListener('activate', (event) => { self.addEventListener('fetch', (event) => { const { request } = event; + const url = new URL(request.url); if (request.method !== 'GET') return; - if (request.url.includes('/api/')) { + // API calls: Network First + if (url.pathname.includes('/api/')) { event.respondWith(networkFirst(request)); - } else { - event.respondWith(cacheFirst(request)); + return; } + + // HTML and JS: Stale-While-Revalidate (show cached, update in background) + if (url.pathname.endsWith('.html') || url.pathname.endsWith('.js') || url.pathname === '/') { + event.respondWith(staleWhileRevalidate(request)); + return; + } + + // Everything else: Cache First + event.respondWith(cacheFirst(request)); }); async function cacheFirst(request) { @@ -89,3 +105,17 @@ async function networkFirst(request) { }); } } + +async function staleWhileRevalidate(request) { + const cache = await caches.open(CACHE_NAME); + const cached = await cache.match(request); + + const fetchPromise = fetch(request).then((response) => { + if (response.ok) { + cache.put(request, response.clone()); + } + return response; + }).catch(() => cached); + + return cached || fetchPromise; +}