feat: add currency setting with fiat conversion, display prices in user's preferred currency
This commit is contained in:
@@ -4,28 +4,43 @@ import { getXmrRates, formatFiat } from '../services/currency.js'
|
||||
class AppFooter extends HTMLElement {
|
||||
constructor() {
|
||||
super()
|
||||
this.xmrRate = null
|
||||
this.rates = null
|
||||
this.handleCurrencyChange = this.handleCurrencyChange.bind(this)
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
this.render()
|
||||
await this.loadXmrRate()
|
||||
await this.loadXmrRates()
|
||||
window.addEventListener('currency-changed', this.handleCurrencyChange)
|
||||
}
|
||||
|
||||
async loadXmrRate() {
|
||||
disconnectedCallback() {
|
||||
window.removeEventListener('currency-changed', this.handleCurrencyChange)
|
||||
}
|
||||
|
||||
handleCurrencyChange() {
|
||||
this.updateRateDisplay()
|
||||
}
|
||||
|
||||
getCurrency() {
|
||||
return localStorage.getItem('dgray_currency') || 'USD'
|
||||
}
|
||||
|
||||
async loadXmrRates() {
|
||||
try {
|
||||
const rates = await getXmrRates()
|
||||
this.xmrRate = rates.EUR
|
||||
this.rates = await getXmrRates()
|
||||
this.updateRateDisplay()
|
||||
} catch (e) {
|
||||
console.error('Failed to load XMR rate:', e)
|
||||
console.error('Failed to load XMR rates:', e)
|
||||
}
|
||||
}
|
||||
|
||||
updateRateDisplay() {
|
||||
const rateEl = this.querySelector('.xmr-rate')
|
||||
if (rateEl && this.xmrRate) {
|
||||
rateEl.textContent = `1 XMR ≈ ${formatFiat(this.xmrRate, 'EUR')}`
|
||||
if (rateEl && this.rates) {
|
||||
const currency = this.getCurrency()
|
||||
const rate = this.rates[currency] || this.rates.USD
|
||||
rateEl.textContent = `1 XMR ≈ ${formatFiat(rate, currency)}`
|
||||
rateEl.classList.add('loaded')
|
||||
}
|
||||
}
|
||||
@@ -51,7 +66,7 @@ class AppFooter extends HTMLElement {
|
||||
|
||||
updateTranslations() {
|
||||
this.render()
|
||||
if (this.xmrRate) this.updateRateDisplay()
|
||||
if (this.rates) this.updateRateDisplay()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ class ListingCard extends HTMLElement {
|
||||
this.isFavorite = false
|
||||
this.rates = null
|
||||
this.isOwner = false
|
||||
this.handleCurrencyChange = this.handleCurrencyChange.bind(this)
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
@@ -23,6 +24,16 @@ class ListingCard extends HTMLElement {
|
||||
await this.checkOwnership()
|
||||
this.render()
|
||||
this.setupEventListeners()
|
||||
window.addEventListener('currency-changed', this.handleCurrencyChange)
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
window.removeEventListener('currency-changed', this.handleCurrencyChange)
|
||||
}
|
||||
|
||||
handleCurrencyChange() {
|
||||
this.render()
|
||||
this.setupEventListeners()
|
||||
}
|
||||
|
||||
async checkOwnership() {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { t, i18n } from '../../i18n.js'
|
||||
import { router } from '../../router.js'
|
||||
import { auth } from '../../services/auth.js'
|
||||
import { directus } from '../../services/directus.js'
|
||||
import { SUPPORTED_CURRENCIES } from '../../services/currency.js'
|
||||
import { SUPPORTED_CURRENCIES, getDisplayCurrency } from '../../services/currency.js'
|
||||
import { escapeHTML } from '../../utils/helpers.js'
|
||||
import '../location-picker.js'
|
||||
import '../pow-captcha.js'
|
||||
@@ -25,11 +25,16 @@ class PageCreate extends HTMLElement {
|
||||
}
|
||||
|
||||
getEmptyFormData() {
|
||||
// Use user's preferred currency from settings as default
|
||||
const defaultCurrency = getDisplayCurrency()
|
||||
// Map display currencies to listing currencies (XMR not for fiat listings)
|
||||
const currency = ['EUR', 'USD', 'CHF'].includes(defaultCurrency) ? defaultCurrency : 'EUR'
|
||||
|
||||
return {
|
||||
title: '',
|
||||
description: '',
|
||||
price: '',
|
||||
currency: 'EUR',
|
||||
currency,
|
||||
price_mode: 'fiat',
|
||||
price_type: 'fixed',
|
||||
category: '',
|
||||
|
||||
@@ -17,6 +17,7 @@ class PageListing extends HTMLElement {
|
||||
this.isFavorite = false
|
||||
this.rates = null
|
||||
this.isOwner = false
|
||||
this.handleCurrencyChange = this.handleCurrencyChange.bind(this)
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
@@ -24,10 +25,16 @@ class PageListing extends HTMLElement {
|
||||
this.render()
|
||||
this.loadListing()
|
||||
this.unsubscribe = i18n.subscribe(() => this.render())
|
||||
window.addEventListener('currency-changed', this.handleCurrencyChange)
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
if (this.unsubscribe) this.unsubscribe()
|
||||
window.removeEventListener('currency-changed', this.handleCurrencyChange)
|
||||
}
|
||||
|
||||
handleCurrencyChange() {
|
||||
this.render()
|
||||
}
|
||||
|
||||
async loadListing() {
|
||||
|
||||
@@ -50,6 +50,12 @@ class PageSettings extends HTMLElement {
|
||||
i18n.setLocale(e.target.value)
|
||||
})
|
||||
|
||||
// Currency select
|
||||
this.querySelector('#currency-select')?.addEventListener('change', (e) => {
|
||||
this.setCurrency(e.target.value)
|
||||
this.showToast(t('settings.currencyChanged'))
|
||||
})
|
||||
|
||||
// Clear favorites
|
||||
this.querySelector('#clear-favorites')?.addEventListener('click', () => {
|
||||
if (confirm(t('settings.confirmClearFavorites'))) {
|
||||
@@ -93,6 +99,15 @@ class PageSettings extends HTMLElement {
|
||||
return localStorage.getItem('theme') || 'system'
|
||||
}
|
||||
|
||||
getCurrentCurrency() {
|
||||
return localStorage.getItem('dgray_currency') || 'USD'
|
||||
}
|
||||
|
||||
setCurrency(currency) {
|
||||
localStorage.setItem('dgray_currency', currency)
|
||||
window.dispatchEvent(new CustomEvent('currency-changed', { detail: { currency } }))
|
||||
}
|
||||
|
||||
showToast(message) {
|
||||
const existing = document.querySelector('.settings-toast')
|
||||
existing?.remove()
|
||||
@@ -152,6 +167,15 @@ class PageSettings extends HTMLElement {
|
||||
<option value="fr" ${currentLang === 'fr' ? 'selected' : ''}>Français</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<label for="currency-select">${t('settings.currency')}</label>
|
||||
<select id="currency-select">
|
||||
<option value="USD" ${this.getCurrentCurrency() === 'USD' ? 'selected' : ''}>USD ($)</option>
|
||||
<option value="EUR" ${this.getCurrentCurrency() === 'EUR' ? 'selected' : ''}>EUR (€)</option>
|
||||
<option value="CHF" ${this.getCurrentCurrency() === 'CHF' ? 'selected' : ''}>CHF</option>
|
||||
</select>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Account -->
|
||||
|
||||
@@ -165,14 +165,44 @@ export function convertFromXmr(xmrAmount, currency, rates) {
|
||||
return xmrAmount * rates[currency]
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts between fiat currencies using XMR as bridge
|
||||
* @param {number} amount - Amount in source currency
|
||||
* @param {string} fromCurrency - Source currency
|
||||
* @param {string} toCurrency - Target currency
|
||||
* @param {Object} rates - Rates from getXmrRates() (XMR -> Fiat)
|
||||
* @returns {number} Amount in target currency
|
||||
*/
|
||||
export function convertFiat(amount, fromCurrency, toCurrency, rates) {
|
||||
if (fromCurrency === toCurrency) return amount
|
||||
if (!rates[fromCurrency] || !rates[toCurrency]) return amount
|
||||
|
||||
// Use XMR as bridge: EUR -> XMR -> USD
|
||||
// 1 XMR = rates[EUR] EUR, so 1 EUR = 1/rates[EUR] XMR
|
||||
// 1 XMR = rates[USD] USD
|
||||
// Therefore: 1 EUR = rates[USD] / rates[EUR] USD
|
||||
return amount * (rates[toCurrency] / rates[fromCurrency])
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's preferred display currency from settings
|
||||
* @returns {string} Currency code (default: 'USD')
|
||||
*/
|
||||
export function getDisplayCurrency() {
|
||||
return localStorage.getItem('dgray_currency') || 'USD'
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a price for display
|
||||
* Shows price in user's preferred currency with XMR equivalent
|
||||
* @param {Object} listing - Listing with price, currency, price_mode
|
||||
* @param {Object} rates - Rates from getXmrRates()
|
||||
* @param {string} [displayCurrency] - Override display currency
|
||||
* @returns {Object} { primary, secondary, xmrAmount }
|
||||
*/
|
||||
export function formatPrice(listing, rates) {
|
||||
export function formatPrice(listing, rates, displayCurrency = null) {
|
||||
const { price, currency, price_mode } = listing
|
||||
const userCurrency = displayCurrency || getDisplayCurrency()
|
||||
|
||||
if (!price || price === 0) {
|
||||
return {
|
||||
@@ -185,20 +215,25 @@ export function formatPrice(listing, rates) {
|
||||
// XMR mode: XMR is the reference price
|
||||
if (price_mode === 'xmr' || currency === 'XMR') {
|
||||
const xmrPrice = currency === 'XMR' ? price : convertToXmr(price, currency, rates)
|
||||
const fiatEquivalent = currency !== 'XMR' ? price : convertFromXmr(price, 'EUR', rates)
|
||||
const fiatEquivalent = convertFromXmr(xmrPrice, userCurrency, rates)
|
||||
|
||||
return {
|
||||
primary: formatXmr(xmrPrice),
|
||||
secondary: currency !== 'XMR' ? `≈ ${formatFiat(price, currency)}` : `≈ ${formatFiat(fiatEquivalent, 'EUR')}`,
|
||||
primary: formatFiat(fiatEquivalent, userCurrency),
|
||||
secondary: `≈ ${formatXmr(xmrPrice)}`,
|
||||
xmrAmount: xmrPrice
|
||||
}
|
||||
}
|
||||
|
||||
// Fiat mode: Fiat is the reference price
|
||||
// Fiat mode: Convert to user's preferred currency
|
||||
const xmrEquivalent = convertToXmr(price, currency, rates)
|
||||
|
||||
// Always show in user's currency as primary
|
||||
const displayPrice = userCurrency === currency
|
||||
? price
|
||||
: convertFiat(price, currency, userCurrency, rates)
|
||||
|
||||
return {
|
||||
primary: formatFiat(price, currency),
|
||||
primary: formatFiat(displayPrice, userCurrency),
|
||||
secondary: `≈ ${formatXmr(xmrEquivalent)}`,
|
||||
xmrAmount: xmrEquivalent
|
||||
}
|
||||
@@ -253,8 +288,10 @@ export const SUPPORTED_CURRENCIES = ['XMR', 'EUR', 'CHF', 'USD', 'GBP', 'JPY']
|
||||
|
||||
export default {
|
||||
getXmrRates,
|
||||
getDisplayCurrency,
|
||||
convertToXmr,
|
||||
convertFromXmr,
|
||||
convertFiat,
|
||||
formatPrice,
|
||||
formatXmr,
|
||||
formatFiat,
|
||||
|
||||
Reference in New Issue
Block a user