CURRENCY FREAKS - INTERACTIVE APP SOURCE CODE
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CurrencyFreaks</title>
<style>
:root {
color-scheme: light dark;
}
* {
box-sizing: border-box;
font-family: 'Segoe UI', Roboto, system-ui, sans-serif;
}
html, body {
background: transparent;
margin: 0;
padding: 0;
min-height: 100vh;
color: #e5e9f0;
}
.selector-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 18px;
margin-bottom: 30px;
width: 100%;
}
.selector-row select {
background: #1f2632;
border: 1px solid #384153;
border-radius: 40px;
padding: 12px 30px 12px 20px;
font-size: 1rem;
color: white;
appearance: none;
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%239aa8bc' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><polyline points='6 9 12 15 18 9'/></svg>");
background-repeat: no-repeat;
background-position: right 16px center;
background-size: 16px;
cursor: pointer;
flex: 2 1 300px;
}
.selector-row button {
background: #3e5b8a;
border: none;
border-radius: 60px;
padding: 12px 32px;
font-size: 1.1rem;
font-weight: 600;
color: white;
cursor: pointer;
transition: 0.15s;
border: 1px solid #5f7bb0;
box-shadow: 0 4px 10px rgba(0,20,50,0.6);
flex: 0 1 auto;
}
.selector-row button:hover {
background: #5273aa;
border-color: #8dabeb;
transform: scale(1.01);
}
.selector-row button:active {
background: #2c4468;
transform: scale(0.98);
}
.params-grid {
display: flex;
flex-wrap: wrap;
gap: 20px;
margin-top: 5px;
margin-bottom: 30px;
width: 100%;
}
.param-field {
flex: 1 1 180px;
min-width: 150px;
}
.param-field label {
display: block;
font-size: 0.75rem;
color: #8d9caf;
margin-bottom: 4px;
font-weight: 500;
}
.param-field input {
width: 100%;
background: #1b222d;
border: 1px solid #314159;
border-radius: 30px;
padding: 10px 16px;
color: white;
font-size: 0.9rem;
}
.param-field input:disabled {
opacity: 0.5;
background: #161e28;
color: #6a768b;
}
.helper-note {
font-size: 0.8rem;
color: #6d7e98;
margin: -20px 0 20px 8px;
width: 100%;
}
.response-area {
width: 100%;
margin-top: 10px;
}
.response-header {
display: flex;
justify-content: space-between;
color: #b4c2da;
font-weight: 400;
margin-bottom: 14px;
font-size: 0.9rem;
border-bottom: 1px solid #253040;
padding-bottom: 10px;
}
.response-header .status {
background: #283040;
padding: 4px 14px;
border-radius: 40px;
color: #b7d0ff;
}
pre {
background: #0b0f15;
padding: 18px;
border-radius: 0;
color: #d6e2f5;
font-family: 'JetBrains Mono', 'Fira Code', monospace;
font-size: 0.85rem;
overflow-x: auto;
white-space: pre-wrap;
word-wrap: break-word;
border: 1px solid #202834;
max-height: 500px;
margin: 0;
}
.loader {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid #384967;
border-radius: 50%;
border-top-color: #a2c2ff;
animation: spin 0.8s linear infinite;
margin-right: 8px;
}
@keyframes spin { to { transform: rotate(360deg); } }
.flex-row {
display: flex;
align-items: center;
gap: 10px;
}
/* Light Mode Overrides */
html:not(.dark) body { background: transparent; color: #1b1c1c; }
html:not(.dark) .selector-row select {
background-color: #ffffff;
border: 1px solid #cfc4c5;
color: #1b1c1c;
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23474747' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><polyline points='6 9 12 15 18 9'/></svg>");
}
html:not(.dark) .selector-row button { background: #000000; color: #ffffff; border: 1px solid #000000; box-shadow: 0 4px 10px rgba(0,0,0,0.1); }
html:not(.dark) .selector-row button:hover { background: #303031; border-color: #303031; }
html:not(.dark) .param-field label { color: #474747; }
html:not(.dark) .param-field input { background: #ffffff; border: 1px solid #cfc4c5; color: #1b1c1c; }
html:not(.dark) .param-field input:disabled { background: #e4e2e2; color: #7e7576; }
html:not(.dark) .helper-note { color: #474747; }
html:not(.dark) .response-header { color: #4c4546; border-bottom: 1px solid #cfc4c5; }
html:not(.dark) .response-header .status { background: #e4e2e2; color: #1b1b1b; }
html:not(.dark) pre { background: #f5f3f3; color: #1b1c1c; border: 1px solid #cfc4c5; }
html:not(.dark) .loader { border: 3px solid #cfc4c5; border-top-color: #000000; }
</style>
</head>
<body>
<div class="selector-row">
<select id="endpointSelect">
<option value="latest">📈 Latest rates (all)</option>
<option value="latest_symbols">🎯 Latest rates (filter by symbols)</option>
<option value="supported">🌐 Supported currencies (no key needed)</option>
<option value="symbols">🔣 Currency symbols list</option>
<option value="hist_limits">📚 Historical data limits</option>
</select>
<button id="fetchBtn">▶️ FETCH</button>
</div>
<div id="paramsContainer" class="params-grid"></div>
<div class="response-area">
<div class="response-header">
<span class="flex-row"><span class="loader" id="loader" style="display:none;"></span> PROXY RESPONSE</span>
<span class="status" id="httpStatus">—</span>
</div>
<pre id="responsePre">👈 select an endpoint and click FETCH</pre>
</div>
<script>
(function() {
// Theme Sync with Parent Window
function syncTheme() {
try {
if (window.parent && window.parent.document) {
const isDark = window.parent.document.documentElement.classList.contains('dark');
if (isDark) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
}
} catch (e) {}
}
syncTheme();
try {
if (window.parent && window.parent.document) {
const observer = new MutationObserver(syncTheme);
observer.observe(window.parent.document.documentElement, { attributes: true, attributeFilter: ['class'] });
}
} catch (e) {}
const PROXY_URL = '/.netlify/functions/proxy-request';
const NOTE_ID = 'a8f3a403-ab5b-4bfc-86d5-04972aa75852';
const API_BASE_URL = 'https://api.currencyfreaks.com/v2.0';
const endpointSelect = document.getElementById('endpointSelect');
const paramsContainer = document.getElementById('paramsContainer');
const fetchBtn = document.getElementById('fetchBtn');
const loader = document.getElementById('loader');
const httpStatusSpan = document.getElementById('httpStatus');
const responsePre = document.getElementById('responsePre');
let currentEndpoint = 'latest';
const paramConfig = {
latest: [
{ name: 'base', label: 'Base currency (optional)', type: 'text', placeholder: 'USD, EUR, ...', default: '' },
{ name: 'symbols', label: 'Symbols (comma-separated, optional)', type: 'text', placeholder: 'GBP,JPY,CHF', default: '' }
],
latest_symbols: [
{ name: 'symbols', label: '🔸 Symbols (required)', type: 'text', placeholder: 'PKR,GBP,EUR', default: 'PKR,GBP,EUR' },
{ name: 'base', label: 'Base (optional)', type: 'text', placeholder: 'USD', default: '' }
],
supported: [],
symbols: [],
hist_limits: []
};
function renderParams(endpoint) {
const config = paramConfig[endpoint] || [];
if (!config.length) {
paramsContainer.innerHTML = '<div class="helper-note" style="padding: 8px 0;">📍 No parameters needed for this endpoint.</div>';
return;
}
let html = '';
config.forEach(p => {
html += `<div class="param-field">
<label>${p.label}</label>
<input type="${p.type}" id="param_${p.name}" placeholder="${p.placeholder}" value="${p.default}">
</div>`;
});
paramsContainer.innerHTML = html;
}
endpointSelect.addEventListener('change', () => {
currentEndpoint = endpointSelect.value;
renderParams(currentEndpoint);
});
function buildTargetUrl() {
const baseUrl = API_BASE_URL.replace(/\/$/, '');
let endpointPath = '';
const queryParts = [];
const keyNeeded = ['latest', 'latest_symbols'];
const addParam = (key, value) => {
if (value === undefined || value === null || value === '') return;
if (key === 'apikey' && value === '{{CURRENCY_FREAKS_API_KEY}}') {
queryParts.push(`apikey={{CURRENCY_FREAKS_API_KEY}}`);
} else {
queryParts.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`);
}
};
switch (currentEndpoint) {
case 'latest':
case 'latest_symbols':
endpointPath = '/rates/latest';
if (keyNeeded.includes(currentEndpoint)) {
addParam('apikey', '{{CURRENCY_FREAKS_API_KEY}}');
}
const baseLatest = document.getElementById('param_base')?.value;
const symLatest = document.getElementById('param_symbols')?.value;
addParam('base', baseLatest);
addParam('symbols', symLatest);
break;
case 'supported':
endpointPath = '/supported-currencies';
break;
case 'symbols':
endpointPath = '/currency-symbols';
break;
case 'hist_limits':
endpointPath = '/historical-data-limits';
break;
default:
endpointPath = '/rates/latest';
if (keyNeeded.includes('latest')) {
addParam('apikey', '{{CURRENCY_FREAKS_API_KEY}}');
}
}
const baseFull = baseUrl + endpointPath;
if (queryParts.length === 0) return baseFull;
return baseFull + '?' + queryParts.join('&');
}
async function fetchData() {
const targetUrl = buildTargetUrl();
loader.style.display = 'inline-block';
httpStatusSpan.innerText = 'proxying...';
try {
const response = await fetch(PROXY_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
noteId: NOTE_ID,
targetUrl: targetUrl,
method: 'GET',
options: { headers: {} }
})
});
httpStatusSpan.innerText = `${response.status} ${response.statusText}`;
const responseText = await response.text();
try {
const json = JSON.parse(responseText);
responsePre.innerText = JSON.stringify(json, null, 2);
} catch {
responsePre.innerText = responseText;
}
} catch (error) {
httpStatusSpan.innerText = 'fetch error';
responsePre.innerText = `🚨 Proxy request failed:\n${error.message}`;
} finally {
loader.style.display = 'none';
}
}
fetchBtn.addEventListener('click', fetchData);
renderParams('latest');
})();
</script>
</body>
</html>