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
52 changes: 52 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Dependencies (any folder named node_modules)
node_modules/
**/node_modules/

# Build / output
dist/
build/
out/
.next/
.nuxt/
.svelte-kit/

# Environment and secrets
.env
.env.*
!.env.example

# Logs and debug
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*

# Test / coverage
coverage/
.nyc_output/
*.lcov

# Caches
.cache/
.parcel-cache/
.turbo/
*.tsbuildinfo
.eslintcache
.stylelintcache

# OS
.DS_Store
Thumbs.db
Desktop.ini

# Editor / IDE
.idea/
*.swp
*.swo
*~

# Optional tooling
.vercel/
.netlify/
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 27 additions & 0 deletions src/problem1/sum-to-n.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// a. for loop
var sum_to_n_a = function (n) {
if (n < 1) {
return 0;
}
var total = 0;
for (var i = 1; i <= n; i++) {
total += i;
}
return total;
};

// b. formula
var sum_to_n_b = function (n) {
if (n < 1) {
return 0;
}
return (n * (n + 1)) / 2;
};

// c. recursion
var sum_to_n_c = function (n) {
if (n < 1) {
return 0;
}
return n + sum_to_n_c(n - 1);
};
129 changes: 129 additions & 0 deletions src/problem2/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# Problem 2 — Fancy Form (Currency Swap)

A polished, single-page **token swap** UI built as a front-end demo. Pick two assets, type an amount, and see the converted output computed from live USD prices. Submission is simulated with a short delay — there is no real backend or wallet.

Live data comes from the public Switcheo interview feed:

- Prices: <https://interview.switcheo.com/prices.json>
- Icons: <https://github.com/Switcheo/token-icons>

## Stack

| Piece | Role |
| ----- | ---- |
| [React 18](https://react.dev/) + [TypeScript](https://www.typescriptlang.org/) | UI + types |
| [Vite 5](https://vitejs.dev/) | Dev server / bundler |
| [Tailwind CSS v4](https://tailwindcss.com/) (`@tailwindcss/vite`) | Styling |
| [shadcn/ui](https://ui.shadcn.com/) + [Radix UI](https://www.radix-ui.com/) | Accessible primitives (Select, Label, Slot, …) |
| [framer-motion](https://www.framer.com/motion/) | Micro-interactions and transitions |
| [lucide-react](https://lucide.dev/) | Icon set |
| [`@fontsource-variable/geist`](https://fontsource.org/fonts/geist) | Variable font |

## Requirements

- Node.js 20+ (LTS recommended)
- npm

## Install and run

```bash
cd src/problem2
npm install
```

**Development** (Vite dev server with HMR):

```bash
npm run dev
```

Then open the printed URL (typically <http://localhost:5173>).

**Production build**:

```bash
npm run build
npm run preview
```

`preview` serves the built bundle locally so you can sanity-check the production output.

## How the prices feed is wired

To avoid CORS issues during local development, Vite proxies the prices request:

```ts
// vite.config.ts
server: {
proxy: {
"/prices.json": {
target: "https://interview.switcheo.com",
changeOrigin: true,
secure: true,
},
},
}
```

In **dev** the app fetches `/prices.json` (proxied). In **production** it fetches `https://interview.switcheo.com/prices.json` directly. See `src/lib/prices.ts`.

The feed contains multiple rows per currency on different dates; only the **most recent** row per currency is kept (see `dedupeLatestByCurrency`).

## What the form does

- **Token pickers** with searchable Radix-powered select, showing icon, symbol and current USD price for each token.
- **Amount input** with a permissive parser (`src/lib/amountInput.ts`) that:
- strips thousands separators,
- rejects negatives, multiple decimal points, and non-numeric input,
- caps fractional digits (12 by default) and reports a helpful error message.
- **Live conversion** using `convertAmount(amount, fromPriceUsd, toPriceUsd)` — `(amount × fromPrice) / toPrice`.
- **Reverse button** swaps the two tokens and carries the receive-side amount back into the send field.
- **Mock balances** (`src/lib/mockBalance.ts`) per currency so the form can validate "exceeds balance" without a real wallet.
- **Validation surfaces inline**: same token on both sides, invalid number, or amount above the demo balance — all rendered under the input and reflected in `aria-invalid` and the disabled state of the submit button.
- **Submit** is a simulated async action: a 1.8 s delay, then a success toast-style banner with the executed pair and amounts. Nothing is sent anywhere.
- **Loading and error states** handle the price-fetch lifecycle, including a Retry button if the feed fails or returns fewer than two priced tokens.
- **Smart defaults**: prefers `ETH → USDC` when both exist; otherwise falls back to the first two priced tokens.

## Accessibility and UX touches

- Radix primitives provide keyboard navigation and focus management out of the box.
- Inputs are paired with `<Label>` / `htmlFor`; the receive field uses a real `<output for>` element.
- Validation errors are announced via `aria-describedby` and `aria-invalid`.
- Submitting state uses `aria-busy` and a spinner.
- A subtle animated background (grid + radial blur) and motion-driven transitions keep things lively without being noisy.
- Tokens with missing icons gracefully fall back to a circular initials badge (`TokenIcon.tsx`).

## Project layout

| Path | Purpose |
| ---- | ------- |
| `index.html` | Vite entry; sets dark theme on `<html>` |
| `src/main.tsx` | React root |
| `src/App.tsx` | Page shell, background, footer credits |
| `src/components/SwapForm.tsx` | The swap form (the bulk of the UI) |
| `src/components/TokenIcon.tsx` | Token icon with initials fallback |
| `src/components/ui/*` | shadcn/ui primitives (Button, Card, Input, Label, Select) |
| `src/lib/prices.ts` | Fetch + dedupe prices, format amounts, conversion math |
| `src/lib/amountInput.ts` | Tolerant amount parser + formatter |
| `src/lib/mockBalance.ts` | Deterministic per-token mock balances and default pair |
| `src/lib/utils.ts` | `cn` class merge helper |
| `src/index.css` | Tailwind v4 entry + theme tokens |
| `vite.config.ts` | Vite, Tailwind plugin, alias `@ → ./src`, prices proxy |
| `tsconfig.json` | TS config (path alias `@/*`) |
| `components.json` | shadcn registry config |

The `@` alias resolves to `./src`, so imports look like `@/components/ui/button` and `@/lib/prices`.

## npm scripts

| Script | Command |
| ------ | ------- |
| `dev` | `vite` |
| `build` | `vite build` |
| `preview` | `vite preview` |

## Notes and caveats

- This is **front-end only**. There is no wallet integration, no order routing, and the "balance" is a deterministic hash of the currency symbol — purely for UX demonstration.
- Price freshness is whatever the public feed provides; the UI does not auto-refresh.
- The dark theme is hard-coded via `class="dark"` on `<html>`; there is no theme toggle.
24 changes: 24 additions & 0 deletions src/problem2/components.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "radix-nova",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/index.css",
"baseColor": "neutral",
"cssVariables": true
},
"iconLibrary": "lucide",
"rtl": false,
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"menuColor": "default",
"menuAccent": "subtle",
"registries": {}
}
29 changes: 9 additions & 20 deletions src/problem2/index.html
Original file line number Diff line number Diff line change
@@ -1,27 +1,16 @@
<html>
<!DOCTYPE html>
<html lang="en" class="dark">

<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" />
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Swap — Fancy Form</title>
</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>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>

</html>
</html>
Loading