Skip to content
Draft
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
62 changes: 0 additions & 62 deletions src/app/ui/portfolio/PortfolioAssetsSection.tsx

This file was deleted.

53 changes: 53 additions & 0 deletions src/app/ui/portfolio/PortfolioContentSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
'use client';

import { SectionCard } from 'src/components/Cards/SectionCard/SectionCard';
import { PortfolioFilterBar } from '@/components/PortfolioFilterBar/PortfolioFilterBar';
import { useState } from 'react';
import {
HoldingsFilteringProvider,
useHoldingsFiltering,
} from '@/providers/PortfolioProvider/filtering/HoldingsFilteringContext';
import { useAccount } from '@lifi/wallet-management';
import { PortfolioHoldings } from './PortfolioHoldings/PortfolioHoldings';

export enum PortfolioViewBarTab {
HOLDINGS = 'holdings',
PERFORMANCE = 'performance',
TRANSACTIONS = 'transactions',
}

const PortfolioContentSectionInner = () => {
const [tab, setTab] = useState<PortfolioViewBarTab>(
PortfolioViewBarTab.HOLDINGS,
);
const {
balancesIsLoading,
positionsIsLoading,
balancesIsEmpty,
positionsIsEmpty,
} = useHoldingsFiltering();
const { account } = useAccount();
const isDisconnected = !account.isConnected;
const isLoading = balancesIsLoading || positionsIsLoading;
const isEmpty = balancesIsEmpty && positionsIsEmpty;
const isDisabled = isDisconnected || (isEmpty && !isLoading);

return (
<SectionCard>
<PortfolioFilterBar
isDisabled={isDisabled}
value={tab}
onChange={setTab}
/>
{tab === PortfolioViewBarTab.HOLDINGS && <PortfolioHoldings />}
</SectionCard>
);
};

export const PortfolioContentSection = () => {
return (
<HoldingsFilteringProvider>
<PortfolioContentSectionInner />
</HoldingsFilteringProvider>
);
};
30 changes: 30 additions & 0 deletions src/app/ui/portfolio/PortfolioHoldings/PortfolioHoldings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { PortfolioAssetsListContainer } from '../PortfolioPage.styles';
import { PortfolioTokenHoldings } from './PortfolioTokenHoldings';
import { PortfolioPositionHoldings } from './PortfolioPositionHoldings';
import { isChainPortfolioPosition } from '@/providers/PortfolioProvider/utils';
import type { PortfolioPosition } from '@/providers/PortfolioProvider/types';
import { useTranslation } from 'node_modules/react-i18next';

const isDeFiPosition = (positions: PortfolioPosition[]) =>
isChainPortfolioPosition(positions[0]);

const isPerpsPosition = (positions: PortfolioPosition[]) =>
!isChainPortfolioPosition(positions[0]);

