diff --git a/.vscode/launch.json b/.vscode/launch.json index 217032a..b55ef60 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -13,6 +13,16 @@ "request": "launch", "name": "Launch Firefox against localhost", "url": "http://localhost:3000", + "webRoot": "${workspaceFolder}", + "profile": "kristweb", + "keepProfileChanges": true, + "reAttach": true + }, + { + "type": "firefox", + "request": "attach", + "name": "Attach Firefox against localhost", + "url": "http://localhost:3000", "webRoot": "${workspaceFolder}" } ] diff --git a/.vscode/settings.json b/.vscode/settings.json index 3adb723..900afa0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,6 +10,7 @@ "Lngs", "Transpiler", "Unfocus", + "Unmount", "apos", "arraybuffer", "borderless", diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..33602e9 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,10 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "start", + "isBackground": true + } + ] +} diff --git a/package-lock.json b/package-lock.json index 8f12baf..5ed684b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13711,9 +13711,10 @@ "integrity": "sha512-bna6Yi1pRznoo6Bz1cE6btB/Yy8Xywytyfrzu/wc+NFW3ZF0I+2iCGImhBsoYYCOWuICtRO4yHcnDlzgo1AdNg==" }, "typescript": { - "version": "3.9.7", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.7.tgz", - "integrity": "sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw==" + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.0.3.tgz", + "integrity": "sha512-tEu6DGxGgRJPb/mVPIZ48e69xCn2yRmCgYmDugAVwmJ6o+0u1RI18eO7E7WBTLYLaEVVOhwQmcdhQHweux/WPg==", + "dev": true }, "uncontrollable": { "version": "7.1.1", diff --git a/package.json b/package.json index 0bfa697..9943a0b 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,6 @@ "semver": "^7.3.2", "spu-md5": "0.0.4", "typesafe-actions": "^5.1.0", - "typescript": "^3.9.7", "websocket-as-promised": "^1.0.1" }, "scripts": { @@ -83,7 +82,8 @@ "eslint-plugin-tsdoc": "^0.2.7", "node-sass": "^4.14.1", "patch-package": "^6.2.2", - "prettier": "^2.1.1" + "prettier": "^2.1.1", + "typescript": "^4.0.3" }, "stylelint": { "extends": "stylelint-config-recommended", diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index f98fd40..42c354c 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -78,7 +78,10 @@ "columnBalance": "Balance", "columnNames": "Names", "columnCategory": "Category", - "columnFirstSeen": "First Seen" + "columnFirstSeen": "First Seen", + "nameCount": "{{count}} name", + "nameCount_plural": "{{count}} names", + "firstSeen": "First seen {{date}}" }, "credits": { diff --git a/src/layouts/my-wallets/MyWalletsMobileItem.tsx b/src/layouts/my-wallets/MyWalletsMobileItem.tsx new file mode 100644 index 0000000..9d6015a --- /dev/null +++ b/src/layouts/my-wallets/MyWalletsMobileItem.tsx @@ -0,0 +1,54 @@ +import React, { ReactNode } from "react"; + +import { useTranslation } from "react-i18next"; + +import { KristValue } from "@components/krist-value/KristValue"; + +// TODO: temporary +import { Wallet } from "./MyWalletsPage"; + +interface Props { + item: Wallet +}; + +export const Separator: React.FC = () => + + +export const MyWalletsMobileItem: React.FC = ({ item }: Props) => { + const { t } = useTranslation(); + + const formattedFirstSeen = item.firstSeen + ? new Date(item.firstSeen).toLocaleString() + : null; + + return <> +

+ + {item.label ?? item.address} +

+

+ {/* Show the address if it has a label, otherwise this is unnecessary */} + {item.label && <> + {item.address} + } + + {/* Show the category if set */} + {item.category && <> + {item.category} + } + + {/* Show the name count */} + + {t("myWallets.nameCount", { count: item.names })} + + + {/* Show the first seen date and time on a new line if set */} + {formattedFirstSeen && <> +
+ + {t("myWallets.firstSeen", { date: formattedFirstSeen })} + + } +

+ +} diff --git a/src/layouts/my-wallets/MyWalletsPage.tsx b/src/layouts/my-wallets/MyWalletsPage.tsx index 07a9bca..7177961 100644 --- a/src/layouts/my-wallets/MyWalletsPage.tsx +++ b/src/layouts/my-wallets/MyWalletsPage.tsx @@ -6,8 +6,6 @@ import { formatKristValue, formatDateTime, formatNumber } from "@components/list-view/Formatters"; import { ListView } from "@components/list-view/ListView"; -import { KristValue } from "@components/krist-value/KristValue"; - import { IconButton } from "@components/icon-button/IconButton"; import Button from "react-bootstrap/Button"; @@ -15,10 +13,12 @@ import { FilterSelect } from "@components/list-view/FilterSelect"; import { DateString } from "@krist/types/KristTypes"; +import { MyWalletsMobileItem } from "./MyWalletsMobileItem"; + import { sleep } from "@utils"; // TODO: Temporary -interface Wallet { +export interface Wallet { label?: string; address: string; balance: number; @@ -77,6 +77,7 @@ /> } columns={WALLET_COLUMNS} + renderMobileItem={(item: Wallet) => } dataProvider={async (query: QueryStateBase) => { // Provide the data to the list view // TODO: temporary diff --git a/src/scss/_theme.scss b/src/scss/_theme.scss index efef820..49514bb 100644 --- a/src/scss/_theme.scss +++ b/src/scss/_theme.scss @@ -18,9 +18,14 @@ /* Typography */ $body-color: #eaf0fe; $text-muted: #8991ab; +$text-quiet: mix($body-color, $text-muted, 50%); -$body-hover-color: mix($body-color, $text-muted, 50%); -$body-hover-color: mix($body-color, $text-muted, 50%); +$body-hover-color: $text-quiet; +$body-hover-color: $text-quiet; + +.text-quiet { + color: $text-quiet; +} $font-family-base: "Lato", sans-serif; @@ -67,6 +72,10 @@ } } +/* List group */ +$list-group-bg: transparent; +$list-group-border-color: $border-color; + /* Navbar */ $navbar-dark-color: $body-color; $navbar-dark-hover-color: $body-hover-color; diff --git a/src/shared-components/krist-value/KristValue.tsx b/src/shared-components/krist-value/KristValue.tsx index aa420f4..939b17e 100644 --- a/src/shared-components/krist-value/KristValue.tsx +++ b/src/shared-components/krist-value/KristValue.tsx @@ -2,13 +2,15 @@ import "./KristValue.scss"; -interface Props { +interface OwnProps { value: number; long?: boolean; }; -export const KristValue = ({ value, long }: Props): JSX.Element => ( - +type Props = React.HTMLProps & OwnProps; + +export const KristValue = ({ value, long, ...props }: Props): JSX.Element => ( + {value.toLocaleString()} {long && KST} diff --git a/src/shared-components/list-view/ListMobile.scss b/src/shared-components/list-view/ListMobile.scss new file mode 100644 index 0000000..a57e7e2 --- /dev/null +++ b/src/shared-components/list-view/ListMobile.scss @@ -0,0 +1,8 @@ +@import "~scss/variables"; + +.list-view .list-group { + .list-group-item { + padding-left: 0; + padding-right: 0; + } +} diff --git a/src/shared-components/list-view/ListMobile.tsx b/src/shared-components/list-view/ListMobile.tsx new file mode 100644 index 0000000..f3536f0 --- /dev/null +++ b/src/shared-components/list-view/ListMobile.tsx @@ -0,0 +1,34 @@ +import { KristValue } from "@components/krist-value/KristValue"; +import React, { Component, ReactNode } from "react"; + +import { ListGroup } from "react-bootstrap"; + +import { Columns, QueryStateBase, DataStateBase } from "./DataProvider"; + +import "./ListMobile.scss"; + +export type MobileItemRenderer = (item: T) => ReactNode; + +interface Props extends QueryStateBase, DataStateBase { + renderListItem: MobileItemRenderer; +} + +export class ListMobile extends Component> { + render(): ReactNode { + const { renderListItem, loading, data } = this.props; + + // Render skeleton items if the data is loading + if (loading || !data) return "loading"; /* TODO */ + + // TODO: handle potential edge case where loading = false, data = truthy + // TODO: handle errors + + // Otherwise, render the data + return + {data.map((item, i) => + + {renderListItem(item)} + )} + + } +} diff --git a/src/shared-components/list-view/ListTable.tsx b/src/shared-components/list-view/ListTable.tsx index 8b21349..df55a6e 100644 --- a/src/shared-components/list-view/ListTable.tsx +++ b/src/shared-components/list-view/ListTable.tsx @@ -37,9 +37,12 @@ {/* Table rows */} - {/* Render skeleton rows if the table is loading */} + {/* Render skeleton rows if the data is loading */} {loading && } + {/* TODO: handle potential edge case where loading = false, data = truthy */} + {/* TODO: handle errors */} + {/* Otherwise, render the data */} {!loading && data && data.map((row, i) => { title?: string; actions?: ReactNode; @@ -21,25 +24,44 @@ columns: Columns; dataProvider: DataProvider; + + renderMobileItem: MobileItemRenderer; } -interface State extends QueryStateBase, DataStateBase {} +interface State extends QueryStateBase, DataStateBase { + isMobile: boolean; +} export class ListView extends Component, State> { constructor(props: Props) { super(props); this.state = { - loading: true + loading: true, + isMobile: false }; } + // Arrow function to implicitly bind 'this' + checkDimensions = (): void => { + this.setState({ + isMobile: window.innerWidth < MOBILE_BREAKPOINT + }); + } + componentDidMount(): void { + window.addEventListener("resize", this.checkDimensions); + this.checkDimensions(); + this.loadData(); } - // Assign the new sort orderBy key and direction to the state and refresh the - // data immediately. + componentWillUnmount(): void { + window.addEventListener("resize", this.checkDimensions); + } + + /** Assign the new sort orderBy key and direction to the state and refresh the + * data immediately. */ setSort(orderBy?: ColumnKey, order?: SortDirection): void { this.setState({ orderBy, order, @@ -58,11 +80,29 @@ }); } + /** Render data based on whether or not this is on mobile */ + renderData(): ReactNode { + const { columns, renderMobileItem } = this.props; + const { isMobile } = this.state; + + if (isMobile) { // Mobile, show a list + return ; + } else { // Not mobile, show a table + return ; + } + } + render(): ReactNode { const { title, actions, filters, - page, pages, - columns + page, pages } = this.props; return @@ -92,18 +132,8 @@ } - {/* Main table */} - + {/* Render the data: list on mobile, table on desktop */} + {this.renderData()} ; } }