diff --git a/.vscode/launch.json b/.vscode/launch.json index 37ba873..217032a 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -7,6 +7,13 @@ "name": "Launch Chrome against localhost", "url": "http://localhost:3000", "webRoot": "${workspaceFolder}" + }, + { + "type": "firefox", + "request": "launch", + "name": "Launch Firefox against localhost", + "url": "http://localhost:3000", + "webRoot": "${workspaceFolder}" } ] } diff --git a/src/app/App.scss b/src/app/App.scss deleted file mode 100644 index e69de29..0000000 --- a/src/app/App.scss +++ /dev/null diff --git a/src/app/App.tsx b/src/app/App.tsx index 7d7828d..8f2674a 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -1,7 +1,6 @@ import React, { Suspense } from "react"; -import "./App.scss"; -import { MainLayout } from "../layouts/main"; +import { MainLayout } from "../layouts/main/MainLayout"; import { MasterPasswordDialog } from "@layouts/dialogs/MasterPasswordDialog"; diff --git a/src/index.scss b/src/index.scss index 4d0d85a..be2feef 100644 --- a/src/index.scss +++ b/src/index.scss @@ -1,4 +1,5 @@ /* All root CSS declarations */ @import "~scss/font"; @import "~scss/variables"; -@import "~bootstrap/scss/bootstrap"; \ No newline at end of file +@import "~bootstrap/scss/bootstrap"; +@import "~scss/modal"; diff --git a/src/layouts/credits/CreditsPage.tsx b/src/layouts/credits/CreditsPage.tsx new file mode 100644 index 0000000..fafdb68 --- /dev/null +++ b/src/layouts/credits/CreditsPage.tsx @@ -0,0 +1,47 @@ +import React, { Component, ReactNode } from "react"; + +import { Trans, withTranslation, WithTranslation } from "react-i18next"; + +import Container from "react-bootstrap/Container"; +import Row from "react-bootstrap/Row"; +import Col from "react-bootstrap/Col"; + +import { Supporters } from "./Supporters"; +import { Translators } from "./Translators"; + +import packageJson from "@/package.json"; + +class CreditsPageComponent extends Component { + render(): ReactNode { + const { t } = this.props; + + const authorName = packageJson.author || "Lemmmy"; + const authorURL = `https://github.com/${authorName}`; + + return + {/* Main section */} + + +

KristWeb v2

+

+ + Made by {{authorName}} + +

+ +
+ +
+ + {/* Supporters section */} + + +
+ + {/* Translators section */} + +
; + } +} + +export const CreditsPage = withTranslation()(CreditsPageComponent); diff --git a/src/layouts/credits/index.tsx b/src/layouts/credits/index.tsx deleted file mode 100644 index fafdb68..0000000 --- a/src/layouts/credits/index.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import React, { Component, ReactNode } from "react"; - -import { Trans, withTranslation, WithTranslation } from "react-i18next"; - -import Container from "react-bootstrap/Container"; -import Row from "react-bootstrap/Row"; -import Col from "react-bootstrap/Col"; - -import { Supporters } from "./Supporters"; -import { Translators } from "./Translators"; - -import packageJson from "@/package.json"; - -class CreditsPageComponent extends Component { - render(): ReactNode { - const { t } = this.props; - - const authorName = packageJson.author || "Lemmmy"; - const authorURL = `https://github.com/${authorName}`; - - return - {/* Main section */} - - -

KristWeb v2

-

- - Made by {{authorName}} - -

- -
- -
- - {/* Supporters section */} - - -
- - {/* Translators section */} - -
; - } -} - -export const CreditsPage = withTranslation()(CreditsPageComponent); diff --git a/src/layouts/main/MainLayout.scss b/src/layouts/main/MainLayout.scss new file mode 100644 index 0000000..fc59fa7 --- /dev/null +++ b/src/layouts/main/MainLayout.scss @@ -0,0 +1,23 @@ +@import "~scss/variables"; + +// Fix main container height overflow issues +html, body { + height: 100%; +} + +#root { + height: 100%; +} + +#main-container { + padding-top: $navbar-height; + + overflow: scroll; + + height: calc(100% - #{$navbar-height}); + + // Fix some strange wrapping issues on small screens + &>.row { + flex-wrap: nowrap; + } +} diff --git a/src/layouts/main/MainLayout.tsx b/src/layouts/main/MainLayout.tsx new file mode 100644 index 0000000..ce81b12 --- /dev/null +++ b/src/layouts/main/MainLayout.tsx @@ -0,0 +1,65 @@ +import React, { Component, ReactNode } from "react"; + +import { BrowserRouter as Router, Switch, Route } from "react-router-dom"; + +import Container from "react-bootstrap/Container"; +import Row from "react-bootstrap/Row"; +import Col from "react-bootstrap/Col"; + +import { MainNav } from "./components/nav/MainNav"; +import { MainSidebar } from "./components/sidebar/MainSidebar"; + +import { MyWalletsPage } from "@layouts/my-wallets/MyWalletsPage"; +import { CreditsPage } from "@layouts/credits/CreditsPage"; + +import "./MainLayout.scss"; + +interface State { + sidebarCollapsed: boolean; +} + +export class MainLayout extends Component { + constructor(props: unknown) { + super(props); + + this.state = { + sidebarCollapsed: true // Collapse by default on mobile + }; + } + + render(): ReactNode { + const { sidebarCollapsed } = this.state; + + return ( + + {/* Top nav bar */} + this.setState({ + sidebarCollapsed: !sidebarCollapsed + })} /> + + {/* Main container */} + + + {/* Left sidebar */} + + + {/* Page container */} + + + + {/* Home */} + + + + + + + + + + + + + ); + } +} diff --git a/src/layouts/main/components/nav/Brand.scss b/src/layouts/main/components/nav/Brand.scss index fb13f24..42d8cc0 100644 --- a/src/layouts/main/components/nav/Brand.scss +++ b/src/layouts/main/components/nav/Brand.scss @@ -1,7 +1,7 @@ @import "~scss/variables"; .navbar-brand { - /* Make the left half of the nav equal to the sidebar width */ + // Make the left half of the nav equal to the sidebar width width: $main-sidebar-width; padding: 0; margin: 0; @@ -10,8 +10,16 @@ align-items: center; justify-content: center; + // Make it full height + align-self: stretch; + + // Force the search bar to shrink instead + flex-shrink: 0; + border-right: 1px solid $border-color-darker; + user-select: none; + a { color: $navbar-dark-brand-color; text-align: center; diff --git a/src/layouts/main/components/nav/ConnectionIndicator.scss b/src/layouts/main/components/nav/ConnectionIndicator.scss index 787a0b0..a1ae976 100644 --- a/src/layouts/main/components/nav/ConnectionIndicator.scss +++ b/src/layouts/main/components/nav/ConnectionIndicator.scss @@ -26,4 +26,9 @@ background-color: $success; box-shadow: 0 0 0 3px rgba($success, 0.3); } -} \ No newline at end of file + + // Hide on mobile + @include media-breakpoint-down(md) { + display: none; + } +} diff --git a/src/layouts/main/components/nav/ConnectionIndicator.tsx b/src/layouts/main/components/nav/ConnectionIndicator.tsx index 25deef8..032f891 100644 --- a/src/layouts/main/components/nav/ConnectionIndicator.tsx +++ b/src/layouts/main/components/nav/ConnectionIndicator.tsx @@ -8,8 +8,10 @@ const { t } = useTranslation(); return ( -
- {t("nav.connection.online")} -
+
); }; diff --git a/src/layouts/main/components/nav/MainNav.scss b/src/layouts/main/components/nav/MainNav.scss new file mode 100644 index 0000000..e821210 --- /dev/null +++ b/src/layouts/main/components/nav/MainNav.scss @@ -0,0 +1,50 @@ +@import "~scss/variables"; + +#main-nav { + width: 100%; + height: $navbar-height; + + padding: 0; + align-items: stretch; + + box-shadow: $box-shadow; + + border-bottom: 1px solid $border-color-darker; + + align-items: center; + + .navbar-collapse { + margin: 0 1rem 0 0; + } + + .navbar-nav { + align-items: center; + + .nav-link { + user-select: none; + + border-right: 1px solid $border-color-darker; + padding: 0 1.5rem; + + // Don't word wrap + white-space: nowrap; + + .nav-icon { + margin-right: 0.5em; + } + + &:last-child { + border-right: none; + } + + @include media-breakpoint-down(md) { + padding: 0 1rem; + } + } + + // Hide the links on mobile, they'll be shown in the sidebar instead + @include media-breakpoint-down(sm) { + display: none; + } + } +} diff --git a/src/layouts/main/components/nav/MainNav.tsx b/src/layouts/main/components/nav/MainNav.tsx new file mode 100644 index 0000000..ae00315 --- /dev/null +++ b/src/layouts/main/components/nav/MainNav.tsx @@ -0,0 +1,70 @@ +import React from "react"; + +import { useTranslation } from "react-i18next"; + +import Navbar from "react-bootstrap/Navbar"; +import Nav from "react-bootstrap/Nav"; +import { LinkContainer } from "react-router-bootstrap"; + +import { Brand } from "./Brand"; +import { Search } from "./Search"; +import { ConnectionIndicator } from "./ConnectionIndicator"; +import { SettingsCog } from "./SettingsCog"; + +import { connect } from "react-redux"; +import { RootState } from "@store"; + +import "./MainNav.scss"; +import { SidebarCollapseButton } from "../sidebar/SidebarCollapseButton"; + +interface OwnProps { + onCollapseSidebar: () => void; +} + +interface StateProps { + isGuest: boolean; +} + +const mapStateToProps = (state: RootState): StateProps => ({ + isGuest: state.walletManager.isGuest +}); + +type Props = StateProps & OwnProps; + +const MainNavComponent: React.FC = ({ isGuest, onCollapseSidebar }: Props): JSX.Element => { + const { t } = useTranslation(); + + return ( + + + + + + {/* Main nav buttons, only show if logged in */} + {!isGuest && } + + + + + + + ); +}; + +export const MainNav = connect(mapStateToProps)(MainNavComponent); diff --git a/src/layouts/main/components/nav/Search.scss b/src/layouts/main/components/nav/Search.scss index d6d51af..86005bd 100644 --- a/src/layouts/main/components/nav/Search.scss +++ b/src/layouts/main/components/nav/Search.scss @@ -3,13 +3,17 @@ /* Center the search box */ #main-nav-search.form-inline { margin: 0 auto; + padding: 0 1rem; + width: 100%; input.form-control[type=text] { border: none; - width: 30vw; + width: 100%; max-width: 400px; + margin: 0 auto; + /* All the tedious recolouring nonsense */ background: $navbar-search-bg; color: $navbar-search-color; @@ -42,5 +46,10 @@ color: $navbar-search-focus-placeholder-color; } } + + // Make the search box full width on mobile + @include media-breakpoint-down(sm) { + max-width: 100%; + } } } diff --git a/src/layouts/main/components/nav/index.scss b/src/layouts/main/components/nav/index.scss deleted file mode 100644 index 00409d0..0000000 --- a/src/layouts/main/components/nav/index.scss +++ /dev/null @@ -1,31 +0,0 @@ -@import "~scss/variables"; - -#main-nav { - height: $navbar-height; - - padding: 0; - align-items: stretch; - - box-shadow: $box-shadow; - - border-bottom: 1px solid $border-color-darker; - - .navbar-collapse { - margin: 0 1rem 0 0; - } - - .navbar-nav { - .nav-link { - border-right: 1px solid $border-color-darker; - padding: 0 1.5rem; - - .nav-icon { - margin-right: 0.5em; - } - - &:last-child { - border-right: none; - } - } - } -} diff --git a/src/layouts/main/components/nav/index.tsx b/src/layouts/main/components/nav/index.tsx deleted file mode 100644 index b027554..0000000 --- a/src/layouts/main/components/nav/index.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import React from "react"; - -import { useTranslation } from "react-i18next"; - -import Navbar from "react-bootstrap/Navbar"; -import Nav from "react-bootstrap/Nav"; -import { LinkContainer } from "react-router-bootstrap"; - -import { Brand } from "./Brand"; -import { Search } from "./Search"; -import { ConnectionIndicator } from "./ConnectionIndicator"; -import { SettingsCog } from "./SettingsCog"; - -import { connect } from "react-redux"; -import { RootState } from "@store"; - -import "./index.scss"; - -interface Props { - isGuest: boolean; -} - -const mapStateToProps = (state: RootState): Props => ({ - isGuest: state.walletManager.isGuest -}); - -const MainNavComponent: React.FC = ({ isGuest }: Props): JSX.Element => { - const { t } = useTranslation(); - - return ( - - - - - {/* Main nav buttons, only show if logged in */} - {!isGuest && } - - - - - - ); -}; - -export const MainNav = connect(mapStateToProps)(MainNavComponent); diff --git a/src/layouts/main/components/sidebar/Footer.scss b/src/layouts/main/components/sidebar/Footer.scss index a5d5757..59bb15f 100644 --- a/src/layouts/main/components/sidebar/Footer.scss +++ b/src/layouts/main/components/sidebar/Footer.scss @@ -1,6 +1,8 @@ @import "~scss/variables"; #main-sidebar .sidebar-footer { + user-select: none; + padding: 0.5rem; text-align: center; @@ -11,4 +13,4 @@ a { color: $body-color; } -} \ No newline at end of file +} diff --git a/src/layouts/main/components/sidebar/MainSidebar.scss b/src/layouts/main/components/sidebar/MainSidebar.scss new file mode 100644 index 0000000..26c809a --- /dev/null +++ b/src/layouts/main/components/sidebar/MainSidebar.scss @@ -0,0 +1,98 @@ +@import "~scss/variables"; + +/* Sidebar collapsing nonsense */ +#main-sidebar-container { + transition: margin-right $main-sidebar-collapse-duration ease; + margin-right: $main-sidebar-width; // Push the rest of the page to the right + + // Above the backdrop, below Bootstrap modals + z-index: 910; + + #main-sidebar { + transition: left $main-sidebar-collapse-duration ease; + left: 0; // Default: open + } + + @include media-breakpoint-down(sm) { + // On mobile, make the sidebar overlap the content instead of moving it + margin-right: 0 !important; + + // Support collapsing only on mobile + &.closed { + #main-sidebar { + left: -$main-sidebar-width; + } + } + } +} + +/* Actual sidebar styling */ +#main-sidebar { + width: $main-sidebar-width; + + position: fixed; + top: $navbar-height; + bottom: 0; + left: 0; + + display: flex; + flex-direction: column; + flex-wrap: nowrap; + + border-right: 1px solid $border-color-darker; + background: $main-sidebar-bg; + + .sidebar-content { + display: flex; + flex-direction: column; + flex-wrap: nowrap; + + overflow-y: auto; + + flex: 1; + } + + /* Separator headers */ + h6 { + font-size: 0.8em; + font-weight: bold; + text-transform: uppercase; + + color: $text-muted; + + // margin: 1rem 0 0 0; + margin: 0; + padding: 1rem 1rem 0.5rem 1rem; + + border-top: 1px solid $border-color; + + user-select: none; + } +} + +/* Backdrop for mobile */ +#main-sidebar-backdrop { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + + transition: opacity $main-sidebar-collapse-duration ease; + + background: $main-sidebar-backdrop-bg; + opacity: 0; + + // Below the sidebar and Bootstrap modals + z-index: 900; + + // Only show on mobile + display: none; + @include media-breakpoint-down(sm) { + display: block; + + &.show { + opacity: $main-sidebar-backdrop-opacity; + } + } +} diff --git a/src/layouts/main/components/sidebar/MainSidebar.tsx b/src/layouts/main/components/sidebar/MainSidebar.tsx new file mode 100644 index 0000000..89bca11 --- /dev/null +++ b/src/layouts/main/components/sidebar/MainSidebar.tsx @@ -0,0 +1,73 @@ +import React from "react"; + +import { useTranslation } from "react-i18next"; + +import Nav from "react-bootstrap/Nav"; + +import { GuestIndicator } from "./GuestIndicator"; +import { TotalBalance } from "./TotalBalance"; +import { SidebarItem } from "./SidebarItem"; +import { Footer } from "./Footer"; + +import { connect } from "react-redux"; +import { RootState } from "@store"; + +import "./MainSidebar.scss"; + +interface OwnProps { + isCollapsed: boolean; +} + +interface StateProps { + isGuest: boolean; +} + +const mapStateToProps = (state: RootState): StateProps => ({ + isGuest: state.walletManager.isGuest +}); + +type Props = StateProps & OwnProps; + +const MainSidebarComponent: React.FC = ({ isGuest, isCollapsed }: Props): JSX.Element => { + const { t } = useTranslation(); + + return ( + <> +
+ +
+ + {/* Fade the rest of the app out on mobile when the sidebar is open */} +
+ + ); +}; + +export const MainSidebar = connect(mapStateToProps)(MainSidebarComponent); diff --git a/src/layouts/main/components/sidebar/SidebarCollapseButton.scss b/src/layouts/main/components/sidebar/SidebarCollapseButton.scss new file mode 100644 index 0000000..671e27f --- /dev/null +++ b/src/layouts/main/components/sidebar/SidebarCollapseButton.scss @@ -0,0 +1,29 @@ +@import "~scss/variables"; + +#main-nav .navbar-toggler { + border: none; + border-right: 1px solid $border-color; + border-radius: 0; + + // Make it full height + align-self: stretch; + + &:hover { + background: $slighter; + } + + &:active { + background: $lighter; + } + + &:focus { + outline: none; + box-shadow: $input-btn-focus-box-shadow; + } + + // Hide unless mobile + display: none !important; + @include media-breakpoint-down(sm) { + display: block !important; + } +} diff --git a/src/layouts/main/components/sidebar/SidebarCollapseButton.tsx b/src/layouts/main/components/sidebar/SidebarCollapseButton.tsx new file mode 100644 index 0000000..cc9a848 --- /dev/null +++ b/src/layouts/main/components/sidebar/SidebarCollapseButton.tsx @@ -0,0 +1,17 @@ +import React from "react"; + +import Navbar from "react-bootstrap/Navbar"; + +import "./SidebarCollapseButton.scss"; + +interface Props { + onCollapseSidebar: () => void; +} + +export const SidebarCollapseButton: React.FC = ({ onCollapseSidebar }: Props): JSX.Element => { + return ( + + ); +}; diff --git a/src/layouts/main/components/sidebar/SidebarItem.scss b/src/layouts/main/components/sidebar/SidebarItem.scss index fb5bbba..cfc1008 100644 --- a/src/layouts/main/components/sidebar/SidebarItem.scss +++ b/src/layouts/main/components/sidebar/SidebarItem.scss @@ -1,6 +1,8 @@ @import "~scss/variables"; #main-sidebar .nav-item { + user-select: none; + .nav-link { color: $main-sidebar-color; diff --git a/src/layouts/main/components/sidebar/TotalBalance.tsx b/src/layouts/main/components/sidebar/TotalBalance.tsx index fd3050f..7c968ba 100644 --- a/src/layouts/main/components/sidebar/TotalBalance.tsx +++ b/src/layouts/main/components/sidebar/TotalBalance.tsx @@ -2,7 +2,7 @@ import { useTranslation } from "react-i18next"; -import { KristValue } from "@components/krist-value"; +import { KristValue } from "@components/krist-value/KristValue"; import "./TotalBalance.scss"; diff --git a/src/layouts/main/components/sidebar/index.scss b/src/layouts/main/components/sidebar/index.scss deleted file mode 100644 index adf0a46..0000000 --- a/src/layouts/main/components/sidebar/index.scss +++ /dev/null @@ -1,42 +0,0 @@ -@import "~scss/variables"; - -#main-sidebar { - width: $main-sidebar-width; - - position: fixed; - top: $navbar-height; - bottom: 0; - left: 0; - - display: flex; - flex-direction: column; - flex-wrap: nowrap; - - border-right: 1px solid $border-color-darker; - background: $main-sidebar-bg; - - .sidebar-content { - display: flex; - flex-direction: column; - flex-wrap: nowrap; - - overflow-y: auto; - - flex: 1; - } - - /* Separator headers */ - h6 { - font-size: 0.8em; - font-weight: bold; - text-transform: uppercase; - - color: $text-muted; - - // margin: 1rem 0 0 0; - margin: 0; - padding: 1rem 1rem 0.5rem 1rem; - - border-top: 1px solid $border-color; - } -} diff --git a/src/layouts/main/components/sidebar/index.tsx b/src/layouts/main/components/sidebar/index.tsx deleted file mode 100644 index 3d3eae6..0000000 --- a/src/layouts/main/components/sidebar/index.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import React from "react"; - -import { useTranslation } from "react-i18next"; - -import Nav from "react-bootstrap/Nav"; - -import { GuestIndicator } from "./GuestIndicator"; -import { TotalBalance } from "./TotalBalance"; -import { SidebarItem } from "./SidebarItem"; -import { Footer } from "./Footer"; - -import { connect } from "react-redux"; -import { RootState } from "@store"; - -import "./index.scss"; - -interface Props { - isGuest: boolean; -} - -const mapStateToProps = (state: RootState): Props => ({ - isGuest: state.walletManager.isGuest -}); - -const MainSidebarComponent: React.FC = ({ isGuest }: Props): JSX.Element => { - const { t } = useTranslation(); - - return ( - - ); -}; - -export const MainSidebar = connect(mapStateToProps)(MainSidebarComponent); diff --git a/src/layouts/main/index.scss b/src/layouts/main/index.scss deleted file mode 100644 index ae9dddf..0000000 --- a/src/layouts/main/index.scss +++ /dev/null @@ -1,9 +0,0 @@ -@import "~scss/variables"; - -#main-container { - margin-top: $navbar-height; - - #page-container { - margin-left: $main-sidebar-width; - } -} \ No newline at end of file diff --git a/src/layouts/main/index.tsx b/src/layouts/main/index.tsx deleted file mode 100644 index 4625eb2..0000000 --- a/src/layouts/main/index.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import React from "react"; - -import { BrowserRouter as Router, Switch, Route } from "react-router-dom"; - -import Container from "react-bootstrap/Container"; -import Row from "react-bootstrap/Row"; -import Col from "react-bootstrap/Col"; - -import { MainNav } from "./components/nav"; -import { MainSidebar } from "./components/sidebar"; - -import { MyWalletsPage } from "@layouts/my-wallets"; -import { CreditsPage } from "@layouts/credits"; - -import "./index.scss"; - -export const MainLayout: React.FC = () => ( - - - - - - - - - {/* Home */} - - - - - - - - - - - - -); diff --git a/src/layouts/my-wallets/MyWalletsPage.tsx b/src/layouts/my-wallets/MyWalletsPage.tsx new file mode 100644 index 0000000..b5b042f --- /dev/null +++ b/src/layouts/my-wallets/MyWalletsPage.tsx @@ -0,0 +1,69 @@ +import React, { Component, ReactNode } from "react"; + +import { withTranslation, WithTranslation } from "react-i18next"; + +import { ListView, HeaderSpec } from "@components/list-view/ListView"; + +import { IconButton } from "@components/icon-button/IconButton"; +import Button from "react-bootstrap/Button"; + +import { SearchTextbox } from "@components/list-view/SearchTextbox"; +import { FilterSelect } from "@components/list-view/FilterSelect"; +import { DateString } from "@krist/types/KristTypes"; + +// TODO: Temporary +interface Wallet { + label?: string; + address: string; + balance: number; + names: number; + category?: string; + firstSeen?: DateString; +} + +const WALLET_HEADERS = new Map, HeaderSpec>() + .set("label", { nameKey: "myWallets.columnLabel" }) + .set("address", { nameKey: "myWallets.columnAddress" }) + .set("balance", { nameKey: "myWallets.columnBalance" }) + .set("names", { nameKey: "myWallets.columnNames" }) + .set("category", { nameKey: "myWallets.columnCategory" }) + .set("firstSeen", { nameKey: "myWallets.columnFirstSeen" }); + +class MyWalletsPageComponent extends Component { + render(): ReactNode { + const { t } = this.props; + + return + title="23 wallets" + page={1} + pages={3} + actions={<> + + {t("myWallets.manageBackups")} + + + {t("myWallets.createWallet")} + + + } + filters={<> + {/* Search filter textbox */} + + + {/* Category selection box */} + + } + headers={WALLET_HEADERS} + />; + } +} + +export const MyWalletsPage = withTranslation()(MyWalletsPageComponent); diff --git a/src/layouts/my-wallets/index.tsx b/src/layouts/my-wallets/index.tsx deleted file mode 100644 index ae16d82..0000000 --- a/src/layouts/my-wallets/index.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import React, { Component, ReactNode } from "react"; - -import { withTranslation, WithTranslation } from "react-i18next"; - -import { ListView, HeaderSpec } from "@components/list-view"; - -import { IconButton } from "@components/icon-button"; -import Button from "react-bootstrap/Button"; - -import { SearchTextbox } from "@components/list-view/SearchTextbox"; -import { FilterSelect } from "@components/list-view/FilterSelect"; -import { DateString } from "@krist/types/KristTypes"; - -// TODO: Temporary -interface Wallet { - label?: string; - address: string; - balance: number; - names: number; - category?: string; - firstSeen?: DateString; -} - -const WALLET_HEADERS = new Map, HeaderSpec>() - .set("label", { nameKey: "myWallets.columnLabel" }) - .set("address", { nameKey: "myWallets.columnAddress" }) - .set("balance", { nameKey: "myWallets.columnBalance" }) - .set("names", { nameKey: "myWallets.columnNames" }) - .set("category", { nameKey: "myWallets.columnCategory" }) - .set("firstSeen", { nameKey: "myWallets.columnFirstSeen" }); - -class MyWalletsPageComponent extends Component { - render(): ReactNode { - const { t } = this.props; - - return - title="23 wallets" - page={1} - pages={3} - actions={<> - - {t("myWallets.manageBackups")} - - - {t("myWallets.createWallet")} - - - } - filters={<> - {/* Search filter textbox */} - - - {/* Category selection box */} - - } - headers={WALLET_HEADERS} - />; - } -} - -export const MyWalletsPage = withTranslation()(MyWalletsPageComponent); diff --git a/src/scss/_modal.scss b/src/scss/_modal.scss new file mode 100644 index 0000000..7aa5738 --- /dev/null +++ b/src/scss/_modal.scss @@ -0,0 +1,20 @@ +@import "~scss/variables"; + +.modal-header { + /* Close button isn't vertically aligned in modal header by default. */ + align-items: center; +} + +.modal-footer { + /* Display the buttons in a column on mobile */ + @include media-breakpoint-down(sm) { + display: flex; + flex-direction: column; + align-items: initial; + + /* Force buttons with `margin-right: auto` to not */ + .btn { + margin-right: 0 !important; + } + } +} diff --git a/src/scss/_theme.scss b/src/scss/_theme.scss index ad55ab4..efef820 100644 --- a/src/scss/_theme.scss +++ b/src/scss/_theme.scss @@ -83,23 +83,23 @@ $close-color: $text-muted; $close-text-shadow: none; -.modal-header { - /* Close button isn't vertically aligned in modal header by default. */ - align-items: center; -} - /* Custom scrollbar */ +$scrollbar-width: 8px; +$scrollbar-thumb-color: $secondary; +$scrollbar-track-color: rgba(0, 0, 0, 0.1); + +/* WebKit */ ::-webkit-scrollbar { - width: 8px; + width: $scrollbar-width; } ::-webkit-scrollbar-track { - background: rgba(0, 0, 0, 0.1); + background: $scrollbar-track-color; border-radius: $border-radius; } ::-webkit-scrollbar-thumb { - background: $secondary; + background: $scrollbar-thumb-color; border-radius: $border-radius; } @@ -107,3 +107,8 @@ background: $slighter; } +/* Mozilla */ +:root { + scrollbar-color: $scrollbar-thumb-color $scrollbar-track-color !important; + scrollbar-width: thin !important; +} diff --git a/src/scss/_variables.scss b/src/scss/_variables.scss index 7be475b..00bef53 100644 --- a/src/scss/_variables.scss +++ b/src/scss/_variables.scss @@ -32,6 +32,9 @@ $main-sidebar-hover-bg: mix($darker, $darkest, 50%); $main-sidebar-top-bg: $darkest; $main-sidebar-top-hover-bg: $main-sidebar-hover-bg; +$main-sidebar-collapse-duration: 350ms; +$main-sidebar-backdrop-bg: $modal-backdrop-bg; +$main-sidebar-backdrop-opacity: $modal-backdrop-opacity; /* -------------------------------------------------------------------------- */ /* MISC */ diff --git a/src/shared-components/icon-button/IconButton.scss b/src/shared-components/icon-button/IconButton.scss new file mode 100644 index 0000000..3b8e16c --- /dev/null +++ b/src/shared-components/icon-button/IconButton.scss @@ -0,0 +1,13 @@ +@import "~scss/variables"; + +.btn .btn-icon { + margin-right: $btn-padding-x; +} + +.btn-sm .btn-icon { + margin-right: $btn-padding-x-sm; +} + +.btn-lg .btn-icon { + margin-right: $btn-padding-x-lg; +} diff --git a/src/shared-components/icon-button/IconButton.tsx b/src/shared-components/icon-button/IconButton.tsx new file mode 100644 index 0000000..0864262 --- /dev/null +++ b/src/shared-components/icon-button/IconButton.tsx @@ -0,0 +1,16 @@ +import React, { PropsWithChildren } from "react"; + +import Button, { ButtonProps } from "react-bootstrap/Button"; + +import "./IconButton.scss"; + +interface Props extends ButtonProps { + icon: string; +} + +export const IconButton: React.FC = (props: PropsWithChildren) => { + return ; +}; diff --git a/src/shared-components/icon-button/index.scss b/src/shared-components/icon-button/index.scss deleted file mode 100644 index 3b8e16c..0000000 --- a/src/shared-components/icon-button/index.scss +++ /dev/null @@ -1,13 +0,0 @@ -@import "~scss/variables"; - -.btn .btn-icon { - margin-right: $btn-padding-x; -} - -.btn-sm .btn-icon { - margin-right: $btn-padding-x-sm; -} - -.btn-lg .btn-icon { - margin-right: $btn-padding-x-lg; -} diff --git a/src/shared-components/icon-button/index.tsx b/src/shared-components/icon-button/index.tsx deleted file mode 100644 index 09c51ee..0000000 --- a/src/shared-components/icon-button/index.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import React, { PropsWithChildren } from "react"; - -import Button, { ButtonProps } from "react-bootstrap/Button"; - -import "./index.scss"; - -interface Props extends ButtonProps { - icon: string; -} - -export const IconButton: React.FC = (props: PropsWithChildren) => { - return ; -}; diff --git a/src/shared-components/krist-value/KristValue.scss b/src/shared-components/krist-value/KristValue.scss new file mode 100644 index 0000000..d27eb84 --- /dev/null +++ b/src/shared-components/krist-value/KristValue.scss @@ -0,0 +1,26 @@ +@import "~scss/variables"; + +.krist-value { + font-size: 100%; + + .icon-krist { + /* Hack to make it consistent with Lato */ + position: relative; + bottom: 0.15em; + margin: 0 -0.2em 0 -0.2em; + font-size: 0.7em; + color: $krist-value-alt-color; + } + + .krist-value-amount { + font-weight: bold; + } + + .krist-currency-long { + color: $krist-value-alt-color; + + &::before { + content: " "; + } + } +} \ No newline at end of file diff --git a/src/shared-components/krist-value/KristValue.tsx b/src/shared-components/krist-value/KristValue.tsx new file mode 100644 index 0000000..aa420f4 --- /dev/null +++ b/src/shared-components/krist-value/KristValue.tsx @@ -0,0 +1,16 @@ +import React from "react"; + +import "./KristValue.scss"; + +interface Props { + value: number; + long?: boolean; +}; + +export const KristValue = ({ value, long }: Props): JSX.Element => ( + + + {value.toLocaleString()} + {long && KST} + +); diff --git a/src/shared-components/krist-value/index.scss b/src/shared-components/krist-value/index.scss deleted file mode 100644 index d27eb84..0000000 --- a/src/shared-components/krist-value/index.scss +++ /dev/null @@ -1,26 +0,0 @@ -@import "~scss/variables"; - -.krist-value { - font-size: 100%; - - .icon-krist { - /* Hack to make it consistent with Lato */ - position: relative; - bottom: 0.15em; - margin: 0 -0.2em 0 -0.2em; - font-size: 0.7em; - color: $krist-value-alt-color; - } - - .krist-value-amount { - font-weight: bold; - } - - .krist-currency-long { - color: $krist-value-alt-color; - - &::before { - content: " "; - } - } -} \ No newline at end of file diff --git a/src/shared-components/krist-value/index.tsx b/src/shared-components/krist-value/index.tsx deleted file mode 100644 index a3e9699..0000000 --- a/src/shared-components/krist-value/index.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import React from "react"; - -import "./index.scss"; - -interface Props { - value: number; - long?: boolean; -}; - -export const KristValue = ({ value, long }: Props): JSX.Element => ( - - - {value.toLocaleString()} - {long && KST} - -); diff --git a/src/shared-components/list-view/ColumnHeader.tsx b/src/shared-components/list-view/ColumnHeader.tsx index 0fd2bee..59eb4d9 100644 --- a/src/shared-components/list-view/ColumnHeader.tsx +++ b/src/shared-components/list-view/ColumnHeader.tsx @@ -2,7 +2,7 @@ import { Translation } from "react-i18next"; -import { HeaderSpec, SortDirection } from "."; +import { HeaderSpec, SortDirection } from "./ListView"; import "./ColumnHeader.scss"; diff --git a/src/shared-components/list-view/ListTable.tsx b/src/shared-components/list-view/ListTable.tsx index 28f1994..56113e8 100644 --- a/src/shared-components/list-view/ListTable.tsx +++ b/src/shared-components/list-view/ListTable.tsx @@ -1,10 +1,10 @@ import React, { Component, ReactNode } from "react"; -import { HeaderSpec, SortDirection } from "."; +import { HeaderSpec, SortDirection } from "./ListView"; import Table from "react-bootstrap/Table"; import { ColumnHeader } from "./ColumnHeader"; -import { KristValue } from "@components/krist-value"; +import { KristValue } from "@components/krist-value/KristValue"; import "./ListTable.scss"; import { SkeletonText } from "@components/skeleton/SkeletonText"; diff --git a/src/shared-components/list-view/ListView.scss b/src/shared-components/list-view/ListView.scss new file mode 100644 index 0000000..4660662 --- /dev/null +++ b/src/shared-components/list-view/ListView.scss @@ -0,0 +1,15 @@ +.list-view { + .list-view-actions, .list-view-filters { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + + .btn, .form-control, .custom-select { + margin-right: 0.5rem; + + &:last-child { + margin-right: 0; + } + } + } +} diff --git a/src/shared-components/list-view/ListView.tsx b/src/shared-components/list-view/ListView.tsx new file mode 100644 index 0000000..c7511e0 --- /dev/null +++ b/src/shared-components/list-view/ListView.tsx @@ -0,0 +1,74 @@ +import React, { Component, ReactNode } from "react"; + +import Container from "react-bootstrap/Container"; +import Row from "react-bootstrap/Row"; +import Col from "react-bootstrap/Col"; + +import { ListPagination } from "./ListPagination"; +import { ListTable } from "./ListTable"; + +import "./ListView.scss"; + +export enum SortDirection { + ASC, DESC +} + +export interface HeaderSpec { + /** i18n key for column/header name */ + nameKey: string; + sortable?: boolean; + + skeletonEmWidth?: number; +} + +interface Props { + title?: string; + actions?: ReactNode; + filters?: ReactNode; + + page?: number; + pages?: number; + + headers?: Map, HeaderSpec>; +} + +export class ListView extends Component> { + render(): ReactNode { + const { + title, actions, filters, + page, pages, + headers + } = this.props; + + return + {/* Main header row - wallet count and action buttons */} + + + {/* List title */} + {title &&

