2.2.7 Cycle 7 - Improved Webportal (Mobile Support)
Design
Objectives
Now that basic inter-nodal communication, cryptography and some other essentials are setup for the node software I think it's a good time to work on the webportal a bit more and I also to happen to have noticed that at this point the webportal does not render very well on mobile, so that's a good place to start.
Usability Features
The site should have a consistent colour scheme so as to make the whole site recognisable and easily usable by users.
The site should fit on a variety of screen sizes and resolutions so as to ensure as many users can access the site as possible.
Key Variables
mobile
A boolean value which tells the navigation component whether to render the mobile or desktop view.
window
An object that stores the width and height of the browser window to be used to calculate the value for the mobile variable.
breakpoint
An integer value which when the width of window is less than, the mobile navigation bar is enabled.
Pseudocode
The code for the rendering issues are mainly just css changes to ensure that as many browser sizes as possible are supported, therefore there's not really any pseudocode to be written.
However for the new navigation bar and the new buttons on the home screen there is some actual code to write.
Starting with the new navigation bar, this will introduce two 'react hooks' that allows the 'mobile' variable referenced in key variables to be generated every time the window changes size. The first one will be called 'useWindowSize()' and will be kept in a separate file to the navigation bar file incase it is useful in other parts of the website later on, and the second hook being a basic one that re-renders any time the window variable referencing the 'useWindowSize()' hook changes.
The 'useWindowSize()' hook
// useWindowSize Hook
FUNCTION useWindowSize():
// generate the window size variable
windowSize = {
width: 0
height: 0
}
// Handler to call on window resize
FUNCTION handleResize():
// Set window width/height to state
window = {
width: window.innerWidth,
height: window.innerHeight,
}
END FUNCTION
RELOAD_ON_CHANGE (window):
IF (window != "undefined"):
// Add event listener to call handleResize everytime the window is resized.
window.addEventListener("resize", handleResize);
// Call handler right away so state gets updated with initial window size
handleResize();
// Remove event listener on cleanup
RETURN window.removeEventListener("resize", handleResize);
END IF
END RELOAD_ON_CHANGE
RETURN windowSize;
END FUNCTION
The mobile navigation bar hook
// initialise the nav bar as desktop
mobile = false
// set the breakpoint as a width of 700 pixels
breakpoint = 700
RELOAD_ON_CHANGE (window.width):
// reloads any time the window.width changes
IF (window.width < breakpoint):
mobile = true
ELSE IF (window.width > breakpoint):
mobile = false
END IF
END
Development
Most of the development for implementing these changes were pretty simple as the logic for deciding whether to use the mobile or desktop nav bar is very simple and the buttons have basically no logic, so although it looks like a lot of code, most of it is just styling and layering to make it actually look nice and reusable in different parts of the website.
The outcome of these changes can be visited here:
https://monochain-webportal-5ph33emep-alfieran.vercel.app
Although if I switch web hosting providors at somepoint between writing this (26/10/22) and when you are reading it then the link will probably not work anymore.
Outcome - Navigation Bar
useWindowSize() Hook
This simply just generates a few variables and functions that will run and change the value of the windowSize
variable as the window object - an object supplied by the browser and not written by myself - changes in size and shape.
import { useState, useEffect } from "react";
// Hook
export default function useWindowSize() {
const [windowSize, setWindowSize] = useState<{
width: undefined | number;
height: undefined | number;
}>({
width: undefined,
height: undefined,
});
// Handler to call on window resize
function handleResize() {
// Set window width/height to state
setWindowSize({
width: window.innerWidth,
height: window.innerHeight,
});
}
useEffect(() => {
// only execute all the code below in client side
if (typeof window !== "undefined") {
// Add event listener
window.addEventListener("resize", handleResize);
// Call handler right away so state gets updated with initial window size
handleResize();
// Remove event listener on cleanup
return () => window.removeEventListener("resize", handleResize);
}
}, []); // Empty array ensures that effect is only run on mount
return windowSize;
}
Mobile hook
Since this code is part of the NavBar component, there's a lot more code that isn't relevant here so this only shows the relevant code, but the entire file can be found here. As shown below this code simply changes the state of the 'mobile' variable as the width of the window goes above or below the 'breakpoint' variable - currently set to 700 pixels.
const NavBar = () => {
// there is some other code ran here
// generate the required constants
const window = useWindowSize();
const breakpoint = 700;
const [mobile, setMobile] = useState<boolean>(true);
// create the hook
useEffect(() => {
if ((window.width || 0) < breakpoint) {
setMobile(true);
} else if ((window.width || 0) > breakpoint) {
setMobile(false);
}
}, [window.width]);
// then moves onto to the code for some other hooks & the actual components
}
The mobile and desktop rendering splitter
Once again, this code is part of the NavBar component, but since that file is so big and including all of it would make it unclear which parts have been changed, so only relevant sections have been included here.
The top section shows the desktop navigation bar, with the div that houses it having the parameter hidden={mobile}
such that as the mobile view is toggled it will disappear from the user's view and then the mobile navigation component will appear - shown by the parameter hidden={!mobile}
<div hidden={mobile}>
<Flex css={NavBarCss} justifyContent={"center"}>
{Links.map((data) => {
if (data.type === "component") {
return (
<Flex flexDir={"column"} key={data.name}>
<Button
bg={""}
my={0}
mx={LinkMarginAmount}
p={0}
_hover={LinkHover}
_active={LinkHover}
onClick={() => {
data.state.change(!data.state.value);
}}
>
<Text
fontSize={"xl"}
fontWeight={"normal"}
h={"full"}
w={"full"}
lineHeight={"base"}
pt={1.5}
>
{data.name}
</Text>
</Button>
</Flex>
);
} else {
return (
<Link
key={data.name}
href={data.href}
mx={LinkMarginAmount}
isExternal={!!data.external}
_hover={LinkHover}
>
<Text fontSize={"xl"} h={"full"} w={"full"} pt={1.5}>
{data.name}
</Text>
</Link>
);
}
})}
</Flex>
{Links.filter((item) => item.type === "component").map((data) => {
if (data.type === "component") {
return (
<Collapse key={data.name} in={data.state.value} dir="top">
<data.comp />
</Collapse>
);
}
})}
</div>
<MobileNav hidden={!mobile} />
Movile Navigation component
This is the component that is shown to users who are on mobile devices - or any browser with a window width of less than 700 pixels and simply represents a toggle-able side menu that can open and close as the user wishes. This is in exchange for the desktop navigation bar which has all the options along the top of the screen and is not toggle-able.
import {
Button,
Drawer,
DrawerBody,
DrawerContent,
DrawerHeader,
DrawerOverlay,
Flex,
Link,
Text,
useDisclosure,
} from "@chakra-ui/react";
import { NavBarCss } from "../styles/navBar";
import { scaling_button } from "../styles/buttons";
import React from "react";
const MobileNav = (props: { hidden?: boolean }) => {
const Links: { name: string; href: string }[] = [
{ name: "Home", href: "/" },
{ name: "Learn More", href: "/info" },
{ name: "Wallet", href: "/wallet" },
];
const {
isOpen: menuOpen,
onOpen: menuOnOpen,
onClose: menuOnClose,
} = useDisclosure();
if (props.hidden) return null;
return (
<>
<Flex
css={NavBarCss}
justifyContent={"space-between"}
alignItems={"center"}
>
<Flex css={scaling_button()} pl={2}>
<Link _hover={{ textDecoration: "none" }} href={"/"}>
<Text m={2}>Home</Text>
</Link>
</Flex>
<Button css={scaling_button()} onClick={menuOnOpen} px={3}>
<Text m={2}>Menu</Text>
</Button>
</Flex>
<Drawer placement="right" onClose={menuOnClose} isOpen={menuOpen}>
<DrawerOverlay />
<DrawerContent>
<DrawerHeader borderBottomWidth="2px">
<Flex
dir={"row"}
w={"100%"}
justifyContent={"space-between"}
alignItems={"center"}
>
<Text>MonoChain</Text>
<Button css={scaling_button()} onClick={menuOnClose}>
X
</Button>
</Flex>
</DrawerHeader>
<DrawerBody>
{Links.map((linkObj) => (
<Link
href={linkObj.href}
_hover={{ textDecoration: "none" }}
key={linkObj.name}
>
<Flex
borderWidth={"2px"}
borderRadius={"lg"}
my={3}
p={2}
fontSize={"lg"}
bg={"rgba(0,0,0,0.1)"}
_active={{ bg: "rgba(0,0,0,0.2)" }}
>
<Text>{linkObj.name}</Text>
</Flex>
</Link>
))}
</DrawerBody>
</DrawerContent>
</Drawer>
</>
);
};
export default MobileNav;
Outcome - Homepage buttons
Since the buttons to be produced will all be identical apart form their contents and where they send the user, the button only needs to be coded once and can then be looped over for each item in an array of contexts for the button to show.
The context info for the buttons
This is simple an array of objects which abide by the type 'gridItemProps' such that they all have exactly the same parameters and are easily usable by the button code.
type gridItemProps = {
link: string;
title: string;
subtitle: string;
};
const gridItems: gridItemProps[] = [
{
link: "/wallet",
title: "Wallet",
subtitle: "Create and manage your wallet.",
},
{
link: "/info",
title: "Learn More",
subtitle: "Learn more about the MonoChain.",
},
{
link: "/download",
title: "Setup a Node",
subtitle:
"Download a node to support the MonoChain in exchange for a fee.",
},
{
link: "https://github.com/AlfieRan/A-Level-Project",
title: "View the Code",
subtitle: "View the open source code for the default Node and webportal.",
},
];
The button code
This turns the data supplied by each of the objects within the 'gridItems' array into a button with the context supplied by that object.
<Grid gridTemplateColumns={["repeat(1, 1fr)", "repeat(2, 1fr)"]}>
{gridItems.map((item) => (
<GridItem
key={item.title}
m={2}
maxW={"100%"}
w={["4xs", "md"]}
h={["2xs", "xs"]}
>
<Link
href={item.link}
_hover={{ textDecoration: "none" }}
w={"100%"}
h={"100%"}
>
<Center
w={"100%"}
h={"100%"}
p={2}
bg={"rgba(100,100,255,0.2)"}
transitionDuration={"0.15s"}
_hover={{
bg: "rgba(100,100,255,0.3)",
transform: "scale(1.03)",
}}
_active={{
bg: "rgba(100,100,255,0.1)",
transform: "scale(0.97)",
}}
borderRadius={"xl"}
flexDir={"column"}
>
<Text fontSize={"2xl"} fontWeight={"semibold"}>
{item.title}
</Text>
<Text fontSize={"lg"}>{item.subtitle}</Text>
</Center>
</Link>
</GridItem>
))}
</Grid>
Challenges
The main challenge of this cycle was simply just tweaking css enough to ensure that the new ui looks good on as many devices and resolutions as possible, which involved changing my simulated resolution using the chrome developer tools to ensure that it looked good.
Testing
Tests
1
Navigate throughout the website on a desktop/laptop device
The website to stay "intact" with no components becomming hidden or breaking.
As Expected
2
Navigate throughout the website on a phone/small device
The website to stay "intact" with no components becomming hidden or breaking.
As Expected
3
Open the site on desktop device.
The desktop styled navigation bar should appear along the top of the screen.
As Expected
4
Open the site on mobile device.
The mobile styled navigation bar should appear with a menu button to open and close the bulk of the contents.
As Expected
Evidence
Test 1 & 3
This showcases the evidence for both test 1 and 3, both of which are regarding the site on a desktop device as is emulated here.
Test 2 & 4
This showcases the evidence for both test 2 and 4, both of which are regarding the site on a mobile device as is emulated here. (In this case an iPhone 13 Pro Max was chosen to be emulated.)
Last updated