diff --git a/js/components/auth-modal.js b/js/components/auth-modal.js new file mode 100644 index 0000000..d510b75 --- /dev/null +++ b/js/components/auth-modal.js @@ -0,0 +1,426 @@ +/** + * Auth Modal - Login/Register with UUID + */ + +import { t, i18n } from '../i18n.js'; +import { auth } from '../services/auth.js'; + +class AuthModal extends HTMLElement { + constructor() { + super(); + this.mode = 'login'; // 'login' | 'register' | 'show-uuid' + this.generatedUuid = null; + this.error = null; + this.loading = false; + } + + connectedCallback() { + this.render(); + this.unsubscribe = i18n.subscribe(() => this.render()); + } + + disconnectedCallback() { + if (this.unsubscribe) this.unsubscribe(); + } + + show(mode = 'login') { + this.mode = mode; + this.error = null; + this.generatedUuid = null; + this.hidden = false; + this.render(); + document.body.style.overflow = 'hidden'; + } + + hide() { + this.hidden = true; + document.body.style.overflow = ''; + this.dispatchEvent(new CustomEvent('close')); + } + + switchMode(mode) { + this.mode = mode; + this.error = null; + this.render(); + } + + render() { + if (this.hidden) { + this.innerHTML = ''; + return; + } + + this.innerHTML = /* html */` + + `; + + this.setupEventListeners(); + } + + renderLogin() { + const storedUuid = auth.getStoredUuid(); + + return /* html */` + + + ${this.error ? `
${this.error}
` : ''} + +
+
+ + +
+ + +
+ + + `; + } + + renderRegister() { + return /* html */` + + +
+

${t('auth.registerInfo')}

+
+ + ${this.error ? `
${this.error}
` : ''} + + + + + `; + } + + renderShowUuid() { + return /* html */` + + +
+ ${t('auth.important')} +

${t('auth.saveUuidWarning')}

+
+ +
+ ${this.generatedUuid} + +
+ +
+ +
+ + + `; + } + + setupEventListeners() { + // Close modal + this.querySelector('#modal-overlay')?.addEventListener('click', (e) => { + if (e.target.id === 'modal-overlay') this.hide(); + }); + this.querySelector('#modal-close')?.addEventListener('click', () => this.hide()); + + // Switch modes + this.querySelector('#switch-register')?.addEventListener('click', () => this.switchMode('register')); + this.querySelector('#switch-login')?.addEventListener('click', () => this.switchMode('login')); + + // Login form + this.querySelector('#login-form')?.addEventListener('submit', (e) => this.handleLogin(e)); + + // Generate UUID + this.querySelector('#generate-uuid')?.addEventListener('click', () => this.handleRegister()); + + // Copy UUID + this.querySelector('#copy-uuid')?.addEventListener('click', () => this.copyUuid()); + + // Download backup + this.querySelector('#download-uuid')?.addEventListener('click', () => this.downloadBackup()); + + // Confirm saved + this.querySelector('#confirm-saved')?.addEventListener('click', () => this.hide()); + + // Escape key + document.addEventListener('keydown', this.handleKeydown.bind(this)); + } + + handleKeydown(e) { + if (e.key === 'Escape' && !this.hidden) { + this.hide(); + } + } + + async handleLogin(e) { + e.preventDefault(); + const uuid = this.querySelector('#uuid').value.trim(); + + if (!uuid) { + this.error = t('auth.enterUuid'); + this.render(); + return; + } + + this.loading = true; + this.error = null; + this.render(); + + const result = await auth.login(uuid); + + this.loading = false; + + if (result.success) { + this.hide(); + this.dispatchEvent(new CustomEvent('login', { detail: { success: true } })); + } else { + this.error = result.error || t('auth.invalidUuid'); + this.render(); + } + } + + async handleRegister() { + this.loading = true; + this.error = null; + this.render(); + + const result = await auth.createAccount(); + + this.loading = false; + + if (result.success) { + this.generatedUuid = result.uuid; + this.mode = 'show-uuid'; + this.render(); + this.dispatchEvent(new CustomEvent('register', { detail: { uuid: result.uuid } })); + } else { + this.error = result.error || t('auth.registrationFailed'); + this.render(); + } + } + + async copyUuid() { + try { + await navigator.clipboard.writeText(this.generatedUuid); + const btn = this.querySelector('#copy-uuid'); + btn.innerHTML = ''; + setTimeout(() => this.render(), 2000); + } catch (e) { + console.error('Copy failed:', e); + } + } + + downloadBackup() { + const content = `dgray.io Account Backup +======================== + +Your UUID (keep this secret!): +${this.generatedUuid} + +Login URL: +https://dgray.io/#/login + +Created: ${new Date().toISOString()} + +WARNING: If you lose this UUID, you cannot recover your account! +`; + + const blob = new Blob([content], { type: 'text/plain' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `dgray-backup-${this.generatedUuid.slice(0, 8)}.txt`; + a.click(); + URL.revokeObjectURL(url); + } +} + +customElements.define('auth-modal', AuthModal); + +const style = document.createElement('style'); +style.textContent = /* css */` + auth-modal { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: var(--z-modal); + } + + auth-modal[hidden] { + display: none; + } + + auth-modal .modal-overlay { + position: absolute; + inset: 0; + background: var(--color-overlay); + display: flex; + align-items: center; + justify-content: center; + padding: var(--space-md); + } + + auth-modal .modal-content { + background: var(--color-bg); + border-radius: var(--radius-lg); + padding: var(--space-xl); + width: 100%; + max-width: 420px; + position: relative; + box-shadow: var(--shadow-xl); + } + + auth-modal .modal-close { + position: absolute; + top: var(--space-md); + right: var(--space-md); + background: none; + border: none; + cursor: pointer; + color: var(--color-text-muted); + padding: var(--space-xs); + } + + auth-modal .modal-close:hover { + color: var(--color-text); + } + + auth-modal .modal-title { + margin: 0 0 var(--space-lg); + font-size: var(--font-size-2xl); + } + + auth-modal .auth-form .form-group { + margin-bottom: var(--space-lg); + } + + auth-modal .auth-info { + background: var(--color-bg-secondary); + border-radius: var(--radius-md); + padding: var(--space-md); + margin-bottom: var(--space-lg); + font-size: var(--font-size-sm); + color: var(--color-text-secondary); + } + + auth-modal .auth-warning { + background: var(--color-bg-tertiary); + border-left: 4px solid var(--color-accent); + border-radius: var(--radius-md); + padding: var(--space-md); + margin-bottom: var(--space-lg); + } + + auth-modal .auth-warning strong { + display: block; + margin-bottom: var(--space-xs); + color: var(--color-accent); + } + + auth-modal .auth-error { + background: var(--color-bg-tertiary); + border-left: 4px solid var(--color-error); + border-radius: var(--radius-md); + padding: var(--space-md); + margin-bottom: var(--space-lg); + color: var(--color-text); + } + + auth-modal .uuid-display { + display: flex; + gap: var(--space-sm); + margin-bottom: var(--space-lg); + } + + auth-modal .uuid-display code { + flex: 1; + background: var(--color-bg-secondary); + padding: var(--space-md); + border-radius: var(--radius-md); + font-family: monospace; + font-size: var(--font-size-sm); + word-break: break-all; + border: 1px solid var(--color-border); + } + + auth-modal .uuid-actions { + margin-bottom: var(--space-lg); + } + + auth-modal .btn-block { + width: 100%; + } + + auth-modal .auth-footer { + margin-top: var(--space-lg); + text-align: center; + font-size: var(--font-size-sm); + color: var(--color-text-muted); + } + + auth-modal .link-btn { + background: none; + border: none; + color: var(--color-accent); + cursor: pointer; + font-size: inherit; + text-decoration: underline; + } + + auth-modal .link-btn:hover { + color: var(--color-accent-hover); + } +`; +document.head.appendChild(style); + +export default AuthModal; diff --git a/js/components/pages/page-create.js b/js/components/pages/page-create.js index 2a1b942..898c5b8 100644 --- a/js/components/pages/page-create.js +++ b/js/components/pages/page-create.js @@ -1,6 +1,8 @@ import { t, i18n } from '../../i18n.js'; import { router } from '../../router.js'; -import { cryptoService } from '../../services/crypto.js'; +import { auth } from '../../services/auth.js'; +import { directus } from '../../services/directus.js'; +import { SUPPORTED_CURRENCIES } from '../../services/currency.js'; class PageCreate extends HTMLElement { constructor() { @@ -9,19 +11,62 @@ class PageCreate extends HTMLElement { title: '', description: '', price: '', + currency: 'EUR', + price_mode: 'fiat', + price_type: 'fixed', category: '', + condition: 'good', location: '', + shipping: false, moneroAddress: '' }; this.imageFiles = []; this.imagePreviews = []; + this.categories = []; + this.submitting = false; } - connectedCallback() { + async connectedCallback() { + // Check if logged in + if (!auth.isLoggedIn()) { + this.showLoginRequired(); + return; + } + + await this.loadCategories(); this.render(); this.unsubscribe = i18n.subscribe(() => this.render()); } + showLoginRequired() { + this.innerHTML = /* html */` +
+
+

${t('auth.loginRequired')}

+ +
+
+ `; + this.querySelector('#login-btn')?.addEventListener('click', () => { + const authModal = document.querySelector('auth-modal'); + if (authModal) { + authModal.show('login'); + authModal.addEventListener('login', () => { + this.connectedCallback(); + }, { once: true }); + } + }); + } + + async loadCategories() { + try { + this.categories = await directus.getCategories(); + } catch (e) { + console.error('Failed to load categories:', e); + this.categories = []; + } + } + disconnectedCallback() { if (this.unsubscribe) this.unsubscribe(); } @@ -47,33 +92,67 @@ class PageCreate extends HTMLElement {
- + +
+ +
+ +
+
+
+ + +
+
+ + +
+
+
- - + + +

${t('create.priceModeHint')}

+
+ +
+
@@ -154,13 +233,19 @@ class PageCreate extends HTMLElement { form.addEventListener('submit', (e) => this.handleSubmit(e)); cancelBtn.addEventListener('click', () => router.back()); - form.querySelectorAll('input, textarea, select').forEach(input => { + form.querySelectorAll('input:not([type="checkbox"]), textarea, select').forEach(input => { input.addEventListener('input', (e) => { if (e.target.name) { this.formData[e.target.name] = e.target.value; } }); }); + + // Checkbox handler + const shippingCheckbox = this.querySelector('#shipping'); + shippingCheckbox?.addEventListener('change', (e) => { + this.formData.shipping = e.target.checked; + }); imageInput?.addEventListener('change', (e) => this.handleImageSelect(e)); } @@ -226,16 +311,55 @@ class PageCreate extends HTMLElement { async handleSubmit(e) { e.preventDefault(); + if (this.submitting) return; + this.submitting = true; + const form = e.target; const submitBtn = form.querySelector('[type="submit"]'); submitBtn.disabled = true; submitBtn.textContent = t('create.publishing'); - await new Promise(resolve => setTimeout(resolve, 1000)); - - console.log('Creating listing:', this.formData); - - router.navigate('/'); + try { + // Upload images first + let imageIds = []; + if (this.imageFiles.length > 0) { + const uploadedFiles = await directus.uploadMultipleFiles(this.imageFiles); + imageIds = uploadedFiles.map(f => f.id); + } + + // Create listing + const listingData = { + title: this.formData.title, + description: this.formData.description, + price: parseFloat(this.formData.price) || 0, + currency: this.formData.currency, + price_mode: this.formData.price_mode, + price_type: this.formData.price_type, + category: this.formData.category || null, + condition: this.formData.condition, + shipping: this.formData.shipping, + monero_address: this.formData.moneroAddress, + status: 'published' + }; + + // Add images if uploaded + if (imageIds.length > 0) { + listingData.images = imageIds.map((id, index) => ({ + directus_files_id: id, + sort: index + })); + } + + const listing = await directus.createListing(listingData); + + router.navigate(`/listing/${listing.id}`); + } catch (error) { + console.error('Failed to create listing:', error); + submitBtn.disabled = false; + submitBtn.textContent = t('create.publish'); + this.submitting = false; + alert(error.message || 'Failed to create listing'); + } } escapeHtml(text) { @@ -344,6 +468,41 @@ style.textContent = /* css */` margin-top: var(--space-xs); } + page-create .form-row { + display: flex; + gap: var(--space-md); + } + + page-create .form-group-price { + flex: 2; + } + + page-create .form-group-currency { + flex: 1; + } + + page-create .checkbox-label { + display: flex; + align-items: center; + gap: var(--space-sm); + cursor: pointer; + } + + page-create .checkbox-label input { + width: 18px; + height: 18px; + accent-color: var(--color-accent); + } + + page-create .login-required { + text-align: center; + padding: var(--space-3xl) 0; + } + + page-create .login-required h2 { + margin-bottom: var(--space-lg); + } + page-create .form-actions { display: flex; gap: var(--space-md); diff --git a/js/services/auth.js b/js/services/auth.js new file mode 100644 index 0000000..274ce8f --- /dev/null +++ b/js/services/auth.js @@ -0,0 +1,186 @@ +/** + * Auth Service - UUID-based anonymous authentication + * + * No email addresses, no personal data + * User remembers only their UUID + */ + +import { directus } from './directus.js'; + +const AUTH_DOMAIN = 'dgray.io'; + +class AuthService { + constructor() { + this.currentUser = null; + this.listeners = new Set(); + } + + /** + * Generates a new UUID for account creation + * @returns {string} UUID v4 + */ + generateUuid() { + return crypto.randomUUID(); + } + + /** + * Converts UUID to fake email for Directus + * @param {string} uuid - User UUID + * @returns {string} Fake email address + */ + uuidToEmail(uuid) { + return `${uuid}@${AUTH_DOMAIN}`; + } + + /** + * Creates a new anonymous account + * @returns {Promise<{uuid: string, success: boolean, error?: string}>} + */ + async createAccount() { + const uuid = this.generateUuid(); + const email = this.uuidToEmail(uuid); + + try { + await directus.register(email, uuid); + + // Auto-login after registration + await this.login(uuid); + + return { uuid, success: true }; + } catch (error) { + console.error('Registration failed:', error); + return { + uuid: null, + success: false, + error: error.message || 'Registration failed' + }; + } + } + + /** + * Logs in with UUID + * @param {string} uuid - User UUID + * @returns {Promise<{success: boolean, error?: string}>} + */ + async login(uuid) { + const email = this.uuidToEmail(uuid); + + try { + await directus.login(email, uuid); + this.currentUser = await directus.getCurrentUser(); + this.notifyListeners(); + + // Store UUID for convenience (optional) + this.storeUuid(uuid); + + return { success: true }; + } catch (error) { + console.error('Login failed:', error); + return { + success: false, + error: error.message || 'Invalid UUID' + }; + } + } + + /** + * Logs out the current user + */ + async logout() { + try { + await directus.logout(); + } catch (e) { + // Ignore errors + } + + this.currentUser = null; + this.clearStoredUuid(); + this.notifyListeners(); + } + + /** + * Checks if user is logged in + * @returns {boolean} + */ + isLoggedIn() { + return directus.isAuthenticated(); + } + + /** + * Gets current user data + * @returns {Promise} + */ + async getUser() { + if (!this.isLoggedIn()) return null; + + if (!this.currentUser) { + try { + this.currentUser = await directus.getCurrentUser(); + } catch (e) { + return null; + } + } + + return this.currentUser; + } + + /** + * Stores UUID in localStorage (optional convenience) + * User should still backup their UUID + * @param {string} uuid + */ + storeUuid(uuid) { + localStorage.setItem('dgray_uuid', uuid); + } + + /** + * Gets stored UUID if available + * @returns {string|null} + */ + getStoredUuid() { + return localStorage.getItem('dgray_uuid'); + } + + /** + * Clears stored UUID + */ + clearStoredUuid() { + localStorage.removeItem('dgray_uuid'); + } + + /** + * Subscribe to auth state changes + * @param {Function} callback + * @returns {Function} Unsubscribe function + */ + subscribe(callback) { + this.listeners.add(callback); + return () => this.listeners.delete(callback); + } + + /** + * Notifies all listeners of auth state change + */ + notifyListeners() { + this.listeners.forEach(cb => cb(this.isLoggedIn(), this.currentUser)); + } + + /** + * Tries to restore session from stored tokens + */ + async tryRestoreSession() { + if (directus.isAuthenticated()) { + try { + this.currentUser = await directus.getCurrentUser(); + this.notifyListeners(); + return true; + } catch (e) { + return false; + } + } + return false; + } +} + +export const auth = new AuthService(); +export default auth; diff --git a/locales/de.json b/locales/de.json index 2af8941..d62c56e 100644 --- a/locales/de.json +++ b/locales/de.json @@ -118,7 +118,19 @@ "titlePlaceholder": "Was möchtest du verkaufen?", "category": "Kategorie", "selectCategory": "Kategorie wählen", - "price": "Preis (€)", + "condition": "Zustand", + "conditionNew": "Neu", + "conditionLikeNew": "Wie neu", + "conditionGood": "Gut", + "conditionFair": "Akzeptabel", + "conditionPoor": "Stark gebraucht", + "price": "Preis", + "currency": "Währung", + "priceMode": "Preismodus", + "priceModeFiat": "Fiat-Preis fix", + "priceModeXmr": "XMR-Preis fix", + "priceModeHint": "Fiat-fix: Der Fiat-Betrag bleibt gleich. XMR-fix: Der XMR-Betrag bleibt gleich.", + "shippingAvailable": "Versand möglich", "location": "Standort", "locationPlaceholder": "Stadt, PLZ oder Adresse", "description": "Beschreibung", @@ -136,5 +148,27 @@ "title": "Seite nicht gefunden", "message": "Die gesuchte Seite existiert leider nicht.", "backHome": "Zur Startseite" + }, + "auth": { + "login": "Anmelden", + "logout": "Abmelden", + "loggingIn": "Anmelden...", + "yourUuid": "Deine UUID", + "enterUuid": "Bitte UUID eingeben", + "invalidUuid": "Ungültige UUID oder Account nicht gefunden", + "noAccount": "Noch kein Account?", + "hasAccount": "Bereits einen Account?", + "createAccount": "Account erstellen", + "registerInfo": "Es wird eine einzigartige UUID für dich generiert. Diese UUID ist dein einziger Zugang - speichere sie sicher!", + "generateUuid": "UUID generieren", + "creating": "Wird erstellt...", + "accountCreated": "Account erstellt!", + "important": "Wichtig!", + "saveUuidWarning": "Speichere diese UUID sicher ab. Sie ist dein einziger Zugang zu deinem Account. Es gibt keine Möglichkeit, sie wiederherzustellen!", + "copy": "Kopieren", + "downloadBackup": "Backup herunterladen", + "confirmSaved": "Ich habe meine UUID gespeichert", + "registrationFailed": "Registrierung fehlgeschlagen", + "loginRequired": "Bitte melde dich an, um fortzufahren" } } diff --git a/locales/en.json b/locales/en.json index e3c6a99..09910b4 100644 --- a/locales/en.json +++ b/locales/en.json @@ -118,7 +118,19 @@ "titlePlaceholder": "What do you want to sell?", "category": "Category", "selectCategory": "Select category", - "price": "Price (€)", + "condition": "Condition", + "conditionNew": "New", + "conditionLikeNew": "Like new", + "conditionGood": "Good", + "conditionFair": "Fair", + "conditionPoor": "Poor", + "price": "Price", + "currency": "Currency", + "priceMode": "Price mode", + "priceModeFiat": "Fiat price fixed", + "priceModeXmr": "XMR price fixed", + "priceModeHint": "Fiat-fixed: The fiat amount stays the same. XMR-fixed: The XMR amount stays the same.", + "shippingAvailable": "Shipping available", "location": "Location", "locationPlaceholder": "City, ZIP or address", "description": "Description", @@ -136,5 +148,27 @@ "title": "Page Not Found", "message": "The page you are looking for does not exist.", "backHome": "Back to Home" + }, + "auth": { + "login": "Login", + "logout": "Logout", + "loggingIn": "Logging in...", + "yourUuid": "Your UUID", + "enterUuid": "Please enter your UUID", + "invalidUuid": "Invalid UUID or account not found", + "noAccount": "No account yet?", + "hasAccount": "Already have an account?", + "createAccount": "Create Account", + "registerInfo": "A unique UUID will be generated for you. This UUID is your only access - save it securely!", + "generateUuid": "Generate UUID", + "creating": "Creating...", + "accountCreated": "Account Created!", + "important": "Important!", + "saveUuidWarning": "Save this UUID securely. It is your only access to your account. There is no way to recover it!", + "copy": "Copy", + "downloadBackup": "Download Backup", + "confirmSaved": "I have saved my UUID", + "registrationFailed": "Registration failed", + "loginRequired": "Please log in to continue" } } diff --git a/locales/fr.json b/locales/fr.json index f6e84b7..dee284d 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -118,7 +118,19 @@ "titlePlaceholder": "Que voulez-vous vendre ?", "category": "Catégorie", "selectCategory": "Choisir une catégorie", - "price": "Prix (€)", + "condition": "État", + "conditionNew": "Neuf", + "conditionLikeNew": "Comme neuf", + "conditionGood": "Bon", + "conditionFair": "Acceptable", + "conditionPoor": "Mauvais", + "price": "Prix", + "currency": "Devise", + "priceMode": "Mode de prix", + "priceModeFiat": "Prix fiat fixe", + "priceModeXmr": "Prix XMR fixe", + "priceModeHint": "Fiat-fixe: Le montant fiat reste le même. XMR-fixe: Le montant XMR reste le même.", + "shippingAvailable": "Livraison disponible", "location": "Emplacement", "locationPlaceholder": "Ville, code postal ou adresse", "description": "Description", @@ -136,5 +148,27 @@ "title": "Page non trouvée", "message": "La page que vous recherchez n'existe pas.", "backHome": "Retour à l'accueil" + }, + "auth": { + "login": "Connexion", + "logout": "Déconnexion", + "loggingIn": "Connexion en cours...", + "yourUuid": "Votre UUID", + "enterUuid": "Veuillez entrer votre UUID", + "invalidUuid": "UUID invalide ou compte non trouvé", + "noAccount": "Pas encore de compte ?", + "hasAccount": "Vous avez déjà un compte ?", + "createAccount": "Créer un compte", + "registerInfo": "Un UUID unique sera généré pour vous. Cet UUID est votre seul accès - conservez-le en sécurité !", + "generateUuid": "Générer un UUID", + "creating": "Création en cours...", + "accountCreated": "Compte créé !", + "important": "Important !", + "saveUuidWarning": "Sauvegardez cet UUID en lieu sûr. C'est votre seul accès à votre compte. Il n'y a aucun moyen de le récupérer !", + "copy": "Copier", + "downloadBackup": "Télécharger la sauvegarde", + "confirmSaved": "J'ai sauvegardé mon UUID", + "registrationFailed": "Échec de l'inscription", + "loginRequired": "Veuillez vous connecter pour continuer" } }