• 01 - Critter Models 02
  • 02 - VantaJS Shaders
  • 03 - App Bundle 02
  • 04 - Cathedrals
  • 05 - App Bundle 01
  • 06 - Retroscopic
  • 07 - Games
  • 08 - Voronoi
  • 09 - Terrain
  • 10 - Critter Models 01
  • 11 - Chromatophores
  • 12 - Conformal Maps
  • 13 - Echo Logix
  • 14 - Music
  • 15 - Colour Theory
MIKE'S NOTEBOOK
Notes
Number:
Project:
01Critter Models 02
└ Locust└ Butterfly└ Cockroach└ Spider└ Scorpion└ Dragon└ Brazilian Treehopper└ Tardigrade└ Diatoms└ Bumblebee└ Wasp└ Hammerhead Worm└ Giraffe Weevil└ Trilobite Beetle
02VantaJS Shaders
└ Clouds└ Waves└ Cells└ Rings└ Halo└ Globe└ Net└ Dots└ Birds└ Trunk└ Topology└ About VantaJS
03App Bundle 02
└ Gutindex└ Insect Builder└ OpenLayers
04Cathedrals
└ Lattice└ Lost└ Lucent└ Lumen└ Glass└ Disco└ Hyperion└ Latent
05App Bundle 01
└ Robohash└ PrismJS└ Horoscope└ Binary Jazz└ Morse Code└ URL Extractor└ Entity Escaper└ Phonetic Alphabet└ Toki Pona└ Semaphore└ Cut Up Machine└ Text Mixer└ Currency Freaks└ Host IO└ CrUX API└ Logo Dev└ Web Speech API└ GEO DB
06Retroscopic
└ Galaxy└ Ocean└ Slime└ 24-Cell└ Unicorn└ Polyp└ Oneiric 1└ Oneiric 2└ Node└ Torus Knot└ GLSL Grid└ Liquid Crystal└ Silky└ Butterfly└ Ocean 2└ Icosahedron└ Luma└ Swarm└ Broken Solid└ Hypersphere└ Constraints└ Psycho Killer
01 / 03

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>
MikeHiggins.me.uk LinkedIn GitHub Bluesky Contra Upwork
© 2026 Mike Higgins. All rights reserved.