From bd79aee0856b99cdba9228f39df3bc110a15319d Mon Sep 17 00:00:00 2001 From: vietantran1215 Date: Sat, 7 Feb 2026 13:14:07 +0700 Subject: [PATCH] Challenge submission for Front-end position --- src/problem1/index.js | 44 ++++++ src/problem2/index.html | 98 +++++++++++-- src/problem2/script.js | 301 ++++++++++++++++++++++++++++++++++++++ src/problem2/style.css | 109 +++++++++++++- src/problem3/README.md | 111 ++++++++++++++ src/problem3/Refactor.tsx | 62 ++++++++ 6 files changed, 708 insertions(+), 17 deletions(-) create mode 100644 src/problem1/index.js create mode 100644 src/problem3/README.md create mode 100644 src/problem3/Refactor.tsx diff --git a/src/problem1/index.js b/src/problem1/index.js new file mode 100644 index 000000000..3d361ef4b --- /dev/null +++ b/src/problem1/index.js @@ -0,0 +1,44 @@ +var sum_to_n_a = function (n) { + // your code here + + // Recursive function approach + if (n < 0) return 0; + + if ([0, 1].includes(n)) return n; + + // Complexity: O(n) + return n + sum_to_n_a(n - 1); +}; + +var sum_to_n_b = function (n) { + // your code here + + // Iterative function approach + if (n < 0) return 0; + if ([0, 1].includes(n)) return n; + + // Complexity: O(n) + let sum = 0; + for (let i = 0; i <= n; i++) { + sum += i; + } + return sum; +}; + +var sum_to_n_c = function (n) { + // your code here + + // Mathematical formula approach + if (n < 0) return 0; + if ([0, 1].includes(n)) return n; + + /** + * S = 1 + 2 + 3 + ... + n + * S = n + (n-1) + (n-2) + ... + 1 + * ───────────────────────────────── + * 2S = (n+1) + (n+1) + (n+1) + ... + (n+1) + * S = (n * (n + 1)) / 2 + */ + // Complexity: O(1) + return (n * (n + 1)) / 2; +}; diff --git a/src/problem2/index.html b/src/problem2/index.html index 4058a68bf..600ba90e1 100644 --- a/src/problem2/index.html +++ b/src/problem2/index.html @@ -1,27 +1,97 @@ - + + - + + Fancy Form - + + + + +
+
+
+
+
+

Swap

+ +
+
+ +
+ + +
+
+ + +
+
+ +
+ +
+ + +
+
+ + +
+ + +
+
+
+
+
+
- -
-
Swap
- - + + - - + + - -
- + + - + \ No newline at end of file diff --git a/src/problem2/script.js b/src/problem2/script.js index e69de29bb..d0310007a 100644 --- a/src/problem2/script.js +++ b/src/problem2/script.js @@ -0,0 +1,301 @@ +const TOKEN_SVG_API = 'https://raw.githubusercontent.com/Switcheo/token-icons/main/tokens'; +const CURRENCY_API = 'https://interview.switcheo.com/prices.json' + +let tokenIconsCache = {}; +let currencyData = []; + +async function getTokenData(token) { + try { + // Check cache first + if (tokenIconsCache[token]) { + return tokenIconsCache[token]; + } + + const response = await fetch(`${TOKEN_SVG_API}/${token}.svg`); + if (response.ok) { + const svgText = await response.text(); + tokenIconsCache[token] = svgText; + return svgText; + } + return null; + } catch (error) { + console.error(`Error fetching icon for ${token}:`, error); + return null; + } +} + +async function updateCurrencyIcon(currency, iconElementId) { + const iconElement = document.getElementById(iconElementId); + if (!currency || !iconElement) { + if (iconElement) { + iconElement.innerHTML = ''; + } + return; + } + + const iconSvg = await getTokenData(currency); + if (iconSvg) { + iconElement.innerHTML = iconSvg; + // Style the SVG inside + const svg = iconElement.querySelector('svg'); + if (svg) { + svg.style.width = '24px'; + svg.style.height = '24px'; + } + } else { + // Fallback to currency initials if icon not available + iconElement.innerHTML = currency.substring(0, 2).toUpperCase(); + iconElement.style.fontSize = '10px'; + iconElement.style.fontWeight = 'bold'; + } +} + +async function getCurrencyData() { + const response = await fetch(CURRENCY_API); + return response.json(); +} + +function calculateSwapAmount() { + const inputCurrency = document.getElementById('input-currency').value; + const outputCurrency = document.getElementById('output-currency').value; + const inputAmount = parseFloat(document.getElementById('input-amount').value) || 0; + const outputAmountField = document.getElementById('output-amount'); + + if (!inputCurrency || !outputCurrency) { + outputAmountField.value = ''; + return; + } + + // Find currency prices + const inputCurrencyData = currencyData.find(c => c.currency === inputCurrency); + const outputCurrencyData = currencyData.find(c => c.currency === outputCurrency); + + if (!inputCurrencyData || !outputCurrencyData) { + outputAmountField.value = ''; + return; + } + + if (inputAmount > 0) { + const inputPrice = parseFloat(inputCurrencyData.price); + const outputPrice = parseFloat(outputCurrencyData.price); + + // Calculate: (inputAmount * inputPrice) / outputPrice + const outputAmount = (inputAmount * inputPrice) / outputPrice; + outputAmountField.value = outputAmount.toFixed(6); + } else { + outputAmountField.value = ''; + } +} + +function showError(fieldId, errorMessage) { + const field = document.getElementById(fieldId); + const errorElement = document.getElementById(`${fieldId}-error`); + + if (field && errorElement) { + field.classList.add('is-invalid'); + field.classList.remove('is-valid'); + errorElement.textContent = errorMessage; + errorElement.classList.add('show'); + } +} + +function clearError(fieldId) { + const field = document.getElementById(fieldId); + const errorElement = document.getElementById(`${fieldId}-error`); + + if (field && errorElement) { + field.classList.remove('is-invalid'); + field.classList.add('is-valid'); + errorElement.textContent = ''; + errorElement.classList.remove('show'); + } +} + +function validateInputCurrency() { + const inputCurrency = document.getElementById('input-currency').value; + const outputCurrency = document.getElementById('output-currency').value; + + if (!inputCurrency) { + showError('input-currency', 'Please select a currency to send from.'); + return false; + } + + if (inputCurrency && outputCurrency && inputCurrency === outputCurrency) { + showError('input-currency', 'Please select a different currency from the "To" currency.'); + return false; + } + + clearError('input-currency'); + return true; +} + +function validateOutputCurrency() { + const inputCurrency = document.getElementById('input-currency').value; + const outputCurrency = document.getElementById('output-currency').value; + + if (!outputCurrency) { + showError('output-currency', 'Please select a currency to receive.'); + return false; + } + + if (inputCurrency && outputCurrency && inputCurrency === outputCurrency) { + showError('output-currency', 'Please select a different currency from the "From" currency.'); + return false; + } + + clearError('output-currency'); + return true; +} + +function validateInputAmount() { + const inputAmount = parseFloat(document.getElementById('input-amount').value) || 0; + + if (inputAmount <= 0) { + showError('input-amount', 'Please enter a valid amount greater than 0.'); + return false; + } + + clearError('input-amount'); + return true; +} + +function validateForm() { + const inputCurrencyValid = validateInputCurrency(); + const outputCurrencyValid = validateOutputCurrency(); + const inputAmountValid = validateInputAmount(); + + return inputCurrencyValid && outputCurrencyValid && inputAmountValid; +} + +function handleFormSubmit(event) { + event.preventDefault(); + + if (!validateForm()) { + return; + } + + const inputCurrency = document.getElementById('input-currency').value; + const outputCurrency = document.getElementById('output-currency').value; + const inputAmount = parseFloat(document.getElementById('input-amount').value); + const outputAmount = parseFloat(document.getElementById('output-amount').value); + + // Find currency data for display + const inputCurrencyData = currencyData.find(c => c.currency === inputCurrency); + const outputCurrencyData = currencyData.find(c => c.currency === outputCurrency); + + const swapDetails = { + from: { + currency: inputCurrency.toUpperCase(), + amount: inputAmount, + price: parseFloat(inputCurrencyData.price) + }, + to: { + currency: outputCurrency.toUpperCase(), + amount: outputAmount, + price: parseFloat(outputCurrencyData.price) + } + }; + + // Log swap details (in a real app, this would be sent to a server) + console.log('Swap confirmed:', swapDetails); + + // Populate modal with swap details + document.getElementById('modal-sending-details').textContent = + `${swapDetails.from.amount} ${swapDetails.from.currency} ($${swapDetails.from.price})`; + + document.getElementById('modal-receiving-details').textContent = + `${swapDetails.to.amount.toFixed(6)} ${swapDetails.to.currency} ($${swapDetails.to.price})`; + + // Show modal + const modal = new bootstrap.Modal(document.getElementById('swapConfirmationModal')); + modal.show(); + + // Optionally reset form or keep values + // document.getElementById('swap-form').reset(); +} + +async function populateCurrencySelects() { + currencyData = await getCurrencyData(); + const inputSelect = document.getElementById('input-currency'); + const outputSelect = document.getElementById('output-currency'); + + // Clear loading messages + inputSelect.innerHTML = ''; + outputSelect.innerHTML = ''; + + // Populate both selects with currencies + // const currencies = Object.keys(currencyData).sort(); + + console.log(currencyData); + + currencyData.forEach(option => { + const { currency, price } = option; + + // Create option for input select + const inputOption = document.createElement('option'); + inputOption.value = currency; + inputOption.textContent = `${currency.toUpperCase()} ($${parseFloat(price).toFixed(2)})`; + inputSelect.appendChild(inputOption); + + // Create option for output select + const outputOption = document.createElement('option'); + outputOption.value = currency; + outputOption.textContent = `${currency.toUpperCase()} ($${parseFloat(price).toFixed(2)})`; + outputSelect.appendChild(outputOption); + }); + + // Set default selections if available + if (currencyData.length > 0) { + inputSelect.value = currencyData[0].currency; + await updateCurrencyIcon(currencyData[0].currency, 'input-currency-icon'); + + if (currencyData.length > 1) { + outputSelect.value = currencyData[1].currency; + await updateCurrencyIcon(currencyData[1].currency, 'output-currency-icon'); + } else { + outputSelect.value = currencyData[0].currency; + await updateCurrencyIcon(currencyData[0].currency, 'output-currency-icon'); + } + } +} + +async function main() { + await populateCurrencySelects(); + + // Add event listeners to update icons when selection changes + const inputSelect = document.getElementById('input-currency'); + const outputSelect = document.getElementById('output-currency'); + const inputAmount = document.getElementById('input-amount'); + const form = document.getElementById('swap-form'); + + inputSelect.addEventListener('change', async (e) => { + await updateCurrencyIcon(e.target.value, 'input-currency-icon'); + calculateSwapAmount(); + // Validate on change + validateInputCurrency(); + // Also re-validate output currency in case they're now the same + validateOutputCurrency(); + }); + + outputSelect.addEventListener('change', async (e) => { + await updateCurrencyIcon(e.target.value, 'output-currency-icon'); + calculateSwapAmount(); + // Validate on change + validateOutputCurrency(); + // Also re-validate input currency in case they're now the same + validateInputCurrency(); + }); + + // Calculate swap amount when input amount changes + inputAmount.addEventListener('input', calculateSwapAmount); + + // Inline validation on blur + inputSelect.addEventListener('blur', validateInputCurrency); + outputSelect.addEventListener('blur', validateOutputCurrency); + inputAmount.addEventListener('blur', validateInputAmount); + + // Handle form submission + form.addEventListener('submit', handleFormSubmit); +} + +await main(); \ No newline at end of file diff --git a/src/problem2/style.css b/src/problem2/style.css index 915af91c7..0fd32c3a5 100644 --- a/src/problem2/style.css +++ b/src/problem2/style.css @@ -1,8 +1,111 @@ body { + min-height: 100vh; + min-width: 360px; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + background: #f5f5f5; + padding: 20px; +} + +.card { + border: none; + border-radius: 16px; + overflow: hidden; +} + +.card-body { + background: #ffffff; +} + +.form-control:focus, +.form-select:focus { + border-color: #6c757d; + box-shadow: 0 0 0 0.2rem rgba(108, 117, 125, 0.25); +} + +.btn { + background-color: #212529; + color: #ffffff; + border: none; + transition: transform 0.2s, box-shadow 0.2s; +} + +.btn:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); + background-color: #212529; + color: #ffffff; +} + +.btn:active { + transform: translateY(0); +} + +.input-group-text { + background-color: #f8f9fa; + border: 1px solid #ced4da; + border-right: none; + padding: 0.5rem 0.75rem; display: flex; - flex-direction: row; align-items: center; justify-content: center; - min-width: 360px; - font-family: Arial, Helvetica, sans-serif; + min-width: 40px; + /* Match the height of form-control (default/medium size) */ + height: calc(1.5em + 0.5rem + 2px); +} + +.form-select:focus+.input-group-text, +.input-group:focus-within .input-group-text { + border-color: #6c757d; +} + +#input-currency-icon, +#output-currency-icon { + width: auto; + height: 100%; + display: flex; + align-items: center; + justify-content: center; +} + +#input-currency-icon svg, +#output-currency-icon svg { + width: auto; + height: 100%; + object-fit: contain; +} + +/* Validation styles */ +.invalid-feedback { + display: none; + width: 100%; + margin-top: 0.25rem; + font-size: 0.875em; + color: #dc3545; +} + +.invalid-feedback.show { + display: block; +} + +.form-control.is-invalid~.invalid-feedback, +.input-group+.invalid-feedback.show { + display: block; +} + +.is-valid { + border-color: #198754; +} + +.is-invalid { + border-color: #dc3545; +} + +.is-valid:focus { + border-color: #198754; + box-shadow: 0 0 0 0.2rem rgba(25, 135, 84, 0.25); } + +.is-invalid:focus { + border-color: #dc3545; + box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25); +} \ No newline at end of file diff --git a/src/problem3/README.md b/src/problem3/README.md new file mode 100644 index 000000000..e5ebb3c4f --- /dev/null +++ b/src/problem3/README.md @@ -0,0 +1,111 @@ +# Found Inefficiencies and Anti-patterns + +### 1. Inside the `filter()` callback and in the outer scope, there's no declaration of `lhsPriority` +Suggested fix: replacing `lshPriority` with `balancePriority` + +### 2. Inverted filter logic in the in `filter()` callback return. + +Suggested fix: +replace: +``` +if (lhsPriority > -99) { + if (balance.amount <= 0) { + return true; + } +} +``` + +with +``` +return lhsPriority > -99 && balance.amount > 0 +``` + +### 3. Incomplete sort function, missing return for equal priority + +- Most recommended solution: sorting the list in the API + +- Front-end logic solution: + +``` +return rightPriority - leftPriority +``` + +### 4. Type `any` inferrence in `getPriority` +- Suggested fix: replace `balance: any` to `string` +- Better approach: refactor the `getPriority()`, use index annotation: + ``` + interface BlockchainPriority { + [key: string]: number; + } + const getPriority = (blockchain: string): number => { + // In the future may be stored persistently in the database and Redis cache for better performance and scalability, and responds to front-end whenever requested + // Trade-off: latency because of API communcation through http + + const blockchainPriority: BlockchainPriority = { + Osmosis: 100, + Ethereum: 50, + Arbitrum: 30 + Zilliqa: 20, + Neo: 20 + } + + return blockchainPriority[blockchain as keyof BlockchainPriority] || -99; + } + ``` + +### 5. `prices` is the redundant dependencies of sortedBalances memo + +Inside the computed callback, there's no usage of `prices`, which also added to the `useMemo()` dependencies array => This means even if not using `prices`, the dependency change detected by sortedBalances computing function is unecessary and will also cause unecessary recalculated (while the result remains the same) + +Fix: remove the `prices` out of the dependencies array + +### 6. `formattedBalances` is recalculrated in every re-render. + +The recalculation of `formattedBalances` on every re-render is also unecessary because the results remain the same if the `sortedBalances` stay unchanged + +Fix: wrap the computation inside `useMemo()` with `sortedBalances` as the only one dependencies. + +### 7. Using index as React key + +Using index as React key causes issues in list re-rendering. It is a good practices to use another unique identifier instead. (Such as: `id` fields returns from the API with database querying or `useId()` value) + +Fix: in this case specifically, we can use currency as the identifier because the currency name is unique + +### 8. Missing `blockchain` field in the `WalletBalance` interface + +in the `sortedBalances` computed value, there's an access of `balance.blockchain` which is previously infered with type `WalletBalance. + +Suggested fix: +``` +interface WalletBalance { + currency: string; + amount: number; + blockchain: string; // Add missing property +} +``` + +### 9. Unsafe price computation + +In the statement `const usdValue = prices[balance.currency] * balance.amount;`, the logic can't guarantee and predict if `balance.currency` exists in prices because the balance.currency is unpredictable. This can leads to make the calculation to `undefined * balance.amount` which results in `NaN`. + +There should be a fallback for accessing `prices[balance.currency]`. + +Fix: update it to: +``` const usdValue = (prices[balance.currency] || 0) * balance.amount;` + +### 10. Code repetation in 2 interfaces `WalletBalance` and `FormattedWalletBalance` + +Use TypeScript interface extension (inheritance) for better reusability. Fix: + +``` +interface WalletBalance { + currency: string; + amount: number; + blockchain: string; // fixed for 8. +} +interface FormattedWalletBalance extends WalletBalance { + formatted: string; +} +``` + +### [Optional for scalability] 11. API should have supported and returned a sorted list of balances by priority \ No newline at end of file diff --git a/src/problem3/Refactor.tsx b/src/problem3/Refactor.tsx new file mode 100644 index 000000000..8388d04af --- /dev/null +++ b/src/problem3/Refactor.tsx @@ -0,0 +1,62 @@ +interface WalletBalance { + currency: string; + amount: number; + blockchain: string; +} + +interface FormattedWalletBalance extends WalletBalance { + formatted: string; +} + +interface BlockchainPriority { + [key: string]: number; +} + +const getPriority = (blockchain: string): number => { + // TODO: Should have processed in the API response + const blockchainPriority: BlockchainPriority = { + Osmosis: 100, + Ethereum: 50, + Arbitrum: 30, + Zilliqa: 20, + Neo: 20 + } + return blockchainPriority[blockchain as keyof BlockchainPriority] || -99; +}; + +const sortedBalances = useMemo(() => { + // TODO: should have processed in the API response + return balances + .filter((balance: WalletBalance) => { + const balancePriority = getPriority(balance.blockchain); + if (balancePriority > -99 && balance.amount > 0) { // Fixed logic + return true; + } + return false; + }) + .sort((lhs: WalletBalance, rhs: WalletBalance) => { + const leftPriority = getPriority(lhs.blockchain); + const rightPriority = getPriority(rhs.blockchain); + return rightPriority - leftPriority; + }); +}, [balances]); + +const formattedBalances = useMemo(() => { + return sortedBalances.map((balance: WalletBalance) => ({ + ...balance, + formatted: balance.amount.toFixed(2) + })); +}, [sortedBalances]); + +const rows = formattedBalances.map((balance: FormattedWalletBalance) => { + const usdValue = (prices[balance.currency] || 0) * balance.amount; + return ( + + ); +}); \ No newline at end of file