400 lines
15 KiB
JavaScript
400 lines
15 KiB
JavaScript
|
import {revive, persist, labelInput, labelSelect} from '/js/util.js';
|
|||
|
|
|||
|
export default {
|
|||
|
id:'currency', name:'Currency Converter', about:'Convert between currencies using real-time exchange rates.',
|
|||
|
render(root){
|
|||
|
const key='calc_currency_v1';
|
|||
|
const s = revive(key,{amount:100, from:'USD', to:'EUR', manualRate: null});
|
|||
|
const ui = document.createElement('div');
|
|||
|
|
|||
|
// Popular currencies with better formatting
|
|||
|
const currencies = [
|
|||
|
['USD', 'US Dollar'],
|
|||
|
['EUR', 'Euro'],
|
|||
|
['GBP', 'British Pound'],
|
|||
|
['JPY', 'Japanese Yen'],
|
|||
|
['CAD', 'Canadian Dollar'],
|
|||
|
['AUD', 'Australian Dollar'],
|
|||
|
['CHF', 'Swiss Franc'],
|
|||
|
['CNY', 'Chinese Yuan'],
|
|||
|
['INR', 'Indian Rupee'],
|
|||
|
['BRL', 'Brazilian Real']
|
|||
|
];
|
|||
|
|
|||
|
// Create a more user-friendly form layout
|
|||
|
const formContainer = document.createElement('div');
|
|||
|
formContainer.style.cssText = `
|
|||
|
display: grid;
|
|||
|
grid-template-columns: 1fr 1fr;
|
|||
|
gap: 15px;
|
|||
|
margin-bottom: 20px;
|
|||
|
`;
|
|||
|
|
|||
|
// Amount input (full width)
|
|||
|
const amountDiv = document.createElement('div');
|
|||
|
amountDiv.style.gridColumn = '1 / -1';
|
|||
|
amountDiv.innerHTML = `
|
|||
|
<label style="display: block; margin-bottom: 8px; font-weight: 500; color: var(--text);">
|
|||
|
Amount to Convert
|
|||
|
</label>
|
|||
|
<input type="number" name="amount" value="${s.amount}" step="0.01" min="0"
|
|||
|
style="width: 100%; padding: 12px; border: 1px solid var(--border); border-radius: 8px; font-size: 16px;"
|
|||
|
placeholder="Enter amount">
|
|||
|
`;
|
|||
|
|
|||
|
// From currency (left column) - using Select Lite
|
|||
|
const fromDiv = document.createElement('div');
|
|||
|
fromDiv.innerHTML = `
|
|||
|
<label style="display: block; margin-bottom: 8px; font-weight: 500; color: var(--text);">
|
|||
|
From Currency
|
|||
|
</label>
|
|||
|
<select name="from" data-ui="lite" style="width: 100%;">
|
|||
|
${currencies.map(([code, name]) =>
|
|||
|
`<option value="${code}" ${code === s.from ? 'selected' : ''}>${code} - ${name}</option>`
|
|||
|
).join('')}
|
|||
|
</select>
|
|||
|
`;
|
|||
|
|
|||
|
// To currency (right column) - using Select Lite
|
|||
|
const toDiv = document.createElement('div');
|
|||
|
toDiv.innerHTML = `
|
|||
|
<label style="display: block; margin-bottom: 8px; font-weight: 500; color: var(--text);">
|
|||
|
To Currency
|
|||
|
</label>
|
|||
|
<select name="to" data-ui="lite" style="width: 100%;">
|
|||
|
${currencies.map(([code, name]) =>
|
|||
|
`<option value="${code}" ${code === s.to ? 'selected' : ''}>${code} - ${name}</option>`
|
|||
|
).join('')}
|
|||
|
</select>
|
|||
|
`;
|
|||
|
|
|||
|
formContainer.append(amountDiv, fromDiv, toDiv);
|
|||
|
ui.append(formContainer);
|
|||
|
|
|||
|
// Manual rate input option
|
|||
|
const manualRateDiv = document.createElement('div');
|
|||
|
manualRateDiv.innerHTML = `
|
|||
|
<div style="margin: 15px 0; padding: 15px; border: 1px solid var(--border); border-radius: 8px; background: var(--k-bg);">
|
|||
|
<label style="display: block; margin-bottom: 8px; font-weight: 500; color: var(--text);">
|
|||
|
Custom Rate (Optional)
|
|||
|
</label>
|
|||
|
<input type="number" name="manualRate" placeholder="Enter custom conversion rate" step="0.0001" min="0"
|
|||
|
style="width: 100%; padding: 10px; border: 1px solid var(--border); border-radius: 6px; font-size: 14px;">
|
|||
|
<div style="font-size: 12px; color: var(--muted); margin-top: 8px; line-height: 1.4;">
|
|||
|
<strong>Format:</strong> 1 ${s.from} = X ${s.to}<br>
|
|||
|
<strong>Example:</strong> If 1 USD = 0.85 EUR, enter 0.85<br>
|
|||
|
<strong>Leave empty</strong> to use current market rates
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
`;
|
|||
|
ui.append(manualRateDiv);
|
|||
|
|
|||
|
// Fetch rates button with better styling
|
|||
|
const fetchBtn = document.createElement('button');
|
|||
|
fetchBtn.type = 'button';
|
|||
|
fetchBtn.textContent = 'Update Exchange Rates';
|
|||
|
fetchBtn.className = 'btn';
|
|||
|
fetchBtn.style.cssText = `
|
|||
|
width: 100%;
|
|||
|
padding: 12px;
|
|||
|
margin: 15px 0;
|
|||
|
font-size: 16px;
|
|||
|
font-weight: 500;
|
|||
|
background: var(--accent);
|
|||
|
color: white;
|
|||
|
border: none;
|
|||
|
border-radius: 8px;
|
|||
|
cursor: pointer;
|
|||
|
transition: background 0.2s;
|
|||
|
`;
|
|||
|
fetchBtn.addEventListener('mouseenter', () => fetchBtn.style.background = 'var(--accent-hover)');
|
|||
|
fetchBtn.addEventListener('mouseleave', () => fetchBtn.style.background = 'var(--accent)');
|
|||
|
ui.append(fetchBtn);
|
|||
|
|
|||
|
const out = document.createElement('div');
|
|||
|
out.className = 'result';
|
|||
|
out.style.cssText = `
|
|||
|
margin: 20px 0;
|
|||
|
padding: 15px;
|
|||
|
background: var(--k-bg);
|
|||
|
border-radius: 8px;
|
|||
|
border-left: 4px solid var(--accent);
|
|||
|
`;
|
|||
|
ui.append(out);
|
|||
|
|
|||
|
const status = document.createElement('div');
|
|||
|
status.className='status';
|
|||
|
ui.append(status);
|
|||
|
|
|||
|
// Static rates from today (2025-09-01) - no prefetching
|
|||
|
const staticRates = {
|
|||
|
USD: 1,
|
|||
|
EUR: 0.856,
|
|||
|
GBP: 0.741,
|
|||
|
JPY: 147.15,
|
|||
|
CAD: 1.37,
|
|||
|
AUD: 1.53,
|
|||
|
CHF: 0.801,
|
|||
|
CNY: 7.13,
|
|||
|
INR: 88.19,
|
|||
|
BRL: 5.43
|
|||
|
};
|
|||
|
|
|||
|
let rates = staticRates;
|
|||
|
let lastFetch = 0;
|
|||
|
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
|
|||
|
|
|||
|
// Function to get current static rate for currency pair
|
|||
|
function getCurrentStaticRate(from, to) {
|
|||
|
if (from === to) return 1;
|
|||
|
if (rates && rates[from] && rates[to]) {
|
|||
|
return rates[to] / rates[from];
|
|||
|
}
|
|||
|
return null;
|
|||
|
}
|
|||
|
|
|||
|
// Function to update custom rate with current static rate
|
|||
|
function updateCustomRateWithStatic() {
|
|||
|
const from = ui.querySelector('[name=from]').value;
|
|||
|
const to = ui.querySelector('[name=to]').value;
|
|||
|
const staticRate = getCurrentStaticRate(from, to);
|
|||
|
|
|||
|
if (staticRate !== null) {
|
|||
|
const manualRateInput = ui.querySelector('[name=manualRate]');
|
|||
|
// Update manual rate to current market rate when fetching new rates
|
|||
|
manualRateInput.value = staticRate.toFixed(4);
|
|||
|
manualRateInput.placeholder = `Current rate: ${staticRate.toFixed(4)}`;
|
|||
|
// Trigger calculation with the new rates
|
|||
|
calc();
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
async function fetchRates(){
|
|||
|
const now = Date.now();
|
|||
|
if (rates && (now - lastFetch) < CACHE_DURATION) {
|
|||
|
status.textContent = 'Using cached rates (updated within 5 minutes)';
|
|||
|
status.className = 'status success';
|
|||
|
status.style.display = 'block';
|
|||
|
setTimeout(() => {
|
|||
|
status.textContent = '';
|
|||
|
status.style.display = 'none';
|
|||
|
}, 3000);
|
|||
|
return rates;
|
|||
|
}
|
|||
|
|
|||
|
try {
|
|||
|
status.textContent = 'Fetching latest exchange rates...';
|
|||
|
status.className = 'status loading';
|
|||
|
|
|||
|
const response = await fetch('https://api.exchangerate-api.com/v4/latest/USD');
|
|||
|
if (!response.ok) throw new Error('Failed to fetch rates');
|
|||
|
|
|||
|
const data = await response.json();
|
|||
|
rates = data.rates;
|
|||
|
rates.USD = 1; // Base currency
|
|||
|
lastFetch = now;
|
|||
|
|
|||
|
status.textContent = 'Rates updated successfully!';
|
|||
|
status.className = 'status success';
|
|||
|
status.style.display = 'block';
|
|||
|
setTimeout(() => {
|
|||
|
status.textContent = '';
|
|||
|
status.style.display = 'none';
|
|||
|
}, 1500);
|
|||
|
|
|||
|
// Update custom rate with new rates and recalculate
|
|||
|
updateCustomRateWithStatic();
|
|||
|
|
|||
|
return rates;
|
|||
|
} catch (error) {
|
|||
|
console.error('Failed to fetch exchange rates:', error);
|
|||
|
status.textContent = 'Failed to fetch rates. Using static rates from today.';
|
|||
|
status.className = 'status error';
|
|||
|
// Keep using static rates
|
|||
|
return rates;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
function calc(){
|
|||
|
const amount = +ui.querySelector('[name=amount]').value || 0;
|
|||
|
const from = ui.querySelector('[name=from]').value;
|
|||
|
const to = ui.querySelector('[name=to]').value;
|
|||
|
const manualRate = +ui.querySelector('[name=manualRate]').value || null;
|
|||
|
|
|||
|
console.log('Calc function called with:', { amount, from, to, manualRate });
|
|||
|
|
|||
|
if (amount <= 0) {
|
|||
|
out.innerHTML = '<div class="muted">Enter a positive amount to convert</div>';
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
if (from === to) {
|
|||
|
out.innerHTML = `
|
|||
|
<div style="font-size: 18px; font-weight: 600; color: var(--text);">
|
|||
|
${amount.toFixed(2)} ${from}
|
|||
|
</div>
|
|||
|
<div style="color: var(--muted); margin-top: 5px;">
|
|||
|
Same currency - no conversion needed
|
|||
|
</div>
|
|||
|
`;
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
let conversionRate;
|
|||
|
let rateSource;
|
|||
|
let rateDate;
|
|||
|
|
|||
|
if (manualRate && manualRate > 0) {
|
|||
|
console.log('Using manual rate:', manualRate);
|
|||
|
// Use manual rate
|
|||
|
conversionRate = manualRate;
|
|||
|
|
|||
|
// Determine if this is user-entered or default custom rate
|
|||
|
const staticRate = staticRates[to] / staticRates[from];
|
|||
|
const isUserEntered = Math.abs(manualRate - staticRate) > 0.0001; // Allow for floating point precision
|
|||
|
|
|||
|
rateSource = isUserEntered ? 'Custom rate' : 'Static rates';
|
|||
|
rateDate = isUserEntered ? 'Manual input' : '2025-09-01';
|
|||
|
|
|||
|
out.innerHTML = `
|
|||
|
<div style="font-size: 24px; font-weight: 700; color: var(--accent); margin-bottom: 10px;">
|
|||
|
${(amount * conversionRate).toFixed(2)} ${to}
|
|||
|
</div>
|
|||
|
<div style="color: var(--text); margin-bottom: 5px;">
|
|||
|
<strong>Conversion:</strong> ${amount} ${from} × ${conversionRate.toFixed(4)} = ${(amount * conversionRate).toFixed(2)} ${to}
|
|||
|
</div>
|
|||
|
<div style="color: var(--muted); font-size: 14px;">
|
|||
|
<strong>Rate:</strong> 1 ${from} = ${conversionRate.toFixed(4)} ${to}<br>
|
|||
|
<strong>Source:</strong> ${rateSource}<br>
|
|||
|
<strong>Date:</strong> ${rateDate}
|
|||
|
</div>
|
|||
|
`;
|
|||
|
} else if (rates) {
|
|||
|
console.log('Using market rates, manual rate was:', manualRate);
|
|||
|
// Use fetched or static rates (fallback when no manual rate)
|
|||
|
conversionRate = rates[to] / rates[from];
|
|||
|
rateSource = lastFetch > 0 ? 'Live rates' : 'Static rates';
|
|||
|
rateDate = lastFetch > 0 ? new Date(lastFetch).toLocaleString() : '2025-09-01';
|
|||
|
|
|||
|
out.innerHTML = `
|
|||
|
<div style="font-size: 24px; font-weight: 700; color: var(--accent); margin-bottom: 10px;">
|
|||
|
${(amount * conversionRate).toFixed(2)} ${to}
|
|||
|
</div>
|
|||
|
<div style="color: var(--text); margin-bottom: 5px;">
|
|||
|
<strong>Conversion:</strong> ${amount} ${from} × ${conversionRate.toFixed(4)} = ${(amount * conversionRate).toFixed(2)} ${to}
|
|||
|
</div>
|
|||
|
<div style="color: var(--muted); font-size: 14px;">
|
|||
|
<strong>Rate:</strong> 1 ${from} = ${conversionRate.toFixed(4)} ${to}<br>
|
|||
|
<strong>Rate:</strong> 1 ${to} = ${(1/conversionRate).toFixed(4)} ${from}<br>
|
|||
|
<strong>Source:</strong> ${rateSource}<br>
|
|||
|
<strong>Rates accurate as of:</strong> ${rateDate}
|
|||
|
</div>
|
|||
|
`;
|
|||
|
} else {
|
|||
|
out.innerHTML = '<div class="muted">Unable to calculate conversion</div>';
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
persist(key, {amount, from, to, manualRate});
|
|||
|
}
|
|||
|
|
|||
|
// Manual rate input handler
|
|||
|
const manualRateInput = manualRateDiv.querySelector('[name="manualRate"]');
|
|||
|
manualRateInput.value = s.manualRate || '';
|
|||
|
|
|||
|
// Debug logging
|
|||
|
console.log('Setting up manual rate input handler');
|
|||
|
|
|||
|
// Set up the event listener
|
|||
|
manualRateInput.addEventListener('input', (e) => {
|
|||
|
console.log('Manual rate input event fired:', e.target.value);
|
|||
|
|
|||
|
// Clear any existing status messages
|
|||
|
status.textContent = '';
|
|||
|
status.style.display = 'none';
|
|||
|
|
|||
|
// Update the stored manual rate
|
|||
|
s.manualRate = +e.target.value || null;
|
|||
|
console.log('Updated manual rate to:', s.manualRate);
|
|||
|
|
|||
|
// Simple test - just update the display immediately
|
|||
|
if (s.manualRate && s.manualRate > 0) {
|
|||
|
const amount = +ui.querySelector('[name=amount]').value || 0;
|
|||
|
const from = ui.querySelector('[name=from]').value;
|
|||
|
const to = ui.querySelector('[name=to]').value;
|
|||
|
|
|||
|
console.log('Immediate calculation test:', { amount, from, to, manualRate: s.manualRate });
|
|||
|
|
|||
|
// Determine if this is user-entered or default custom rate
|
|||
|
const staticRate = staticRates[to] / staticRates[from];
|
|||
|
const isUserEntered = Math.abs(s.manualRate - staticRate) > 0.0001; // Allow for floating point precision
|
|||
|
|
|||
|
const rateSource = isUserEntered ? 'Custom rate' : 'Static rates';
|
|||
|
|
|||
|
// Show immediate result
|
|||
|
out.innerHTML = `
|
|||
|
<div style="font-size: 24px; font-weight: 700; color: var(--accent); margin-bottom: 10px;">
|
|||
|
${(amount * s.manualRate).toFixed(2)} ${to}
|
|||
|
</div>
|
|||
|
<div style="color: var(--text); margin-bottom: 5px;">
|
|||
|
<strong>Conversion:</strong> ${amount} ${from} × ${s.manualRate.toFixed(4)} = ${(amount * s.manualRate).toFixed(2)} ${to}
|
|||
|
</div>
|
|||
|
<div style="color: var(--muted); font-size: 14px;">
|
|||
|
<strong>Rate:</strong> 1 ${from} = ${s.manualRate.toFixed(4)} ${to}<br>
|
|||
|
<strong>Source:</strong> ${rateSource}
|
|||
|
</div>
|
|||
|
`;
|
|||
|
} else {
|
|||
|
// Fall back to normal calculation
|
|||
|
calc();
|
|||
|
}
|
|||
|
});
|
|||
|
|
|||
|
// Also add change event as backup
|
|||
|
manualRateInput.addEventListener('change', (e) => {
|
|||
|
console.log('Manual rate change event fired:', e.target.value);
|
|||
|
s.manualRate = +e.target.value || null;
|
|||
|
calc();
|
|||
|
});
|
|||
|
|
|||
|
// Currency change handlers
|
|||
|
const fromSelect = fromDiv.querySelector('[name=from]');
|
|||
|
const toSelect = toDiv.querySelector('[name=to]');
|
|||
|
|
|||
|
fromSelect.addEventListener('change', () => {
|
|||
|
s.from = fromSelect.value;
|
|||
|
// Update manual rate description and custom rate
|
|||
|
const desc = manualRateDiv.querySelector('div');
|
|||
|
desc.innerHTML = desc.innerHTML.replace(/1 [A-Z]{3} = X [A-Z]{3}/, `1 ${s.from} = X ${s.to}`);
|
|||
|
updateCustomRateWithStatic();
|
|||
|
calc();
|
|||
|
});
|
|||
|
|
|||
|
toSelect.addEventListener('change', () => {
|
|||
|
s.to = toSelect.value;
|
|||
|
// Update manual rate description and custom rate
|
|||
|
const desc = manualRateDiv.querySelector('div');
|
|||
|
desc.innerHTML = desc.innerHTML.replace(/1 [A-Z]{3} = X [A-Z]{3}/, `1 ${s.from} = X ${s.to}`);
|
|||
|
updateCustomRateWithStatic();
|
|||
|
calc();
|
|||
|
});
|
|||
|
|
|||
|
// Amount input handler
|
|||
|
const amountInput = amountDiv.querySelector('[name="amount"]');
|
|||
|
amountInput.addEventListener('input', calc);
|
|||
|
|
|||
|
// Fetch rates button click handler
|
|||
|
fetchBtn.addEventListener('click', async () => {
|
|||
|
await fetchRates();
|
|||
|
calc(); // Recalculate with new rates
|
|||
|
});
|
|||
|
|
|||
|
// Initialize custom rate with current static rate
|
|||
|
updateCustomRateWithStatic();
|
|||
|
|
|||
|
// Initial calculation
|
|||
|
calc();
|
|||
|
root.append(ui);
|
|||
|
}
|
|||
|
}
|