JSONCrack Codebase Analysis - Part 5 - Toolbar and Bottom bar
jsoncrack is a popular opensource tool used to visualise json into a mindmap. It is built using Next.js.
We, at TThroo, love open source and perform codebase analysis on popular repositories, document, and provide a detailed explanation of the codebase. This enables OSS enthusiasts to learn and contribute to open source projects, and we also apply these learnings in our projects.
In this part 5, we understand how Toolbar
and BottomBar
are configured.
Interested to learn from opensource?
I am going to write a tutorial series where we use what we learnt from this jsoncrack codebase analysis, potentially involves usage of monaco editor, mantine, authentication and zustand configuration and modals, pretty much everything that is worth applying.
Before You ask, no, it is not going to be a jsoncrack clone but a unique project that uses similar codebase and similar approach and configuration to what we have seen in this series. It involves using supabase, zustand, mantine, monaco editor, nextjs.
Join the waitlist and I will send you the link to the tutorials once they are ready.
Where are they used?
They are used in Editor component. Part 4 explains about Editor
component.
Toolbar
You will find Toolbar inside src/containers/Toolbar.
export const Toolbar: React.FC<{ isWidget?: boolean }> = ({ isWidget = false }) => {
const getJson = useJson(state => state.getJson);
const setVisible = useModal(state => state.setVisible);
const setFormat = useFile(state => state.setFormat);
const format = useFile(state => state.format);
const premium = useUser(state => state.premium);
const handleSave = () => {
const a = document.createElement("a");
const file = new Blob([getJson()], { type: "text/plain" });
a.href = window.URL.createObjectURL(file);
a.download = "jsoncrack.json";
a.click();
};
return (
<Styles.StyledTools>
{isWidget && <Logo />}
{!isWidget && (
<Group gap="xs" justify="left" w="100%" style={{ flexWrap: "nowrap" }}>
<Styles.StyledToolElement title="JSON Crack">
<Flex gap="xs" align="center" justify="center">
<JSONCrackLogo fontSize="1.2em" />
</Flex>
</Styles.StyledToolElement>
<Select
defaultValue="json"
size="xs"
value={format}
onChange={e => setFormat(e as FileFormat)}
miw={80}
w={120}
data={[
{ value: FileFormat.JSON, label: "JSON" },
{ value: FileFormat.YAML, label: "YAML" },
{ value: FileFormat.XML, label: "XML" },
{ value: FileFormat.TOML, label: "TOML" },
{ value: FileFormat.CSV, label: "CSV" },
]}
/>
<ViewModeMenu />
<Styles.StyledToolElement title="Import File" onClick={() => setVisible("import")(true)}>
Import
</Styles.StyledToolElement>
<ViewMenu />
<ToolsMenu />
<Styles.StyledToolElement title="Cloud" onClick={() => setVisible("cloud")(true)}>
Cloud
</Styles.StyledToolElement>
<Styles.StyledToolElement title="Download as File" onClick={handleSave}>
Download
</Styles.StyledToolElement>
</Group>
)}
<Group gap="xs" justify="right" w="100%" style={{ flexWrap: "nowrap" }}>
{!premium && !isWidget && (
<Styles.StyledToolElement onClick={() => setVisible("premium")(true)}>
<Text display="flex" c="teal" fz="xs" fw="bold" style={{ textAlign: "center", gap: 4 }}>
<MdWorkspacePremium size="18" />
Get Premium
</Text>
</Styles.StyledToolElement>
)}
<SearchInput />
{!isWidget && (
<>
<Styles.StyledToolElement
title="Save as Image"
onClick={() => setVisible("download")(true)}
>
<FiDownload size="18" />
</Styles.StyledToolElement>
<ZoomMenu />
<AccountMenu />
<OptionsMenu />
<Styles.StyledToolElement
title="Fullscreen"
$hide={isWidget}
onClick={fullscreenBrowser}
>
<AiOutlineFullscreen size="18" />
</Styles.StyledToolElement>
</>
)}
</Group>
</Styles.StyledTools>
);
};
isWidget
flag is used to show either logo or the below optiions.
Select
<Select
defaultValue="json"
size="xs"
value={format}
onChange={e => setFormat(e as FileFormat)}
miw={80}
w={120}
data={[
{ value: FileFormat.JSON, label: "JSON" },
{ value: FileFormat.YAML, label: "YAML" },
{ value: FileFormat.XML, label: "XML" },
{ value: FileFormat.TOML, label: "TOML" },
{ value: FileFormat.CSV, label: "CSV" },
]}
/>
It is a Select component from mantine
import { Flex, Group, Select, Text } from "@mantine/core";
onChange
sets format in useFile
store
ViewMode
ViewMode deserves a file with its own html and is a standalone that directly updates zustand store.
Import
Import sets visible
prop in useModal
store.
ViewMenu
ViewMenu has a lot of changes to update useGraph
store that is directly correlated to the way visualisation is rendered using Reaflow.
And this is again in it own file under Toolbar folder because it has additional HTML that is not the right fit to be in Toolbar/index.tsx
The same approach above is applied to ToolsMenu.
Cloud
Cloud triggers a modal by setting visible prop in useModal store.
Modals are handled super neatly.
Took me a while to figure this one out.
If you open _app.tsx, You will find ModalController
const ModalController = () => {
const setVisible = useModal(state => state.setVisible);
const modalStates = useModal(state => modalComponents.map(modal => state[modal.key]));
return (
<EditorWrapper>
{modalComponents.map(({ key, component }, index) => {
const ModalComponent = component;
const opened = modalStates[index];
return <ModalComponent key={key} opened={opened} onClose={() => setVisible(key)(false)} />;
})}
</EditorWrapper>
);
};
That is basically telling to render what is set to true in useModal store. All the Modals used are placed in a container specifically dedicated for Modals. How NEAT!.
This is one of the way cleanest ways to deal with Modals especially when you have a lot of them :)
You can follow through similarly to understand the other items available in Toolbar.
Fullscreen Bar Icon
function fullscreenBrowser() {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen().catch(() => {
toast.error("Unable to enter fullscreen mode.");
});
} else if (document.exitFullscreen) {
document.exitFullscreen();
}
}
Next time, You are looking for a piece of code to toggle in and out of a full screenmode, You now know how!
BottomBar
BottomBar
does not contain as many options as Topbar
.
Interestingly, Bottombar
is part of Editor
, instead of it being a separate folder due to its limited number of options/features.
<StyledBottomBar>
{data?.name && (
<Head>
<title>{data.name} | JSON Crack</title>
</Head>
)}
<StyledLeft>
<StyledBottomBarItem onClick={toggleEditor}>
<BiSolidDockLeft />
</StyledBottomBarItem>
{fileName && (
<StyledBottomBarItem onClick={() => setVisible("cloud")(true)}>
<VscSourceControl />
{fileName}
</StyledBottomBarItem>
)}
<StyledBottomBarItem>
{error ? (
<Popover width="auto" shadow="md" position="top" withArrow>
<Popover.Target>
<Flex align="center" gap={2}>
<VscError color="red" size={16} />
<Text c="red" fw={500} fz="xs">
Invalid
</Text>
</Flex>
</Popover.Target>
<Popover.Dropdown
style={{
pointerEvents: "none",
}}
>
<Text size="xs">{error}</Text>
</Popover.Dropdown>
</Popover>
) : (
<Flex align="center" gap={2}>
<MdOutlineCheckCircleOutline />
<Text size="xs">Valid</Text>
</Flex>
)}
</StyledBottomBarItem>
{(data?.owner_email === user?.email || (!data && user)) && (
<StyledBottomBarItem onClick={handleSaveJson} disabled={isUpdating || error}>
{hasChanges || !user ? <AiOutlineCloudUpload /> : <AiOutlineCloudSync />}
{hasChanges || !user ? (query?.json ? "Unsaved Changes" : "Save to Cloud") : "Saved"}
</StyledBottomBarItem>
)}
{data?.owner_email === user?.email && (
<StyledBottomBarItem onClick={setPrivate} disabled={isUpdating}>
{isPrivate ? <AiOutlineLock /> : <AiOutlineUnlock />}
{isPrivate ? "Private" : "Public"}
</StyledBottomBarItem>
)}
<StyledBottomBarItem
onClick={() => setVisible("share")(true)}
disabled={isPrivate || !data}
>
<AiOutlineLink />
Share
</StyledBottomBarItem>
{liveTransformEnabled ? (
<StyledBottomBarItem onClick={() => toggleLiveTransform(false)}>
<VscSync />
<Text fz="xs">Live Transform</Text>
</StyledBottomBarItem>
) : (
<StyledBottomBarItem onClick={() => toggleLiveTransform(true)}>
<VscSyncIgnored />
<Text fz="xs">Manual Transform</Text>
</StyledBottomBarItem>
)}
{!liveTransformEnabled && (
<StyledBottomBarItem onClick={() => setContents({})}>
<TbTransform />
Transform
</StyledBottomBarItem>
)}
</StyledLeft>
<StyledRight>
<StyledBottomBarItem>Nodes: {nodeCount}</StyledBottomBarItem>
<StyledBottomBarItem onClick={() => setVisible("review")(true)}>
<VscFeedback />
Feedback
</StyledBottomBarItem>
</StyledRight>
</StyledBottomBar>
The code is pretty straigt forward, modifies what needs to be set in stores and few flags are to display what’s needed.
If You ask me to refactor the above code, I would choose to move the following to separate component.
{liveTransformEnabled ? (
<StyledBottomBarItem onClick={() => toggleLiveTransform(false)}>
<VscSync />
<Text fz="xs">Live Transform</Text>
</StyledBottomBarItem>
) : (
<StyledBottomBarItem onClick={() => toggleLiveTransform(true)}>
<VscSyncIgnored />
<Text fz="xs">Manual Transform</Text>
</StyledBottomBarItem>
)}
{!liveTransformEnabled && (
<StyledBottomBarItem onClick={() => setContents({})}>
<TbTransform />
Transform
</StyledBottomBarItem>
)}
In this new file, You could do the following:
const liveTransformEnabled = useConfig(state => state.liveTransformEnabled);
This could improve the readability, again, it is choice and not hard set rule, depends on dev teams and their preferences. You cannot over engineer at the same time, it is a fine balance. How you draw this fine balance is directly correlated to code maintainability and readability.
Conclusion
We learnt how to use zustand stores and perform operations and do the state updates.
Modal
configuration was interesting, I have never seen it before. All the modals are placed inside a folder and a separate store for modals. You can leverage this pattern if your project is dealing with modals heavy operations to handle and maintain the code easily.
Up next, we have part 6 where we talk about how the payments and subscription is integrated into this product using lemonsqueezy.
If you have any questions or need help with your project, feel free to reach out to me at ram@tthroo.com
Subscribe to my newsletter
Read articles from TThroo directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by