improve page-create, service-worker and manifest
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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..."
|
||||
|
||||
@@ -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..."
|
||||
|
||||
@@ -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..."
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user