diff --git a/frontends/web/src/routes/account/account.module.css b/frontends/web/src/routes/account/account.module.css index 71043ef7a9..4dca2fef3f 100644 --- a/frontends/web/src/routes/account/account.module.css +++ b/frontends/web/src/routes/account/account.module.css @@ -132,4 +132,77 @@ .txHistorySkeleton { margin-top: var(--space-quarter); +} + +.searchContainer { + position: relative; + display: flex; + align-items: center; + margin-bottom: var(--space-half); +} + +.searchInput { + background-color: var(--background-secondary); + border: 1px solid transparent; + border-radius: 0; + color: var(--color-default); + font-size: var(--size-default); + width: 100%; + padding: calc(var(--space-quarter) + var(--space-eight)) var(--space-half); + padding-left: 28px; + outline: none; + transition: border-color .2s ease-out; +} + +.searchInput:focus { + border: 1px solid var(--color-blue); + outline: none; + box-shadow: none; +} + +.searchInput.withClearButton { + padding-right: 28px; +} + +.searchIcon { + position: absolute; + left: 8px; + top: 50%; + transform: translateY(-50%); + width: 16px; + height: 16px; + pointer-events: none; + opacity: 0.6; +} + +.clearButton { + position: absolute; + right: 12px; + top: 50%; + transform: translateY(-50%); + background: none; + border: none; + cursor: pointer; + font-size: 16px; + padding: 0; + width: 16px; + height: 16px; + display: flex; + align-items: center; + justify-content: center; +} + +.clearButton:hover { + opacity: 1; +} + +.headerControls { + display: flex; + gap: var(--space-quarter); + align-items: center; +} + +.clearButtonIcon { + width: 16px; + height: 16px; } \ No newline at end of file diff --git a/frontends/web/src/routes/account/account.tsx b/frontends/web/src/routes/account/account.tsx index 118c939078..3a3ae05402 100644 --- a/frontends/web/src/routes/account/account.tsx +++ b/frontends/web/src/routes/account/account.tsx @@ -15,7 +15,7 @@ * limitations under the License. */ -import { useCallback, useContext, useEffect, useState } from 'react'; +import React, { useCallback, useContext, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom'; import * as accountApi from '@/api/account'; @@ -27,7 +27,7 @@ import { useSDCard } from '@/hooks/sdcard'; import { alertUser } from '@/components/alert/Alert'; import { Balance } from '@/components/balance/balance'; import { HeadersSync } from '@/components/headerssync/headerssync'; -import { Info } from '@/components/icon'; +import { CloseXDark, CloseXWhite, Info, Loupe } from '@/components/icon'; import { GuidedContent, GuideWrapper, Header, Main } from '@/components/layout'; import { Spinner } from '@/components/spinner/Spinner'; import { Status } from '@/components/status/status'; @@ -51,8 +51,62 @@ import { TransactionDetails } from '@/components/transactions/details'; import { Button } from '@/components/forms'; import { SubTitle } from '@/components/title'; import { TransactionHistorySkeleton } from '@/routes/account/transaction-history-skeleton'; -import style from './account.module.css'; import { RatesContext } from '@/contexts/RatesContext'; +import { useDarkmode } from '@/hooks/darkmode'; +import style from './account.module.css'; + +const SearchInput = React.memo(({ + placeholder, + onSearchChange +}: { + placeholder: string; + onSearchChange?: (searchTerm: string) => void; +}) => { + const [query, setQuery] = useState(''); + const { isDarkMode } = useDarkmode(); + + const handleChange = useCallback((e: React.ChangeEvent) => { + const value = e.target.value; + setQuery(value); + }, []); + + const handleClear = useCallback(() => { + setQuery(''); + onSearchChange?.(''); + }, [onSearchChange]); + + useEffect(() => { + const timeoutId = setTimeout(() => { + onSearchChange?.(query); + }, 300); + + return () => clearTimeout(timeoutId); + }, [query, onSearchChange]); + + return ( +
+ + + {query && ( + + )} +
+ ); +}); + +SearchInput.displayName = 'SearchInput'; type Props = { accounts: accountApi.IAccount[]; @@ -91,6 +145,8 @@ const RemountAccount = ({ const [detailID, setDetailID] = useState(null); const supportedExchanges = useLoad(getExchangeSupported(code), [code]); + const [debouncedSearchQuery, setDebouncedSearchQuery] = useState(''); + const account = accounts && accounts.find(acct => acct.code === code); const getBitsuranceGuideLink = (): string => { @@ -196,6 +252,22 @@ const RemountAccount = ({ .catch(console.error); }; + const handleDebouncedSearch = useCallback((searchTerm: string) => { + setDebouncedSearchQuery(searchTerm); + }, []); + + // Filter transactions based on debounced search query + const filteredTransactions = debouncedSearchQuery && transactions?.success + ? { + ...transactions, + list: transactions.list.filter(tx => { + const searchLower = debouncedSearchQuery.toLowerCase(); + return tx.txID.toLowerCase().includes(searchLower) || + tx.note.toLowerCase().includes(searchLower); + }) + } + : transactions; + const hasDataLoaded = balance !== undefined && transactions !== undefined; if (!account) { @@ -235,7 +307,6 @@ const RemountAccount = ({ && transactions.success && transactions.list.length === 0; - const actionButtonsProps = { code, accountDataLoaded: hasDataLoaded, @@ -247,6 +318,7 @@ const RemountAccount = ({ const loadingTransactions = transactions?.success === undefined; const hasTransactions = transactions?.success && transactions.list.length > 0; + const showingFilteredResults = debouncedSearchQuery && transactions?.success; return ( @@ -299,7 +371,7 @@ const RemountAccount = ({
- {isAccountEmpty && ( + {isAccountEmpty && balance && ( {t('accountSummary.transactionHistory')} - +
+ +
)} + + {hasTransactions && ( + + )} + + {showingFilteredResults && filteredTransactions?.success && filteredTransactions.list.length === 0 && ( +

+ No transactions found matching "{debouncedSearchQuery}" in transaction ID or notes +

+ )}
{loadingTransactions && } - {hasTransactions ? ( - transactions.list.map(tx => ( - { - setDetailID(internalID); - }} - {...tx} - /> - )) - ) : transactions?.success && ( -

- {t('transactions.placeholder')} -

- )} +
+ {filteredTransactions?.success && filteredTransactions.list.length > 0 ? ( + filteredTransactions.list.map(tx => ( + { + setDetailID(internalID); + }} + {...tx} + /> + )) + ) : transactions?.success && !showingFilteredResults ? ( +

+ {t('transactions.placeholder')} +

+ ) : null} +