Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/problem1/sumToN.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
var sum_to_n_a = function(n) {
const nums = Array.from({length: n}, (_, i) => i + 1);
return nums.reduce((a, b) => a + b, 0);
};

var sum_to_n_b = function(n) {
return n * (n + 1) / 2;
};

var sum_to_n_c = function(n) {
let total = 0;

for (let i = 1; i <= n; i++) {
total += i;
}

return total;
};
41 changes: 41 additions & 0 deletions src/problem2/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

# Dependency directories
node_modules/
jspm_packages/

# IDEs and editors
.idea/
.project
.classpath
.c9/
*.launch
*.sublime-project
*.sublime-workspace

# IDE - VSCode
.vscode/
.history

# Build outputs
dist/
dist-ssr/
*.local

# TS build info
*.tsbuildinfo

# OS files
.DS_Store
Thumbs.db

# Vite & Vitest
.vite
.vitest
4 changes: 4 additions & 0 deletions src/problem2/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
dist
node_modules
coverage
pnpm-lock.yaml
6 changes: 6 additions & 0 deletions src/problem2/.prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"semi": true,
"singleQuote": true,
"trailingComma": "all",
"plugins": ["prettier-plugin-tailwindcss"]
}
100 changes: 100 additions & 0 deletions src/problem2/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# Problem 2: Currency Swap UI

This folder contains a quote-driven currency swap interface built with Vite, React, and TypeScript.

## Tech Stack

- Vite
- React 18 + TypeScript
- Tailwind CSS
- TanStack Query
- shadcn/base-ui generated primitives

## Features

- Fetches and normalizes latest prices from:
- `https://interview.switcheo.com/prices.json`
- Fetches token icons from Switcheo token icon repository
- Two-way quote calculation:
- editing `pay` recalculates `receive`
- editing `receive` recalculates `pay`
- Input validation:
- numeric only
- decimal precision limit
- max input cap
- Debounced quote refresh on amount changes
- Fresh price fetch before each quote recalculation
- Re-quote after swap direction changes using the newly active edited side
- Loading skeletons for:
- initial prices load
- icon load
- quote refresh
- Searchable currency dropdown
- Light theme with semantic Tailwind color tokens
- Responsive input layout:
- mobile: currency row and amount row are stacked on 2 lines
- desktop: currency and amount are shown side by side
- Middle switch button for currency direction
- Bottom `Swap` action:
- enabled only when a valid quote is present
- resets the form inputs
- Error state with retry if prices API fails

## Project Structure

- `src/modules/swap-form/index.tsx`: Main swap UI container and rendering
- `src/modules/swap-form/hook/useSwapQuote.ts`: Quote orchestration, debounce, abort cleanup, swap behavior
- `src/modules/swap-form/api/queries.ts`: React Query hooks
- `src/components/common/CurrencyInput.tsx`: Reusable amount/currency input row
- `src/api/currency.ts`: Price and icon API helpers
- `src/modules/swap-form/lib/utils.ts`: Input validation and quote/notional helpers
- `src/index.css`: Tailwind + global theme/base styles
- `tailwind.config.js`: Semantic color tokens used by the light theme

## Run Locally

From this folder:

```bash
# pnpm
pnpm install
pnpm dev

# npm
npm install
npm run dev
```

## Build and Checks

```bash
# pnpm
pnpm build
pnpm lint
pnpm format:check

# npm
npm run build
npm run lint
npm run format:check
```

## Architecture Decisions and Trade-offs

- Data fetching with TanStack Query
- `useFetchPrices` and `useFetchIcons` manage loading, retry, and icon caching.
- Quote recalculation itself is still orchestrated in `useSwapQuote` because it needs debounce and cancellation behavior tied to local form state.
- Quote orchestration in a dedicated hook
- `useSwapQuote` owns amount state, selected currencies, `AbortController`, debounce timer, and swap-direction behavior.
- This keeps `SwapForm` focused on rendering and derived display values.
- Price normalization before UI usage
- The raw upstream payload is filtered to remove invalid entries and collapsed to the latest valid price per currency.
- This avoids surfacing stale or malformed rows directly in the UI.
- Semantic theming
- Swap UI colors are defined in `tailwind.config.js` instead of hardcoded class literals in JSX.
- This makes the current light theme easier to maintain and evolve.

## Notes

- The bottom `Swap` button is currently a reset action, not a real swap execution request.
- There is no automated test runner configured in this folder yet; current verification is via `build`, `lint`, and formatting checks.
25 changes: 25 additions & 0 deletions src/problem2/components.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "base-nova",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "src/index.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"rtl": false,
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"menuColor": "default",
"menuAccent": "subtle",
"registries": {}
}
38 changes: 38 additions & 0 deletions src/problem2/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import js from '@eslint/js';
import globals from 'globals';
import importPlugin from 'eslint-plugin-import';
import reactHooks from 'eslint-plugin-react-hooks';
import reactRefresh from 'eslint-plugin-react-refresh';
import tseslint from 'typescript-eslint';

export default tseslint.config(
{ ignores: ['dist'] },
js.configs.recommended,
...tseslint.configs.recommended,
{
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
import: importPlugin,
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
settings: {
'import/resolver': {
typescript: true,
node: true,
},
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
'import/no-unresolved': 'error',
},
},
);
37 changes: 11 additions & 26 deletions src/problem2/index.html
Original file line number Diff line number Diff line change
@@ -1,27 +1,12 @@
<html>

<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Fancy Form</title>

<!-- You may add more stuff here -->
<link href="style.css" rel="stylesheet" />
</head>

<body>

<!-- You may reorganise the whole HTML, as long as your form achieves the same effect. -->
<form onsubmit="return !1">
<h5>Swap</h5>
<label for="input-amount">Amount to send</label>
<input id="input-amount" />

<label for="output-amount">Amount to receive</label>
<input id="output-amount" />

<button>CONFIRM SWAP</button>
</form>
<script src="script.js"></script>
</body>

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Currency Swap</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
49 changes: 49 additions & 0 deletions src/problem2/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"name": "currency-swap",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --max-warnings 0",
"preview": "vite preview",
"format": "prettier --write .",
"format:check": "prettier --check ."
},
"dependencies": {
"@base-ui/react": "^1.3.0",
"@fontsource-variable/geist": "^5.2.8",
"@tanstack/react-query": "^5.96.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"lucide-react": "^0.360.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"tailwind-merge": "^3.5.0",
"tw-animate-css": "^1.4.0"
},
"devDependencies": {
"@eslint/js": "^9.39.4",
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22",
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.27",
"eslint": "^9.39.4",
"eslint-import-resolver-node": "^0.3.10",
"eslint-import-resolver-typescript": "^4.4.4",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.4.0",
"postcss": "^8.5.8",
"prettier": "^3.8.1",
"prettier-plugin-tailwindcss": "^0.7.2",
"shadcn": "^4.1.2",
"tailwindcss": "^3.4.17",
"typescript": "^5.2.2",
"typescript-eslint": "^8.58.0",
"vite": "^5.2.0"
}
}
Loading