+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Currency Converter</title>
+ <style>
+* {
+ box-sizing: border-box;
+}
+
+body {
+ font-family: Helvetica, Arial, sans-serif;
+ max-width: 600px;
+ margin: 50px auto;
+ padding: 20px;
+ background: #f5f5f5;
+}
+
+.container {
+ background: white;
+ padding: 30px;
+ border-radius: 8px;
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
+}
+
+h1 {
+ margin: 0 0 30px 0;
+ font-size: 24px;
+ color: #333;
+ text-align: center;
+}
+
+.input-group {
+ margin-bottom: 20px;
+}
+
+label {
+ display: block;
+ margin-bottom: 5px;
+ color: #555;
+ font-size: 14px;
+ font-weight: 600;
+}
+
+input, select {
+ font-size: 16px;
+ padding: 10px;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ width: 100%;
+ font-family: Helvetica, Arial, sans-serif;
+ background: white;
+}
+
+.row {
+ display: flex;
+ gap: 10px;
+ align-items: flex-end;
+ margin-bottom: 20px;
+}
+
+.col {
+ flex: 1;
+}
+
+.col-swap {
+ flex: 0 0 auto;
+}
+
+.swap-btn {
+ padding: 10px 15px;
+ background: #f0f0f0;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 20px;
+ line-height: 1;
+ margin-bottom: 0;
+}
+
+.swap-btn:hover {
+ background: #e0e0e0;
+}
+
+.convert-btn, .share-btn {
+ width: 100%;
+ padding: 12px;
+ background: #007bff;
+ color: white;
+ border: none;
+ border-radius: 4px;
+ font-size: 16px;
+ cursor: pointer;
+ font-family: Helvetica, Arial, sans-serif;
+ margin-bottom: 10px;
+}
+
+.convert-btn:hover, .share-btn:hover {
+ background: #0056b3;
+}
+
+.share-btn {
+ background: #28a745;
+}
+
+.share-btn:hover {
+ background: #218838;
+}
+
+.result {
+ margin-top: 30px;
+ padding: 20px;
+ background: #f8f9fa;
+ border-radius: 4px;
+ text-align: center;
+ display: none;
+}
+
+.result-amount {
+ font-size: 32px;
+ font-weight: bold;
+ color: #333;
+ margin-bottom: 10px;
+}
+
+.result-rate {
+ font-size: 14px;
+ color: #666;
+}
+
+.meta {
+ margin-top: 20px;
+ font-size: 12px;
+ color: #999;
+ text-align: center;
+}
+
+.error {
+ color: #dc3545;
+ padding: 10px;
+ background: #f8d7da;
+ border-radius: 4px;
+ margin-top: 15px;
+ display: none;
+ font-size: 14px;
+}
+
+.success {
+ color: #155724;
+ padding: 10px;
+ background: #d4edda;
+ border-radius: 4px;
+ margin-top: 15px;
+ display: none;
+ font-size: 14px;
+}
+ </style>
+</head>
+<body>
+ <div class="container">
+ <h1>Currency Converter</h1>
+
+ <div class="input-group">
+ <label for="amount">Amount</label>
+ <input type="number" id="amount" value="100" min="0" step="any">
+ </div>
+
+ <div class="row">
+ <div class="col">
+ <label for="from">From</label>
+ <select id="from"></select>
+ </div>
+ <div class="col-swap">
+ <button id="swap" class="swap-btn">⇄</button>
+ </div>
+ <div class="col">
+ <label for="to">To</label>
+ <select id="to"></select>
+ </div>
+ </div>
+
+ <button id="share" class="share-btn">Copy Share Link</button>
+
+ <div id="success" class="success"></div>
+ <div id="error" class="error"></div>
+
+ <div id="result" class="result">
+ <div class="result-amount" id="result-amount"></div>
+ <div class="result-rate" id="result-rate"></div>
+ </div>
+
+ <div class="meta">
+ <span id="last-updated">Loading rates...</span>
+ </div>
+ </div>
+
+ <script type="module">
+const currencies = [
+ { code: 'AUD', name: 'Australian Dollar', country: 'AU' },
+ { code: 'USD', name: 'US Dollar', country: 'US' },
+ { code: 'CAD', name: 'Canadian Dollar', country: 'CA' },
+ { code: 'GBP', name: 'British Pound', country: 'GB' },
+ { code: 'EUR', name: 'Euro', country: 'EU' },
+ { code: 'JPY', name: 'Japanese Yen', country: 'JP' },
+ { code: 'CNY', name: 'Chinese Yuan', country: 'CN' }
+];
+
+const STORAGE_KEY = 'currencyConverterPrefs';
+const CACHE_KEY = 'exchangeRates';
+const CACHE_TIME_KEY = 'exchangeRatesTime';
+const CACHE_DURATION = 60 * 60 * 1000;
+
+const amountInput = document.getElementById('amount');
+const fromSelect = document.getElementById('from');
+const toSelect = document.getElementById('to');
+const swapBtn = document.getElementById('swap');
+const shareBtn = document.getElementById('share');
+const resultDiv = document.getElementById('result');
+const resultAmount = document.getElementById('result-amount');
+const resultRate = document.getElementById('result-rate');
+const errorDiv = document.getElementById('error');
+const successDiv = document.getElementById('success');
+const lastUpdatedSpan = document.getElementById('last-updated');
+
+function populateSelects() {
+ currencies.forEach(curr => {
+ const optionText = `${curr.code} - ${curr.name}`;
+ fromSelect.add(new Option(optionText, curr.code));
+ toSelect.add(new Option(optionText, curr.code));
+ });
+}
+
+function formatAmount(amount, currency) {
+ if (currency === 'JPY') {
+ return amount.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 0 });
+ }
+ return amount.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
+}
+
+async function detectUserCountry() {
+ try {
+ const response = await fetch('https://ipapi.co/json/', { timeout: 5000 });
+ if (response.ok) {
+ const data = await response.json();
+ return data.country_code;
+ }
+ } catch (e) {
+ console.log('IP detection failed, using timezone fallback');
+ }
+
+ try {
+ const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
+ const country = timeZone.split('/')[0];
+ if (country === 'Australia') return 'AU';
+ if (country === 'America' || country === 'US') return 'US';
+ if (country === 'Canada') return 'CA';
+ if (country === 'Europe' || country === 'London') return 'GB';
+ if (country === 'Japan' || country === 'Tokyo') return 'JP';
+ if (country === 'China' || country === 'Shanghai') return 'CN';
+ } catch (e) {
+ console.log('Timezone detection failed');
+ }
+
+ return null;
+}
+
+function getCurrencyFromCountry(countryCode) {
+ const mapping = {
+ 'AU': 'AUD', 'US': 'USD', 'CA': 'CAD', 'GB': 'GBP',
+ 'EU': 'EUR', 'JP': 'JPY', 'CN': 'CNY'
+ };
+ return mapping[countryCode] || null;
+}
+
+function loadFromURL() {
+ const params = new URLSearchParams(window.location.search);
+ const amount = params.get('amount');
+ const from = params.get('from');
+ const to = params.get('to');
+
+ if (amount && !isNaN(parseFloat(amount))) {
+ amountInput.value = amount;
+ }
+ if (from && currencies.find(c => c.code === from)) {
+ fromSelect.value = from;
+ }
+ if (to && currencies.find(c => c.code === to)) {
+ toSelect.value = to;
+ }
+
+ return !!(amount && from && to);
+}
+
+function loadFromStorage() {
+ try {
+ const stored = localStorage.getItem(STORAGE_KEY);
+ if (stored) {
+ const data = JSON.parse(stored);
+ if (data.amount) amountInput.value = data.amount;
+ if (data.from && currencies.find(c => c.code === data.from)) fromSelect.value = data.from;
+ if (data.to && currencies.find(c => c.code === data.to)) toSelect.value = data.to;
+ return true;
+ }
+ } catch (e) {
+ console.log('Error loading from storage');
+ }
+ return false;
+}
+
+function saveToStorage() {
+ try {
+ const data = {
+ amount: amountInput.value,
+ from: fromSelect.value,
+ to: toSelect.value,
+ timestamp: Date.now()
+ };
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
+ } catch (e) {
+ console.log('Error saving to storage');
+ }
+}
+
+async function initializeDefaults() {
+ populateSelects();
+
+ const loadedFromURL = loadFromURL();
+
+ if (!loadedFromURL) {
+ const loadedFromStorage = loadFromStorage();
+
+ if (!loadedFromStorage) {
+ const country = await detectUserCountry();
+ const defaultCurrency = getCurrencyFromCountry(country);
+
+ if (defaultCurrency && currencies.find(c => c.code === defaultCurrency)) {
+ fromSelect.value = defaultCurrency;
+ toSelect.value = defaultCurrency === 'USD' ? 'EUR' : 'USD';
+ } else {
+ fromSelect.value = 'USD';
+ toSelect.value = 'EUR';
+ }
+ }
+ }
+}
+
+async function fetchRates(base) {
+ const cache = localStorage.getItem(CACHE_KEY);
+ const cacheTime = localStorage.getItem(CACHE_TIME_KEY);
+ const now = Date.now();
+
+ if (cache && cacheTime && (now - parseInt(cacheTime)) < CACHE_DURATION) {
+ const parsed = JSON.parse(cache);
+ if (parsed.base === base) {
+ return parsed;
+ }
+ }
+
+ const response = await fetch(`https://api.exchangerate-api.com/v4/latest/${base}`);
+ if (!response.ok) throw new Error('Failed to fetch exchange rates');
+ const data = await response.json();
+
+ localStorage.setItem(CACHE_KEY, JSON.stringify(data));
+ localStorage.setItem(CACHE_TIME_KEY, now.toString());
+
+ return data;
+}
+
+function showError(message) {
+ errorDiv.textContent = message;
+ errorDiv.style.display = 'block';
+ resultDiv.style.display = 'none';
+ setTimeout(() => { errorDiv.style.display = 'none'; }, 5000);
+}
+
+function showSuccess(message) {
+ successDiv.textContent = message;
+ successDiv.style.display = 'block';
+ setTimeout(() => { successDiv.style.display = 'none'; }, 3000);
+}
+
+function hideError() {
+ errorDiv.style.display = 'none';
+}
+
+function updateTimestamp(dateStr) {
+ if (!dateStr) {
+ lastUpdatedSpan.textContent = 'Rates loaded';
+ return;
+ }
+ const date = new Date(dateStr);
+ lastUpdatedSpan.textContent = `Last updated: ${date.toLocaleString()}`;
+}
+
+function displayResult(amount, from, converted, to, rate) {
+ resultAmount.textContent = `${formatAmount(converted, to)} ${to}`;
+ resultRate.innerHTML = `${formatAmount(amount, from)} ${from} = ${formatAmount(converted, to)} ${to}<br>1 ${from} = ${formatAmount(rate, to)} ${to}`;
+ resultDiv.style.display = 'block';
+}
+
+async function convert() {
+ const amount = parseFloat(amountInput.value);
+ const from = fromSelect.value;
+ const to = toSelect.value;
+
+ if (isNaN(amount) || amount < 0) {
+ showError('Please enter a valid amount');
+ return;
+ }
+
+ hideError();
+
+ if (from === to) {
+ displayResult(amount, from, amount, to, 1);
+ updateTimestamp(new Date());
+ saveToStorage();
+ return;
+ }
+
+ try {
+ const data = await fetchRates(from);
+
+ if (!data.rates || !(to in data.rates)) {
+ throw new Error('Exchange rate not available for selected currency pair');
+ }
+
+ const rate = data.rates[to];
+ const converted = amount * rate;
+
+ displayResult(amount, from, converted, to, rate);
+ updateTimestamp(data.date);
+ saveToStorage();
+ } catch (error) {
+ showError('Unable to fetch exchange rates. Please check your connection and try again.');
+ }
+}
+
+function swapCurrencies() {
+ const temp = fromSelect.value;
+ fromSelect.value = toSelect.value;
+ toSelect.value = temp;
+ convert();
+}
+
+function generateShareLink() {
+ const params = new URLSearchParams({
+ amount: amountInput.value,
+ from: fromSelect.value,
+ to: toSelect.value
+ });
+
+ const url = `${window.location.origin}${window.location.pathname}?${params.toString()}`;
+
+ navigator.clipboard.writeText(url).then(() => {
+ showSuccess('Link copied to clipboard!');
+ }).catch(() => {
+ showError('Failed to copy link. URL is: ' + url);
+ });
+}
+
+amountInput.addEventListener('input', convert);
+fromSelect.addEventListener('change', convert);
+toSelect.addEventListener('change', convert);
+swapBtn.addEventListener('click', swapCurrencies);
+shareBtn.addEventListener('click', generateShareLink);
+
+document.addEventListener('DOMContentLoaded', async () => {
+ await initializeDefaults();
+ convert();
+});
+ </script>
+</body>
+</html>
+# Currency Converter - Project Plan
+
+## Goal
+
+Build a single-page currency converter web application that enables users to convert between seven major world currencies: Australian Dollar (AUD), US Dollar (USD), Canadian Dollar (CAD), British Pound (GBP), Euro (EUR), Japanese Yen (JPY), and Chinese Yuan (CNY). The application will fetch real-time exchange rates from a free external API and provide instant conversion calculations in a clean, usable interface. All code will be contained within a single HTML file incorporating HTML, CSS, and JavaScript with minimal dependencies. The application will intelligently detect the user's location to set appropriate defaults and persist user preferences across sessions.
+
+## Requirements
+
+- MUST support conversion between all seven currencies: AUD, USD, CAD, GBP, EUR, JPY, CNY
+- MUST fetch current exchange rates from a free API without requiring an API key
+- MUST provide an input field for the amount to convert
+- MUST provide dropdown selectors for both source and target currencies
+- MUST display the converted result with appropriate decimal formatting
+- MUST handle API errors gracefully with user-friendly error messages
+- MUST detect local country from user and if it is in From list, must default to that
+- MUST always remember the last amount, from and to you set (localStorage persistence)
+- MUST provide a share link which saves amount, from and to (URL query parameters)
+- SHOULD display the last updated timestamp for exchange rates
+- SHOULD include a swap button to reverse source and target currencies
+- SHOULD cache exchange rates locally to minimise API calls (valid for the session)
+
+## Research
+
+- **ExchangeRate-API** (exchangerate-api.com) offers an open access endpoint at `https://api.exchangerate-api.com/v4/latest/{base}` that requires no API key and returns JSON data suitable for personal and commercial use with attribution
+- **exchangerate.host** provides a completely free REST API with real-time rates for 168+ currencies, no API key required, and returns data in JSON format
+- **IP Geolocation**: IP detection services like ipapi.co can detect user country code from IP address for currency default selection
+- **Timezone fallback**: `Intl.DateTimeFormat().resolvedOptions().timeZone` can be used as fallback to guess user location when IP detection fails
+- **Country to Currency mapping**: AU→AUD, US→USD, CA→CAD, GB→GBP, EU→EUR (or individual EU countries), JP→JPY, CN→CNY
+- Currency precision standards: JPY typically displays 0 decimal places, while AUD, USD, CAD, GBP, EUR, and CNY display 2 decimal places
+- API endpoints return base currency rates where all values represent the conversion rate from the base currency to the target currency
+- Free APIs typically update rates once per day (daily rates) rather than true real-time market rates
+- URLSearchParams API provides easy query string manipulation for share link functionality
+- localStorage persists data indefinitely until cleared by user
+
+## Phases
+
+### Phase 1: Core Conversion Functionality
+
+- Create HTML structure with single-file layout (HTML, CSS, JS combined)
+- Implement CSS styling with Helvetica font, 16px inputs, minimal box-sizing reset
+- Build amount input field with number type validation
+- Build source and target currency dropdowns populated with the 7 supported currencies
+- Integrate with ExchangeRate-API open access endpoint to fetch latest rates
+- Implement conversion calculation logic using fetched rates
+- Display converted result with proper formatting (2 decimals for most, 0 for JPY)
+- Add error handling for network failures or API unavailability
+
+### Phase 2: Persistence & Defaults
+
+- Implement localStorage persistence for amount, from, and to selections
+- Implement URL query parameter parsing for share links (amount, from, to)
+- Implement IP-based country detection to set default "from" currency
+- Add timezone-based fallback detection if IP detection fails
+- Ensure priority: URL params > localStorage > IP detection > USD default
+
+### Phase 3: UI/UX Enhancements
+
+- Add swap button functionality to exchange source and target currencies
+- Display last updated timestamp from API response
+- Add loading indicator during API fetch
+- Implement localStorage caching for exchange rates (refresh only if older than 1 hour)
+- Add share button that generates URL with current parameters and copies to clipboard
+- Add keyboard support (Enter key triggers conversion)
+
+### Phase 4: Additional Features
+
+- Add quick-select buttons for common conversion pairs
+- Display inverse exchange rate (1 target = X source)
+- Add copy-to-clipboard functionality for the converted amount