Skip to content

Commit

Permalink
feat(ECO-2036): Add "My emojicoins" page (#606)
Browse files Browse the repository at this point in the history
  • Loading branch information
0xForkh authored Feb 20, 2025
1 parent 69a9068 commit 1b818b4
Show file tree
Hide file tree
Showing 27 changed files with 894 additions and 57 deletions.
5 changes: 5 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"editor.codeActionsOnSave": {
"source.fixAll": "explicit"
}
}
21 changes: 21 additions & 0 deletions src/typescript/frontend/components.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"aliases": {
"components": "components",
"hooks": "hooks",
"lib": "lib",
"ui": "components/ui",
"utils": "lib/utils/class-name"
},
"iconLibrary": "lucide",
"rsc": true,
"style": "new-york",
"tailwind": {
"baseColor": "slate",
"config": "tailwind.config.js",
"css": "src/app/global.css",
"cssVariables": true,
"prefix": ""
},
"tsx": true
}
1 change: 1 addition & 0 deletions src/typescript/frontend/src/app/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
@tailwind utilities;

:root {
color: white;
.Toastify__toast-container > * {
font-family: var(--font-forma) !important;
line-height: 1.4rem;
Expand Down
95 changes: 95 additions & 0 deletions src/typescript/frontend/src/app/wallet/[address]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { getSymbolEmojisInString, symbolBytesToEmojis } from "@sdk/emoji_data";
import { fetchSpecificMarkets } from "@sdk/indexer-v2/queries";
import { toNominalPrice } from "@sdk/utils";
import { WalletClientPage } from "components/pages/wallet/WalletClientPage";
import { AptPriceContextProvider } from "context/AptPrice";
import {
fetchAllFungibleAssetsBalance,
type TokenBalance,
} from "lib/aptos-indexer/fungible-assets";
import { getAptPrice } from "lib/queries/get-apt-price";
import { toNominal } from "lib/utils/decimals";
import { emojisToName } from "lib/utils/emojis-to-name-or-symbol";
import { type Metadata } from "next";
import { emojiNamesToPath } from "utils/pathname-helpers";

export const metadata: Metadata = {
title: "Explore the cult",
description: `Explore the emojicoin cult`,
};

export type FullCoinData = Omit<TokenBalance, "amount"> &
Awaited<ReturnType<typeof fetchSpecificMarkets>>[number] & {
symbol: string;
marketCap: number;
emojiData: ReturnType<typeof symbolBytesToEmojis>;
emojiName: string;
emojiPath: string;
amount: number;
price: number;
ownedValue: number;
percentage?: number;
};

export default async function WalletPage({ params }: { params: { address: string } }) {
const [ownedTokens, aptPrice] = await Promise.all([
fetchAllFungibleAssetsBalance({ ownerAddress: params.address }),
getAptPrice(),
]);
const markets = await fetchSpecificMarkets(
ownedTokens.map((t) => getSymbolEmojisInString(t.metadata.symbol))
);
const marketDataMap: Record<string, (typeof markets)[number]> = markets.reduce((acc, market) => {
acc[market.market.symbolData.symbol] = market;
return acc;
}, {});

const coinsWithData = ownedTokens.map((coin) => {
const marketData = marketDataMap[coin.metadata.symbol];
const emojiData = symbolBytesToEmojis(coin.metadata.symbol);
const emojiName = emojisToName(emojiData.emojis);
const emojiPath = emojiNamesToPath(emojiData.emojis.map((x) => x.name));
const price = marketData ? toNominalPrice(marketData.lastSwap.avgExecutionPriceQ64) : 0;
const amount = toNominal(BigInt(coin.amount));
const ownedValue = amount * price;
const marketCap = marketData ? toNominal(marketData.state.instantaneousStats.marketCap) : 0;
return {
...coin,
...marketData,
marketCap,
emojiName,
symbol: coin.metadata.symbol,
emojiData,
emojiPath,
amount,
price,
ownedValue,
// Will be calculated later.
percentage: 0,
};
});

const walletStats = coinsWithData.reduce(
(acc, coin) => {
acc.totalValue += coin.ownedValue;
return acc;
},
{ totalValue: 0 }
);

coinsWithData.forEach((coin) => {
coin.percentage = (coin.ownedValue / walletStats.totalValue) * 100;
});

return (
<div className="mx-auto">
<AptPriceContextProvider aptPrice={aptPrice}>
<WalletClientPage
address={params.address}
ownedCoins={coinsWithData}
walletStats={walletStats}
/>
</AptPriceContextProvider>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,23 @@ import {
isAptosConnectWallet,
useWallet,
} from "@aptos-labs/wallet-adapter-react";
import { Copy, LogOut, User } from "lucide-react";
import { Copy, LogOut, User, UserRound } from "lucide-react";
import { useWalletModal } from "context/wallet-context/WalletModalContext";
import { AnimatePresence, useAnimationControls } from "framer-motion";
import AnimatedDropdownItem from "./components/animated-dropdown-item";
import useIsUserGeoblocked from "@hooks/use-is-user-geoblocked";
import { ROUTES } from "router/routes";
import { useRouter } from "next/navigation";

const IconClass = "w-[22px] h-[22px] m-auto ml-[3ch] mr-[1.5ch]";
const IconClass = "w-[22px] h-[22px] m-auto ml-[3ch] mr-[1.5ch] text-black";

export const MobileMenu: React.FC<MobileMenuProps> = ({
isOpen,
setIsOpen,
linksForCurrentPage,
}) => {
const { wallet, account, disconnect } = useWallet();
const router = useRouter();
const { copyAddress } = useAptos();
const { openWalletModal } = useWalletModal();
const subMenuControls = useAnimationControls();
Expand Down Expand Up @@ -118,6 +121,14 @@ export const MobileMenu: React.FC<MobileMenuProps> = ({
/>
</a>
)}
<AnimatedDropdownItem
key="my-emojicoins-dropdown"
onClick={() => router.push(`${ROUTES.wallet}/${account?.address}`)}
title="My emojicoins"
icon={<UserRound className={IconClass} />}
controls={subMenuControls}
borderControls={borderControls}
/>
<AnimatedDropdownItem
key="copy-address-dropdown"
title="Copy address"
Expand Down
2 changes: 1 addition & 1 deletion src/typescript/frontend/src/components/modal/BaseModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export const BaseModal: React.FC<
className="absolute group right-0 top-0 !z-50 flex h-[70px] w-[70px] cursor-pointer items-center justify-center"
onClick={onClose}
>
<ClosePixelated className="w-[15px] h-[16px] transition-all group-hover:w-[18px] group-hover:h-[19px]" />
<ClosePixelated className="w-[15px] h-[16px] transition-all group-hover:w-[18px] group-hover:h-[19px] text-black" />
</div>
</DialogTitle>
) : null}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export default function CultClientPage() {
defaultPosition: { x: width / 2, y: height / 2 },
}}
type="info"
className="max-w-[700px] -translate-x-1/2 -translate-y-1/2 [&>*]:!pl-2 !font-pixelar !lowercase [&>div>button]:lowercase text-[27px]"
className="text-black max-w-[700px] -translate-x-1/2 -translate-y-1/2 [&>*]:!pl-2 !font-pixelar !lowercase [&>div>button]:lowercase text-[27px]"
titleBarOptions={<TitleBar.Close onClick={() => setModalContent(undefined)} />}
buttons={modalContent.buttons || []}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
"use client";

import React, { useMemo, type PropsWithChildren } from "react";
import { type TableRowDesktopProps } from "./types";
import { ExplorerLink } from "components/explorer-link/ExplorerLink";
import { darkColors } from "theme";
import { useNameResolver } from "@hooks/use-name-resolver";
import { formatDisplayName } from "@sdk/utils";
import { FormattedNumber } from "components/FormattedNumber";
import { ColoredPriceDisplay } from "components/misc/ColoredPriceDisplay";
import Popup from "components/popup";
import { motion } from "framer-motion";
import { useMemo, type PropsWithChildren } from "react";
import { ROUTES } from "router/routes";
import { darkColors } from "theme";
import { emoji } from "utils";
import { Emoji } from "utils/emoji";
import { useNameResolver } from "@hooks/use-name-resolver";
import { FormattedNumber } from "components/FormattedNumber";
import { ColoredPriceDisplay } from "components/misc/ColoredPriceDisplay";
import { type TableRowDesktopProps } from "./types";
import { useRouter } from "next/navigation";
import { toExplorerLink } from "lib/utils/explorer-link";

type TableRowTextItemProps = {
className: string;
Expand All @@ -34,7 +36,7 @@ const TableRowTextItem = ({
</td>
);

const TableRowStyles = "flex relative w-full font-forma body-sm group";
const TableRowStyles = "flex relative w-full font-forma body-sm group cursor-pointer";
const Height = "h-[33px]";

const TableRow = ({
Expand All @@ -44,6 +46,7 @@ const TableRow = ({
numSwapsDisplayed,
shouldAnimateAsInsertion,
}: TableRowDesktopProps) => {
const router = useRouter();
const resolvedName = useNameResolver(item.swapper);
const { displayName, isANSName } = useMemo(() => {
return {
Expand All @@ -52,6 +55,11 @@ const TableRow = ({
};
}, [item.swapper, resolvedName]);

const explorerLink = toExplorerLink({
linkType: "txn",
value: `${item.version}`,
});

return (
<motion.tr
className={TableRowStyles}
Expand All @@ -73,6 +81,7 @@ const TableRow = ({
delay: shouldAnimateAsInsertion ? 0.2 : (numSwapsDisplayed - index) * 0.02,
},
}}
onClick={() => router.push(explorerLink)}
>
<td
className={
Expand Down Expand Up @@ -129,7 +138,11 @@ const TableRow = ({
/>
</TableRowTextItem>
<td className={`group/explorer w-[22%] md:w-[18%] border-r-[1px] z-[2] ${Height}`}>
<ExplorerLink className="flex w-full h-full" value={item.version} type="txn">
<a
href={`${ROUTES.wallet}/${item.swapper}`}
className="flex h-full"
onClick={(e) => e.stopPropagation()}
>
<span
className={
`${isANSName ? "brightness-[1.4] contrast-[1.4]" : ""}` +
Expand All @@ -139,7 +152,7 @@ const TableRow = ({
>
{displayName}
</span>
</ExplorerLink>
</a>
</td>
</motion.tr>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { type FullCoinData } from "app/wallet/[address]/page";
import { FormattedNumber } from "components/FormattedNumber";
import { AptCell } from "components/ui/table-cells/apt-cell";
import { TableCell, TableRow } from "components/ui/table/table";
import { useAptPrice } from "context/AptPrice";
import { useRouter } from "next/navigation";
import { useMemo, type FC } from "react";
import { ROUTES } from "router/routes";
import { Emoji } from "utils/emoji";

interface Props {
index: number;
coinData: FullCoinData;
walletStats: {
totalValue: number;
};
}

export const PortfolioRow: FC<Props> = ({ coinData, index, walletStats }) => {
const router = useRouter();
const aptPrice = useAptPrice();
const usdOwnedValue = useMemo(
() => (aptPrice || 0) * coinData.ownedValue,
[aptPrice, coinData.ownedValue]
);
return (
<TableRow
index={index}
onClick={() => router.push("/market/" + coinData.emojiPath)}
className="cursor-pointer"
>
<TableCell className="text-center">
{!coinData.inBondingCurve ? (
<a
className="hover:underline"
href={`${ROUTES.pools}?pool=${coinData.symbol}`}
onClick={(e) => e.stopPropagation()}
>
<Emoji
className={`${coinData.symbol.length <= 2 ? "text-[24px]" : "text-[20px]"} text-nowrap`}
emojis={coinData.symbol}
/>
</a>
) : (
<Emoji
className={`${coinData.symbol.length <= 2 ? "text-[24px]" : "text-[20px]"} text-nowrap`}
emojis={coinData.symbol}
/>
)}
</TableCell>
<TableCell className="text-end">
<FormattedNumber
scramble
value={(coinData.ownedValue / walletStats.totalValue) * 100}
suffix="%"
style={"fixed"}
/>
</TableCell>
<TableCell className="text-right">
<FormattedNumber scramble value={coinData.amount} style={"fixed"} />
</TableCell>
<TableCell className="text-right">
<span className="flex items-center justify-end gap-1">
<AptCell value={coinData.marketCap} />
</span>
</TableCell>
<TableCell className="text-right">
<span className="flex items-center justify-end gap-1">
<FormattedNumber scramble value={usdOwnedValue} style={"fixed"} prefix="$" />
</span>
</TableCell>
<TableCell className="text-right px-6">
<span className="flex items-center justify-end gap-1">
<AptCell value={coinData.ownedValue} />
</span>
</TableCell>
</TableRow>
);
};
Loading

0 comments on commit 1b818b4

Please sign in to comment.