Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Portfolio holdings details #278

Merged
merged 13 commits into from
Mar 5, 2024
6 changes: 6 additions & 0 deletions packages/frontend/src/MobileApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ const ConnectWalletPage = lazyRetry(
);
const AddWalletPage = lazyRetry(() => import("./mobile-pages/add-wallet"));
const AddHoldingPage = lazyRetry(() => import("./mobile-pages/add-holding"));
const PortfolioHoldingsPage = lazyRetry(
() => import("./mobile-pages/portfolio-holdings")
);

const CustomNavTab: React.FC<{
label: string;
Expand Down Expand Up @@ -100,6 +103,9 @@ const TabNavigator: React.FC = () => {
exact
component={AddHoldingPage}
/>
<Route exact path="/portfolio/holdings">
<PortfolioHoldingsPage />
</Route>
<Route exact path="/auth*">
<AuthPage />
</Route>
Expand Down
4 changes: 2 additions & 2 deletions packages/frontend/src/assets/icons/shown.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ const PortfolioStats: FC<IPortfolioStats> = ({
Total Balance{" "}
<ShowSVG
onClick={toggleBalance}
className="cursor-pointer ml-0.5 p-[1px]"
className="cursor-pointer ml-0.5 p-[1px] text-primaryVariant100"
/>
</p>
{portfolioData === undefined ? (
Expand Down
130 changes: 130 additions & 0 deletions packages/frontend/src/mobile-components/portfolio/PortfolioChart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { FC, useMemo } from "react";
import { ApexDonutChart, themeColors } from "@alphaday/ui-kit";
import { makeRepeated } from "src/api/utils/itemUtils";
import { getAssetPrefix } from "src/api/utils/portfolioUtils";
import { ITEM_COLORS } from "src/components/item-colors";
import { TPortfolioDataForAddress } from "src/components/portfolio/types";
import CONFIG from "src/config";
import { portfolioData } from "./mockData";

const { DONUT_TOKENS_COUNT } = CONFIG.WIDGETS.PORTFOLIO;

const PortfolioChart: FC = () => {
const assets = useMemo<TPortfolioDataForAddress["assets"]>(
() =>
portfolioData !== undefined
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't this be defined as a prop?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah but that's for later

? [...portfolioData.assets].sort(
(a, b) => b.token.balanceUSD - a.token.balanceUSD
)
: [],
[]
);

const labels: string[] = [];
const series = assets.map((item) => {
labels.push(
`${item.token.symbol}${getAssetPrefix(item).toUpperCase()}`
);
return Number(item.token.balanceUSD);
});
const othersBalance =
series.length > DONUT_TOKENS_COUNT
? series.splice(DONUT_TOKENS_COUNT).reduce((n, p) => n + p)
: 0;
const donutData = {
options: {
chart: {
id: "portfolio-donut",
sparkline: {
enabled: false,
},
background: "transparent",
redrawOnWindowResize: true,
height: "500px",
},
labels: [
...labels.slice(0, DONUT_TOKENS_COUNT),
...(othersBalance ? ["Others"] : []),
],
dataLabels: {
enabled: false,
},
tooltip: {
y: {
formatter(value: number) {
return `$${new Intl.NumberFormat("en-US", {
maximumFractionDigits: 2,
}).format(value)}`;
},
},
},
plotOptions: {
pie: {
donut: {
customScale: 1.5,
size: 170,
background: "transparent",
},
},
},
stroke: {
colors: undefined,
},
legend: {
show: true,
fontSize: "11px",
position: "left",
offsetX: 0,
offsetY: 0,
height: 500,
labels: {
colors: [themeColors.primaryVariant100],
useSeriesColors: false,
},
markers: {
radius: 3,
},
onItemHover() {},
formatter(
label: string,
opts: {
w: {
globals: {
series: number[];
};
};
seriesIndex: number;
}
) {
const percent =
(100 * opts.w.globals.series[opts.seriesIndex]) /
opts.w.globals.series.reduce((a, b) => a + b);
return `
<span style="display: inline-flex; justify-content: space-between; width: 70%">
<div>${label}</div>
<div>&nbsp;${percent.toFixed(0)}%</div>
</span>`;
},
},
colors: makeRepeated(ITEM_COLORS, assets.length),
},
series: [
...series.slice(0, DONUT_TOKENS_COUNT),
...(othersBalance ? [othersBalance] : []),
],
};

return (
<div className="flex justify-between donut-chart ml-2 [&_.apexcharts-inner]:!translate-x-[200px] [&_.apexcharts-inner]:!translate-y-10 [&_.apexcharts-svg]:!w-80 ">
{donutData.series && (
<ApexDonutChart
options={donutData?.options}
series={donutData?.series}
width="260px"
height="400px"
/>
)}
</div>
);
};
export default PortfolioChart;
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { FC } from "react";
import { OutlineButton, twMerge } from "@alphaday/ui-kit";
import { ReactComponent as CopySVG } from "src/assets/icons/copy.svg";
import { ReactComponent as HandSVG } from "src/assets/icons/hand.svg";
import { ReactComponent as WalletSVG } from "src/assets/icons/wallet.svg";

const WalletConnectionOptions: FC<{
isAuthenticated: boolean;
onClick: (path: string) => void;
className?: string;
}> = ({ isAuthenticated, onClick, className }) => {
return (
<div className={twMerge("flex flex-col items-center mt-4", className)}>
<OutlineButton
title="Add Wallet"
subtext="Add your wallet manually to get started"
icon={<WalletSVG className="w-[24px] mr-1" />}
onClick={() => onClick("/portfolio/add-wallet")}
isAuthenticated={isAuthenticated}
/>
<OutlineButton
title="Connect Wallet"
subtext="Connect your wallet to get started"
icon={<CopySVG className="w-[22px] mr-1" />}
onClick={() => onClick("/portfolio/connect-wallet")}
isAuthenticated={isAuthenticated}
Copy link
Contributor

Choose a reason for hiding this comment

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

FYI: we've been told today that this option was not going to be included. But it's fine to keep it for now

/>
<OutlineButton
title="Add Holdings Manually"
subtext="Add your holdings manually"
icon={<HandSVG className="w-[20px] mr-1" />}
onClick={() => onClick("/portfolio/add-holding")}
isAuthenticated={isAuthenticated}
/>
</div>
);
};

export default WalletConnectionOptions;
177 changes: 177 additions & 0 deletions packages/frontend/src/mobile-components/portfolio/mockData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import { TPortfolioDataForAddress } from "src/components/portfolio/types";

export const portfolioData: TPortfolioDataForAddress = {
assets: [
Copy link
Contributor

Choose a reason for hiding this comment

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

ideally we need to define a portoflio component API that is decoupled from the data API provider, but this is work for a separate PR.

{
key: "2216096905",
address: "0x2874c4e9cb5f183ef3d8adfa6d6bbd584dc67842",
network: "avalanche",
updatedAt: "2024-03-03T09:08:45.405Z",
token: {
id: "47",
address: "0x0000000000000000000000000000000000000000",
name: "AVAX",
symbol: "AVAX",
decimals: 18,
coingeckoId: "avalanche-2",
hide: false,
canExchange: true,
updatedAt: "2024-03-04T07:10:32.643Z",
createdAt: "2022-05-18T12:54:47.542Z",
price: 42.63,
networkId: 3,
status: "approved",
totalSupply: "435942308.442695",
dailyVolume: 621105842.2333122,
verified: true,
holdersEnabled: true,
marketCap: 16035840022.326565,
priceUpdatedAt: "2024-03-04T07:10:32.643Z",
externallyVerified: false,
label: "avax",
balance: 0.0021,
balanceUSD: 0.089523,
balanceRaw: "2100000000000000",
tokenImage:
"https://storage.googleapis.com/zapper-fi-assets/tokens/avalanche/0x0000000000000000000000000000000000000000.png",
},
},
{
key: "921602654",
address: "0x2874c4e9cb5f183ef3d8adfa6d6bbd584dc67842",
network: "optimism",
updatedAt: "2024-03-03T09:08:45.244Z",
token: {
id: "78",
address: "0x0000000000000000000000000000000000000000",
name: "Ether",
symbol: "ETH",
decimals: 18,
coingeckoId: "ethereum",
hide: false,
canExchange: true,
updatedAt: "2024-03-04T07:10:32.643Z",
createdAt: "2022-05-18T12:54:47.542Z",
price: 3463.77,
networkId: 11,
status: "approved",
totalSupply: "36811.197214188697856699",
dailyVolume: 17641941138.93259,
verified: true,
holdersEnabled: true,
marketCap: 416110233226.25,
priceUpdatedAt: "2024-03-04T07:10:32.643Z",
externallyVerified: false,
label: "eth",
balance: 0.008459516477201893,
balanceUSD: 29.3018193882376,
balanceRaw: "8459516477201892",
tokenImage:
"https://storage.googleapis.com/zapper-fi-assets/tokens/optimism/0x0000000000000000000000000000000000000000.png",
},
},
{
key: "3612627573",
address: "0x2874c4e9cb5f183ef3d8adfa6d6bbd584dc67842",
network: "ethereum",
updatedAt: "2024-03-03T09:08:45.213Z",
token: {
id: "3067",
address: "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
name: "Wrapped Ether",
symbol: "WETH",
decimals: 18,
coingeckoId: "weth",
hide: false,
canExchange: true,
updatedAt: "2024-03-04T07:10:32.643Z",
createdAt: "2022-05-18T12:54:47.695Z",
price: 3463.77,
networkId: 1,
status: "approved",
totalSupply: "3081838.685889420235408401",
dailyVolume: 553589763.6055393,
verified: false,
holdersEnabled: true,
marketCap: 0,
priceUpdatedAt: "2024-03-04T07:10:32.643Z",
externallyVerified: true,
label: "WETH",
balance: 30.56325,
balanceUSD: 105864.0684525,
balanceRaw: "30563250000000000000",
tokenImage:
"https://storage.googleapis.com/zapper-fi-assets/tokens/ethereum/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2.png",
},
},
{
key: "80289008",
address: "0x2874c4e9cb5f183ef3d8adfa6d6bbd584dc67842",
network: "ethereum",
updatedAt: "2024-03-03T09:08:45.213Z",
token: {
id: "1496",
address: "0x0000000000000000000000000000000000000000",
name: "Ether",
symbol: "ETH",
decimals: 18,
coingeckoId: "ethereum",
hide: false,
canExchange: true,
updatedAt: "2024-03-04T07:10:32.643Z",
createdAt: "2022-05-18T12:54:47.695Z",
price: 3463.77,
networkId: 1,
status: "approved",
totalSupply: "122373866.2178",
dailyVolume: 17641941138.93259,
verified: true,
holdersEnabled: true,
marketCap: 416110233226.25,
priceUpdatedAt: "2024-03-04T07:10:32.643Z",
externallyVerified: false,
label: "eth",
balance: 37.68335669707697,
balanceUSD: 130526.48042663428,
balanceRaw: "37683356697076965333",
tokenImage:
"https://storage.googleapis.com/zapper-fi-assets/tokens/ethereum/0x0000000000000000000000000000000000000000.png",
},
},
{
key: "51396930",
address: "0x2874c4e9cb5f183ef3d8adfa6d6bbd584dc67842",
network: "ethereum",
updatedAt: "2024-03-02T23:39:41.598Z",
token: {
id: "2626",
address: "0x95ad61b0a150d79219dcf64e1e6cc01f0b64c4ce",
name: "SHIBA INU",
symbol: "SHIB",
decimals: 18,
coingeckoId: "shiba-inu",
hide: false,
canExchange: true,
updatedAt: "2024-03-04T07:10:32.643Z",
createdAt: "2022-05-18T12:54:47.695Z",
price: 0.00002693,
networkId: 1,
status: "approved",
totalSupply: "999982367742850.863791106162366397",
dailyVolume: 4359038452.289908,
verified: false,
holdersEnabled: true,
marketCap: 15957709021.025589,
priceUpdatedAt: "2024-03-04T07:10:32.643Z",
externallyVerified: true,
label: "Shiba Inu",
balance: 434877.1472059143,
balanceUSD: 11.711241574255274,
balanceRaw: "434877147205914300000000",
tokenImage:
"https://storage.googleapis.com/zapper-fi-assets/tokens/ethereum/0x95ad61b0a150d79219dcf64e1e6cc01f0b64c4ce.png",
},
},
],
totalValue: 236431.6514630968,
};
Loading
Loading