From c29eb65667257dd7df9e9203505cf9fd457630ad Mon Sep 17 00:00:00 2001 From: Ryan Leung Date: Sat, 2 Mar 2024 14:19:26 -0500 Subject: [PATCH] [UI] Add options sidebar --- .../sidebar-menu-navigation-button.tsx | 48 +++++++ .../dropdowns/network-selection-dropdown.tsx | 84 ++++++++++++ src/frontend/components/layout/main.tsx | 35 +++-- .../components/layout/sidebar-toggle.tsx | 46 +++++++ src/frontend/components/layout/sidebar.tsx | 78 +++++++++++ src/frontend/components/layout/top-bar.tsx | 7 +- src/frontend/contexts.tsx | 44 +++++- src/frontend/index.tsx | 126 +++++++++--------- src/frontend/router.tsx | 9 +- src/frontend/theme/theme.ts | 4 + src/frontend/views/chat.tsx | 4 +- .../{settings.tsx => underConstruction.tsx} | 4 +- 12 files changed, 403 insertions(+), 86 deletions(-) create mode 100644 src/frontend/components/buttons/sidebar-menu-navigation-button.tsx create mode 100644 src/frontend/components/dropdowns/network-selection-dropdown.tsx create mode 100644 src/frontend/components/layout/sidebar-toggle.tsx create mode 100644 src/frontend/components/layout/sidebar.tsx rename src/frontend/views/{settings.tsx => underConstruction.tsx} (94%) diff --git a/src/frontend/components/buttons/sidebar-menu-navigation-button.tsx b/src/frontend/components/buttons/sidebar-menu-navigation-button.tsx new file mode 100644 index 0000000..a69ddaf --- /dev/null +++ b/src/frontend/components/buttons/sidebar-menu-navigation-button.tsx @@ -0,0 +1,48 @@ +import Styled from 'styled-components'; +import { Link, useLocation } from 'react-router-dom'; + +interface ButtonProps { + active: 'active' | ''; +} + +interface MainNavButtonProps { + text: string; + href: string; + exact: boolean; +} + +const SidebarMenuNavigationButton = ({ text, href, exact }: MainNavButtonProps) => { + const path = useLocation(); + const active = exact ? path.pathname === href : path.pathname.includes(href); + + return ( + + {text} + + ); +}; + +const MenuNavItem = { + Wrapper: Styled(Link)` + display: flex; + flex-direction: column; + padding: 24px 0 24px 12px; + color: ${(props) => (props.active === 'active' ? props.theme.colors.black : props.theme.colors.balance)}; + text-decoration: none; + background-color: ${(props) => props.active && props.theme.colors.balance}; + &:hover span { + color: ${(props) => !props.active && props.theme.colors.gray}; + } + `, + Icon: Styled.img` + display: flex; + width: 30px; + height: 30px; + `, + ButtonText: Styled.span` + font-family: ${(props) => props.theme.fonts.family.primary.regular}; + font-size: ${(props) => props.theme.fonts.size.medium}; + `, +}; + +export default SidebarMenuNavigationButton; diff --git a/src/frontend/components/dropdowns/network-selection-dropdown.tsx b/src/frontend/components/dropdowns/network-selection-dropdown.tsx new file mode 100644 index 0000000..fea98c8 --- /dev/null +++ b/src/frontend/components/dropdowns/network-selection-dropdown.tsx @@ -0,0 +1,84 @@ +import React from 'react'; +import Styled from 'styled-components'; + +import { getChainInfoByChainId } from '../../utils/chain'; +import { useSDK } from '@metamask/sdk-react'; +import { useAIMessagesContext } from '../../contexts'; + +const NetworkSelectionDropdown = () => { + const { provider, account } = useSDK(); + const [dialogueEntries, setDialogueEntries] = useAIMessagesContext(); + + const handleNetworkChange = async (e: React.ChangeEvent) => { + const selectedChain = e.target.value; + + // Check if the default option is selected + if (!selectedChain) { + console.log('No network selected.'); + return; // Early return to avoid further execution + } + + // Sanity Checks: + if (!account || !provider) { + const errorMessage = `Error: Please connect to MetaMask`; + setDialogueEntries([ + ...dialogueEntries, + { question: '', answer: errorMessage, answered: true }, + ]); + return; + } + + try { + const response = await provider.request({ + method: 'wallet_switchEthereumChain', + params: [{ chainId: selectedChain }], + }); + console.log(response); + } catch (error) { + //if switch chain fails then add the chain + try { + const chainInfo = getChainInfoByChainId(selectedChain); + const response = await provider.request({ + method: 'wallet_addEthereumChain', + params: [chainInfo], + }); + } catch (error) { + console.error('Failed to switch networks:', error); + } + } + }; + + return ( + + + + + + + + ); +}; + +const Dropdown = Styled.select` + padding: 8px 10px; + margin: 12px 0 12px 12px; + + border-radius: 10px; + background-color: ${(props) => props.theme.colors.core}; + color: ${(props) => props.theme.colors.notice}; + border: 2px solid ${(props) => props.theme.colors.hunter}; + font-family: ${(props) => props.theme.fonts.family.primary.regular}; + font-size: ${(props) => props.theme.fonts.size.small}; + cursor: pointer; + + &:hover { + border: 2px solid ${(props) => props.theme.colors.emerald}; + } + + option { + background-color: ${(props) => props.theme.colors.core}; + color: ${(props) => props.theme.colors.emerald}; + } +`; + +export default NetworkSelectionDropdown; diff --git a/src/frontend/components/layout/main.tsx b/src/frontend/components/layout/main.tsx index 3d1f9df..03427ba 100644 --- a/src/frontend/components/layout/main.tsx +++ b/src/frontend/components/layout/main.tsx @@ -9,18 +9,25 @@ import BottomBar from './bottom-bar'; // router import { MainRouter } from '../../router'; +import SidebarComponent from './sidebar'; +import SidebarToggle from './sidebar-toggle'; + export default () => { return ( - - - - - - - - - + + + + + + + + + + + + + ); }; @@ -28,12 +35,19 @@ export default () => { const Main = { Layout: Styled.div` display: flex; - flex-direction: column; align-items: center; width: 100%; height: 100%; background: ${(props) => props.theme.colors.core}; `, + Content: Styled.div` + display: flex; + flex-direction: column; + align-items: center; + flex-grow: 1; + height: 100%; + background: ${(props) => props.theme.colors.core}; + `, TopWrapper: Styled.div` display: flex; width: 100%; @@ -48,6 +62,7 @@ const Main = { border-radius: 30px; border: 5px solid ${(props) => props.theme.colors.hunter}; padding: 10px; + overflow: hidden; `, BottomWrapper: Styled.div` display: flex; diff --git a/src/frontend/components/layout/sidebar-toggle.tsx b/src/frontend/components/layout/sidebar-toggle.tsx new file mode 100644 index 0000000..84df333 --- /dev/null +++ b/src/frontend/components/layout/sidebar-toggle.tsx @@ -0,0 +1,46 @@ +// libs +import React from 'react'; +import Styled from 'styled-components'; +import { useSidebarContext } from '../../contexts'; + +export default () => { + const { isOpen, setIsOpen } = useSidebarContext(); + return ( + + { + setIsOpen((isOpen) => !isOpen); + }} + isOpen={isOpen} + /> + + ); +}; + +type SidebarToggleCarat = { + isOpen: boolean; +}; + +const SidebarToggle = { + Layout: Styled.div` + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 100%; + background: ${(props) => props.theme.colors.core}; + padding-left: 8px; + `, + Carat: Styled.div` + display: inline-block; + width: 20px; + height: 20px; + border-top: 2px solid ${(props) => props.theme.colors.emerald}; + border-left: 2px solid ${(props) => props.theme.colors.emerald}; + transform: rotate(${(props) => (props.isOpen ? '-45deg' : '135deg')}); + &:hover { + cursor: pointer; + opacity: 0.8; + } + `, +}; diff --git a/src/frontend/components/layout/sidebar.tsx b/src/frontend/components/layout/sidebar.tsx new file mode 100644 index 0000000..8b51672 --- /dev/null +++ b/src/frontend/components/layout/sidebar.tsx @@ -0,0 +1,78 @@ +import Styled from 'styled-components'; + +import Logo from './../../assets/images/logo_white.png'; +import SidebarMenuNavigationButton from '../buttons/sidebar-menu-navigation-button'; +import NetworkSelectionDropdown from '../dropdowns/network-selection-dropdown'; +import { useSidebarContext } from '../../contexts'; + +export default (): JSX.Element => { + const { isOpen } = useSidebarContext(); + return ( + + + + + + + + + + + + + + + + MorpheusAI.org + + ); +}; + +type SidebarLayoutProps = { + isOpen: boolean; +}; + +const Sidebar = { + Layout: Styled.div` + width: ${(props) => (props.isOpen ? '250px' : '0px')}; + height: 100%; + background: black; + display: flex; + flex-direction: column; + flex-shrink: 0; + transition: 0.2s; + `, + Top: Styled.div` + height: 50px; + display: flex; + justify-content: center; + flex-shrink: 0; + `, + Logo: Styled.img` + height: 50px; + width: 50px; + `, + Menu: Styled.div` + flex-grow: 1; + overflow-y: auto; + `, + MenuItem: Styled.div` + display: flex; + align-items: center; + `, + + Bottom: Styled.div` + height: 50px; + flex-shrink: 0; + color: ${(props) => props.theme.colors.balance}; + font-family: ${(props) => props.theme.fonts.family.primary.regular}; + font-size: ${(props) => props.theme.fonts.size.small}; + display: flex; + align-items: center; + justify-content: center; + `, + SeparatorLine: Styled.div` + height: 1px; + background: ${(props) => props.theme.colors.gray}; + `, +}; diff --git a/src/frontend/components/layout/top-bar.tsx b/src/frontend/components/layout/top-bar.tsx index 92035df..345fa39 100644 --- a/src/frontend/components/layout/top-bar.tsx +++ b/src/frontend/components/layout/top-bar.tsx @@ -69,7 +69,7 @@ export default () => { */} - + {/* */} Morpheus @@ -150,11 +150,6 @@ const TopBar = { height: 25px; cursor: pointer; `, - Logo: Styled.img` - display: flex; - height: 100px; - width: 100px; - `, Header: Styled.h2` font-size: ${(props) => props.theme.fonts.size.medium}; font-family: ${(props) => props.theme.fonts.family.primary.regular}; diff --git a/src/frontend/contexts.tsx b/src/frontend/contexts.tsx index 8645e3b..78f1198 100644 --- a/src/frontend/contexts.tsx +++ b/src/frontend/contexts.tsx @@ -1,6 +1,13 @@ /* eslint-disable @typescript-eslint/no-empty-function */ /* eslint-disable @typescript-eslint/no-unused-vars */ -import { createContext, PropsWithChildren, useContext, useState } from 'react'; +import { + createContext, + PropsWithChildren, + useContext, + useState, + SetStateAction, + Dispatch, +} from 'react'; import { AIMessage } from './types'; export type AIMessagesContextType = [Array, (messages: Array) => void]; @@ -29,3 +36,38 @@ export const useAIMessagesContext = () => { return context; }; + +export type SidebarContextType = { + isOpen: boolean; + setIsOpen: Dispatch>; +}; + +export const SidebarContext = createContext({ + isOpen: false, + setIsOpen: (isOpen: boolean) => {}, +}); + +export const SidebarProvider = ({ children }: PropsWithChildren) => { + const [isOpen, setIsOpen] = useState(false); + + return ( + + {children} + + ); +}; + +export const useSidebarContext = () => { + const context = useContext(SidebarContext); + + if (!context) { + throw new Error(`useSidebarContext must be used within SidebarProvider`); + } + + return context; +}; diff --git a/src/frontend/index.tsx b/src/frontend/index.tsx index 1970e9d..b8b424e 100644 --- a/src/frontend/index.tsx +++ b/src/frontend/index.tsx @@ -18,7 +18,7 @@ import GlobalStyle from './theme/index'; import './index.css'; // context -import { AIMessagesProvider } from './contexts'; +import { AIMessagesProvider, SidebarProvider } from './contexts'; // constants import { LOGO_METAMASK_BASE64 } from './constants'; @@ -96,68 +96,70 @@ const AppRoot = () => { return ( - - { - let modalContainer: HTMLElement; - - return { - mount: () => { - if (modalContainer) return; - - modalContainer = document.createElement('div'); - - modalContainer.id = 'meta-mask-modal-container'; - - document.body.appendChild(modalContainer); - - ReactDOM.render( - { - ReactDOM.unmountComponentAtNode(modalContainer); - modalContainer.remove(); - }} - />, - modalContainer, - ); - - setTimeout(() => { - updateQrCode(link); - }, 100); - }, - unmount: () => { - if (modalContainer) { - ReactDOM.unmountComponentAtNode(modalContainer); - - modalContainer.remove(); - } - }, - }; + + + - {!isInitialized && } - {isInitialized &&
} - {/* {modelsPathFetched && !isModelsPathSet && await handleSelectFolderClicked()} />} */} - - + communicationServerUrl: 'https://metamask-sdk-socket.metafi.codefi.network/', + checkInstallationImmediately: false, + i18nOptions: { + enabled: true, + }, + dappMetadata: { + name: 'Morpheus Node', + url: 'https://mor.org', + base64Icon: LOGO_METAMASK_BASE64, + }, + modals: { + install: ({ link }) => { + let modalContainer: HTMLElement; + + return { + mount: () => { + if (modalContainer) return; + + modalContainer = document.createElement('div'); + + modalContainer.id = 'meta-mask-modal-container'; + + document.body.appendChild(modalContainer); + + ReactDOM.render( + { + ReactDOM.unmountComponentAtNode(modalContainer); + modalContainer.remove(); + }} + />, + modalContainer, + ); + + setTimeout(() => { + updateQrCode(link); + }, 100); + }, + unmount: () => { + if (modalContainer) { + ReactDOM.unmountComponentAtNode(modalContainer); + + modalContainer.remove(); + } + }, + }; + }, + }, + }} + > + {!isInitialized && } + {isInitialized &&
} + {/* {modelsPathFetched && !isModelsPathSet && await handleSelectFolderClicked()} />} */} + + + ); diff --git a/src/frontend/router.tsx b/src/frontend/router.tsx index c22cea9..d5c1f99 100644 --- a/src/frontend/router.tsx +++ b/src/frontend/router.tsx @@ -5,15 +5,18 @@ import Styled from 'styled-components'; // views // import HomeView from './views/home'; -import SettingsView from './views/settings'; +import UnderConstruction from './views/underConstruction'; import ChatView from './views/chat'; export const RoutesWrapper = () => { return ( - } /> - + + + + + ); }; diff --git a/src/frontend/theme/theme.ts b/src/frontend/theme/theme.ts index 0f0feb2..8865de5 100644 --- a/src/frontend/theme/theme.ts +++ b/src/frontend/theme/theme.ts @@ -5,6 +5,8 @@ export interface ITheme { hunter: string; notice: string; balance: string; + black: string; + gray: string; }; layout: { topBarHeight: number; @@ -37,6 +39,8 @@ const common = { hunter: '#106F48', notice: '#FDB366', balance: '#FFFFFF', + black: '#000000', + gray: '#B0B0B0', }, layout: { topBarHeight: 130, diff --git a/src/frontend/views/chat.tsx b/src/frontend/views/chat.tsx index 37c8b5a..95d416e 100644 --- a/src/frontend/views/chat.tsx +++ b/src/frontend/views/chat.tsx @@ -233,13 +233,13 @@ const ChatView = (): JSX.Element => { return ( - + {/* - + */} {dialogueEntries.map((entry, index) => { return ( diff --git a/src/frontend/views/settings.tsx b/src/frontend/views/underConstruction.tsx similarity index 94% rename from src/frontend/views/settings.tsx rename to src/frontend/views/underConstruction.tsx index 02c14bf..8978657 100644 --- a/src/frontend/views/settings.tsx +++ b/src/frontend/views/underConstruction.tsx @@ -5,7 +5,7 @@ import Styled from 'styled-components'; // helpers // import { truncateString } from '../helpers'; -const SettingsView = (): JSX.Element => { +const UnderConstructionView = (): JSX.Element => { // const { ready, sdk, connected, connecting, provider, chainId, account, balance } = // useSDK(); @@ -53,4 +53,4 @@ const Settings = { `, }; -export default SettingsView; +export default UnderConstructionView;