export const PortfolioHoldings = () => {
const { t } = useTranslation();

return (
<PortfolioAssetsListContainer useFlexGap direction="column">
<PortfolioTokenHoldings title={t('portfolio.holdings.tokens')} />
<PortfolioPositionHoldings
title={t('portfolio.holdings.defiPositions')}
filter={isDeFiPosition}
/>
<PortfolioPositionHoldings
title={t('portfolio.holdings.perps')}
filter={isPerpsPosition}
/>
</PortfolioAssetsListContainer>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { ExpandableSection } from '@/components/core/sections/ExpandableSection/ExpandableSection';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { defaultConfig } from './constants';
import { SectionCard } from '@/components/Cards/SectionCard/SectionCard';
import { useTranslation } from 'react-i18next';
import useMediaQuery from '@mui/material/useMediaQuery';
import { PortfolioHoldingsSectionHeaderSkeleton } from './PortfolioHoldingsSectionHeaderSkeleton';

interface PortfolioHoldingsSectionProps<T> {
title: string;
amount: number;
progress: number;
items: T[];
renderItem: (item: T) => React.ReactNode;
onItemClick?: (item: T) => void;
shouldExpand?: boolean;
isLoading?: boolean;
}

export const PortfolioHoldingsSection = <T,>({
title,
amount,
progress,
items,
renderItem,
onItemClick,
shouldExpand,
isLoading,
}: PortfolioHoldingsSectionProps<T>) => {
const { t } = useTranslation();
const isMobile = useMediaQuery((theme) => theme.breakpoints.down('sm'));

const header = isLoading ? (
<PortfolioHoldingsSectionHeaderSkeleton />
) : (
<Stack
flexDirection="row"
gap={2}
justifyContent="space-between"
width="100%"
>
<Stack flexDirection="row" gap={2}>
<Typography variant={defaultConfig.titleVariant}>{title}</Typography>
<Typography variant={defaultConfig.titleVariant} color="textSecondary">
{t('format.percent', { value: progress / 100 })}
</Typography>
</Stack>
<Typography variant={defaultConfig.titleVariant} sx={{ mr: 1 }}>
{t(`format.${isMobile ? 'currencyCompact' : 'currency'}`, {
value: amount,
})}
</Typography>
</Stack>
);

return (
<SectionCard
sx={(theme) => ({
padding: theme.spacing(1.5),
backgroundColor: (theme.vars || theme).palette.surface1.main,
})}
>
<ExpandableSection
header={header}
items={items}
renderItem={renderItem}
onItemClick={onItemClick}
shouldExpand={shouldExpand}
/>
</SectionCard>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import Stack from '@mui/material/Stack';
import { BaseSurfaceSkeleton } from '@/components/core/skeletons/BaseSurfaceSkeleton/BaseSurfaceSkeleton.style';

export const PortfolioHoldingsSectionHeaderSkeleton = () => (
<Stack
flexDirection="row"
gap={2}
justifyContent="space-between"
width="100%"
>
<Stack flexDirection="row" gap={2}>
<BaseSurfaceSkeleton variant="rounded" sx={{ width: 64, height: 24 }} />
<BaseSurfaceSkeleton variant="rounded" sx={{ width: 36, height: 24 }} />
</Stack>
<BaseSurfaceSkeleton
variant="rounded"
sx={{ width: 64, height: 24, mr: 1 }}
/>
</Stack>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { useMemo } from 'react';
import { mapValues, pickBy } from 'lodash';
import { useHoldingsFiltering } from '@/providers/PortfolioProvider/filtering/HoldingsFilteringContext';
import { usePortfolioSummary } from '@/providers/PortfolioProvider/PortfolioContext';
import { hasPositionDataToDisplay } from '@/components/composite/PositionCard/utils';
import { PositionSummaryRow } from '@/components/composite/PositionCard/components/PositionSummaryRow';
import type { PortfolioPosition } from '@/providers/PortfolioProvider/types';
import type { FC } from 'react';
import { PortfolioHoldingsSection } from './PortfolioHoldingsSection';
import { useHoldingAmountProgress } from './useHoldingAmountProgress';

interface PortfolioPositionHoldingsProps {
title: string;
filter: (positions: PortfolioPosition[]) => boolean;
}

type PositionGroup = [string, PortfolioPosition[]];

const getPositionValue = (position: PortfolioPosition) => position.netUsd;

export const PortfolioPositionHoldings: FC<PortfolioPositionHoldingsProps> = ({
title,
filter,
}) => {
const {
positionsData: data,
positionsIsLoading: isLoading,
positionsIsEmpty: isEmpty,
} = useHoldingsFiltering();
const { totalPortfolioUsd } = usePortfolioSummary();

const positionGroups: PositionGroup[] = useMemo(() => {
const filtered = pickBy(
mapValues(data, (positions) =>
positions.filter(hasPositionDataToDisplay),
),
(positions) => positions.length > 0 && filter(positions),
);

return Object.entries(filtered);
}, [data, filter]);

const { amount, progress } = useHoldingAmountProgress(
positionGroups,
getPositionValue,
totalPortfolioUsd,
);

return (
<PortfolioHoldingsSection
title={title}
amount={amount}
progress={progress}
shouldExpand={!isEmpty || isLoading}
isLoading={isLoading}
items={positionGroups}
renderItem={([, positions]) => (
<PositionSummaryRow positions={positions} />
)}
/>
);
};
47 changes: 47 additions & 0 deletions src/app/ui/portfolio/PortfolioHoldings/PortfolioTokenHoldings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { useHoldingsFiltering } from '@/providers/PortfolioProvider/filtering/HoldingsFilteringContext';
import { usePortfolioSummary } from '@/providers/PortfolioProvider/PortfolioContext';
import { TokenSummaryRow } from '@/components/composite/BalanceCard/components/TokenSummaryRow';
import type { PortfolioBalance, WalletToken } from '@/types/tokens';
import { PortfolioHoldingsSection } from './PortfolioHoldingsSection';
import { useHoldingAmountProgress } from './useHoldingAmountProgress';
import { defaultConfig } from './constants';
import type { FC } from 'react';

const getTokenValue = (balance: PortfolioBalance<WalletToken>) =>
balance.amountUSD;

interface PortfolioTokenHoldingsProps {
title: string;
}

export const PortfolioTokenHoldings: FC<PortfolioTokenHoldingsProps> = ({
title,
}) => {
const {
balancesData: data,
balancesIsLoading: isLoading,
balancesIsEmpty: isEmpty,
} = useHoldingsFiltering();
const { totalPortfolioUsd } = usePortfolioSummary();

const balanceGroups = Object.entries(data);
const { amount, progress } = useHoldingAmountProgress(
balanceGroups,
getTokenValue,
totalPortfolioUsd,
);

return (
<PortfolioHoldingsSection
title={title}
amount={amount}
progress={progress}
shouldExpand={!isEmpty || isLoading}
isLoading={isLoading}
items={balanceGroups}
renderItem={([, balances]) => (
<TokenSummaryRow balances={balances} config={defaultConfig} />
)}
/>
);
};
12 changes: 12 additions & 0 deletions src/app/ui/portfolio/PortfolioHoldings/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { AvatarSize } from '@/components/core/AvatarStack/AvatarStack.types';

export const defaultConfig = {
titleVariant: 'bodyLargeStrong',
descriptionVariant: 'bodyXSmall',
tokenSize: AvatarSize.XXL,
chainsSize: AvatarSize.SM,
inlineChainsSize: AvatarSize.XS,
chainsLimit: 8,
chainsSpacing: -0.5,
infoContainerGap: 0.5,
} as const;
14 changes: 14 additions & 0 deletions src/app/ui/portfolio/PortfolioHoldings/useHoldingAmountProgress.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { useMemo } from 'react';
import { sumBy } from 'lodash';
import { calcPercentage } from '@/providers/PortfolioProvider/utils';

export function useHoldingAmountProgress<T>(
groups: [string, T[]][],
getValue: (item: T) => number,
totalPortfolioUsd: number,
) {
return useMemo(() => {
const amount = sumBy(groups, ([, items]) => sumBy(items, getValue));
return { amount, progress: calcPercentage(amount, totalPortfolioUsd) };
}, [groups, getValue, totalPortfolioUsd]);
}
4 changes: 2 additions & 2 deletions src/app/ui/portfolio/PortfolioPage.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { PortfolioAssetsSection } from './PortfolioAssetsSection';
import { PortfolioContentSection } from './PortfolioContentSection';
import { PortfolioHeaderSection } from './PortfolioHeaderSection';

export const PortfolioPage = () => {
return (
<>
<PortfolioHeaderSection />
<PortfolioAssetsSection />
<PortfolioContentSection />
</>
);
};
Loading
Loading