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
13 changes: 13 additions & 0 deletions src/problem1/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
var sum_to_n_a = function(n) {
return n * (n + 1) / 2;
}

var sum_to_n_b = function(n) {
return Array.from({length: n}, (_,i) => i + 1).reduce((acc, curr) => acc + curr, 0);
}

var sum_to_n_c = function(n){
if(n <= 0) return 0;

return sum_to_n_c(n - 1) + n;
}
64 changes: 64 additions & 0 deletions src/problem2/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Token Swap Application

## Structure

### 1. Types
**Path:** `src/types/token.type.ts`
Define interface:
- `TokenPrice`
- `Token`
- `SwapFormData`
- `SwapResult`

---

### 2. API Module
**Path:** `src/apis/token.api.ts`
- Fetch token prices:
`https://interview.switcheo.com/prices.json`
- Generate icon URLs from GitHub repository `https://github.com/Switcheo/token-icons/tree/main/tokens`

---

### 3. Custom Hook – `useTokenPrices`
**Path:** `src/hooks/useTokenPrices.ts`
- Using **TanStack Query** to fetch & cache data
---

### 4. TokenSelect Component
**Path:** `src/components/TokenSelect/`
- Dropdown select token includes **search**
- Display **icon token** (includes fallback)
- Animation smooth with **Framer Motion**

---

### 5. SwapForm Component
**Path:** `src/components/SwapForm/`
- Two-way calculation (FROM ↔ TO)
- Validation with **Yup**:
- Positive numbers
- Non-empty numbers
- Valid format
- Mock loading state (**2s**)
- Display exchange rate
- Swap button to change token positions

---

### 6. Swap Page
**Path:** `src/pages/Swap/`
- Gradient background
- Responsive layout
- Card-based UI

---
**Installation:**
- Install Node version >=20.0.0
- Install yarn version >= 1.22.21 or npm version >= 10.9.4
- Execution:
- `npm install` or `yarn install`
- `npm run dev` or `yarn dev`

**App is running:** `http://localhost:3000/`

