From ee8cf52a436abf1b61a00e78552b88f9d4e8c1a7 Mon Sep 17 00:00:00 2001 From: Hayden Shively <17186559+haydenshively@users.noreply.github.com> Date: Wed, 21 Feb 2024 00:15:35 -0600 Subject: [PATCH] Fresh coat of paint for the Advanced page --- .../src/components/advanced/BorrowMetrics.tsx | 10 +- .../components/advanced/GlobalStatsTable.tsx | 1 - .../advanced/ManageAccountButtons.tsx | 2 +- .../components/advanced/SmartWalletButton.tsx | 15 +- .../advanced/UniswapPositionList.tsx | 7 +- .../advanced/modal/NewSmartWalletModal.tsx | 7 +- .../components/common/UniswapPositionCard.tsx | 4 +- earn/src/pages/AdvancedPage.tsx | 493 +++++++----------- shared/src/components/banner/Banner.tsx | 87 ++++ shared/src/components/common/AppPage.tsx | 3 +- 10 files changed, 291 insertions(+), 338 deletions(-) create mode 100644 shared/src/components/banner/Banner.tsx diff --git a/earn/src/components/advanced/BorrowMetrics.tsx b/earn/src/components/advanced/BorrowMetrics.tsx index bc26a2d9..68502ab3 100644 --- a/earn/src/components/advanced/BorrowMetrics.tsx +++ b/earn/src/components/advanced/BorrowMetrics.tsx @@ -2,7 +2,7 @@ import { useMemo } from 'react'; import Tooltip from 'shared/lib/components/common/Tooltip'; import { Display, Text } from 'shared/lib/components/common/Typography'; -import { GREY_800 } from 'shared/lib/data/constants/Colors'; +import { GREY_700 } from 'shared/lib/data/constants/Colors'; import { formatTokenAmount } from 'shared/lib/util/Numbers'; import styled from 'styled-components'; @@ -22,7 +22,7 @@ const MetricCardContainer = styled.div` flex-direction: column; justify-content: center; align-items: flex-start; - background-color: ${GREY_800}; + background-color: ${GREY_700}; border-radius: 8px; padding: 16px; `; @@ -32,10 +32,10 @@ const MetricCardPlaceholder = styled.div.attrs((props: { height: number; $animat border-radius: 8px; padding: 16px; height: ${(props) => props.height}px; - background-color: #0d171e; + background-color: ${GREY_700}; animation: ${(props) => (props.$animate ? 'metricCardShimmer 0.75s forwards linear infinite' : '')}; background-image: ${(props) => - props.$animate ? 'linear-gradient(to right, #0d171e 0%, #131f28 20%, #0d171e 40%, #0d171e 100%)' : ''}; + props.$animate ? `linear-gradient(to right, ${GREY_700} 0%, #131f28 20%, ${GREY_700} 40%, ${GREY_700} 100%)` : ''}; background-repeat: no-repeat; background-size: 200% 100%; overflow: hidden; @@ -57,7 +57,7 @@ const HorizontalMetricCardContainer = styled.div` justify-content: space-between; align-items: center; gap: 8px; - background-color: ${GREY_800}; + background-color: ${GREY_700}; border-radius: 8px; padding: 16px; `; diff --git a/earn/src/components/advanced/GlobalStatsTable.tsx b/earn/src/components/advanced/GlobalStatsTable.tsx index 5e0fdd31..83fa46d6 100644 --- a/earn/src/components/advanced/GlobalStatsTable.tsx +++ b/earn/src/components/advanced/GlobalStatsTable.tsx @@ -16,7 +16,6 @@ const Wrapper = styled.div` flex-direction: column; /* 16px due to the bottom padding already being 8px making the total space 24px */ gap: 16px; - margin-bottom: 64px; @media (max-width: ${RESPONSIVE_BREAKPOINT_XS}) { margin-bottom: 48px; diff --git a/earn/src/components/advanced/ManageAccountButtons.tsx b/earn/src/components/advanced/ManageAccountButtons.tsx index 341fcd54..74edbe0e 100644 --- a/earn/src/components/advanced/ManageAccountButtons.tsx +++ b/earn/src/components/advanced/ManageAccountButtons.tsx @@ -21,7 +21,7 @@ export default function ManageAccountButtons(props: ManageAccountButtonsProps) { const { onAddCollateral, onRemoveCollateral, onBorrow, onRepay, onWithdrawAnte, isWithdrawAnteDisabled, isDisabled } = props; return ( -
+
} position='leading' diff --git a/earn/src/components/advanced/SmartWalletButton.tsx b/earn/src/components/advanced/SmartWalletButton.tsx index 2dd16874..b6157e4f 100644 --- a/earn/src/components/advanced/SmartWalletButton.tsx +++ b/earn/src/components/advanced/SmartWalletButton.tsx @@ -1,5 +1,4 @@ -import React from 'react'; - +import TokenIcons from 'shared/lib/components/common/TokenIcons'; import { Display } from 'shared/lib/components/common/Typography'; import { Token } from 'shared/lib/data/Token'; import styled from 'styled-components'; @@ -7,7 +6,6 @@ import styled from 'styled-components'; import { ReactComponent as PlusIcon } from '../../assets/svg/plus.svg'; import useProminentColor from '../../data/hooks/UseProminentColor'; import { rgba } from '../../util/Colors'; -import TokenPairIcons from '../common/TokenPairIcons'; const Container = styled.button.attrs( (props: { backgroundGradient: string; active: boolean; $animate: boolean }) => props @@ -87,14 +85,9 @@ export default function SmartWalletButton(props: SmartWalletButtonProps) { return ( -
- - +
+ + {token0.symbol} / {token1.symbol} {tokenId === null ? '' : ` (#${tokenId})`} diff --git a/earn/src/components/advanced/UniswapPositionList.tsx b/earn/src/components/advanced/UniswapPositionList.tsx index ee203fb1..414d92ff 100644 --- a/earn/src/components/advanced/UniswapPositionList.tsx +++ b/earn/src/components/advanced/UniswapPositionList.tsx @@ -3,6 +3,7 @@ import { useState } from 'react'; import { SendTransactionResult } from '@wagmi/core'; import { FilledGreyButton } from 'shared/lib/components/common/Buttons'; import { Display, Text } from 'shared/lib/components/common/Typography'; +import { GREY_700 } from 'shared/lib/data/constants/Colors'; import { formatTokenAmount, roundPercentage } from 'shared/lib/util/Numbers'; import styled from 'styled-components'; @@ -59,7 +60,7 @@ function UniswapPositionCard(props: UniswapPositionCardProps) { if (!borrower || !uniswapPosition) { return ( - + Empty @@ -97,7 +98,7 @@ function UniswapPositionCard(props: UniswapPositionCardProps) { }); return ( - +
{slot} index ? uniswapPositions[index] : undefined} + uniswapPosition={uniswapPositions.at(index)} withdrawableUniswapNFTs={withdrawableUniswapNFTs} setSelectedUniswapPosition={setSelectedUniswapPosition} setPendingTxn={props.setPendingTxn} diff --git a/earn/src/components/advanced/modal/NewSmartWalletModal.tsx b/earn/src/components/advanced/modal/NewSmartWalletModal.tsx index d6ddb083..5b51e1b8 100644 --- a/earn/src/components/advanced/modal/NewSmartWalletModal.tsx +++ b/earn/src/components/advanced/modal/NewSmartWalletModal.tsx @@ -25,9 +25,9 @@ const TERTIARY_COLOR = '#4b6980'; const SmartWalletOptionsPage = styled.div` display: flex; flex-direction: column; - gap: 16px; + gap: 8px; // The height of 5 buttons + gap between them - min-height: 304px; + min-height: 212px; `; type CreateSmartWalletButtonProps = { @@ -177,7 +177,8 @@ export default function NewSmartWalletModal(props: NewSmartWalletModalProps) {
- Select a pair to borrow from + On Aloe, all borrows are managed inside smart wallets, represented by NFTs. Select a pair and mint an NFT to + get started. } diff --git a/earn/src/components/common/UniswapPositionCard.tsx b/earn/src/components/common/UniswapPositionCard.tsx index 0eacfa08..20c3a390 100644 --- a/earn/src/components/common/UniswapPositionCard.tsx +++ b/earn/src/components/common/UniswapPositionCard.tsx @@ -15,11 +15,11 @@ export const UniswapPositionCardContainer = styled.div` gap: 8px; `; -export const UniswapPositionCardWrapper = styled.div` +export const UniswapPositionCardWrapper = styled.div<{ $color?: string }>` display: flex; flex-direction: column; gap: 8px; - background-color: ${GREY_800}; + background-color: ${(props) => props.$color ?? GREY_800}; border-radius: 8px; padding: 16px; width: 100%; diff --git a/earn/src/pages/AdvancedPage.tsx b/earn/src/pages/AdvancedPage.tsx index e0dad2fd..f002584e 100644 --- a/earn/src/pages/AdvancedPage.tsx +++ b/earn/src/pages/AdvancedPage.tsx @@ -8,13 +8,13 @@ import { useNavigate, useSearchParams } from 'react-router-dom'; import { borrowerAbi } from 'shared/lib/abis/Borrower'; import { borrowerLensAbi } from 'shared/lib/abis/BorrowerLens'; import { lenderLensAbi } from 'shared/lib/abis/LenderLens'; +import Banner from 'shared/lib/components/banner/Banner'; import AppPage from 'shared/lib/components/common/AppPage'; -import { LABEL_TEXT_COLOR } from 'shared/lib/components/common/Modal'; import { Text } from 'shared/lib/components/common/Typography'; import { ALOE_II_LENDER_LENS_ADDRESS, ALOE_II_BORROWER_LENS_ADDRESS, - ALOE_II_ORACLE_ADDRESS, + ALOE_II_BORROWER_NFT_ADDRESS, } from 'shared/lib/data/constants/ChainSpecific'; import { GetNumericFeeTier } from 'shared/lib/data/FeeTier'; import { GN } from 'shared/lib/data/GoodNumber'; @@ -28,8 +28,6 @@ import { Address, useAccount, useContract, useProvider, useContractRead, useBala import { ChainContext } from '../App'; import { ReactComponent as InfoIcon } from '../assets/svg/info.svg'; -import BorrowGraph, { BorrowGraphData } from '../components/advanced/BorrowGraph'; -import { BorrowGraphPlaceholder } from '../components/advanced/BorrowGraphPlaceholder'; import { BorrowMetrics } from '../components/advanced/BorrowMetrics'; import GlobalStatsTable from '../components/advanced/GlobalStatsTable'; import ManageAccountButtons from '../components/advanced/ManageAccountButtons'; @@ -42,10 +40,8 @@ import WithdrawAnteModal from '../components/advanced/modal/WithdrawAnteModal'; import SmartWalletButton, { NewSmartWalletButton } from '../components/advanced/SmartWalletButton'; import { UniswapPositionList } from '../components/advanced/UniswapPositionList'; import PendingTxnModal, { PendingTxnModalStatus } from '../components/common/PendingTxnModal'; -import { computeLTV } from '../data/BalanceSheet'; import { BorrowerNftBorrower, fetchListOfBorrowerNfts } from '../data/BorrowerNft'; -import { RESPONSIVE_BREAKPOINT_MD, RESPONSIVE_BREAKPOINT_SM } from '../data/constants/Breakpoints'; -import { TOPIC0_UPDATE_ORACLE } from '../data/constants/Signatures'; +import { RESPONSIVE_BREAKPOINT_SM } from '../data/constants/Breakpoints'; import { primeUrl } from '../data/constants/Values'; import useAvailablePools from '../data/hooks/UseAvailablePools'; import { fetchBorrowerDatas } from '../data/MarginAccount'; @@ -58,145 +54,84 @@ import { UniswapPositionPrior, } from '../data/Uniswap'; -const SECONDARY_COLOR = 'rgba(130, 160, 182, 1)'; const BORROW_TITLE_TEXT_COLOR = 'rgba(130, 160, 182, 1)'; -const TOPIC1_PREFIX = '0x000000000000000000000000'; const FETCH_UNISWAP_POSITIONS_DEBOUNCE_MS = 500; const SELECTED_MARGIN_ACCOUNT_KEY = 'account'; -const ExplainerWrapper = styled.div` - display: flex; - flex-direction: row; - position: relative; - - padding: 16px; - margin-top: 1rem; - margin-bottom: 2rem; - - background-color: rgba(10, 20, 27, 1); - border-radius: 8px; - - &:before { - content: ''; - position: absolute; - inset: 0; - pointer-events: none; - border-radius: 8px; - /* 1.25px instead of 1px since it avoids the buggy appearance */ - padding: 1.25px; - background: linear-gradient(90deg, #9baaf3 0%, #7bd8c0 100%); - mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); - -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); - -webkit-mask-composite: xor; - mask-composite: exclude; - } -`; - const Container = styled.div` - display: flex; - align-items: flex-start; - gap: 64px; + display: grid; + gap: 20px; max-width: 1280px; margin: 0 auto; + margin-top: 32px; - @media (max-width: ${RESPONSIVE_BREAKPOINT_MD}) { - gap: 32px; - } + grid-template-columns: 1fr 6fr; + grid-template-rows: auto auto; + grid-template-areas: + 'title buttons' + 'list data'; @media (max-width: ${RESPONSIVE_BREAKPOINT_SM}) { - flex-direction: column; - gap: 0; - align-items: center; - } -`; - -const PageGrid = styled.div` - display: grid; - grid-template-columns: 1fr 1.2fr; - grid-template-rows: auto auto auto auto auto; - grid-template-areas: - 'monitor graph' - 'metrics metrics' - 'uniswap uniswap' - 'stats stats' - 'link link'; - flex-grow: 1; - margin-top: 26px; - - @media (max-width: ${RESPONSIVE_BREAKPOINT_MD}) { - width: 100%; grid-template-columns: 1fr; grid-template-rows: auto auto auto auto; grid-template-areas: - 'monitor' - 'graph' - 'metrics' - 'uniswap' - 'stats' - 'link'; + 'title' + 'list' + 'buttons' + 'data'; } `; -const StyledExternalLink = styled.a` +const GridAreaForButtons = styled.div` display: flex; - align-items: center; - gap: 4px; - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; - max-width: 100%; - &:hover { - text-decoration: underline; - } + grid-area: buttons; + + background: rgba(13, 23, 30, 1); + + padding: 16px; + border-radius: 16px; `; -const SmartWalletsContainer = styled.div` +const GridAreaForNFTList = styled.div` display: flex; flex-direction: column; - gap: 8px; + grid-area: list; + + gap: 4px; @media (max-width: ${RESPONSIVE_BREAKPOINT_SM}) { + flex-direction: row; width: 100%; + overflow-x: scroll; } `; -const SmartWalletsList = styled.div` +const GridAreaForData = styled.div` display: flex; flex-direction: column; - gap: 8px; - width: max-content; - min-width: 280px; -`; - -const MonitorContainer = styled.div` - grid-area: monitor; - margin-bottom: 64px; - display: flex; - flex-direction: column; - gap: 32px; -`; + grid-area: data; -const GraphContainer = styled.div` - grid-area: graph; - margin-bottom: 64px; -`; - -const MetricsContainer = styled.div` - grid-area: metrics; - margin-bottom: 64px; -`; + background: rgba(13, 23, 30, 1); -const UniswapPositionsContainer = styled.div` - grid-area: uniswap; - margin-bottom: 64px; + gap: 64px; + padding: 16px; + border-radius: 16px; `; -const StatsContainer = styled.div` - grid-area: stats; +const StyledExternalLink = styled.a` + display: flex; + align-items: center; + gap: 4px; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + max-width: 100%; + &:hover { + text-decoration: underline; + } `; const LinkContainer = styled.div` - grid-area: link; display: flex; align-items: center; justify-content: center; @@ -214,8 +149,6 @@ export default function AdvancedPage() { const provider = useProvider({ chainId: activeChain.id }); const { address: userAddress, isConnected } = useAccount(); - const [cachedGraphDatas, setCachedGraphDatas] = useSafeState>(new Map()); - const [graphData, setGraphData] = useSafeState(null); const [borrowerNftBorrowers, setBorrowerNftBorrowers] = useChainDependentState( null, activeChain.id @@ -327,58 +260,6 @@ export default function AdvancedPage() { })(); }, [selectedMarginAccount, provider, cachedMarketInfos, activeChain.id, setSelectedMarketInfo, setCachedMarketInfos]); - // MARK: Fetch GraphData - useEffect(() => { - const cachedGraphData = cachedGraphDatas.get(selectedMarginAccount?.address ?? ''); - if (cachedGraphData !== undefined) { - setGraphData(cachedGraphData); - return; - } - (async () => { - if (selectedMarginAccount == null) return; - - const chainId = (await provider.getNetwork()).chainId; - const updateLogs = await provider.getLogs({ - address: ALOE_II_ORACLE_ADDRESS[chainId], - topics: [TOPIC0_UPDATE_ORACLE, `${TOPIC1_PREFIX}${selectedMarginAccount?.uniswapPool.slice(2)}`], - fromBlock: 0, - toBlock: 'latest', - }); - - const blockI = updateLogs.at(0); - const blockF = updateLogs.at(-1); - - if (blockI === undefined || blockF === undefined) return; - - const [tI, tF] = await Promise.all( - [blockI, blockF].map(async (log) => { - return (await provider.getBlock(log.blockNumber)).timestamp; - }) - ); - - const results = await Promise.all( - updateLogs.map(async (result) => { - const approxTime = tI + ((tF - tI) / (blockF.blockNumber - blockI.blockNumber)) * result.blockNumber; - - const decoded = ethers.utils.defaultAbiCoder.decode(['uint160', 'uint256'], result.data); - const iv = ethers.BigNumber.from(decoded[1]).div(1e6).toNumber() / 1e6; - const ltv = computeLTV(iv, selectedMarginAccount.nSigma); - - const resultData: BorrowGraphData = { - IV: iv * Math.sqrt(365) * 100, - LTV: ltv * 100, - x: new Date(approxTime * 1000), - }; - return resultData; - }) - ); - setCachedGraphDatas((prev) => { - return new Map(prev).set(selectedMarginAccount.address, results); - }); - setGraphData(results); - })(); - }, [activeChain, cachedGraphDatas, provider, selectedMarginAccount, setCachedGraphDatas, setGraphData]); - // MARK: Fetch Uniswap positions for this MarginAccount (debounced to avoid double-fetching) useDebouncedEffect( () => { @@ -510,6 +391,9 @@ export default function AdvancedPage() { const baseEtherscanUrl = getEtherscanUrlForChain(activeChain); const selectedMarginAccountEtherscanUrl = `${baseEtherscanUrl}/address/${selectedMarginAccount?.address}`; + const selectedBorrowerOpenseaUrl = `https://opensea.io/assets/${activeChain.network}/${ + ALOE_II_BORROWER_NFT_ADDRESS[activeChain.id] + }/${selectedMarginAccount ? ethers.BigNumber.from(selectedMarginAccount!.tokenId).toString() : ''}`; const hasLiabilities = Object.values(selectedMarginAccount?.liabilities ?? {}).some((liability) => { return liability > 0; @@ -522,47 +406,20 @@ export default function AdvancedPage() { const userHasNoMarginAccounts = borrowerNftBorrowers?.length === 0; return ( - - - - When you borrow on the Markets page or Boost page, Borrower NFTs are created behind the scenes. This page - gives you fine-grained control over those NFTs. However, once you make changes here, the NFTs won't show up - elsewhere. - - - - - - Borrower NFTs - - - {borrowerNftBorrowers?.map((account) => ( - { - // When a new account is selected, we need to update the - // selectedMarginAccount, selectedMarketInfo, and uniswapPositions - // setSelectedMarginAccount(account); - setSearchParams({ [SELECTED_MARGIN_ACCOUNT_KEY]: account.address }); - setSelectedMarketInfo(cachedMarketInfos.get(account.address) ?? undefined); - setUniswapPositions(cachedUniswapPositionsMap.get(account.address) ?? []); - }} - key={account.address} - /> - ))} - { - setNewSmartWalletModalOpen(true); - }} - /> - - - - + <> + + + +
+ + Borrower NFTs + +
+ { if (isConnected) setIsAddCollateralModalOpen(true); @@ -588,25 +445,33 @@ export default function AdvancedPage() { isWithdrawAnteDisabled={isUnableToWithdrawAnte} isDisabled={!selectedMarginAccount} /> -
- -
- {graphData && graphData.length > 0 ? ( - - ) : ( - - )} -
- - - IV comes from an on-chain oracle. It influences the current collateral factor, which impacts the - health of your account. - - -
-
-
- + + + {borrowerNftBorrowers?.map((account) => ( + { + // When a new account is selected, we need to update the + // selectedMarginAccount, selectedMarketInfo, and uniswapPositions + // setSelectedMarginAccount(account); + setSearchParams({ [SELECTED_MARGIN_ACCOUNT_KEY]: account.address }); + setSelectedMarketInfo(cachedMarketInfos.get(account.address) ?? undefined); + setUniswapPositions(cachedUniswapPositionsMap.get(account.address) ?? []); + }} + key={account.address} + /> + ))} + { + setNewSmartWalletModalOpen(true); + }} + /> + + - - - - - - {selectedMarginAccount && ( - - - - - View this account on Etherscan - - - - )} -
-
- {availablePools.size > 0 && ( - - )} - {selectedMarginAccount && selectedMarketInfo && ( - <> - - - - - + + + + + View on Etherscan + + + + + + + + View on OpenSea + + + +
+ )} + + + {availablePools.size > 0 && ( + - - )} - { - setIsPendingTxnModalOpen(isOpen); - if (!isOpen) { - setPendingTxn(null); - } - }} - txnHash={pendingTxn?.hash} - onConfirm={() => { - setIsPendingTxnModalOpen(false); - setTimeout(() => { - navigate(0); - }, 100); - }} - status={pendingTxnModalStatus} - /> - + )} + {selectedMarginAccount && selectedMarketInfo && ( + <> + + + + + + + )} + { + setIsPendingTxnModalOpen(isOpen); + if (!isOpen) { + setPendingTxn(null); + } + }} + txnHash={pendingTxn?.hash} + onConfirm={() => { + setIsPendingTxnModalOpen(false); + setTimeout(() => { + navigate(0); + }, 100); + }} + status={pendingTxnModalStatus} + /> + + ); } diff --git a/shared/src/components/banner/Banner.tsx b/shared/src/components/banner/Banner.tsx new file mode 100644 index 00000000..27ff396a --- /dev/null +++ b/shared/src/components/banner/Banner.tsx @@ -0,0 +1,87 @@ +import { Text } from '../common/Typography'; +import styled from 'styled-components'; + +import CloseModal from '../../assets/svg/CloseModal'; +import useLocalStorage from '../../data/hooks/UseLocalStorage'; +import { GREY_700 } from '../../data/constants/Colors'; + +const BannerWrapper = styled.div` + position: fixed; + z-index: 9; + display: flex; + justify-content: center; + align-items: center; + width: 100%; +`; + +const BannerContainer = styled.div<{ bgColor: string }>` + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + background-color: ${(props) => props.bgColor}; + border: 1px solid ${GREY_700}; + border-top: 0; + border-radius: 4px; + border-top-left-radius: 0; + border-top-right-radius: 0; + padding: 12px; +`; + +const BetaBadge = styled.div<{ bgColor: string }>` + display: flex; + justify-content: center; + align-items: center; + + background-color: ${(props) => props.bgColor}; + border-radius: 4px; + padding: 4px 8px; + margin-right: 12px; +`; + +const CloseButton = styled.button` + display: flex; + justify-content: center; + align-items: center; + background-color: transparent; + border: none; + cursor: pointer; + margin-left: 12px; +`; + +type BannerProps = { + bannerName: string; + bannerText: string; + bannerColor: string; +}; + +const hashCode = (s: string) => s.split('').reduce((a, b) => ((a << 5) - a + b.charCodeAt(0)) | 0, 0); + +export default function Banner(props: BannerProps) { + const { bannerName, bannerText, bannerColor } = props; + + const bannerKey = hashCode(bannerName.concat(bannerText)).toFixed(); + const [isShowing, setIsShowing] = useLocalStorage(bannerKey, true); + if (!isShowing) return null; + return ( + + +
+ + + {bannerName} + + + + {bannerText} + +
+
+ setIsShowing(false)}> + + +
+
+
+ ); +} diff --git a/shared/src/components/common/AppPage.tsx b/shared/src/components/common/AppPage.tsx index 81205b31..567d2f10 100644 --- a/shared/src/components/common/AppPage.tsx +++ b/shared/src/components/common/AppPage.tsx @@ -1,12 +1,13 @@ import React, { ReactNode } from 'react'; export type AppPageProps = { + extraTwTags?: string; children?: ReactNode; }; export default function AppPage(props: AppPageProps) { return ( -
+
{props.children}
);