DEV Community

임세환
임세환

Posted on • Originally published at seansble.hashnode.dev

Building an Offline-First Exchange Calculator with Vanilla JavaScript

SudangHelp is a Korean financial decision engine that turns scattered government policies and financial data into simple calculator outputs. The product direction is intentionally practical: users should be able to open a page, enter a number, and get a clear result without reading through multiple policy documents.

One of the more interesting tools in this system is the travel exchange calculator. At first, it sounds like a basic currency converter, but the real use case is a little different: travelers need fast answers, country-specific defaults, and a page that still behaves reasonably when the network is unstable.

People do not always need a full finance dashboard when they are traveling. They need to know things like:

  • "How much is 100,000 VND in KRW?"
  • "Can I use this in the airport without installing an app?"
  • "Can I open the same page again if the connection is bad?"
  • "Can the page start with the right currency for Vietnam, Thailand, or Japan?"

That changed the implementation direction. I did not want to build a heavy app. I wanted a fast static page that behaves like a small app when needed.

The result is the live SudangHelp exchange calculator, built with plain HTML, CSS, and Vanilla JavaScript.

This post is a short implementation note on how I structured it.

1. Why I used Vanilla JavaScript

The calculator does not need a framework. Most of the state is local and temporary:

  • the source currency
  • one or more target currencies
  • the amount entered by the user
  • the latest loaded exchange rates
  • the country preset inferred from the URL

Using Vanilla JavaScript kept the runtime small and made the page easier to cache. It also reduced the amount of build tooling needed for a static site.

The core conversion logic is intentionally boring:

function convertCurrency(amount, from, to) {
  if (!EXCHANGE_RATES[from] || !EXCHANGE_RATES[to]) return 0;
  return (amount / EXCHANGE_RATES[from]) * EXCHANGE_RATES[to];
}
Enter fullscreen mode Exit fullscreen mode

For a calculator, boring code is usually a good thing. The more important work was around page behavior: presets, fallback data, and offline access.

2. URL-driven country presets (briefly)

The calculator uses URL-based routing where each country path (e.g., /vietnam/, /japan/, /thailand/) initializes the calculator with the correct default currency pair — all served from a single static HTML file.

I covered this pattern in detail in an earlier post:

👉 Building a 49-Country Exchange Calculator with a Single Static Page

For this article, what matters is that the URL-derived preset determines which currency pair the offline cache prioritizes. That brings us to the more interesting part — making the whole thing work without an internet connection.

3. Live rates with a safe fallback

The calculator loads exchange rates from a worker endpoint. If the request succeeds, the app updates the in-memory rate table and recalculates the visible result.

If the request fails, the calculator does not crash. It falls back to default rates and shows an offline-mode message.

The simplified flow:

async function loadRates() {
  try {
    const response = await fetch(EXCHANGE_API_URL);

    if (!response.ok) {
      throw new Error(`HTTP error: ${response.status}`);
    }

    const data = await response.json();
    const rates = data.rates || data;

    if (typeof rates !== 'object' || rates === null) {
      throw new Error('Invalid rates data');
    }

    Object.keys(EXCHANGE_RATES).forEach(code => {
      if (rates[code] && typeof rates[code] === 'number' && rates[code] > 0) {
        EXCHANGE_RATES[code] = rates[code];
      }
    });

    updateDisplay();
    updateRateDisplay();
  } catch (error) {
    rateInfo.textContent = 'Offline mode using fallback rates';
  }
}
Enter fullscreen mode Exit fullscreen mode

There are two important ideas here:

  • Validate external data before using it.
  • Keep the calculator usable even when the network is unstable.

For a travel tool, that fallback behavior is not a bonus feature. It is part of the product.

4. PWA-style caching

The project uses a service worker under the travel section. The service worker pre-caches the main travel pages, selected country pages, icons, and an offline page.

For HTML navigation requests, the service worker uses a network-first strategy:

if (
  event.request.mode === 'navigate' ||
  event.request.headers.get('accept')?.includes('text/html')
) {
  event.respondWith(networkFirst(event.request));
  return;
}
Enter fullscreen mode Exit fullscreen mode

For static assets, it uses cache-first:

event.respondWith(cacheFirst(event.request));
Enter fullscreen mode Exit fullscreen mode

This split is useful:

  • HTML should be fresh when possible.
  • CSS, JavaScript, and icons can be served quickly from cache.
  • If navigation fails, the cached page or offline page can still respond.

I also excluded API endpoints and external worker URLs from the cache list. Exchange rate data should not be silently frozen by a static asset cache.

5. DOM caching for repeated updates

The calculator updates the UI frequently when users type, switch currencies, or add target currencies.

Instead of querying the same DOM nodes repeatedly, I initialize a small DOM reference object on page load:

DOM.amountValue = document.getElementById('amount-value-input');
DOM.symbolInput = document.getElementById('symbol-input');
DOM.amountKorean = document.getElementById('amount-korean-input');
DOM.fromCurrency = document.getElementById('from-currency');
DOM.operationDisplay = document.getElementById('operation-display');
DOM.conversionResults = document.getElementById('conversion-results');
Enter fullscreen mode Exit fullscreen mode

This is not a complex optimization. It is just a practical one. Calculator interfaces are input-heavy, so small repeated operations add up.

6. What I would improve next

The current version works as a fast static calculator, but there are still improvements I would like to make:

  • separate the country preset data from the calculator runtime
  • generate country pages from a single source of truth
  • add better test coverage for URL preset detection
  • improve stale-rate messaging when the app is offline
  • make the install prompt more consistent across iOS and Android

Final thoughts

The biggest lesson from this project was that a "simple calculator" is rarely just a formula. The formula is the easy part. The difficult part is designing the surrounding behavior: loading, fallback, routing, caching, and the first state the user sees.

That is where a small static tool starts to feel like a real product.

You can try the calculator yourself at sudanghelp.co.kr/travel/exchange/.

Top comments (0)