diff --git a/src/components/ConditionalLink.tsx b/src/components/ConditionalLink.tsx index c512d46..906f8ec 100644 --- a/src/components/ConditionalLink.tsx +++ b/src/components/ConditionalLink.tsx @@ -6,7 +6,7 @@ import { Link, useRouteMatch } from "react-router-dom"; interface Props { - to: string; + to?: string; condition?: boolean; replace?: boolean; diff --git a/src/components/addresses/ContextualAddress.tsx b/src/components/addresses/ContextualAddress.tsx index e52cee1..64e0070 100644 --- a/src/components/addresses/ContextualAddress.tsx +++ b/src/components/addresses/ContextualAddress.tsx @@ -6,7 +6,6 @@ import { Tooltip } from "antd"; import { useTranslation } from "react-i18next"; -import { Link } from "react-router-dom"; import { KristAddress } from "@api/types"; import { Wallet, useWallets } from "@wallets"; @@ -33,6 +32,7 @@ allowWrap?: boolean; neverCopyable?: boolean; nonExistent?: boolean; + noLink?: boolean; className?: string; } @@ -46,6 +46,7 @@ allowWrap, neverCopyable, nonExistent, + noLink, className }: Props): JSX.Element { const { t } = useTranslation(); @@ -113,6 +114,7 @@ address={address} source={!!source} hideNameAddress={!!hideNameAddress} + noLink={!!noLink} name={cmName} recipient={cmRecipient} @@ -122,14 +124,18 @@ ) : (verified // Display the verified address if possible - ? + ? : ( // Display the regular address or label ) ), [ - hideNameAddress, nonExistent, source, nameSuffix, + hideNameAddress, nonExistent, noLink, source, nameSuffix, address, walletLabel, contactLabel, verified, cmName, cmRecipient, cmReturn, cmReturnName, hasMetaname, ]); @@ -187,6 +193,7 @@ address: string; source: boolean; hideNameAddress: boolean; + noLink: boolean; name?: string; recipient?: string; @@ -199,6 +206,7 @@ address, source, hideNameAddress, + noLink, name: cmName, recipient: cmRecipient, @@ -215,7 +223,12 @@ return verified ? ( // Verified address - + ) : ( // Regular address @@ -224,6 +237,7 @@ to={"/network/addresses/" + encodeURIComponent(address)} matchTo matchExact + condition={!noLink} > ({address}) @@ -238,6 +252,7 @@ className="address-name" name={nameWithoutSuffix!} text={rawMetaname} + noLink={noLink} /> {/* Display the original address too */} @@ -247,8 +262,13 @@ : ( // Display the raw metaname, but link to the owner address - + {rawMetaname} - + ); } diff --git a/src/components/addresses/VerifiedAddress.tsx b/src/components/addresses/VerifiedAddress.tsx index 452fa53..7621c01 100644 --- a/src/components/addresses/VerifiedAddress.tsx +++ b/src/components/addresses/VerifiedAddress.tsx @@ -37,6 +37,7 @@ address: string; verified: VerifiedAddress; parens?: boolean; + noLink?: boolean; className?: string; } @@ -44,6 +45,7 @@ address, verified, parens, + noLink, className }: Props): JSX.Element { const classes = classNames("address-verified", className, { @@ -56,6 +58,7 @@ to={"/network/addresses/" + encodeURIComponent(address)} matchTo matchExact + condition={!noLink} > {parens && <>(} diff --git a/src/components/names/KristNameLink.tsx b/src/components/names/KristNameLink.tsx index 0d842f5..b147992 100644 --- a/src/components/names/KristNameLink.tsx +++ b/src/components/names/KristNameLink.tsx @@ -4,7 +4,7 @@ import classNames from "classnames"; import { Typography } from "antd"; -import { Link } from "react-router-dom"; +import { ConditionalLink } from "@comp/ConditionalLink"; import { useNameSuffix } from "@utils/currency"; import { useBooleanSetting } from "@utils/settings"; @@ -35,12 +35,13 @@ const classes = classNames("krist-name", props.className); return - {noLink - ? content - : ( - - {content} - - )} + + {content} + ; } diff --git a/src/components/transactions/TransactionItem.tsx b/src/components/transactions/TransactionItem.tsx index dbbbeeb..7bf2354 100644 --- a/src/components/transactions/TransactionItem.tsx +++ b/src/components/transactions/TransactionItem.tsx @@ -1,21 +1,23 @@ // Copyright (c) 2020-2021 Drew Lemmy // This file is part of KristWeb 2 under AGPL-3.0. // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt -import classNames from "classnames"; import { Row, Col, Tooltip, Grid } from "antd"; +import { RightOutlined } from "@ant-design/icons"; -import { useTranslation, Trans } from "react-i18next"; +import { useTFns } from "@utils/i18n"; + import { Link } from "react-router-dom"; import { KristTransaction } from "@api/types"; -import { WalletAddressMap } from "@wallets"; -import { DateTime } from "../DateTime"; -import { KristValue } from "../krist/KristValue"; -import { KristNameLink } from "../names/KristNameLink"; -import { ContextualAddress } from "../addresses/ContextualAddress"; -import { getTransactionType, TransactionType, INTERNAL_TYPES_SHOW_VALUE } from "./TransactionType"; +import { WalletAddressMap, Wallet } from "@wallets"; -const MAX_A_LENGTH = 24; +import { DateTime } from "../DateTime"; + +import * as Parts from "./TransactionItemParts"; + +import { + getTransactionType, TransactionType, InternalTransactionType +} from "./TransactionType"; interface Props { transaction: KristTransaction; @@ -24,26 +26,23 @@ wallets: WalletAddressMap; } -export function TransactionARecord({ metadata }: { metadata: string | undefined | null }): JSX.Element { - const { t } = useTranslation(); +interface ItemProps { + type: InternalTransactionType; - return metadata - ? - - {metadata.length > MAX_A_LENGTH - ? <>{metadata.substring(0, MAX_A_LENGTH)}… - : metadata} - - - : ( - - {t("transactionSummary.itemARecordRemoved")} - - ); + tx: KristTransaction; + txTime: Date; + txLink: string; + + fromWallet?: Wallet; + toWallet?: Wallet; + + hideNameAddress: boolean; } -export function TransactionItem({ transaction: tx, wallets }: Props): JSX.Element { - const { t } = useTranslation(); +export function TransactionItem({ + transaction: tx, + wallets +}: Props): JSX.Element { const bps = Grid.useBreakpoint(); // Whether or not the from/to addresses are a wallet we own @@ -53,20 +52,39 @@ const type = getTransactionType(tx, fromWallet, toWallet); const txTime = new Date(tx.time); - const isNew = (new Date().getTime() - txTime.getTime()) < 360000; - const txLink = "/network/transactions/" + encodeURIComponent(tx.id); const hideNameAddress = !bps.xl; - const classes = classNames("card-list-item", "transaction-summary-item", { - "new": isNew - }); + // Return a different element (same data, different layout) depending on + // whether this is mobile or desktop + return bps.sm + ? + : ; +} - return +function TransactionItemDesktop({ + type, + tx, txTime, txLink, + fromWallet, toWallet, + hideNameAddress +}: ItemProps): JSX.Element { + const { t, tKey } = useTFns("transactionSummary."); + + return {/* Transaction type and link to transaction */} - + @@ -77,69 +95,75 @@ - {/* Transaction name */} - {(type === "name_a_record" || type === "name_purchased") && ( - - Name: - - - )} + {/* Name and A record */} + + - {/* Transaction A record */} - {type === "name_a_record" && ( - - A record: - - - )} + {/* To */} + - {/* Transaction to */} - {type !== "name_a_record" && ( - - To: - {type === "name_purchased" - ? - : } - - )} - - {/* Transaction from */} - {type !== "name_a_record" && type !== "name_purchased" && type !== "mined" && ( - - From: - - - )} + {/* From */} + - {INTERNAL_TYPES_SHOW_VALUE.includes(type) - ? ( - // Transaction value - - ) - : tx.type === "name_transfer" && ( - // Transaction name - - )} + {/* Value / name */} + ; } + +function TransactionItemMobile({ + type, + tx, txTime, txLink, + fromWallet, toWallet, + hideNameAddress +}: ItemProps): JSX.Element { + const { tKey } = useTFns("transactionSummary."); + + return + {/* Type and primary value */} +
+ + +
+ + {/* Name and A record */} + + + + {/* To */} + + + {/* From */} + + + {/* Time */} + + + {/* Right chevron */} + + ; +} diff --git a/src/components/transactions/TransactionItemParts.tsx b/src/components/transactions/TransactionItemParts.tsx new file mode 100644 index 0000000..7650892 --- /dev/null +++ b/src/components/transactions/TransactionItemParts.tsx @@ -0,0 +1,177 @@ +// Copyright (c) 2020-2021 Drew Lemmy +// This file is part of KristWeb 2 under AGPL-3.0. +// Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt +import { Tooltip } from "antd"; + +import { Trans } from "react-i18next"; +import { useTFns, TKeyFn } from "@utils/i18n"; + +import { KristTransaction } from "@api/types"; +import { Wallet } from "@wallets"; + +import { KristNameLink } from "../names/KristNameLink"; +import { ContextualAddress } from "../addresses/ContextualAddress"; +import { KristValue } from "../krist/KristValue"; + +import { + InternalTransactionType, INTERNAL_TYPES_SHOW_VALUE +} from "./TransactionType"; + +const MAX_A_LENGTH = 24; + +interface PartBaseProps { + tKey: TKeyFn; + type: InternalTransactionType; + noLink?: boolean; +} + +interface PartTxProps extends PartBaseProps { + tx: KristTransaction; +} + +interface PartAddressProps extends PartTxProps { + fromWallet?: Wallet; + toWallet?: Wallet; + hideNameAddress: boolean; +} + +// ----------------------------------------------------------------------------- +// NAME +// ----------------------------------------------------------------------------- +export function TransactionName({ tKey, type, name, noLink }: PartBaseProps & { + name?: string; +}): JSX.Element | null { + if (type !== "name_a_record" && type !== "name_purchased") return null; + + return + + Name: + + + ; +} + +// ----------------------------------------------------------------------------- +// A RECORD +// ----------------------------------------------------------------------------- +export function TransactionARecordContent({ metadata }: { + metadata: string | undefined | null; +}): JSX.Element { + const { tStr } = useTFns("transactionSummary."); + + return metadata + ? + + {metadata.length > MAX_A_LENGTH + ? <>{metadata.substring(0, MAX_A_LENGTH)}… + : metadata} + + + : ( + + {tStr("itemARecordRemoved")} + + ); +} + +export function TransactionARecord({ tKey, type, metadata }: PartBaseProps & { + metadata: string | undefined | null; +}): JSX.Element | null { + if (type !== "name_a_record") return null; + + return + + A record: + + + ; +} + +// ----------------------------------------------------------------------------- +// TO +// ----------------------------------------------------------------------------- +export function TransactionTo({ + tKey, + type, tx, + fromWallet, toWallet, + hideNameAddress, noLink +}: PartAddressProps): JSX.Element | null { + if (type === "name_a_record") return null; + + return + + To: + {type === "name_purchased" + ? + : } + + ; +} + +// ----------------------------------------------------------------------------- +// FROM +// ----------------------------------------------------------------------------- +export function TransactionFrom({ + tKey, + type, tx, + fromWallet, + hideNameAddress, noLink +}: Omit): JSX.Element | null { + if (type === "name_a_record" || type === "name_purchased" || type === "mined") + return null; + + return + + From: + + + ; +} + +// ----------------------------------------------------------------------------- +// VALUE / NAME +// ----------------------------------------------------------------------------- +export function TransactionPrimaryValue({ + type, + tx +}: Omit): JSX.Element | null { + return + {INTERNAL_TYPES_SHOW_VALUE.includes(type) + ? ( + // Transaction value + + ) + : (tx.type === "name_transfer" + ? ( + // Transaction name + + ) + : null + )} + ; +} diff --git a/src/components/transactions/TransactionSummary.less b/src/components/transactions/TransactionSummary.less index 7224972..cce55c3 100644 --- a/src/components/transactions/TransactionSummary.less +++ b/src/components/transactions/TransactionSummary.less @@ -6,19 +6,44 @@ .transaction-summary-item { flex-flow: nowrap; + .date-time { + color: @text-color-secondary; + font-size: 90%; + + @media (max-width: @screen-xl) { + font-size: 85%; + } + } + + .transaction-field { + font-weight: bold; + white-space: nowrap; + color: @text-color-secondary; + } + + .transaction-a-record-value { + font-family: monospace; + font-size: 90%; + color: @text-color-secondary; + } + + .transaction-a-record-removed { + font-style: italic; + font-size: 90%; + color: @text-color-secondary; + + white-space: nowrap; + text-overflow: ellipsis; + } + + .transaction-name { + font-weight: bold; + } + .transaction-left { display: flex; flex-direction: column; justify-content: center; - - .transaction-time { - color: @text-color-secondary; - font-size: 90%; - - @media (max-width: @screen-xl) { - font-size: 85%; - } - } } .transaction-middle { @@ -29,27 +54,6 @@ justify-content: center; overflow: hidden; - - .transaction-field { - font-weight: bold; - white-space: nowrap; - color: @text-color-secondary; - } - - .transaction-a-record-value { - font-family: monospace; - font-size: 90%; - color: @text-color-secondary; - } - - .transaction-a-record-removed { - font-style: italic; - font-size: 90%; - color: @text-color-secondary; - - white-space: nowrap; - text-overflow: ellipsis; - } } .transaction-right { @@ -59,9 +63,50 @@ display: flex; justify-content: center; align-items: center; + } - .transaction-name { - font-weight: bold; + &.transaction-summary-item-mobile { + display: block; + margin-bottom: @padding-xs; + + color: @text-color; + font-size: @font-size-sm; + + position: relative; + + .transaction-mobile-top { + font-size: @font-size-base; + margin-bottom: @padding-xss; + + .transaction-type { + font-size: @font-size-base; + } + + .transaction-primary-value { + display: inline-block; + margin-left: @padding-xs; + } + } + + .transaction-to, .transaction-from { + display: block; + } + + .date-time { + font-size: @font-size-sm; + } + + .transaction-mobile-right { + display: flex; + align-items: center; + + position: absolute; + top: 0; + bottom: 0; + right: @padding-md; + + font-size: 32px; + color: fade(@text-color-secondary, 50%); } } } diff --git a/src/components/transactions/TransactionType.tsx b/src/components/transactions/TransactionType.tsx index 1a50597..3cf11e7 100644 --- a/src/components/transactions/TransactionType.tsx +++ b/src/components/transactions/TransactionType.tsx @@ -4,7 +4,8 @@ import classNames from "classnames"; import { useTranslation } from "react-i18next"; -import { Link } from "react-router-dom"; + +import { ConditionalLink } from "@comp/ConditionalLink"; import { KristTransaction, KristTransactionType } from "@api/types"; import { Wallet, useWallets } from "@wallets"; @@ -22,7 +23,11 @@ "transfer", "mined", "name_purchase" ]; -export function getTransactionType(tx: KristTransaction, from?: Wallet, to?: Wallet): InternalTransactionType { +export function getTransactionType( + tx: KristTransaction, + from?: Wallet, + to?: Wallet +): InternalTransactionType { switch (tx.type) { case "transfer": if (tx.from && tx.to && tx.from === tx.to) return "bumped"; @@ -55,7 +60,14 @@ } type Props = React.HTMLProps & OwnProps; -export function TransactionType({ type, transaction, from, to, link, className }: Props): JSX.Element { +export function TransactionType({ + type, + transaction, + from, + to, + link, + className +}: Props): JSX.Element { const { t } = useTranslation(); const { walletAddressMap } = useWallets(); @@ -73,8 +85,13 @@ }); return - {link - ? {contents} - : {contents}} + + {contents} + ; } diff --git a/src/layout/PageLayout.less b/src/layout/PageLayout.less index 688c03f..9cf68c2 100644 --- a/src/layout/PageLayout.less +++ b/src/layout/PageLayout.less @@ -33,6 +33,8 @@ // Make tables full-width on mobile (though most should be replaced with // custom views) @media (max-width: @screen-md) { + padding: @padding-md; + >.ant-table-wrapper { .ant-table { margin: 0 -@padding-lg; @@ -55,6 +57,10 @@ } } } + + @media (max-width: @screen-sm) { + padding: @padding-sm; + } } &.page-layout-no-top-padding { diff --git a/src/style/card.less b/src/style/card.less index cfa6007..8f49a16 100644 --- a/src/style/card.less +++ b/src/style/card.less @@ -123,4 +123,30 @@ } } } + + @media (max-width: @screen-sm) { + .ant-card-head { + padding: 0 @padding-md; + + .ant-card-head-title { + padding-top: @padding-md; + } + } + + .ant-card-body { + padding: @padding-md; + } + + .card-more { + height: 32px; + margin-bottom: -6px; + + a { + display: inline; + line-height: 32px; + vertical-align: middle; + margin-bottom: 0; + } + } + } }