Skip to content
Merged
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
25 changes: 21 additions & 4 deletions app/markets/components/AssetFilter.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';
import { useState, useRef, useEffect, KeyboardEvent } from 'react';
import { ChevronDownIcon, TrashIcon } from '@radix-ui/react-icons';
import { ChevronDownIcon, TrashIcon, ExclamationTriangleIcon } from '@radix-ui/react-icons';
import Image from 'next/image';
import { ERC20Token, infoToKey } from '@/utils/tokens';

Expand All @@ -10,7 +10,7 @@ type FilterProps = {
selectedAssets: string[];
setSelectedAssets: (assets: string[]) => void;
items: ERC20Token[];
loading?: boolean; // Made optional since it's not used
loading: boolean;
};

export default function AssetFilter({
Expand All @@ -19,11 +19,21 @@ export default function AssetFilter({
selectedAssets,
setSelectedAssets,
items,
loading,
}: FilterProps) {
const [query, setQuery] = useState('');
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);

// Precompute a set of valid asset keys
const validAssetKeys = new Set(
items.map((item) => item.networks.map((n) => infoToKey(n.address, n.chain.id)).join('|')),
);
const invalidSelection =
!loading &&
selectedAssets.length > 0 &&
selectedAssets.every((asset) => !validAssetKeys.has(asset));

useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
Expand Down Expand Up @@ -80,7 +90,14 @@ export default function AssetFilter({
>
<span className="absolute left-2 top-2 px-1 text-xs">{label}</span>
<div className="flex items-center justify-between pt-4">
{selectedAssets.length > 0 ? (
{loading ? (
<span className="p-[2px] text-sm text-gray-400">Loading...</span>
) : invalidSelection ? (
<div className="flex items-center gap-2">
<ExclamationTriangleIcon className="h-4 w-4 text-yellow-500" />
<span className="pt-[4px] text-sm text-yellow-500">Invalid</span>
</div>
) : selectedAssets.length > 0 ? (
<div className="flex-scroll flex gap-2 p-1 pb-[2px]">
{selectedAssets.map((asset) => {
const token = items.find(
Expand All @@ -100,7 +117,7 @@ export default function AssetFilter({
</span>
</div>
</div>
{isOpen && (
{isOpen && !loading && (
<div className="absolute z-10 mt-1 w-full rounded-sm bg-secondary shadow-lg">
<input
type="text"
Expand Down
4 changes: 3 additions & 1 deletion app/markets/components/NetworkFilter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@ import Image from 'next/image';
import { SupportedNetworks, getNetworkImg, isSupportedChain, networks } from '@/utils/networks';

type FilterProps = {
selectedNetwork: SupportedNetworks | null;
setSelectedNetwork: (network: SupportedNetworks | null) => void;
};
export default function NetworkFilter({ setSelectedNetwork }: FilterProps) {
export default function NetworkFilter({ setSelectedNetwork, selectedNetwork }: FilterProps) {
return (
<Select
label="Network"
selectionMode="single"
placeholder="All networks"
selectedKeys={selectedNetwork ? [selectedNetwork.toString()] : []}
onChange={(e) => {
if (!e.target.value) setSelectedNetwork(null);
const newId = Number(e.target.value);
Expand Down
98 changes: 89 additions & 9 deletions app/markets/components/markets.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use client';
import { useCallback, useEffect, useState } from 'react';
import { useCallback, useEffect, useState, useRef } from 'react';
import storage from 'local-storage-fallback';
import { useRouter, useSearchParams } from 'next/navigation';
import Header from '@/components/layout/header/Header';
import LoadingScreen from '@/components/Status/LoadingScreen';
import useMarkets from '@/hooks/useMarkets';
Expand Down Expand Up @@ -33,14 +34,24 @@ const defaultStaredMarkets = JSON.parse(
* that you want toLowerCase() render on the page.
*/
export default function Markets() {
const router = useRouter();
const searchParams = useSearchParams();

const { loading, data: rawMarkets } = useMarkets();

// token keys, aggregated with | for each "ERC20Token" object
// Parse and validate network parameter
const defaultNetwork = (() => {
const networkParam = searchParams.get('network');
return networkParam &&
Object.values(SupportedNetworks).includes(Number(networkParam) as SupportedNetworks)
? (Number(networkParam) as SupportedNetworks)
: null;
})();
Comment on lines +43 to +49
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

defaultNetwork may not reflect URL changes

Assigning defaultNetwork outside of state hooks won't update it when searchParams change. Use state and useEffect to keep it in sync.

Suggested fix:

- const defaultNetwork = (() => {
-   const networkParam = searchParams.get('network');
-   return networkParam &&
-     Object.values(SupportedNetworks).includes(Number(networkParam) as SupportedNetworks)
-     ? (Number(networkParam) as SupportedNetworks)
-     : null;
- })();
+ const [defaultNetwork, setDefaultNetwork] = useState<SupportedNetworks | null>(null);

+ useEffect(() => {
+   const networkParam = searchParams.get('network');
+   setDefaultNetwork(
+     networkParam &&
+     Object.values(SupportedNetworks).includes(Number(networkParam) as SupportedNetworks)
+       ? (Number(networkParam) as SupportedNetworks)
+       : null
+   );
+ }, [searchParams]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const defaultNetwork = (() => {
const networkParam = searchParams.get('network');
return networkParam &&
Object.values(SupportedNetworks).includes(Number(networkParam) as SupportedNetworks)
? (Number(networkParam) as SupportedNetworks)
: null;
})();
const [defaultNetwork, setDefaultNetwork] = useState<SupportedNetworks | null>(null);
useEffect(() => {
const networkParam = searchParams.get('network');
setDefaultNetwork(
networkParam &&
Object.values(SupportedNetworks).includes(Number(networkParam) as SupportedNetworks)
? (Number(networkParam) as SupportedNetworks)
: null
);
}, [searchParams]);

Comment on lines +37 to +49
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Move defaultNetwork calculation into a useEffect hook.

The current implementation won't update when URL parameters change. Use a useEffect hook to keep it in sync with searchParams.

Example fix:

const [defaultNetwork, setDefaultNetwork] = useState<SupportedNetworks | null>(null);

useEffect(() => {
  const networkParam = searchParams.get('network');
  setDefaultNetwork(
    networkParam &&
    Object.values(SupportedNetworks).includes(Number(networkParam) as SupportedNetworks)
      ? (Number(networkParam) as SupportedNetworks)
      : null
  );
}, [searchParams]);


// Initialize states
const [selectedCollaterals, setSelectedCollaterals] = useState<string[]>([]);
const [selectedLoanAssets, setSelectedLoanAssets] = useState<string[]>([]);

// single choice: null for all networks
const [selectedNetwork, setSelectedNetwork] = useState<SupportedNetworks | null>(null);
const [selectedNetwork, setSelectedNetwork] = useState<SupportedNetworks | null>(defaultNetwork);
Comment on lines +51 to +54
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Initialize states with URL parameters.

Consider initializing selectedCollaterals, selectedLoanAssets, and selectedNetwork with values from URL parameters for immediate sync on load.

Example:

const [selectedCollaterals, setSelectedCollaterals] = useState<string[]>(() => {
  const collaterals = searchParams.get('collaterals');
  return collaterals ? collaterals.split(',').filter(Boolean) : [];
});

const [selectedLoanAssets, setSelectedLoanAssets] = useState<string[]>(() => {
  const loanAssets = searchParams.get('loanAssets');
  return loanAssets ? loanAssets.split(',').filter(Boolean) : [];
});

const [selectedNetwork, setSelectedNetwork] = useState<SupportedNetworks | null>(defaultNetwork);


const [uniqueCollaterals, setUniqueCollaterals] = useState<ERC20Token[]>([]);
const [uniqueLoanAssets, setUniqueLoanAssets] = useState<ERC20Token[]>([]);
Expand All @@ -61,6 +72,31 @@ export default function Markets() {

const [filteredMarkets, setFilteredMarkets] = useState(rawMarkets);

// Use useRef to store the previous URL parameters
const prevParamsRef = useRef<string>('');

// Synchronize state with URL parameters
useEffect(() => {
const currentParams = searchParams.toString();
if (currentParams !== prevParamsRef.current) {
const collaterals = searchParams.get('collaterals');
setSelectedCollaterals(collaterals ? collaterals.split(',').filter(Boolean) : []);

const loanAssets = searchParams.get('loanAssets');
setSelectedLoanAssets(loanAssets ? loanAssets.split(',').filter(Boolean) : []);

const networkParam = searchParams.get('network');
setSelectedNetwork(
networkParam &&
Object.values(SupportedNetworks).includes(Number(networkParam) as SupportedNetworks)
? (Number(networkParam) as SupportedNetworks)
: null,
);

prevParamsRef.current = currentParams;
}
}, [searchParams]);

const starMarket = useCallback(
(id: string) => {
setStaredIds([...staredIds, id]);
Expand Down Expand Up @@ -92,7 +128,34 @@ export default function Markets() {
}
}, [rawMarkets]);

// Update the all markets pass to the table
const updateUrlParams = useCallback(
(collaterals: string[], loanAssets: string[], network: SupportedNetworks | null) => {
const params = new URLSearchParams(searchParams);
if (collaterals.length > 0) {
params.set('collaterals', collaterals.join(','));
} else {
params.delete('collaterals');
}
if (loanAssets.length > 0) {
params.set('loanAssets', loanAssets.join(','));
} else {
params.delete('loanAssets');
}
if (network) {
params.set('network', network.toString());
} else {
params.delete('network');
}
const newParams = params.toString();
if (newParams !== prevParamsRef.current) {
router.push(`?${newParams}`, { scroll: false });
prevParamsRef.current = newParams;
}
},
[router, searchParams],
);

// Update filtered markets
useEffect(() => {
const filtered = applyFilterAndSort(
rawMarkets,
Expand All @@ -116,6 +179,11 @@ export default function Markets() {
selectedNetwork,
]);

// Update URL params when filters change
useEffect(() => {
updateUrlParams(selectedCollaterals, selectedLoanAssets, selectedNetwork);
}, [selectedCollaterals, selectedLoanAssets, selectedNetwork, updateUrlParams]);

const titleOnclick = useCallback(
(column: number) => {
setSortColumn(column);
Expand Down Expand Up @@ -146,13 +214,22 @@ export default function Markets() {
{/* left section: asset filters */}
<div className="flex flex-col gap-2 lg:flex-row">
{/* network filter */}
<NetworkFilter setSelectedNetwork={setSelectedNetwork} />
<NetworkFilter
selectedNetwork={selectedNetwork}
setSelectedNetwork={(network) => {
setSelectedNetwork(network);
updateUrlParams(selectedCollaterals, selectedLoanAssets, network);
}}
/>

<AssetFilter
label="Loan Asset"
placeholder="All loan asset"
selectedAssets={selectedLoanAssets}
setSelectedAssets={setSelectedLoanAssets}
setSelectedAssets={(assets) => {
setSelectedLoanAssets(assets);
updateUrlParams(selectedCollaterals, assets, selectedNetwork);
}}
items={uniqueLoanAssets}
loading={loading}
/>
Expand All @@ -162,7 +239,10 @@ export default function Markets() {
label="Collateral"
placeholder="All collateral"
selectedAssets={selectedCollaterals}
setSelectedAssets={setSelectedCollaterals}
setSelectedAssets={(assets) => {
setSelectedCollaterals(assets);
updateUrlParams(assets, selectedLoanAssets, selectedNetwork);
}}
items={uniqueCollaterals}
loading={loading}
/>
Expand Down
Binary file added src/imgs/tokens/usd0.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/imgs/tokens/wsuperOETHb.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/imgs/tokens/wusdm.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 21 additions & 0 deletions src/utils/tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ const supportedTokens = [
isProxy: true,
},
},
{
symbol: 'USD0',
img: require('../imgs/tokens/usd0.png') as string,
decimals: 18,
networks: [{ chain: mainnet, address: '0x73A15FeD60Bf67631dC6cd7Bc5B6e8da8190aCF5' }],
},
{
symbol: 'USD0++',
img: require('../imgs/tokens/usd0pp.svg') as string,
Expand Down Expand Up @@ -105,6 +111,15 @@ const supportedTokens = [
isProxy: true,
},
},
{
symbol: 'wUSDM',
img: require('../imgs/tokens/wusdm.png') as string,
decimals: 18,
networks: [
{ chain: mainnet, address: '0x57F5E098CaD7A3D1Eed53991D4d66C45C9AF7812' },
{ chain: base, address: '0x57F5E098CaD7A3D1Eed53991D4d66C45C9AF7812' },
],
},
{
symbol: 'EURe',
img: require('../imgs/tokens/eure.png') as string,
Expand Down Expand Up @@ -353,6 +368,12 @@ const supportedTokens = [
decimals: 18,
networks: [{ chain: mainnet, address: '0xec53bF9167f50cDEB3Ae105f56099aaaB9061F83' }],
},
{
symbol: 'wsuperOETHb',
img: require('../imgs/tokens/wsuperOETHb.png') as string,
decimals: 18,
networks: [{ chain: base, address: '0x7FcD174E80f264448ebeE8c88a7C4476AAF58Ea6' }],
},
];

const isWhitelisted = (address: string, chainId: number) => {
Expand Down