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.mode === 'login' ? this.renderLogin() : ''}
+ ${this.mode === 'register' ? this.renderRegister() : ''}
+ ${this.mode === 'show-uuid' ? this.renderShowUuid() : ''}
+
+
+ `;
+
+ this.setupEventListeners();
+ }
+
+ renderLogin() {
+ const storedUuid = auth.getStoredUuid();
+
+ return /* html */`
+ ${t('auth.login')}
+
+ ${this.error ? `${this.error}
` : ''}
+
+
+
+
+ `;
+ }
+
+ renderRegister() {
+ return /* html */`
+ ${t('auth.createAccount')}
+
+
+
${t('auth.registerInfo')}
+
+
+ ${this.error ? `${this.error}
` : ''}
+
+
+
+
+ `;
+ }
+
+ renderShowUuid() {
+ return /* html */`
+ ${t('auth.accountCreated')}
+
+
+
${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 {
-
+
+
+
+
+
+
+
+
+
+
+
@@ -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