diff --git a/packages/frontend/src/MobileApp.tsx b/packages/frontend/src/MobileApp.tsx
index 2496c0d8..ca7dbb39 100644
--- a/packages/frontend/src/MobileApp.tsx
+++ b/packages/frontend/src/MobileApp.tsx
@@ -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;
@@ -100,6 +103,9 @@ const TabNavigator: React.FC = () => {
exact
component={AddHoldingPage}
/>
+
+
+
diff --git a/packages/frontend/src/assets/icons/shown.svg b/packages/frontend/src/assets/icons/shown.svg
index a78c62df..e8c9da84 100644
--- a/packages/frontend/src/assets/icons/shown.svg
+++ b/packages/frontend/src/assets/icons/shown.svg
@@ -1,4 +1,4 @@
diff --git a/packages/frontend/src/components/portfolio/PortfolioStats.tsx b/packages/frontend/src/components/portfolio/PortfolioStats.tsx
index aaf7c372..96a17c97 100644
--- a/packages/frontend/src/components/portfolio/PortfolioStats.tsx
+++ b/packages/frontend/src/components/portfolio/PortfolioStats.tsx
@@ -200,7 +200,7 @@ const PortfolioStats: FC = ({
Total Balance{" "}
{portfolioData === undefined ? (
diff --git a/packages/frontend/src/mobile-components/portfolio/PortfolioChart.tsx b/packages/frontend/src/mobile-components/portfolio/PortfolioChart.tsx
new file mode 100644
index 00000000..a42c2679
--- /dev/null
+++ b/packages/frontend/src/mobile-components/portfolio/PortfolioChart.tsx
@@ -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(
+ () =>
+ portfolioData !== undefined
+ ? [...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 `
+
+ ${label}
+ ${percent.toFixed(0)}%
+ `;
+ },
+ },
+ colors: makeRepeated(ITEM_COLORS, assets.length),
+ },
+ series: [
+ ...series.slice(0, DONUT_TOKENS_COUNT),
+ ...(othersBalance ? [othersBalance] : []),
+ ],
+ };
+
+ return (
+
+ {donutData.series && (
+
+ )}
+
+ );
+};
+export default PortfolioChart;
diff --git a/packages/frontend/src/mobile-components/portfolio/WalletConnectionOptions.tsx b/packages/frontend/src/mobile-components/portfolio/WalletConnectionOptions.tsx
new file mode 100644
index 00000000..5601f196
--- /dev/null
+++ b/packages/frontend/src/mobile-components/portfolio/WalletConnectionOptions.tsx
@@ -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 (
+
+ }
+ onClick={() => onClick("/portfolio/add-wallet")}
+ isAuthenticated={isAuthenticated}
+ />
+ }
+ onClick={() => onClick("/portfolio/connect-wallet")}
+ isAuthenticated={isAuthenticated}
+ />
+ }
+ onClick={() => onClick("/portfolio/add-holding")}
+ isAuthenticated={isAuthenticated}
+ />
+
+ );
+};
+
+export default WalletConnectionOptions;
diff --git a/packages/frontend/src/mobile-components/portfolio/mockData.ts b/packages/frontend/src/mobile-components/portfolio/mockData.ts
new file mode 100644
index 00000000..144b7691
--- /dev/null
+++ b/packages/frontend/src/mobile-components/portfolio/mockData.ts
@@ -0,0 +1,177 @@
+import { TPortfolioDataForAddress } from "src/components/portfolio/types";
+
+export const portfolioData: TPortfolioDataForAddress = {
+ assets: [
+ {
+ 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,
+};
diff --git a/packages/frontend/src/mobile-pages/portfolio-holdings.tsx b/packages/frontend/src/mobile-pages/portfolio-holdings.tsx
new file mode 100644
index 00000000..71aa5907
--- /dev/null
+++ b/packages/frontend/src/mobile-pages/portfolio-holdings.tsx
@@ -0,0 +1,252 @@
+import { FC, useState } from "react";
+import { Dialog, DropdownSelect } from "@alphaday/ui-kit";
+import { useHistory } from "react-router";
+import { useKeyPress } from "src/api/hooks";
+import { toggleShowBalance as toggleShowBalanceInStore } from "src/api/store";
+import { useAppDispatch } from "src/api/store/hooks";
+
+import { TPortfolio } from "src/api/types";
+import { ENumberStyle, formatNumber } from "src/api/utils/format";
+import { getAssetPrefix } from "src/api/utils/portfolioUtils";
+import { ReactComponent as ArrowUpSVG } from "src/assets/icons/arrow-up.svg";
+import { ReactComponent as PlusSVG } from "src/assets/icons/plus.svg";
+import { ReactComponent as ShowSVG } from "src/assets/icons/shown.svg";
+
+import PagedMobileLayout from "src/layout/PagedMobileLayout";
+import { portfolioData } from "src/mobile-components/portfolio/mockData";
+import PortfolioChart from "src/mobile-components/portfolio/PortfolioChart";
+import WalletConnectionOptions from "src/mobile-components/portfolio/WalletConnectionOptions";
+import CONFIG from "src/config";
+
+const { SMALL_PRICE_CUTOFF_LG } = CONFIG.WIDGETS.PORTFOLIO;
+
+const UserWalletsInfo: FC<{ toggleBalance: () => void }> = ({
+ toggleBalance,
+}) => {
+ return (
+
+
+
+
+
+ Total Balance{" "}
+
+
+
+ $12,555
+
+
+ +58.54 (1.4%) /
+ 24h
+
+
+
+
+ );
+};
+
+const WalletsList: FC<{
+ onAddWallet: () => void;
+ wallets: {
+ label: string;
+ value: string;
+ }[];
+}> = ({ onAddWallet, wallets }) => {
+ return (
+
+ );
+};
+
+const AssetsList: FC<{ assets: TPortfolio[] }> = ({ assets }) => {
+ if (assets.length > 0) {
+ return (
+
+
+ {assets.map((asset) => {
+ const assetKey = `single-
+ ${asset.address}-${asset.network}-${asset.updatedAt}-${asset.token.id}`;
+
+ return (
+
+
+
+ {asset.token.tokenImage && (
+
+ )}
+
+ {asset.token.symbol}
+ {" "}
+
+ {asset.token.name}
+
+
+ {getAssetPrefix(asset)}
+
+
+
+
+
+
+
+ Balance
+
+
+ {
+ formatNumber({
+ value: asset.token
+ .balance,
+ }).value
+ }
+
+
+
+
+
+
+ Price
+
+
+ {
+ formatNumber({
+ value:
+ asset.token.price ||
+ 0,
+ style: ENumberStyle.Currency,
+ currency: "USD",
+ useEllipsis: true,
+ ellipsisCutoff:
+ SMALL_PRICE_CUTOFF_LG,
+ }).value
+ }
+
+
+
+
+
+
+ Value
+
+
+ {
+ formatNumber({
+ value:
+ asset.token
+ .balanceUSD ||
+ 0,
+ style: ENumberStyle.Currency,
+ currency: "USD",
+ }).value
+ }
+
+
+
+
+
+ );
+ })}
+
+
+ );
+ }
+ return null;
+};
+
+const PortfolioHoldings: React.FC = () => {
+ const dispatch = useAppDispatch();
+ const [showDialog, setShowDialog] = useState(false);
+ const history = useHistory();
+
+ const toggleBalance = () => {
+ dispatch(toggleShowBalanceInStore());
+ };
+ const wallets = [
+ { label: "All Assets", value: "all" },
+ {
+ label: "xavier-charles",
+ value: "0xe70d4BdacC0444CAa973b0A05CB6f2974C34aF0c",
+ },
+ {
+ label: "brine",
+ value: "0x93450d4BdacC0444CAa973b0A05CB6f2974C34DDc",
+ },
+ ];
+ return (
+
+ {/* // TODO (xavier-charles) update classname */}
+
+
+
{
+ setShowDialog(true);
+ }}
+ />
+
+
+
+ Balance
+
+
+
+ {" "}
+ (1.4%)
+ {" "}
+ / 24h
+
+
+
$12,555
+
+
+
+
+
+
+ );
+};
+
+export default PortfolioHoldings;
diff --git a/packages/frontend/src/mobile-pages/portfolio.tsx b/packages/frontend/src/mobile-pages/portfolio.tsx
index 4fb4ea34..5ef3866a 100644
--- a/packages/frontend/src/mobile-pages/portfolio.tsx
+++ b/packages/frontend/src/mobile-pages/portfolio.tsx
@@ -1,12 +1,10 @@
import { FC } from "react";
-import { OutlineButton, Pager } from "@alphaday/ui-kit";
+import { Pager } from "@alphaday/ui-kit";
import { useHistory } from "react-router";
import { useAuth } from "src/api/hooks";
import { ReactComponent as CloseSVG } from "src/assets/icons/close.svg";
-import { ReactComponent as CopySVG } from "src/assets/icons/copy.svg";
-import { ReactComponent as HandSVG } from "src/assets/icons/hand.svg";
import { ReactComponent as InfoSVG } from "src/assets/icons/Info2.svg";
-import { ReactComponent as WalletSVG } from "src/assets/icons/wallet.svg";
+import WalletConnectionOptions from "src/mobile-components/portfolio/WalletConnectionOptions";
const TopSection: FC<{ isAuthenticated: boolean }> = ({ isAuthenticated }) => {
if (isAuthenticated) {
@@ -35,6 +33,8 @@ const TopSection: FC<{ isAuthenticated: boolean }> = ({ isAuthenticated }) => {
const PortfolioPage = () => {
const history = useHistory();
const { isAuthenticated } = useAuth();
+
+ // TODO if user has holdings route to holdings
return (
<>
{
}
/>
-
- }
- onClick={() => history.push("/portfolio/add-wallet")}
- isAuthenticated={isAuthenticated}
- />
- }
- onClick={() => history.push("/portfolio/connect-wallet")}
- isAuthenticated={isAuthenticated}
- />
- }
- onClick={() => history.push("/portfolio/add-holding")}
- isAuthenticated={isAuthenticated}
- />
-
+ {
+ history.push(path);
+ }}
+ className="mx-4"
+ />
>
);
};
diff --git a/packages/ui-kit/index.ts b/packages/ui-kit/index.ts
index b0abeabb..72aac5f2 100644
--- a/packages/ui-kit/index.ts
+++ b/packages/ui-kit/index.ts
@@ -92,8 +92,8 @@ import {
import { breakpoints } from "./src/globalStyles/breakpoints";
import { themeColors } from "./src/globalStyles/themes";
import { OutlineButton } from "./src/mobile-components/button/buttons";
-import { FormInput } from "./src/mobile-components/form-elements/FormElements";
import { Pager } from "./src/mobile-components/pager/Pager";
+import { DropdownSelect } from "./src/mobile-components/select/DropdownSelect";
export type { TViewTabMenuOption, DatesSetArg, EventClickArg, TDatePos };
export * from "./src/mobile-components/form-elements/FormElements";
@@ -179,5 +179,5 @@ export {
Transition,
MiniDialog,
OutlineButton,
- FormInput,
+ DropdownSelect,
};
diff --git a/packages/ui-kit/src/mobile-components/pager/Pager.tsx b/packages/ui-kit/src/mobile-components/pager/Pager.tsx
index f0b25286..6c0c6a85 100644
--- a/packages/ui-kit/src/mobile-components/pager/Pager.tsx
+++ b/packages/ui-kit/src/mobile-components/pager/Pager.tsx
@@ -23,6 +23,7 @@ export const Pager: React.FC = ({
handleBack && "visible"
)}
onClick={handleBack}
+ title="Back"
>
@@ -36,6 +37,7 @@ export const Pager: React.FC = ({
handleClose && "visible"
)}
onClick={handleClose}
+ title="Close"
>
diff --git a/packages/ui-kit/src/mobile-components/select/DropdownSelect.tsx b/packages/ui-kit/src/mobile-components/select/DropdownSelect.tsx
new file mode 100644
index 00000000..a101947b
--- /dev/null
+++ b/packages/ui-kit/src/mobile-components/select/DropdownSelect.tsx
@@ -0,0 +1,104 @@
+import { FC, useState } from "react";
+import { Combobox } from "@headlessui/react";
+import { twMerge } from "tailwind-merge";
+import { ReactComponent as CheckmarkSVG } from "../../assets/svg/checkmark.svg";
+import { ReactComponent as ChevronUpDownSVG } from "../../assets/svg/chevron-up-down.svg";
+
+type TItem = {
+ id: number;
+ name: string;
+};
+
+interface IDropdownSelect {
+ label?: string;
+ items: {
+ label: string;
+ value: string;
+ }[];
+}
+
+export const DropdownSelect: FC = ({ label, items }) => {
+ const [selectedItem, setSelectedItem] = useState(items[0]);
+ return (
+
+ {label && (
+
+ {label}
+
+ )}
+
+
+ setSelectedItem(
+ items.find(
+ (item) => item.value === event.target.value
+ ) || items[0]
+ )
+ }
+ value={selectedItem?.label}
+ displayValue={(Item: TItem) => Item?.name}
+ />
+
+
+
+
+ {items.length > 0 && (
+
+ {items.map((item) => (
+
+ twMerge(
+ "relative cursor-default select-none py-2 pl-3 pr-9",
+ active
+ ? "bg-accentVariant100 cursor-pointer text-white"
+ : "text-primary"
+ )
+ }
+ >
+ {({ active, selected }) => (
+ <>
+
+ {item.label}
+
+
+ {selected && (
+
+
+
+ )}
+ >
+ )}
+
+ ))}
+
+ )}
+
+
+ );
+};