summary history files

commit:d21692a794ad7b996aae0b153dfae7c02a30d7e3
date:Mon Mar 16 21:55:39 2026 +1100
parents:26d90fccdf5754aba94ef48a3b3358b4b311a149
added forex
diff --git a/forex/index.html b/forex/index.html
line changes: +474/-0
index 0000000..a4585c1
--- /dev/null
+++ b/forex/index.html
@@ -0,0 +1,474 @@
+<!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>

diff --git a/forex/plan.md b/forex/plan.md
line changes: +69/-0
index 0000000..2cd1623
--- /dev/null
+++ b/forex/plan.md
@@ -0,0 +1,69 @@
+# 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