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 {
|
class AppFooter extends HTMLElement {
|
||||||
constructor() {
|
constructor() {
|
||||||
super()
|
super()
|
||||||
this.xmrRate = null
|
this.rates = null
|
||||||
|
this.handleCurrencyChange = this.handleCurrencyChange.bind(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
async connectedCallback() {
|
async connectedCallback() {
|
||||||
this.render()
|
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 {
|
try {
|
||||||
const rates = await getXmrRates()
|
this.rates = await getXmrRates()
|
||||||
this.xmrRate = rates.EUR
|
|
||||||
this.updateRateDisplay()
|
this.updateRateDisplay()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to load XMR rate:', e)
|
console.error('Failed to load XMR rates:', e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateRateDisplay() {
|
updateRateDisplay() {
|
||||||
const rateEl = this.querySelector('.xmr-rate')
|
const rateEl = this.querySelector('.xmr-rate')
|
||||||
if (rateEl && this.xmrRate) {
|
if (rateEl && this.rates) {
|
||||||
rateEl.textContent = `1 XMR ≈ ${formatFiat(this.xmrRate, 'EUR')}`
|
const currency = this.getCurrency()
|
||||||
|
const rate = this.rates[currency] || this.rates.USD
|
||||||
|
rateEl.textContent = `1 XMR ≈ ${formatFiat(rate, currency)}`
|
||||||
rateEl.classList.add('loaded')
|
rateEl.classList.add('loaded')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -51,7 +66,7 @@ class AppFooter extends HTMLElement {
|
|||||||
|
|
||||||
updateTranslations() {
|
updateTranslations() {
|
||||||
this.render()
|
this.render()
|
||||||
if (this.xmrRate) this.updateRateDisplay()
|
if (this.rates) this.updateRateDisplay()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ class ListingCard extends HTMLElement {
|
|||||||
this.isFavorite = false
|
this.isFavorite = false
|
||||||
this.rates = null
|
this.rates = null
|
||||||
this.isOwner = false
|
this.isOwner = false
|
||||||
|
this.handleCurrencyChange = this.handleCurrencyChange.bind(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
async connectedCallback() {
|
async connectedCallback() {
|
||||||
@@ -23,6 +24,16 @@ class ListingCard extends HTMLElement {
|
|||||||
await this.checkOwnership()
|
await this.checkOwnership()
|
||||||
this.render()
|
this.render()
|
||||||
this.setupEventListeners()
|
this.setupEventListeners()
|
||||||
|
window.addEventListener('currency-changed', this.handleCurrencyChange)
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
window.removeEventListener('currency-changed', this.handleCurrencyChange)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCurrencyChange() {
|
||||||
|
this.render()
|
||||||
|
this.setupEventListeners()
|
||||||
}
|
}
|
||||||
|
|
||||||
async checkOwnership() {
|
async checkOwnership() {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { t, i18n } from '../../i18n.js'
|
|||||||
import { router } from '../../router.js'
|
import { router } from '../../router.js'
|
||||||
import { auth } from '../../services/auth.js'
|
import { auth } from '../../services/auth.js'
|
||||||
import { directus } from '../../services/directus.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 { escapeHTML } from '../../utils/helpers.js'
|
||||||
import '../location-picker.js'
|
import '../location-picker.js'
|
||||||
import '../pow-captcha.js'
|
import '../pow-captcha.js'
|
||||||
@@ -25,11 +25,16 @@ class PageCreate extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getEmptyFormData() {
|
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 {
|
return {
|
||||||
title: '',
|
title: '',
|
||||||
description: '',
|
description: '',
|
||||||
price: '',
|
price: '',
|
||||||
currency: 'EUR',
|
currency,
|
||||||
price_mode: 'fiat',
|
price_mode: 'fiat',
|
||||||
price_type: 'fixed',
|
price_type: 'fixed',
|
||||||
category: '',
|
category: '',
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ class PageListing extends HTMLElement {
|
|||||||
this.isFavorite = false
|
this.isFavorite = false
|
||||||
this.rates = null
|
this.rates = null
|
||||||
this.isOwner = false
|
this.isOwner = false
|
||||||
|
this.handleCurrencyChange = this.handleCurrencyChange.bind(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
@@ -24,10 +25,16 @@ class PageListing extends HTMLElement {
|
|||||||
this.render()
|
this.render()
|
||||||
this.loadListing()
|
this.loadListing()
|
||||||
this.unsubscribe = i18n.subscribe(() => this.render())
|
this.unsubscribe = i18n.subscribe(() => this.render())
|
||||||
|
window.addEventListener('currency-changed', this.handleCurrencyChange)
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback() {
|
disconnectedCallback() {
|
||||||
if (this.unsubscribe) this.unsubscribe()
|
if (this.unsubscribe) this.unsubscribe()
|
||||||
|
window.removeEventListener('currency-changed', this.handleCurrencyChange)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCurrencyChange() {
|
||||||
|
this.render()
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadListing() {
|
async loadListing() {
|
||||||
|
|||||||
@@ -50,6 +50,12 @@ class PageSettings extends HTMLElement {
|
|||||||
i18n.setLocale(e.target.value)
|
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
|
// Clear favorites
|
||||||
this.querySelector('#clear-favorites')?.addEventListener('click', () => {
|
this.querySelector('#clear-favorites')?.addEventListener('click', () => {
|
||||||
if (confirm(t('settings.confirmClearFavorites'))) {
|
if (confirm(t('settings.confirmClearFavorites'))) {
|
||||||
@@ -93,6 +99,15 @@ class PageSettings extends HTMLElement {
|
|||||||
return localStorage.getItem('theme') || 'system'
|
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) {
|
showToast(message) {
|
||||||
const existing = document.querySelector('.settings-toast')
|
const existing = document.querySelector('.settings-toast')
|
||||||
existing?.remove()
|
existing?.remove()
|
||||||
@@ -152,6 +167,15 @@ class PageSettings extends HTMLElement {
|
|||||||
<option value="fr" ${currentLang === 'fr' ? 'selected' : ''}>Français</option>
|
<option value="fr" ${currentLang === 'fr' ? 'selected' : ''}>Français</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</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>
|
</section>
|
||||||
|
|
||||||
<!-- Account -->
|
<!-- Account -->
|
||||||
|
|||||||
@@ -165,14 +165,44 @@ export function convertFromXmr(xmrAmount, currency, rates) {
|
|||||||
return xmrAmount * rates[currency]
|
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
|
* 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} listing - Listing with price, currency, price_mode
|
||||||
* @param {Object} rates - Rates from getXmrRates()
|
* @param {Object} rates - Rates from getXmrRates()
|
||||||
|
* @param {string} [displayCurrency] - Override display currency
|
||||||
* @returns {Object} { primary, secondary, xmrAmount }
|
* @returns {Object} { primary, secondary, xmrAmount }
|
||||||
*/
|
*/
|
||||||
export function formatPrice(listing, rates) {
|
export function formatPrice(listing, rates, displayCurrency = null) {
|
||||||
const { price, currency, price_mode } = listing
|
const { price, currency, price_mode } = listing
|
||||||
|
const userCurrency = displayCurrency || getDisplayCurrency()
|
||||||
|
|
||||||
if (!price || price === 0) {
|
if (!price || price === 0) {
|
||||||
return {
|
return {
|
||||||
@@ -185,20 +215,25 @@ export function formatPrice(listing, rates) {
|
|||||||
// XMR mode: XMR is the reference price
|
// XMR mode: XMR is the reference price
|
||||||
if (price_mode === 'xmr' || currency === 'XMR') {
|
if (price_mode === 'xmr' || currency === 'XMR') {
|
||||||
const xmrPrice = currency === 'XMR' ? price : convertToXmr(price, currency, rates)
|
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 {
|
return {
|
||||||
primary: formatXmr(xmrPrice),
|
primary: formatFiat(fiatEquivalent, userCurrency),
|
||||||
secondary: currency !== 'XMR' ? `≈ ${formatFiat(price, currency)}` : `≈ ${formatFiat(fiatEquivalent, 'EUR')}`,
|
secondary: `≈ ${formatXmr(xmrPrice)}`,
|
||||||
xmrAmount: xmrPrice
|
xmrAmount: xmrPrice
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fiat mode: Fiat is the reference price
|
// Fiat mode: Convert to user's preferred currency
|
||||||
const xmrEquivalent = convertToXmr(price, currency, rates)
|
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 {
|
return {
|
||||||
primary: formatFiat(price, currency),
|
primary: formatFiat(displayPrice, userCurrency),
|
||||||
secondary: `≈ ${formatXmr(xmrEquivalent)}`,
|
secondary: `≈ ${formatXmr(xmrEquivalent)}`,
|
||||||
xmrAmount: xmrEquivalent
|
xmrAmount: xmrEquivalent
|
||||||
}
|
}
|
||||||
@@ -253,8 +288,10 @@ export const SUPPORTED_CURRENCIES = ['XMR', 'EUR', 'CHF', 'USD', 'GBP', 'JPY']
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
getXmrRates,
|
getXmrRates,
|
||||||
|
getDisplayCurrency,
|
||||||
convertToXmr,
|
convertToXmr,
|
||||||
convertFromXmr,
|
convertFromXmr,
|
||||||
|
convertFiat,
|
||||||
formatPrice,
|
formatPrice,
|
||||||
formatXmr,
|
formatXmr,
|
||||||
formatFiat,
|
formatFiat,
|
||||||
|
|||||||
@@ -305,6 +305,8 @@
|
|||||||
"confirmClearSearch": "Suchverlauf löschen?",
|
"confirmClearSearch": "Suchverlauf löschen?",
|
||||||
"favoritesCleared": "Favoriten gelöscht",
|
"favoritesCleared": "Favoriten gelöscht",
|
||||||
"searchCleared": "Suchverlauf gelöscht",
|
"searchCleared": "Suchverlauf gelöscht",
|
||||||
"about": "Über"
|
"about": "Über",
|
||||||
|
"currency": "Währung",
|
||||||
|
"currencyChanged": "Währung geändert"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -305,6 +305,8 @@
|
|||||||
"confirmClearSearch": "Delete search history?",
|
"confirmClearSearch": "Delete search history?",
|
||||||
"favoritesCleared": "Favorites deleted",
|
"favoritesCleared": "Favorites deleted",
|
||||||
"searchCleared": "Search history deleted",
|
"searchCleared": "Search history deleted",
|
||||||
"about": "About"
|
"about": "About",
|
||||||
|
"currency": "Currency",
|
||||||
|
"currencyChanged": "Currency changed"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -305,6 +305,8 @@
|
|||||||
"confirmClearSearch": "Supprimer l'historique de recherche ?",
|
"confirmClearSearch": "Supprimer l'historique de recherche ?",
|
||||||
"favoritesCleared": "Favoris supprimés",
|
"favoritesCleared": "Favoris supprimés",
|
||||||
"searchCleared": "Historique de recherche supprimé",
|
"searchCleared": "Historique de recherche supprimé",
|
||||||
"about": "À propos"
|
"about": "À propos",
|
||||||
|
"currency": "Devise",
|
||||||
|
"currencyChanged": "Devise modifiée"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user