Professional trading position-size and risk/reward calculator — built as a production-grade PWA.
Calc Trade helps traders calculate the exact position size, margin, and potential profit/loss for any trade — instantly, on device, and fully offline.
Key user flows:
- Enter account balance, risk %, stop-loss %, and leverage.
- See risked capital, required margin, and total position size in real time.
- Adjust the risk/reward slider to preview estimated PnL and ROE.
- Install to the home screen and use it anywhere, even without internet.
All calculations run entirely in the browser — no data is ever sent to a server.
| Category | Details |
|---|---|
| Calculator | Position size, risked capital, margin, PnL, ROE, percent change |
| PWA | Installable, offline-first, update prompt, offline banner |
| i18n | English (en) and Persian/Farsi (fa) with full RTL layout |
| Themes | Light / dark / system via next-themes |
| Persistence | Form values persisted to localStorage via react-hook-form-persist |
| Analytics | Umami event tracking (privacy-friendly, no PII) |
| Fonts | Google Fonts Inter (en) · IRANSansX variable font (fa) |
| Layer | Technology |
|---|---|
| Framework | Next.js 16 — App Router, React Server Components |
| Language | TypeScript 5 |
| Styling | Tailwind CSS 4 + Motion animations |
| UI primitives | Radix UI (shadcn/ui components) |
| Forms | React Hook Form + Zod |
| i18n | next-intl |
| PWA / SW | next-pwa + Workbox |
| Analytics | Umami |
| URL state | nuqs |
| Notifications | Sonner |
| Package manager | pnpm |
| Deployment | Netlify |
- Node.js ≥ 22 (LTS recommended)
- pnpm ≥ 10 — install with
npm i -g pnpm
git clone https://github.com/siamak/calc-trade.git
cd calc-trade
pnpm installCopy the example file and fill in the required values:
cp env.example .env.local| Variable | Required | Description |
|---|---|---|
NEXT_PUBLIC_UMAMI_SCRIPT_URL |
Optional | URL to your Umami tracking script |
NEXT_PUBLIC_UMAMI_WEBSITE_ID |
Optional | Umami website ID |
The app works without Umami configured — analytics events are simply no-ops.
pnpm dev # start dev server at http://localhost:3000The service worker and PWA features are disabled in development (
disable: process.env.NODE_ENV === "development"innext.config.ts). To test PWA locally, use the production build.
pnpm build # compile + generate service worker
pnpm start # serve the production build locallypnpm lintcalc-trade/
├── app/
│ ├── layout.tsx # Root layout — font preloads
│ └── [locale]/
│ ├── layout.tsx # Locale layout — providers, meta, PWA shell
│ └── page.tsx # Main calculator page
├── src/
│ ├── components/
│ │ ├── providers/
│ │ │ └── pwa-provider.tsx # PWA context — SW, online, install, update
│ │ ├── ui/ # shadcn/ui primitives
│ │ ├── calc-form.tsx # Calculator form
│ │ ├── result.tsx # Calculation results
│ │ ├── risk-rewards.tsx # Risk/reward slider + PnL summary
│ │ ├── offline-banner.tsx # Offline / reconnected strip
│ │ ├── pwa-update-prompt.tsx # "Update available" card
│ │ └── pwa-install-button.tsx # Floating install button
│ ├── hooks/
│ │ ├── use-pwa.ts # Re-exports usePWAContext — public API
│ │ └── use-analytics.ts # Umami event helpers
│ ├── lib/
│ │ ├── schemas.ts # Zod form schemas
│ │ └── analytics.ts # Umami event wrappers
│ └── i18n/
│ ├── routing.ts # Locale config (en, fa)
│ └── request.ts # next-intl server config
├── public/
│ ├── manifest.json # PWA web app manifest
│ ├── sw.js # Workbox-generated service worker (gitignored)
│ ├── sw-custom.js # Deprecated stub — self-unregisters old SW
│ ├── icons/ # Android + iOS launcher icons
│ ├── splash/ # iOS splash screens
│ └── webfont/ # Self-hosted IRANSansX
├── messages/
│ ├── en.json # English strings
│ └── fa.json # Persian strings
├── contents/ # Markdown educational content
├── next.config.ts # Next.js + next-pwa + next-intl config
└── netlify.toml # Netlify deployment config
The app uses Workbox generateSW via next-pwa. All caching rules are declared in next.config.ts — no boilerplate SW file is needed.
Why generateSW instead of injectManifest?
Every caching requirement is expressible through Workbox's built-in strategies. generateSW keeps all configuration colocated and readable. injectManifest would only be needed for complex custom fetch logic (e.g., streaming, partial responses, push payloads) which this app does not require.
register: false in next.config.ts — registration is handled manually by PWAProvider so the app controls the full update flow:
New deploy
└─ Browser fetches /sw.js (byte-changed)
└─ New SW enters "installing" → "installed (waiting)" state
└─ PWAProvider detects waiting SW → hasUpdate = true
└─ <PWAUpdatePrompt> shown to user
└─ User clicks "Update"
└─ applyUpdate() sends {type:'SKIP_WAITING'}
└─ controllerchange fires → page reloads
skipWaiting: false ensures the new SW never activates mid-session without the user's consent.
| Resource | Strategy | Cache name | TTL |
|---|---|---|---|
| Google Fonts CSS | Cache First | google-fonts-stylesheets |
1 year |
| Google Fonts binary | Cache First | google-fonts-webfonts |
1 year |
| Local webfonts (woff/woff2) | Cache First | static-font-assets |
1 year |
| Images (png/svg/ico/webp) | Cache First | static-image-assets |
30 days |
/_next/image |
StaleWhileRevalidate | next-image |
24 h |
| JS bundles | StaleWhileRevalidate | static-js-assets |
24 h |
| CSS | StaleWhileRevalidate | static-style-assets |
24 h |
/_next/data JSON |
NetworkFirst (5 s) | next-data |
24 h |
/api/content/* (markdown) |
NetworkFirst (8 s) | api-content |
7 days |
Other GET /api/* |
NetworkFirst (10 s) | api-others |
24 h |
| HTML / navigation | NetworkFirst (5 s) | pages |
24 h |
| Everything else | NetworkFirst (10 s) | others |
24 h |
What is never cached:
- POST / PUT / PATCH / DELETE requests
- Auth tokens (there is no auth in this app)
| Scenario | Behaviour |
|---|---|
| Network unavailable | Amber banner: "You're offline — using cached content" |
| Network restored | Green flash: "Back online" (auto-hides after 3 s) |
| New SW waiting | Bottom-right card with "Update" button |
| Previously visited page | Served from cache (NetworkFirst fallback) |
| Never-visited page offline | Empty cache → browser error (expected) |
/public/manifest.json declares:
id,scope,start_url,display,display_overridepurpose: maskableon the 512 × 512 icon (Android adaptive icons)shortcutsfor quick-launch from the home screencategories: ["finance", "utilities"]dir: autofor bilingual support
Supported locales: en (English, LTR) and fa (Persian, RTL).
Route structure:
/ → redirect to /en/
/en/ → English calculator
/fa/ → Persian calculator (RTL, IRANSansX font)
Adding a new locale:
- Add it to
src/i18n/routing.ts→localesarray. - Create
messages/<locale>.jsonwith the same keys asen.json. - Add a font class and CSS in
app/globals.cssif a custom font is needed.
Analytics are collected via Umami (client-side only, privacy-friendly). Page views are tracked automatically by the Umami script. Custom events include:
| Event | Trigger |
|---|---|
form_input_changed |
Any form field change |
calculation_performed |
Form value change (debounced result) |
risk_reward_ratio_changed |
Slider drag |
form_reset |
Reset button |
theme_changed |
Theme toggle |
locale_changed |
Language switch |
pwa_install_prompt_shown |
beforeinstallprompt fires |
pwa_installed |
User accepts install |
external_link_clicked |
Outbound link |
error_occurred |
Error boundary catches |
All analytics are fire-and-forget. If NEXT_PUBLIC_UMAMI_SCRIPT_URL / NEXT_PUBLIC_UMAMI_WEBSITE_ID are not set, events are silently skipped.
A netlify.toml is included. Push to main and Netlify will:
- Run
pnpm build - Publish the
.next/output
Set NEXT_PUBLIC_UMAMI_SCRIPT_URL and NEXT_PUBLIC_UMAMI_WEBSITE_ID in the Netlify dashboard environment variables (optional).
# Build command
pnpm build
# Output directory
.next
# Install command
pnpm install --frozen-lockfile- Visit the app, then go offline (DevTools → Network → Offline)
- Reload — amber banner appears, calculator is still usable
- All calculations work (no network required)
- Restore connection — green "Back online" flash appears and fades
- Open in Chrome desktop → install button appears in address bar
- Click install → app opens in standalone window
- Install button no longer appears
- All assets load instantly from SW cache (no waterfall)
- Lighthouse PWA score ≥ 90
- Deploy a code change with a unique build hash
- Open the already-installed PWA — update prompt appears within ~5 s
- Click "Update" → page reloads with new version
- Deploy a new build
- Old cached assets are served (StaleWhileRevalidate) until SW activates
- After update prompt and reload, fresh assets load
- Open two tabs — both show the same update prompt when a new SW arrives
- Clicking "Update" in one tab reloads both
- Chrome 90+ (desktop + Android) — full PWA support
- Edge 90+ — full PWA support
- Safari 16.4+ (iOS/macOS) — installable, service worker works, no
beforeinstallprompt - Firefox 90+ — service worker works, no install prompt
-
/fa/route renders in Persian with RTL layout - IRANSansX font loads offline after first visit
- Offline banner and update prompt display Persian text on
/fa/
# Build for production
pnpm build
# Serve the production build
pnpm start
# Open http://localhost:3000
# DevTools → Application → Service Workers to inspect SW state// In DevTools console:
// Unregister all service workers
navigator.serviceWorker.getRegistrations().then(regs => regs.forEach(r => r.unregister()));
// List all caches
caches.keys().then(console.log);
// Delete all caches
caches.keys().then(keys => Promise.all(keys.map(k => caches.delete(k))));MIT — see LICENSE for details.