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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ The app runs at http://localhost:5173 by default.
- **Send Money** — enter a recipient and amount, pick currencies, and see a
live FX quote with the RemitFlow fee broken out before sending.
- **Transfers** — list of your transfers with status badges (pending,
completed, failed), search/status/date-range filters synced to the URL,
completed, failed), debounced search input, status and date-range filters synced to the URL,
plus loading, error and empty states.
- **Tabs** — reusable tabbed interface with swipe gesture support for mobile, accessible keyboard navigation, and controlled/uncontrolled modes.
- **Print support** — a dedicated print stylesheet (`src/print.css`)
Expand Down
1 change: 1 addition & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export default [
languageOptions: {
globals: {
...globals.browser,
...globals.node,
...globals.es2021,
},
parserOptions: {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"lint": "eslint .",
"preview": "vite preview",
"storybook": "storybook dev -p 6006",
"test": "vitest run test/integration test/unit test/lighthouse-config.test.js",
"test": "vitest run test/integration test/unit test/hooks test/lighthouse-config.test.js",
"test:lighthouse": "vitest run test/lighthouse-config.test.js",
"test:watch": "vitest"
},
Expand Down
39 changes: 27 additions & 12 deletions src/pages/Transfers.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import Pagination from '../components/Pagination.jsx';
import PullToRefresh from '../components/PullToRefresh.jsx';
import SelectionToolbar from '../components/SelectionToolbar.jsx';
import { useTransfers } from '../hooks/useTransfers.js';
import { useDebouncedValue } from '../hooks/useDebouncedValue.js';
import { useApp } from '../context/AppContext.jsx';
import { DATE_RANGE_PRESETS, isWithinDateRange } from '../utils/dateRange.js';
import './Transfers.css';
Expand All @@ -37,6 +38,28 @@ export default function Transfers() {
const status = searchParams.get('status') || '';
const range = searchParams.get('range') || '';

const [searchInput, setSearchInput] = useState(search);
const debouncedSearch = useDebouncedValue(searchInput, 250);

// Sync searchInput when URL search parameter changes externally
useEffect(() => {
setSearchInput(search);
}, [search]);

// Sync debounced search value to URL query parameters
useEffect(() => {
setSearchParams((prev) => {
const current = prev.get('search') || '';
if (current === debouncedSearch) return prev;
if (debouncedSearch) {
prev.set('search', debouncedSearch);
} else {
prev.delete('search');
}
return prev;
});
}, [debouncedSearch, setSearchParams]);

// Selection state
const [selectedIds, setSelectedIds] = useState(() => new Set());
const [selectAllAcross, setSelectAllAcross] = useState(false);
Expand Down Expand Up @@ -79,17 +102,9 @@ export default function Transfers() {
: selectedIds.size;
const hasMorePages = totalPages > 1;

const handleSearchChange = useCallback(
(e) => {
const value = e.target.value;
setSearchParams((prev) => {
if (value) prev.set('search', value);
else prev.delete('search');
return prev;
});
},
[setSearchParams],
);
const handleSearchChange = useCallback((e) => {
setSearchInput(e.target.value);
}, []);

const handleStatusChange = useCallback(
(e) => {
Expand Down Expand Up @@ -244,7 +259,7 @@ export default function Transfers() {
type="search"
className="transfers-filters-search"
placeholder="Search by recipient…"
value={search}
value={searchInput}
onChange={handleSearchChange}
aria-label="Search transfers by recipient"
/>
Expand Down
2 changes: 1 addition & 1 deletion src/services/wallet.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export function connectWallet() {
* @param {object} payload - the transaction details to "sign"
* @returns {Promise<{signature: string}>}
*/
export function signTransaction(_payload) {
export function signTransaction() {
return new Promise((resolve) => {
setTimeout(() => {
resolve({
Expand Down
62 changes: 62 additions & 0 deletions test/hooks/useDebouncedValue.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { act, renderHook } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { useDebouncedValue } from '../../src/hooks/useDebouncedValue.js';

describe('useDebouncedValue hook', () => {
beforeEach(() => {
vi.useFakeTimers();
});

afterEach(() => {
vi.useRealTimers();
});

it('returns initial value immediately', () => {
const { result } = renderHook(() => useDebouncedValue('initial', 300));
expect(result.current).toBe('initial');
});

it('updates debounced value after the specified delay', () => {
const { result, rerender } = renderHook(
({ value }) => useDebouncedValue(value, 300),
{ initialProps: { value: 'initial' } },
);

rerender({ value: 'updated' });
expect(result.current).toBe('initial');

act(() => {
vi.advanceTimersByTime(299);
});
expect(result.current).toBe('initial');

act(() => {
vi.advanceTimersByTime(1);
});
expect(result.current).toBe('updated');
});

it('resets timer when value changes rapidly before delay expires', () => {
const { result, rerender } = renderHook(
({ value }) => useDebouncedValue(value, 300),
{ initialProps: { value: 'first' } },
);

rerender({ value: 'second' });
act(() => {
vi.advanceTimersByTime(200);
});
expect(result.current).toBe('first');

rerender({ value: 'third' });
act(() => {
vi.advanceTimersByTime(200);
});
expect(result.current).toBe('first');

act(() => {
vi.advanceTimersByTime(100);
});
expect(result.current).toBe('third');
});
});
5 changes: 4 additions & 1 deletion test/integration/send-money-form.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ import * as api from '../../src/services/api.js';
async function fillValidForm(user) {
await user.type(screen.getByLabelText(/recipient/i), 'amina@example.com');
await user.type(screen.getByLabelText(/amount/i), '15');
await user.selectOptions(screen.getByLabelText(/to/i), 'NGN');
await user.selectOptions(
screen.getByLabelText(/to/i, { selector: 'select' }),
'NGN',
);
}

function createdTransfer(payload) {
Expand Down
2 changes: 1 addition & 1 deletion test/services/wallet.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ describe('connectWallet', () => {

try {
await connectWallet();
} catch (err) {
} catch {
// Expected to throw
}

Expand Down
2 changes: 0 additions & 2 deletions test/touch-targets.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ function readCSS(filename) {
return fs.readFileSync(path.resolve(process.cwd(), 'src', filename), 'utf8');
}

const MIN_TARGET = '44px';

describe('touch target minimum sizes (44px)', () => {
describe('button elements', () => {
const css = readCSS(path.join('components', 'Button.css'));
Expand Down
Loading