add uuid auth

This commit is contained in:
2026-01-31 14:18:57 +01:00
parent 4f69a494c8
commit 57020a8913
6 changed files with 905 additions and 32 deletions

426
js/components/auth-modal.js Normal file
View File

@@ -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 */`
<div class="modal-overlay" id="modal-overlay">
<div class="modal-content">
<button class="modal-close" id="modal-close" aria-label="Close">
<svg width="24" height="24" 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>
${this.mode === 'login' ? this.renderLogin() : ''}
${this.mode === 'register' ? this.renderRegister() : ''}
${this.mode === 'show-uuid' ? this.renderShowUuid() : ''}
</div>
</div>
`;
this.setupEventListeners();
}
renderLogin() {
const storedUuid = auth.getStoredUuid();
return /* html */`
<h2 class="modal-title">${t('auth.login')}</h2>
${this.error ? `<div class="auth-error">${this.error}</div>` : ''}
<form id="login-form" class="auth-form">
<div class="form-group">
<label class="label" for="uuid">${t('auth.yourUuid')}</label>
<input
type="text"
class="input"
id="uuid"
name="uuid"
value="${storedUuid || ''}"
placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
required
autocomplete="off"
spellcheck="false"
>
</div>
<button type="submit" class="btn btn-primary btn-lg btn-block" ${this.loading ? 'disabled' : ''}>
${this.loading ? t('auth.loggingIn') : t('auth.login')}
</button>
</form>
<div class="auth-footer">
<p>${t('auth.noAccount')}
<button class="link-btn" id="switch-register">${t('auth.createAccount')}</button>
</p>
</div>
`;
}
renderRegister() {
return /* html */`
<h2 class="modal-title">${t('auth.createAccount')}</h2>
<div class="auth-info">
<p>${t('auth.registerInfo')}</p>
</div>
${this.error ? `<div class="auth-error">${this.error}</div>` : ''}
<button
class="btn btn-primary btn-lg btn-block"
id="generate-uuid"
${this.loading ? 'disabled' : ''}
>
${this.loading ? t('auth.creating') : t('auth.generateUuid')}
</button>
<div class="auth-footer">
<p>${t('auth.hasAccount')}
<button class="link-btn" id="switch-login">${t('auth.login')}</button>
</p>
</div>
`;
}
renderShowUuid() {
return /* html */`
<h2 class="modal-title">${t('auth.accountCreated')}</h2>
<div class="auth-warning">
<strong>${t('auth.important')}</strong>
<p>${t('auth.saveUuidWarning')}</p>
</div>
<div class="uuid-display">
<code id="uuid-value">${this.generatedUuid}</code>
<button class="btn btn-outline" id="copy-uuid" title="${t('auth.copy')}">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
</button>
</div>
<div class="uuid-actions">
<button class="btn btn-outline btn-lg" id="download-uuid">
${t('auth.downloadBackup')}
</button>
</div>
<button class="btn btn-primary btn-lg btn-block" id="confirm-saved">
${t('auth.confirmSaved')}
</button>
`;
}
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 = '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"></polyline></svg>';
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;

View File

@@ -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 */`
<div class="create-page">
<div class="login-required">
<h2>${t('auth.loginRequired')}</h2>
<button class="btn btn-primary btn-lg" id="login-btn">${t('auth.login')}</button>
</div>
</div>
`;
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 {
</div>
<div class="form-group">
<label class="label" for="category" data-i18n="create.category">${t('create.category')}</label>
<label class="label" for="category">${t('create.category')}</label>
<select class="input" id="category" name="category" required>
<option value="">${t('create.selectCategory')}</option>
<option value="electronics" ${this.formData.category === 'electronics' ? 'selected' : ''}>${t('categories.electronics')}</option>
<option value="furniture" ${this.formData.category === 'furniture' ? 'selected' : ''}>${t('categories.furniture')}</option>
<option value="clothing" ${this.formData.category === 'clothing' ? 'selected' : ''}>${t('categories.clothing')}</option>
<option value="vehicles" ${this.formData.category === 'vehicles' ? 'selected' : ''}>${t('categories.vehicles')}</option>
<option value="sports" ${this.formData.category === 'sports' ? 'selected' : ''}>${t('categories.sports')}</option>
<option value="books" ${this.formData.category === 'books' ? 'selected' : ''}>${t('categories.books')}</option>
<option value="garden" ${this.formData.category === 'garden' ? 'selected' : ''}>${t('categories.garden')}</option>
<option value="other" ${this.formData.category === 'other' ? 'selected' : ''}>${t('categories.other')}</option>
${this.categories.map(cat => `
<option value="${cat.id}" ${this.formData.category === cat.id ? 'selected' : ''}>
${cat.name}
</option>
`).join('')}
</select>
</div>
<div class="form-group">
<label class="label" for="condition">${t('create.condition')}</label>
<select class="input" id="condition" name="condition" required>
<option value="new" ${this.formData.condition === 'new' ? 'selected' : ''}>${t('create.conditionNew')}</option>
<option value="like_new" ${this.formData.condition === 'like_new' ? 'selected' : ''}>${t('create.conditionLikeNew')}</option>
<option value="good" ${this.formData.condition === 'good' ? 'selected' : ''}>${t('create.conditionGood')}</option>
<option value="fair" ${this.formData.condition === 'fair' ? 'selected' : ''}>${t('create.conditionFair')}</option>
<option value="poor" ${this.formData.condition === 'poor' ? 'selected' : ''}>${t('create.conditionPoor')}</option>
</select>
</div>
<div class="form-row">
<div class="form-group form-group-price">
<label class="label" for="price">${t('create.price')}</label>
<input
type="number"
class="input"
id="price"
name="price"
value="${this.formData.price}"
min="0"
step="0.0001"
required
placeholder="0.00"
>
</div>
<div class="form-group form-group-currency">
<label class="label" for="currency">${t('create.currency')}</label>
<select class="input" id="currency" name="currency">
${SUPPORTED_CURRENCIES.map(cur => `
<option value="${cur}" ${this.formData.currency === cur ? 'selected' : ''}>${cur}</option>
`).join('')}
</select>
</div>
</div>
<div class="form-group">
<label class="label" for="price" data-i18n="create.price">${t('create.price')}</label>
<input
type="number"
class="input"
id="price"
name="price"
value="${this.formData.price}"
min="0"
step="0.01"
required
placeholder="0.00"
>
<label class="label" for="price_mode">${t('create.priceMode')}</label>
<select class="input" id="price_mode" name="price_mode">
<option value="fiat" ${this.formData.price_mode === 'fiat' ? 'selected' : ''}>${t('create.priceModeFiat')}</option>
<option value="xmr" ${this.formData.price_mode === 'xmr' ? 'selected' : ''}>${t('create.priceModeXmr')}</option>
</select>
<p class="field-hint">${t('create.priceModeHint')}</p>
</div>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" id="shipping" name="shipping" ${this.formData.shipping ? 'checked' : ''}>
<span>${t('create.shippingAvailable')}</span>
</label>
</div>
<div class="form-group">
@@ -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);

186
js/services/auth.js Normal file
View File

@@ -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<Object|null>}
*/
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;

View File

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

View File

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

View File

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