{title}

} + + {/* Optional action button row */} + {actions &&
{actions}
} + +
+ + {/* Search, filter and pagination row */} + + {/* List filters (e.g. search, category dropdown) */} + {filters && + + {filters} + } + + {/* Pagination */} + {page && pages && + + + } + + + {/* Main table */} + +
; + } +} diff --git a/src/shared-components/list-view/index.scss b/src/shared-components/list-view/index.scss deleted file mode 100644 index 4660662..0000000 --- a/src/shared-components/list-view/index.scss +++ /dev/null @@ -1,15 +0,0 @@ -.list-view { - .list-view-actions, .list-view-filters { - display: flex; - flex-direction: row; - flex-wrap: nowrap; - - .btn, .form-control, .custom-select { - margin-right: 0.5rem; - - &:last-child { - margin-right: 0; - } - } - } -} diff --git a/src/shared-components/list-view/index.tsx b/src/shared-components/list-view/index.tsx deleted file mode 100644 index 744429b..0000000 --- a/src/shared-components/list-view/index.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import React, { Component, ReactNode } from "react"; - -import Container from "react-bootstrap/Container"; -import Row from "react-bootstrap/Row"; -import Col from "react-bootstrap/Col"; - -import { ListPagination } from "./ListPagination"; - -import "./index.scss"; -import { ListTable } from "./ListTable"; - -export enum SortDirection { - ASC, DESC -} - -export interface HeaderSpec { - /** i18n key for column/header name */ - nameKey: string; - sortable?: boolean; - - skeletonEmWidth?: number; -} - -interface Props { - title?: string; - actions?: ReactNode; - filters?: ReactNode; - - page?: number; - pages?: number; - - headers?: Map, HeaderSpec>; -} - -export class ListView extends Component> { - render(): ReactNode { - const { - title, actions, filters, - page, pages, - headers - } = this.props; - - return - {/* Main header row - wallet count and action buttons */} - - - {/* List title */} - {title &&

{title}

} - - {/* Optional action button row */} - {actions &&
{actions}
} - -
- - {/* Search, filter and pagination row */} - - {/* List filters (e.g. search, category dropdown) */} - {filters && - - {filters} - } - - {/* Pagination */} - {page && pages && - - - } - - - {/* Main table */} - -
; - } -} diff --git a/src/shared-components/skeleton/Skeleton.scss b/src/shared-components/skeleton/Skeleton.scss new file mode 100644 index 0000000..420d4ea --- /dev/null +++ b/src/shared-components/skeleton/Skeleton.scss @@ -0,0 +1,32 @@ +@import "~scss/variables"; + +@keyframes skeleton-pulse { + 0% { + opacity: 1; + } + + 50% { + opacity: $skeleton-pulse-opacity; + } + + 100% { + opacity: 1; + } +} + +.skeleton { + background-color: $skeleton-color; + animation: skeleton-pulse $skeleton-pulse-duration ease-in-out $skeleton-pulse-delay infinite; + + &.skeleton-text { + // Make it take up the space of a normal line + display: inline-block; + line-height: 1; + + width: 100%; + max-width: $skeleton-max-width; + height: $skeleton-height; + + border-radius: $skeleton-height; + } +} diff --git a/src/shared-components/skeleton/SkeletonText.tsx b/src/shared-components/skeleton/SkeletonText.tsx index c42266c..2a3fde2 100644 --- a/src/shared-components/skeleton/SkeletonText.tsx +++ b/src/shared-components/skeleton/SkeletonText.tsx @@ -1,6 +1,6 @@ import React from "react"; -import "./index.scss"; +import "./Skeleton.scss"; interface Props { emWidth?: number; diff --git a/src/shared-components/skeleton/index.scss b/src/shared-components/skeleton/index.scss deleted file mode 100644 index 420d4ea..0000000 --- a/src/shared-components/skeleton/index.scss +++ /dev/null @@ -1,32 +0,0 @@ -@import "~scss/variables"; - -@keyframes skeleton-pulse { - 0% { - opacity: 1; - } - - 50% { - opacity: $skeleton-pulse-opacity; - } - - 100% { - opacity: 1; - } -} - -.skeleton { - background-color: $skeleton-color; - animation: skeleton-pulse $skeleton-pulse-duration ease-in-out $skeleton-pulse-delay infinite; - - &.skeleton-text { - // Make it take up the space of a normal line - display: inline-block; - line-height: 1; - - width: 100%; - max-width: $skeleton-max-width; - height: $skeleton-height; - - border-radius: $skeleton-height; - } -}