improve page-create, service-worker and manifest

This commit is contained in:
2026-01-28 08:28:36 +01:00
parent 956cdacd3f
commit 21e05df241
6 changed files with 188 additions and 14 deletions

View File

@@ -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,12 +107,29 @@ class PageCreate extends HTMLElement {
<label class="label" data-i18n="create.images">${t('create.images')}</label>
<div class="image-upload">
<input type="file" id="images" name="images" accept="image/*" multiple hidden>
<label for="images" class="upload-area">
<label for="images" class="upload-area" id="upload-area">
<span class="upload-icon">📷</span>
<span data-i18n="create.uploadImages">${t('create.uploadImages')}</span>
</label>
<div class="image-previews" id="image-previews">
${this.renderImagePreviews()}
</div>
</div>
</div>
<div class="form-group">
<label class="label" for="moneroAddress">${t('create.moneroAddress')}</label>
<input
type="text"
class="input"
id="moneroAddress"
name="moneroAddress"
value="${this.escapeHtml(this.formData.moneroAddress)}"
required
placeholder="${t('create.moneroPlaceholder')}"
>
<p class="field-hint">${t('create.moneroHint')}</p>
</div>
<div class="form-actions">
<button type="button" class="btn btn-outline btn-lg" id="cancel-btn">
@@ -128,13 +149,76 @@ class PageCreate extends HTMLElement {
setupEventListeners() {
const form = this.querySelector('#create-form');
const cancelBtn = this.querySelector('#cancel-btn');
const imageInput = this.querySelector('#images');
form.addEventListener('submit', (e) => this.handleSubmit(e));
cancelBtn.addEventListener('click', () => router.back());
form.querySelectorAll('input, textarea, select').forEach(input => {
input.addEventListener('input', (e) => {
if (e.target.name) {
this.formData[e.target.name] = e.target.value;
}
});
});
imageInput?.addEventListener('change', (e) => this.handleImageSelect(e));
}
handleImageSelect(e) {
const files = Array.from(e.target.files);
files.forEach(file => {
if (this.imageFiles.length >= 5) return;
this.imageFiles.push(file);
const reader = new FileReader();
reader.onload = (event) => {
this.imagePreviews.push(event.target.result);
this.updateImagePreviews();
};
reader.readAsDataURL(file);
});
}
updateImagePreviews() {
const container = this.querySelector('#image-previews');
const uploadArea = this.querySelector('#upload-area');
if (container) {
container.innerHTML = this.renderImagePreviews();
this.setupRemoveListeners();
}
if (uploadArea) {
uploadArea.style.display = this.imageFiles.length >= 5 ? 'none' : 'flex';
}
}
renderImagePreviews() {
if (this.imagePreviews.length === 0) return '';
return this.imagePreviews.map((src, index) => /* html */`
<div class="image-preview">
<img src="${src}" alt="Preview ${index + 1}">
<button type="button" class="remove-image" data-index="${index}" aria-label="Remove">
<svg width="16" height="16" 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>
`).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);

View File

@@ -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..."

View File

@@ -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..."

View File

@@ -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..."

View File

@@ -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",

View File

@@ -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;
}