Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
c1148b1
feat(pulse): Mobile-responsive token search UI with blockchain filter…
aldin4u Nov 21, 2025
8751c12
fix: ensure refresh and chain selector buttons visible on mobile
aldin4u Nov 21, 2025
4fa6f62
test: fix Search.test.tsx - all tests passing
aldin4u Nov 22, 2025
19e99f3
test: fix AppWrapper.test.tsx - all tests passing
aldin4u Nov 22, 2025
3d329f6
fix: resolve all real lint errors manually (no auto-fix)
aldin4u Nov 22, 2025
17cce36
fix: disable Prettier ESLint rule and resolve all lint errors
aldin4u Nov 22, 2025
08f1c2d
fix: correct chain filter logic in parseMarketPairs
aldin4u Nov 22, 2025
f0357d0
fix: address all PR #461 review comments
aldin4u Nov 24, 2025
7b5c2d0
Merge branch 'staging' into feature/pulse-search-ui-mobile-refinements
aldin4u Nov 24, 2025
d3be9cd
fix: restore ESC button in Sell mode, address PR comments (lint/unuse…
aldin4u Nov 24, 2025
1d700d5
fix: remove unused getTokenBalance function and tokenBalance variable
aldin4u Nov 25, 2025
7be09b6
fix: resolve lint errors in ChainOverlay and MarketList, update CardS…
aldin4u Nov 25, 2025
14081f3
changes for unit tests and a few feedback changes
vignesha22 Nov 27, 2025
b0fa0b9
changed styles
vignesha22 Nov 27, 2025
d98fd81
updated snapshop
vignesha22 Nov 27, 2025
94adff1
added specific width and height
vignesha22 Nov 27, 2025
9467f03
fixed lint issue
vignesha22 Nov 27, 2025
78ed786
fixed test
vignesha22 Nov 27, 2025
bae51b0
changes as per feedback
vignesha22 Dec 1, 2025
a0bcfb4
added error handling
vignesha22 Dec 1, 2025
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
9 changes: 9 additions & 0 deletions src/apps/pulse/assets/back-arrow-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions src/apps/pulse/assets/clear-search-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 0 additions & 2 deletions src/apps/pulse/components/App/tests/AppWrapper.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -184,8 +184,6 @@ describe('<AppWrapper />', () => {
expect(
screen.getByDisplayValue('0x1234567890123456789012345678901234567890')
).toBeInTheDocument();
expect(screen.getByText('🔥 Trending')).toBeInTheDocument();
expect(screen.getByText('🌱 Fresh')).toBeInTheDocument();
expect(screen.queryByTestId('pulse-home-view')).not.toBeInTheDocument();
expect(
screen.queryByTestId('pulse-buy-toggle-button')
Expand Down
5 changes: 3 additions & 2 deletions src/apps/pulse/components/Buy/BuyButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -157,8 +157,9 @@ export default function BuyButton(props: BuyButtonProps) {
!token ||
!(parseFloat(usdAmount) > 0) ||
!expressIntentResponse ||
!!(expressIntentResponse as { error: string }).error ||
(expressIntentResponse as ExpressIntentResponse)?.bids?.length === 0
('error' in expressIntentResponse && !!expressIntentResponse.error) ||
('bids' in expressIntentResponse &&
(expressIntentResponse as ExpressIntentResponse)?.bids?.length === 0)
);
};

Expand Down
2 changes: 1 addition & 1 deletion src/apps/pulse/components/Price/TokenPrice.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export default function TokenPrice(props: TokenPriceProps): JSX.Element {
style={{ fontSize: 13, fontWeight: 400 }}
data-testid="pulse-token-price"
>
${value.toFixed(5)}
${value.toFixed(2)}
</p>
);
}
Expand Down
186 changes: 90 additions & 96 deletions src/apps/pulse/components/Search/ChainOverlay.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import React from 'react';
import { chainNameToChainIdTokensData } from '../../../../services/tokensData';
import { getLogoForChainId } from '../../../../utils/blockchain';
import {
getLogoForChainId,
isGnosisEnabled,
} from '../../../../utils/blockchain';
import GlobeIcon from '../../assets/globe-icon.svg';
import SelectedIcon from '../../assets/selected-icon.svg';
import { MobulaChainNames } from '../../utils/constants';
Expand All @@ -12,100 +16,90 @@ export interface ChainOverlayProps {
chains: MobulaChainNames;
}

export default function ChainOverlay(chainOverlayProps: ChainOverlayProps) {
const {
setShowChainOverlay,
setChains,
setOverlayStyle,
overlayStyle,
chains,
} = chainOverlayProps;
return (
<>
<div
style={{
position: 'fixed',
top: 0,
left: 0,
width: 200,
height: 210,
zIndex: 1999,
}}
onClick={() => {
setShowChainOverlay(false);
setOverlayStyle({});
}}
/>
<div style={overlayStyle} onClick={(e) => e.stopPropagation()}>
<div style={{ padding: '12px 0', height: '100%', overflowY: 'auto' }}>
{Object.values(MobulaChainNames).map((chain) => {
const isSelected = chains === chain;
const isAll = chain === MobulaChainNames.All;
let logo = null;
if (isAll) {
logo = (
<span
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: 24,
height: 24,
}}
>
<img src={GlobeIcon} alt="globe-icon" />
</span>
);
} else {
const chainId = chainNameToChainIdTokensData(chain);
logo = (
<img
src={getLogoForChainId(chainId)}
alt={chain}
style={{
width: 24,
height: 24,
borderRadius: '50%',
background: '#23222A',
}}
/>
);
}
return (
<div
key={chain}
onClick={() => {
setChains(chain);
setShowChainOverlay(false);
setOverlayStyle({});
}}
style={{
display: 'flex',
alignItems: 'center',
gap: 8,
padding: '10px 18px',
cursor: 'pointer',
background: isSelected ? '#29292F' : 'transparent',
color: isSelected ? '#fff' : '#b0b0b0',
fontWeight: isSelected ? 500 : 400,
fontSize: 16,
position: 'relative',
}}
>
{logo}
<span style={{ flex: 1, marginLeft: 10 }}>
{chain === MobulaChainNames.All ? 'All chains' : chain}
</span>
{isSelected && (
<div>
<img src={SelectedIcon} alt="selected-icon" />
const ChainOverlay = React.forwardRef<HTMLDivElement, ChainOverlayProps>(
(
{ setShowChainOverlay, setChains, setOverlayStyle, overlayStyle, chains },
ref
) => {
return (
<>
<div
className="fixed inset-0 w-screen h-screen cursor-default"
style={{ zIndex: 1999 }}
onClick={() => {
setShowChainOverlay(false);
setOverlayStyle({});
}}
/>
<div
ref={ref}
style={overlayStyle}
onClick={(e) => e.stopPropagation()}
>
<div className="px-3 py-3 h-full overflow-y-auto">
{Object.values(MobulaChainNames)
.filter(
(chain) => isGnosisEnabled || chain !== MobulaChainNames.XDAI
) // Remove XDAI if Gnosis is not enabled
.sort((a, b) => {
// Put "All" first, then alphabetical
if (a === MobulaChainNames.All) return -1;
if (b === MobulaChainNames.All) return 1;
return a.localeCompare(b);
})
.map((chain) => {
const isSelected = chains === chain;
const isAll = chain === MobulaChainNames.All;
let logo = null;
if (isAll) {
logo = (
<span className="flex items-center justify-center w-6 h-6">
<img src={GlobeIcon} alt="globe-icon" />
</span>
);
} else {
const chainId = chainNameToChainIdTokensData(chain);
logo = (
<img
src={getLogoForChainId(chainId)}
alt={chain}
className="w-6 h-6 rounded-full bg-[#23222A]"
/>
);
}
return (
<div
key={chain}
onClick={() => {
setChains(chain);
setShowChainOverlay(false);
setOverlayStyle({});
}}
className={`flex items-center gap-2 px-2.5 py-2.5 cursor-pointer relative text-base ${
isSelected
? 'bg-[#29292F] text-white font-medium'
: 'bg-transparent text-[#b0b0b0] font-normal'
}`}
>
{logo}
<span className="flex-1 ml-2.5">
{chain === MobulaChainNames.All ? 'All chains' : chain}
</span>
{isSelected && (
<div>
<img src={SelectedIcon} alt="selected-icon" />
</div>
)}
</div>
)}
</div>
);
})}
);
})}
</div>
</div>
</div>
</>
);
}
</>
);
}
);