41 changes: 15 additions & 26 deletions src/problem2/index.html
Original file line number Diff line number Diff line change
@@ -1,27 +1,16 @@
<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" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Token Swap | Currency Exchange</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
90 changes: 90 additions & 0 deletions src/problem2/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
{
"name": "token swap application",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"build:dev": "tsc -b && vite build --mode development",
"build:staging": "tsc -b && vite build --mode staging",
"build:production": "tsc -b && vite build --mode production",
"preview": "vite preview",
"lint": "eslint --ext ts,tsx src/",
"prettier": "prettier --check \"src/**/(*.tsx|*.ts|*.css|*.scss)\"",
"prettier:fix": "prettier --write \"src/**/(*.tsx|*.ts|*.css|*.scss)\"",
"test": "vitest",
"coverage": "vitest run --coverage",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
},
"dependencies": {
"@floating-ui/react": "^0.27.5",
"@hookform/resolvers": "^4.1.3",
"@tanstack/react-query": "^5.69.0",
"axios": "^1.8.4",
"classnames": "^2.5.1",
"dompurify": "^3.2.4",
"framer-motion": "^12.5.0",
"i18next": "^24.2.3",
"i18next-browser-languagedetector": "^8.0.4",
"i18next-http-backend": "^3.0.2",
"lodash": "^4.17.21",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-hook-form": "^7.54.2",
"react-i18next": "^15.4.1",
"react-router-dom": "^7.4.0",
"react-toastify": "^11.0.5",
"yup": "^1.6.1"
},
"devDependencies": {
"@chromatic-com/storybook": "^3",
"@eslint/js": "^9.21.0",
"@storybook/addon-essentials": "^8.6.10",
"@storybook/addon-onboarding": "^8.6.10",
"@storybook/blocks": "^8.6.10",
"@storybook/experimental-addon-test": "^8.6.10",
"@storybook/react": "^8.6.10",
"@storybook/react-vite": "^8.6.10",
"@storybook/test": "^8.6.10",
"@tailwindcss/vite": "^4.0.15",
"@tanstack/react-query-devtools": "^5.69.0",
"@types/lodash": "^4.17.16",
"@types/node": "^22.13.13",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"@types/react-i18next": "^8.1.0",
"@typescript-eslint/eslint-plugin": "^8.28.0",
"@typescript-eslint/parser": "^8.28.0",
"@vitejs/plugin-react": "^4.3.4",
"@vitest/browser": "3.0.9",
"@vitest/coverage-v8": "3.0.9",
"autoprefixer": "^10.4.16",
"eslint": "^9.23.0",
"eslint-config-prettier": "^10.1.1",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-prettier": "^5.2.4",
"eslint-plugin-react": "^7.37.4",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.19",
"eslint-plugin-storybook": "^0.12.0",
"playwright": "^1.51.1",
"postcss": "^8.4.32",
"prettier": "^3.5.3",
"prettier-plugin-tailwindcss": "^0.6.11",
"storybook": "^8.6.10",
"tailwindcss": "^3.4.0",
"ts-node": "^10.9.2",
"typescript": "~5.7.2",
"typescript-eslint": "^8.24.1",
"vite": "^6.2.0",
"vite-plugin-html": "^3.2.2",
"vite-plugin-svgr": "^4.3.0",
"vitest": "^3.0.9"
},
"engines": {
"node": ">=20.0.0"
}
}
6 changes: 6 additions & 0 deletions src/problem2/postcss.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
}
1 change: 1 addition & 0 deletions src/problem2/public/vite.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Empty file removed src/problem2/script.js
Empty file.
1 change: 1 addition & 0 deletions src/problem2/src/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules
5 changes: 5 additions & 0 deletions src/problem2/src/@types/global.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
declare global {
interface Window {
__RUNTIME_CONFIG__: string
}
}
1 change: 1 addition & 0 deletions src/problem2/src/@types/svg.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/// <reference types="vite-plugin-svgr/client" />
26 changes: 26 additions & 0 deletions src/problem2/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { ToastContainer } from 'react-toastify'
import 'react-toastify/dist/ReactToastify.css'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { useRouteElements } from './useRouteElements'

const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false
}
}
})

function App() {
const routeElements = useRouteElements()
return (
<QueryClientProvider client={queryClient}>
{routeElements}
<ToastContainer />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
)
}

export default App
18 changes: 18 additions & 0 deletions src/problem2/src/apis/token.api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import axios from 'axios'
import { TokenPrice } from 'src/types/token.type'

const PRICES_API_URL = 'https://interview.switcheo.com/prices.json'
const TOKEN_ICONS_BASE_URL = 'https://raw.githubusercontent.com/Switcheo/token-icons/main/tokens'

export const tokenApi = {
getTokenPrices: async (): Promise<TokenPrice[]> => {
const response = await axios.get<TokenPrice[]>(PRICES_API_URL)
return response.data
},

getTokenIconUrl: (currency: string): string => {
return `${TOKEN_ICONS_BASE_URL}/${currency}.svg`
}
}

export default tokenApi
20 changes: 20 additions & 0 deletions src/problem2/src/components/LazyLoader/LazyLoader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Suspense, ReactNode } from 'react'

const DefaultFallback = () => (
<div className='animate-pulse space-y-4 p-4 bg-gray-100 rounded-lg'>
<div className='h-4 bg-gray-300 rounded w-3/4'></div>
<div className='h-4 bg-gray-300 rounded w-1/2'></div>
<div className='h-4 bg-gray-300 rounded w-full'></div>
</div>
)

interface LazyLoaderProps {
children: ReactNode
fallback?: ReactNode
}

const LazyLoader = ({ children, fallback = <DefaultFallback /> }: LazyLoaderProps) => {
return <Suspense fallback={fallback}>{children}</Suspense>
}

export default LazyLoader
1 change: 1 addition & 0 deletions src/problem2/src/components/LazyLoader/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './LazyLoader'
Loading