improve page-create, service-worker and manifest
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import { t, i18n } from '../../i18n.js';
|
import { t, i18n } from '../../i18n.js';
|
||||||
import { router } from '../../router.js';
|
import { router } from '../../router.js';
|
||||||
|
import { cryptoService } from '../../services/crypto.js';
|
||||||
|
|
||||||
class PageCreate extends HTMLElement {
|
class PageCreate extends HTMLElement {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -9,8 +10,11 @@ class PageCreate extends HTMLElement {
|
|||||||
description: '',
|
description: '',
|
||||||
price: '',
|
price: '',
|
||||||
category: '',
|
category: '',
|
||||||
location: ''
|
location: '',
|
||||||
|
moneroAddress: ''
|
||||||
};
|
};
|
||||||
|
this.imageFiles = [];
|
||||||
|
this.imagePreviews = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
@@ -103,13 +107,30 @@ class PageCreate extends HTMLElement {
|
|||||||
<label class="label" data-i18n="create.images">${t('create.images')}</label>
|
<label class="label" data-i18n="create.images">${t('create.images')}</label>
|
||||||
<div class="image-upload">
|
<div class="image-upload">
|
||||||
<input type="file" id="images" name="images" accept="image/*" multiple hidden>
|
<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 class="upload-icon">📷</span>
|
||||||
<span data-i18n="create.uploadImages">${t('create.uploadImages')}</span>
|
<span data-i18n="create.uploadImages">${t('create.uploadImages')}</span>
|
||||||
</label>
|
</label>
|
||||||
|
<div class="image-previews" id="image-previews">
|
||||||
|
${this.renderImagePreviews()}
|
||||||
|
</div>
|
||||||
</div>
|
</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">
|
<div class="form-actions">
|
||||||
<button type="button" class="btn btn-outline btn-lg" id="cancel-btn">
|
<button type="button" class="btn btn-outline btn-lg" id="cancel-btn">
|
||||||
${t('create.cancel')}
|
${t('create.cancel')}
|
||||||
@@ -128,13 +149,76 @@ class PageCreate extends HTMLElement {
|
|||||||
setupEventListeners() {
|
setupEventListeners() {
|
||||||
const form = this.querySelector('#create-form');
|
const form = this.querySelector('#create-form');
|
||||||
const cancelBtn = this.querySelector('#cancel-btn');
|
const cancelBtn = this.querySelector('#cancel-btn');
|
||||||
|
const imageInput = this.querySelector('#images');
|
||||||
|
|
||||||
form.addEventListener('submit', (e) => this.handleSubmit(e));
|
form.addEventListener('submit', (e) => this.handleSubmit(e));
|
||||||
cancelBtn.addEventListener('click', () => router.back());
|
cancelBtn.addEventListener('click', () => router.back());
|
||||||
|
|
||||||
form.querySelectorAll('input, textarea, select').forEach(input => {
|
form.querySelectorAll('input, textarea, select').forEach(input => {
|
||||||
input.addEventListener('input', (e) => {
|
input.addEventListener('input', (e) => {
|
||||||
this.formData[e.target.name] = e.target.value;
|
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);
|
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 {
|
page-create .form-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--space-md);
|
gap: var(--space-md);
|
||||||
|
|||||||
@@ -124,7 +124,10 @@
|
|||||||
"description": "Beschreibung",
|
"description": "Beschreibung",
|
||||||
"descriptionPlaceholder": "Beschreibe deinen Artikel ausführlich...",
|
"descriptionPlaceholder": "Beschreibe deinen Artikel ausführlich...",
|
||||||
"images": "Bilder",
|
"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",
|
"cancel": "Abbrechen",
|
||||||
"publish": "Veröffentlichen",
|
"publish": "Veröffentlichen",
|
||||||
"publishing": "Wird veröffentlicht..."
|
"publishing": "Wird veröffentlicht..."
|
||||||
|
|||||||
@@ -124,7 +124,10 @@
|
|||||||
"description": "Description",
|
"description": "Description",
|
||||||
"descriptionPlaceholder": "Describe your item in detail...",
|
"descriptionPlaceholder": "Describe your item in detail...",
|
||||||
"images": "Images",
|
"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",
|
"cancel": "Cancel",
|
||||||
"publish": "Publish",
|
"publish": "Publish",
|
||||||
"publishing": "Publishing..."
|
"publishing": "Publishing..."
|
||||||
|
|||||||
@@ -124,7 +124,10 @@
|
|||||||
"description": "Description",
|
"description": "Description",
|
||||||
"descriptionPlaceholder": "Décrivez votre article en détail...",
|
"descriptionPlaceholder": "Décrivez votre article en détail...",
|
||||||
"images": "Images",
|
"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",
|
"cancel": "Annuler",
|
||||||
"publish": "Publier",
|
"publish": "Publier",
|
||||||
"publishing": "Publication en cours..."
|
"publishing": "Publication en cours..."
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "dgray - Kleinanzeigen",
|
"name": "dgray.io - Marktplatz",
|
||||||
"short_name": "dgray",
|
"short_name": "dgray",
|
||||||
"description": "Deine lokale Kleinanzeigen-Plattform zum Tauschen und Handeln",
|
"description": "Anonymer Marktplatz mit Monero-Bezahlung",
|
||||||
"start_url": "/",
|
"start_url": "/",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
"background_color": "#ffffff",
|
"background_color": "#F8F7F5",
|
||||||
"theme_color": "#2563eb",
|
"theme_color": "#6B7B8C",
|
||||||
"orientation": "portrait-primary",
|
"orientation": "portrait-primary",
|
||||||
"scope": "/",
|
"scope": "/",
|
||||||
"lang": "de",
|
"lang": "de",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const CACHE_NAME = 'dgray-v19';
|
const CACHE_NAME = 'dgray-v20';
|
||||||
const STATIC_ASSETS = [
|
const STATIC_ASSETS = [
|
||||||
'/',
|
'/',
|
||||||
'/index.html',
|
'/index.html',
|
||||||
@@ -10,6 +10,12 @@ const STATIC_ASSETS = [
|
|||||||
'/js/router.js',
|
'/js/router.js',
|
||||||
'/js/i18n.js',
|
'/js/i18n.js',
|
||||||
'/js/services/api.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/de.json',
|
||||||
'/locales/en.json',
|
'/locales/en.json',
|
||||||
'/locales/fr.json',
|
'/locales/fr.json',
|
||||||
@@ -46,14 +52,24 @@ self.addEventListener('activate', (event) => {
|
|||||||
|
|
||||||
self.addEventListener('fetch', (event) => {
|
self.addEventListener('fetch', (event) => {
|
||||||
const { request } = event;
|
const { request } = event;
|
||||||
|
const url = new URL(request.url);
|
||||||
|
|
||||||
if (request.method !== 'GET') return;
|
if (request.method !== 'GET') return;
|
||||||
|
|
||||||
if (request.url.includes('/api/')) {
|
// API calls: Network First
|
||||||
|
if (url.pathname.includes('/api/')) {
|
||||||
event.respondWith(networkFirst(request));
|
event.respondWith(networkFirst(request));
|
||||||
} else {
|
return;
|
||||||
event.respondWith(cacheFirst(request));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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) {
|
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