calculator.127local.net/public/calculators/currency.js
2025-09-01 20:30:45 -07:00

417 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.opacity = '0.9');
fetchBtn.addEventListener('mouseleave', () => fetchBtn.style.opacity = '1');
ui.append(fetchBtn);
// External API notice
const noticeDiv = document.createElement('div');
noticeDiv.style.cssText = `
margin: 10px 0;
padding: 12px;
background: var(--k-bg);
border: 1px solid var(--border);
border-radius: 6px;
font-size: 13px;
color: var(--muted);
line-height: 1.4;
`;
noticeDiv.innerHTML = `
<strong>External API Notice:</strong> Updating exchange rates requires an external call to exchangerate-api.com.
Your browser will make a request to fetch the latest rates. Your IP address will be visible to the API provider.
`;
ui.append(noticeDiv);
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);
}
}