ChainOverlay.displayName = 'ChainOverlay';

export default ChainOverlay;
99 changes: 99 additions & 0 deletions src/apps/pulse/components/Search/MarketList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import React from 'react';
import { chainNameToChainIdTokensData } from '../../../../services/tokensData';
import { getLogoForChainId } from '../../../../utils/blockchain';
import RandomAvatar from '../../../pillarx-app/components/RandomAvatar/RandomAvatar';
import { formatBigNumber } from '../../utils/number';
import { Market } from '../../utils/parseSearchData';
import TokenPriceChange from '../Price/TokenPriceChange';

export interface MarketListProps {
markets: Market[];
handleMarketSelect: (market: Market) => void;
}

export default function MarketList({
markets,
handleMarketSelect,
}: MarketListProps) {
if (!markets || markets.length === 0) {
return null;
}

return (
<>
{markets.map((market) => {
const chainId = chainNameToChainIdTokensData(market.blockchain);

return (
<button
key={`${market.address}-${market.blockchain}`}
className="flex w-full h-9 my-2.5"
onClick={() => {
handleMarketSelect(market);
}}
type="button"
data-testid={`pulse-market-${market.blockchain.toLowerCase()}-${market.pairName.toLowerCase()}`}
>
<div className="relative inline-block">
{/* Token0 Logo */}
{market.token0.logo ? (
<img
src={market.token0.logo || ''}
className="w-9 h-9 ml-2.5 rounded-full"
alt="token0 logo"
/>
) : (
<div className="w-9 h-9 ml-2.5 overflow-hidden rounded-full">
<RandomAvatar name={market.token0.name || ''} />
<span className="absolute inset-0 flex items-center justify-center text-white text-lg font-bold ml-2.5">
{market.token0.symbol?.slice(0, 2)}
</span>
</div>
)}
{/* Chain Logo */}
<img
src={getLogoForChainId(chainId)}
className="absolute -bottom-0.5 -right-0.5 w-3.75 h-3.75 rounded-full"
alt="chain logo"
width="14px"
height="14px"
/>
</div>
<div className="flex flex-col flex-1 min-w-0 ml-2.5">
<div className="flex">
<p className="text-[13px] font-normal">{market.pairName}</p>
<p className="text-[12px] font-normal ml-0.75 text-gray-400">
{market.exchange.name}
</p>
</div>
<div className="flex">
<p className="text-[13px] font-normal text-gray-400">Liq:</p>
<p className="text-[13px] font-normal ml-0.75">
{formatBigNumber(market.liquidity || 0)}
</p>
<p className="text-[13px] font-normal ml-0.75 text-gray-400">
Vol:
</p>
<p className="text-[13px] font-normal ml-0.75">
{formatBigNumber(market.volume24h || 0)}
</p>
</div>
</div>
<div className="flex flex-col ml-auto mr-2.5">
<div>
<p className="text-[13px] font-normal">
${formatBigNumber(market.liquidity || 0)}
</p>
</div>
{market.priceChange24h !== null && (
<div className="ml-auto">
<TokenPriceChange value={market.priceChange24h} />
</div>
)}
</div>
</button>
);
})}
</>
);
}
Loading