| import React from "react"; | |
| import Head from "next/head"; | |
| import { useRouter } from "next/router"; | |
| import { Flex, Popover, Text } from "@mantine/core"; | |
| import styled from "styled-components"; | |
| import toast from "react-hot-toast"; | |
| import { | |
| AiOutlineCloudSync, | |
| AiOutlineCloudUpload, | |
| AiOutlineLink, | |
| AiOutlineLock, | |
| AiOutlineUnlock, | |
| } from "react-icons/ai"; | |
| import { BiSolidDockLeft } from "react-icons/bi"; | |
| import { MdOutlineCheckCircleOutline } from "react-icons/md"; | |
| import { TbTransform } from "react-icons/tb"; | |
| import { VscError, VscFeedback, VscSourceControl, VscSync, VscSyncIgnored } from "react-icons/vsc"; | |
| import { documentSvc } from "src/services/document.service"; | |
| import useConfig from "src/store/useConfig"; | |
| import useFile from "src/store/useFile"; | |
| import useGraph from "src/store/useGraph"; | |
| import useModal from "src/store/useModal"; | |
| import useUser from "src/store/useUser"; | |
| const StyledBottomBar = styled.div` | |
| position: relative; | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| border-top: 1px solid ${({ theme }) => theme.BACKGROUND_MODIFIER_ACCENT}; | |
| background: ${({ theme }) => theme.TOOLBAR_BG}; | |
| max-height: 27px; | |
| height: 27px; | |
| z-index: 35; | |
| padding-right: 6px; | |
| @media screen and (max-width: 320px) { | |
| display: none; | |
| } | |
| `; | |
| const StyledLeft = styled.div` | |
| display: flex; | |
| align-items: center; | |
| justify-content: left; | |
| gap: 4px; | |
| padding-left: 8px; | |
| @media screen and (max-width: 480px) { | |
| display: none; | |
| } | |
| `; | |
| const StyledRight = styled.div` | |
| display: flex; | |
| align-items: center; | |
| justify-content: right; | |
| gap: 4px; | |
| `; | |
| const StyledBottomBarItem = styled.button<{ $bg?: string }>` | |
| display: flex; | |
| align-items: center; | |
| gap: 4px; | |
| width: fit-content; | |
| margin: 0; | |
| height: 28px; | |
| padding: 4px; | |
| font-size: 12px; | |
| font-weight: 400; | |
| color: ${({ theme }) => theme.INTERACTIVE_NORMAL}; | |
| background: ${({ $bg }) => $bg}; | |
| white-space: nowrap; | |
| text-overflow: ellipsis; | |
| overflow: hidden; | |
| &:hover:not(&:disabled) { | |
| background-image: linear-gradient(rgba(0, 0, 0, 0.1) 0 0); | |
| color: ${({ theme }) => theme.INTERACTIVE_HOVER}; | |
| } | |
| &:disabled { | |
| opacity: 0.6; | |
| cursor: default; | |
| } | |
| `; | |
| export const BottomBar = () => { | |
| const { query, replace } = useRouter(); | |
| const data = useFile(state => state.fileData); | |
| const user = useUser(state => state.user); | |
| const toggleLiveTransform = useConfig(state => state.toggleLiveTransform); | |
| const liveTransformEnabled = useConfig(state => state.liveTransformEnabled); | |
| const hasChanges = useFile(state => state.hasChanges); | |
| const error = useFile(state => state.error); | |
| const getContents = useFile(state => state.getContents); | |
| const setContents = useFile(state => state.setContents); | |
| const nodeCount = useGraph(state => state.nodes.length); | |
| const fileName = useFile(state => state.fileData?.name); | |
| const toggleFullscreen = useGraph(state => state.toggleFullscreen); | |
| const fullscreen = useGraph(state => state.fullscreen); | |
| const setVisible = useModal(state => state.setVisible); | |
| const setHasChanges = useFile(state => state.setHasChanges); | |
| const getFormat = useFile(state => state.getFormat); | |
| const [isPrivate, setIsPrivate] = React.useState(false); | |
| const [isUpdating, setIsUpdating] = React.useState(false); | |
| const toggleEditor = () => toggleFullscreen(!fullscreen); | |
| React.useEffect(() => { | |
| setIsPrivate(data?.private ?? true); | |
| }, [data]); | |
| const handleSaveJson = React.useCallback(async () => { | |
| if (!user) return setVisible("login")(true); | |
| if ( | |
| hasChanges && | |
| !error && | |
| (typeof query.json === "string" || typeof query.json === "undefined") | |
| ) { | |
| try { | |
| setIsUpdating(true); | |
| toast.loading("Saving document...", { id: "fileSave" }); | |
| const { data, error } = await documentSvc.upsert({ | |
| id: query?.json, | |
| contents: getContents(), | |
| format: getFormat(), | |
| }); | |
| if (error) throw error; | |
| if (data) replace({ query: { json: data } }); | |
| toast.success("Document saved to cloud", { id: "fileSave" }); | |
| setHasChanges(false); | |
| } catch (error: any) { | |
| toast.error(error.message, { id: "fileSave" }); | |
| } finally { | |
| setIsUpdating(false); | |
| } | |
| } | |
| }, [ | |
| error, | |
| getContents, | |
| getFormat, | |
| hasChanges, | |
| query.json, | |
| replace, | |
| setHasChanges, | |
| setVisible, | |
| user, | |
| ]); | |
| const setPrivate = async () => { | |
| try { | |
| if (!query.json) return handleSaveJson(); | |
| setIsUpdating(true); | |
| const { data: updatedJsonData, error } = await documentSvc.update(query.json as string, { | |
| private: !isPrivate, | |
| }); | |
| if (error) return toast.error(error.message); | |
| if (updatedJsonData[0]) { | |
| setIsPrivate(updatedJsonData[0].private); | |
| toast.success(`Document set to ${isPrivate ? "public" : "private"}.`); | |
| } else throw error; | |
| } catch (error) { | |
| console.error(error); | |
| } finally { | |
| setIsUpdating(false); | |
| } | |
| }; | |
| return ( | |
| <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> | |
| ); | |
| }; | |