feat: update ui

This commit is contained in:
AykutSarac 2023-12-17 14:52:50 +03:00
parent 4895d235f7
commit 6ab9338709
No known key found for this signature in database
33 changed files with 837 additions and 823 deletions

View File

@ -22,5 +22,6 @@
]
},
"extends": ["next/core-web-vitals", "prettier"],
"plugins": ["prettier", "unused-imports"]
"plugins": ["prettier", "unused-imports"],
"ignorePatterns": ["src/enums"]
}

View File

@ -4,4 +4,4 @@ node_modules/
out
public
*-lock.json
tsconfig.json
tsconfig.json

View File

@ -631,8 +631,8 @@ to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
JSON Crack
Copyright (C) 2023 Aykut Saraç
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@ -652,7 +652,7 @@ Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
JSON Crack Copyright (C) 2023 Aykut Saraç
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.

View File

@ -2,6 +2,7 @@
"name": "json-crack",
"private": true,
"version": "v3.0.0",
"license": "GPL-3.0",
"author": "https://github.com/AykutSarac",
"homepage": "https://jsoncrack.com",
"scripts": {
@ -19,22 +20,21 @@
"@mantine/hooks": "^6.0.17",
"@mantine/next": "^6.0.21",
"@mantine/prism": "^6.0.21",
"@monaco-editor/react": "^4.5.2",
"@monaco-editor/react": "^4.6.0",
"@sentry/nextjs": "^7.72.0",
"@supabase/auth-helpers-nextjs": "^0.8.1",
"@supabase/auth-helpers-react": "^0.4.2",
"@supabase/supabase-js": "^2.36.0",
"@tanstack/react-query": "^4.35.3",
"allotment": "^1.19.2",
"allotment": "^1.19.3",
"axios": "^1.5.0",
"dayjs": "^1.11.10",
"html-to-image": "^1.11.11",
"jq-in-the-browser": "^0.7.2",
"jq-web": "^0.5.1",
"json-2-csv": "^4.1.0",
"json-2-csv": "^5.0.1",
"json-to-ts": "^1.7.0",
"jsonc-parser": "^3.2.0",
"jsongraph-react": "^0.0.12",
"jsonwebtoken": "^9.0.2",
"jxon": "^2.0.0-beta.5",
"lodash.debounce": "^4.0.8",
@ -46,35 +46,33 @@
"react-dom": "^18.2.0",
"react-ga4": "^2.1.0",
"react-hot-toast": "^2.4.1",
"react-icons": "^4.11.0",
"react-icons": "^4.12.0",
"react-json-tree": "^0.18.0",
"react-linkify-it": "^1.0.7",
"react-simple-typewriter": "^5.0.1",
"react-zoomable-ui": "^0.11.0",
"reaflow": "5.2.6",
"styled-components": "^6.0.8",
"reaflow": "5.2.8",
"styled-components": "^6.1.1",
"toml": "^3.0.0",
"use-long-press": "^3.1.5",
"zustand": "^4.4.0"
"zustand": "^4.4.7"
},
"devDependencies": {
"@next/bundle-analyzer": "^13.4.12",
"@testing-library/react": "^14.0.0",
"@trivago/prettier-plugin-sort-imports": "^4.2.0",
"@types/jsonwebtoken": "^9.0.3",
"@types/jxon": "^2.0.2",
"@types/lodash.debounce": "^4.0.7",
"@types/lodash.get": "^4.4.7",
"@types/lodash.set": "^4.3.7",
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
"@types/jsonwebtoken": "^9.0.5",
"@types/jxon": "^2.0.5",
"@types/lodash.debounce": "^4.0.9",
"@types/lodash.get": "^4.4.9",
"@types/lodash.set": "^4.3.9",
"@types/node": "^20.4.7",
"@types/react": "18.2.18",
"@types/react-color": "^3.0.6",
"@types/react-syntax-highlighter": "^15.5.7",
"eslint": "8.46.0",
"eslint": "8.56.0",
"eslint-config-next": "13.4.12",
"eslint-config-prettier": "^8.10.0",
"eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-unused-imports": "^3.0.0",
"prettier": "^3.0.1",
"prettier": "^3.1.1",
"ts-node": "^10.9.1",
"typescript": "5.1.6"
}

View File

@ -4,8 +4,8 @@ import { MdLink, MdLinkOff } from "react-icons/md";
import { CustomNodeProps } from "src/components/Graph/CustomNode";
import useToggleHide from "src/hooks/useToggleHide";
import { isContentImage } from "src/lib/utils/graph/calculateNodeSize";
import useConfig from "src/store/useConfig";
import useGraph from "src/store/useGraph";
import useStored from "src/store/useStored";
import { TextRenderer } from "./TextRenderer";
import * as Styled from "./styles";
@ -52,13 +52,13 @@ const Node: React.FC<CustomNodeProps> = ({ node, x, y, hasCollapse = false }) =>
data: { isParent, childrenCount, type },
} = node;
const { validateHiddenNodes } = useToggleHide();
const hideCollapse = useStored(state => state.hideCollapse);
const showChildrenCount = useStored(state => state.childrenCount);
const imagePreview = useStored(state => state.imagePreview);
const collapseButtonVisible = useConfig(state => state.collapseButtonVisible);
const childrenCountVisible = useConfig(state => state.childrenCountVisible);
const imagePreviewEnabled = useConfig(state => state.imagePreviewEnabled);
const expandNodes = useGraph(state => state.expandNodes);
const collapseNodes = useGraph(state => state.collapseNodes);
const isExpanded = useGraph(state => state.collapsedParents.includes(id));
const isImage = imagePreview && isContentImage(text as string);
const isImage = imagePreviewEnabled && isContentImage(text as string);
const value = JSON.stringify(text).replaceAll('"', "");
const handleExpand = (e: React.MouseEvent<HTMLButtonElement>) => {
@ -80,16 +80,16 @@ const Node: React.FC<CustomNodeProps> = ({ node, x, y, hasCollapse = false }) =>
data-x={x}
data-y={y}
data-key={JSON.stringify(text)}
$hasCollapse={isParent && hideCollapse}
$hasCollapse={isParent && collapseButtonVisible}
>
<Styled.StyledKey $value={value} $parent={isParent} $type={type}>
<TextRenderer>{value}</TextRenderer>
</Styled.StyledKey>
{isParent && childrenCount > 0 && showChildrenCount && (
{isParent && childrenCount > 0 && childrenCountVisible && (
<Styled.StyledChildrenCount>({childrenCount})</Styled.StyledChildrenCount>
)}
{isParent && hasCollapse && hideCollapse && (
{isParent && hasCollapse && collapseButtonVisible && (
<StyledExpand aria-label="Expand" onClick={handleExpand}>
{isExpanded ? <MdLinkOff size={18} /> : <MdLink size={18} />}
</StyledExpand>

View File

@ -25,15 +25,15 @@ const isURL =
/(https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})/gi;
export const TextRenderer: React.FC<{ children: string }> = ({ children }) => {
if (isURL.test(children.replaceAll('"', ""))) {
if (isURL.test(children?.replaceAll('"', ""))) {
return <Styled.StyledLinkItUrl>{children}</Styled.StyledLinkItUrl>;
}
if (isColorFormat(children.replaceAll('"', ""))) {
if (isColorFormat(children?.replaceAll('"', ""))) {
return (
<StyledRow>
<ColorSwatch radius={4} h={12} w={12} color={children.replaceAll('"', "")} />
{children.replaceAll('"', "")}
<ColorSwatch radius={4} h={12} w={12} mr={8} color={children?.replaceAll('"', "")} />
{children?.replaceAll('"', "")}
</StyledRow>
);
}

View File

@ -1,14 +1,16 @@
import React from "react";
import dynamic from "next/dynamic";
import styled from "styled-components";
import { toast } from "react-hot-toast";
import { Space } from "react-zoomable-ui";
import { ElkRoot } from "reaflow/dist/layout/useLayout";
import { useLongPress } from "use-long-press";
import { CustomNode } from "src/components/Graph/CustomNode";
import { ViewMode } from "src/enums/viewMode.enum";
import useToggleHide from "src/hooks/useToggleHide";
import { Loading } from "src/layout/Loading";
import useConfig from "src/store/useConfig";
import useGraph from "src/store/useGraph";
import useStored from "src/store/useStored";
import useUser from "src/store/useUser";
import { NodeData } from "src/types/graph";
import { CustomEdge } from "./CustomEdge";
@ -23,7 +25,7 @@ interface GraphProps {
isWidget?: boolean;
}
const StyledEditorWrapper = styled.div<{ $widget: boolean }>`
const StyledEditorWrapper = styled.div<{ $widget: boolean; $showRulers: boolean }>`
position: absolute;
width: 100%;
height: ${({ $widget }) => ($widget ? "calc(100vh - 36px)" : "calc(100vh - 63px)")};
@ -33,20 +35,24 @@ const StyledEditorWrapper = styled.div<{ $widget: boolean }>`
--line-color-2: ${({ theme }) => theme.GRID_COLOR_SECONDARY};
background-color: var(--bg-color);
background-image: linear-gradient(var(--line-color-1) 1.5px, transparent 1.5px),
linear-gradient(90deg, var(--line-color-1) 1.5px, transparent 1.5px),
linear-gradient(var(--line-color-2) 1px, transparent 1px),
linear-gradient(90deg, var(--line-color-2) 1px, transparent 1px);
background-position:
-1.5px -1.5px,
-1.5px -1.5px,
-1px -1px,
-1px -1px;
background-size:
100px 100px,
100px 100px,
20px 20px,
20px 20px;
${({ $showRulers }) =>
$showRulers &&
`
background-image: linear-gradient(var(--line-color-1) 1.5px, transparent 1.5px),
linear-gradient(90deg, var(--line-color-1) 1.5px, transparent 1.5px),
linear-gradient(var(--line-color-2) 1px, transparent 1px),
linear-gradient(90deg, var(--line-color-2) 1px, transparent 1px);
background-position:
-1.5px -1.5px,
-1.5px -1.5px,
-1px -1px,
-1px -1px;
background-size:
100px 100px,
100px 100px,
20px 20px,
20px 20px;
`};
:active {
cursor: move;
@ -76,7 +82,8 @@ const layoutOptions = {
};
const PREMIUM_LIMIT = 200;
const ERROR_LIMIT = 3_000;
const ERROR_LIMIT_TREE = 5_000;
const ERROR_LIMIT = 10_000;
const GraphCanvas = ({ isWidget }: GraphProps) => {
const { validateHiddenNodes } = useToggleHide();
@ -138,6 +145,7 @@ const GraphCanvas = ({ isWidget }: GraphProps) => {
function getViewType(nodes: NodeData[]) {
if (nodes.length > ERROR_LIMIT) return "error";
if (nodes.length > ERROR_LIMIT_TREE) return "tree";
if (nodes.length > PREMIUM_LIMIT) return "premium";
return "graph";
}
@ -147,7 +155,9 @@ export const Graph = ({ isWidget = false }: GraphProps) => {
const loading = useGraph(state => state.loading);
const isPremium = useUser(state => state.premium);
const viewType = useGraph(state => getViewType(state.nodes));
const gesturesEnabled = useStored(state => state.gesturesEnabled);
const gesturesEnabled = useConfig(state => state.gesturesEnabled);
const rulersEnabled = useConfig(state => state.rulersEnabled);
const setViewMode = useConfig(state => state.setViewMode);
const callback = React.useCallback(() => {
const canvas = document.querySelector(".jsoncrack-canvas") as HTMLDivElement | null;
@ -166,7 +176,14 @@ export const Graph = ({ isWidget = false }: GraphProps) => {
if ("activeElement" in document) (document.activeElement as HTMLElement)?.blur();
}, []);
if (viewType === "error") return <ErrorView />;
if (viewType === "error") {
return <ErrorView />;
}
if (viewType === "tree") {
setViewMode(ViewMode.Tree);
toast("This document is too large to display as a graph. Switching to tree view.");
}
if (viewType === "premium" && !isWidget) {
if (!isPremium) return <PremiumView />;
@ -180,6 +197,7 @@ export const Graph = ({ isWidget = false }: GraphProps) => {
onContextMenu={e => e.preventDefault()}
onClick={blurOnClick}
key={String(gesturesEnabled)}
$showRulers={rulersEnabled}
{...bindLongPress()}
>
<Space

View File

@ -2,8 +2,8 @@ import React from "react";
import styled from "styled-components";
import Editor, { loader, useMonaco } from "@monaco-editor/react";
import { Loading } from "src/layout/Loading";
import useConfig from "src/store/useConfig";
import useFile from "src/store/useFile";
import useStored from "src/store/useStored";
loader.config({
paths: {
@ -33,7 +33,7 @@ export const MonacoEditor = () => {
const setError = useFile(state => state.setError);
const jsonSchema = useFile(state => state.jsonSchema);
const getHasChanges = useFile(state => state.getHasChanges);
const theme = useStored(state => (state.lightmode ? "light" : "vs-dark"));
const theme = useConfig(state => (state.darkmodeEnabled ? "vs-dark" : "light"));
const fileType = useFile(state => state.format);
React.useEffect(() => {

View File

@ -11,22 +11,15 @@ import {
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 {
VscAccount,
VscError,
VscFeedback,
VscSourceControl,
VscSync,
VscSyncIgnored,
VscWorkspaceTrusted,
} from "react-icons/vsc";
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 useStored from "src/store/useStored";
import useUser from "src/store/useUser";
const StyledBottomBar = styled.div`
@ -51,6 +44,7 @@ const StyledLeft = styled.div`
align-items: center;
justify-content: left;
gap: 4px;
padding-left: 8px;
@media screen and (max-width: 480px) {
display: none;
@ -95,15 +89,16 @@ export const BottomBar = () => {
const { query, replace } = useRouter();
const data = useFile(state => state.fileData);
const user = useUser(state => state.user);
const premium = useUser(state => state.premium);
const toggleLiveTransform = useStored(state => state.toggleLiveTransform);
const liveTransform = useStored(state => state.liveTransform);
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);
@ -111,6 +106,8 @@ export const BottomBar = () => {
const [isPrivate, setIsPrivate] = React.useState(false);
const [isUpdating, setIsUpdating] = React.useState(false);
const toggleEditor = () => toggleFullscreen(!fullscreen);
React.useEffect(() => {
setIsPrivate(data?.private ?? true);
}, [data]);
@ -191,20 +188,10 @@ export const BottomBar = () => {
</Head>
)}
<StyledLeft>
<StyledBottomBarItem $bg="#1864AB" onClick={handleLoginClick}>
<Flex align="center" gap={5} px={5}>
<VscAccount color="white" />
<Text maw={120} c="white" truncate="end">
{user?.user_metadata.name ?? "Login"}
</Text>
</Flex>
<StyledBottomBarItem onClick={toggleEditor}>
<BiSolidDockLeft />
</StyledBottomBarItem>
{!premium && (
<StyledBottomBarItem onClick={() => setVisible("premium")(true)}>
<VscWorkspaceTrusted />
Upgrade to Premium
</StyledBottomBarItem>
)}
{fileName && (
<StyledBottomBarItem onClick={() => setVisible("cloud")(true)}>
<VscSourceControl />
@ -236,7 +223,7 @@ export const BottomBar = () => {
{(data?.owner_email === user?.email || (!data && user)) && (
<StyledBottomBarItem onClick={handleSaveJson} disabled={isUpdating || error}>
{hasChanges ? <AiOutlineCloudUpload /> : <AiOutlineCloudSync />}
{hasChanges ? (query?.json ? "Unsaved Changes" : "Create Document") : "Saved"}
{hasChanges || !user ? (query?.json ? "Unsaved Changes" : "Save to Cloud") : "Saved"}
</StyledBottomBarItem>
)}
{data?.owner_email === user?.email && (
@ -252,7 +239,7 @@ export const BottomBar = () => {
<AiOutlineLink />
Share
</StyledBottomBarItem>
{liveTransform ? (
{liveTransformEnabled ? (
<StyledBottomBarItem onClick={() => toggleLiveTransform(false)}>
<VscSync />
<Text>Live Transform</Text>
@ -263,7 +250,7 @@ export const BottomBar = () => {
<Text>Manual Transform</Text>
</StyledBottomBarItem>
)}
{!liveTransform && (
{!liveTransformEnabled && (
<StyledBottomBarItem onClick={() => setContents({})}>
<TbTransform />
Transform

View File

@ -123,7 +123,7 @@ const PromptInput = () => {
export const JsonEditor: React.FC = () => {
return (
<StyledEditorWrapper>
<PromptInput />
{/* <PromptInput /> */}
<MonacoEditor />
</StyledEditorWrapper>
);

View File

@ -1,36 +1,51 @@
import React from "react";
import Link from "next/link";
import { useRouter } from "next/router";
import styled from "styled-components";
import { Flex, Group, MediaQuery, Menu, Select, Text } from "@mantine/core";
import { useHotkeys } from "@mantine/hooks";
import {
Avatar,
Flex,
Group,
Input,
MediaQuery,
Menu,
SegmentedControl,
Select,
Text,
} from "@mantine/core";
import { getHotkeyHandler, useHotkeys } from "@mantine/hooks";
import ReactGA from "react-ga4";
import toast from "react-hot-toast";
import { AiOutlineFullscreen, AiOutlineMinus, AiOutlinePlus } from "react-icons/ai";
import { AiOutlineFullscreen } from "react-icons/ai";
import { BsCheck2 } from "react-icons/bs";
import { CgArrowsMergeAltH, CgArrowsShrinkH, CgChevronDown } from "react-icons/cg";
import { FiDownload } from "react-icons/fi";
import { MdCenterFocusWeak } from "react-icons/md";
import { MdOutlineWorkspacePremium } from "react-icons/md";
import { SiJsonwebtokens } from "react-icons/si";
import { TiFlowMerge } from "react-icons/ti";
import {
VscCollapseAll,
VscExpandAll,
VscJson,
VscLayoutSidebarLeft,
VscLayoutSidebarLeftOff,
VscSettingsGear,
VscTarget,
VscSearchFuzzy,
VscGroupByRefType,
VscSignOut,
VscFeedback,
VscSignIn,
} from "react-icons/vsc";
import { SearchInput } from "src/components/SearchInput";
import { FileFormat } from "src/enums/file";
import { FileFormat } from "src/enums/file.enum";
import { ViewMode } from "src/enums/viewMode.enum";
import { JSONCrackLogo } from "src/layout/JsonCrackLogo";
import { getNextDirection } from "src/lib/utils/graph/getNextDirection";
import { isIframe } from "src/lib/utils/widget";
import useConfig from "src/store/useConfig";
import useFile from "src/store/useFile";
import useGraph from "src/store/useGraph";
import useJC from "src/store/useJC";
import useJson from "src/store/useJson";
import useModal from "src/store/useModal";
import useUser from "src/store/useUser";
export const StyledTools = styled.div`
position: relative;
@ -94,17 +109,13 @@ function fullscreenBrowser() {
const ViewMenu = () => {
const [coreKey, setCoreKey] = React.useState("CTRL");
const toggleFold = useGraph(state => state.toggleFold);
const setDirection = useJC(state => state.graphRef?.setDirection);
const direction = useJC(state => state.graphRef?.direction);
const expandGraph = useJC(state => state.graphRef?.expandGraph);
const collapseGraph = useJC(state => state.graphRef?.collapseGraph);
const toggleFullscreen = useGraph(state => state.toggleFullscreen);
const focusFirstNode = useJC(state => state.graphRef?.focusFirstNode);
const setDirection = useGraph(state => state.setDirection);
const direction = useGraph(state => state.direction);
const expandGraph = useGraph(state => state.expandGraph);
const collapseGraph = useGraph(state => state.collapseGraph);
const focusFirstNode = useGraph(state => state.focusFirstNode);
const foldNodes = useGraph(state => state.foldNodes);
const graphCollapsed = useJC(state => state.graphRef?.graphCollapsed);
const fullscreen = useGraph(state => state.fullscreen);
const toggleEditor = () => toggleFullscreen(!fullscreen);
const graphCollapsed = useGraph(state => state.graphCollapsed);
const toggleFoldNodes = () => {
toggleFold(!foldNodes);
@ -124,7 +135,6 @@ const ViewMenu = () => {
};
useHotkeys([
["mod+shift+e", toggleEditor],
["mod+shift+d", toggleDirection],
["mod+shift+f", toggleFoldNodes],
["mod+shift+c", toggleExpandCollapseGraph],
@ -144,7 +154,7 @@ const ViewMenu = () => {
}, []);
return (
<Menu shadow="md" closeOnItemClick={false}>
<Menu shadow="md" closeOnItemClick={false} withArrow>
<Menu.Target>
<StyledToolElement>
<Flex align="center" gap={3}>
@ -153,25 +163,6 @@ const ViewMenu = () => {
</StyledToolElement>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item
fz={12}
onClick={() => {
toggleEditor();
ReactGA.event({
action: "toggle_hide_editor",
category: "User",
label: "Tools",
});
}}
icon={fullscreen ? <VscLayoutSidebarLeft /> : <VscLayoutSidebarLeftOff />}
rightSection={
<Text ml="md" fz={10} color="dimmed">
{coreKey} Shift E
</Text>
}
>
{fullscreen ? "Show" : "Hide"} Editor
</Menu.Item>
<Menu.Item
fz={12}
onClick={() => {
@ -238,16 +229,50 @@ const ViewMenu = () => {
};
export const Tools: React.FC<{ isWidget?: boolean }> = ({ isWidget = false }) => {
const { push } = useRouter();
const getJson = useJson(state => state.getJson);
const setVisible = useModal(state => state.setVisible);
const centerView = useJC(state => state.graphRef?.centerView);
const zoomIn = useJC(state => state.graphRef?.zoomIn);
const zoomOut = useJC(state => state.graphRef?.zoomOut);
const centerView = useGraph(state => state.centerView);
const zoomIn = useGraph(state => state.zoomIn);
const zoomOut = useGraph(state => state.zoomOut);
const toggleGestures = useConfig(state => state.toggleGestures);
const toggleChildrenCount = useConfig(state => state.toggleChildrenCount);
const toggleDarkMode = useConfig(state => state.toggleDarkMode);
const toggleRulers = useConfig(state => state.toggleRulers);
const toggleCollapseButton = useConfig(state => state.toggleCollapseButton);
const setViewMode = useConfig(state => state.setViewMode);
const setZoomFactor = useGraph(state => state.setZoomFactor);
const toggleImagePreview = useConfig(state => state.toggleImagePreview);
const logout = useUser(state => state.logout);
const user = useUser(state => state.user);
const premium = useUser(state => state.premium);
const zoomFactor = useGraph(state => state.viewPort?.zoomFactor || 1);
const gesturesEnabled = useConfig(state => state.gesturesEnabled);
const childrenCountVisible = useConfig(state => state.childrenCountVisible);
const darkmodeEnabled = useConfig(state => state.darkmodeEnabled);
const viewMode = useConfig(state => state.viewMode);
const rulersEnabled = useConfig(state => state.rulersEnabled);
const collapseButtonVisible = useConfig(state => state.collapseButtonVisible);
const imagePreviewEnabled = useConfig(state => state.imagePreviewEnabled);
const setFormat = useFile(state => state.setFormat);
const format = useFile(state => state.format);
const [tempZoomValue, setTempZoomValue] = React.useState(zoomFactor);
const [logoURL, setLogoURL] = React.useState("CTRL");
React.useEffect(() => {
if (!Number.isNaN(zoomFactor)) setTempZoomValue(zoomFactor);
}, [zoomFactor]);
useHotkeys([
["shift+Digit0", () => setZoomFactor(100 / 100)],
["shift+Digit1", centerView],
]);
React.useEffect(() => {
if (typeof window !== "undefined") {
const url = !isIframe()
@ -303,11 +328,30 @@ export const Tools: React.FC<{ isWidget?: boolean }> = ({ isWidget = false }) =>
{ value: FileFormat.CSV, label: "CSV" },
]}
/>
<Menu trigger="click" shadow="md" withArrow>
<Menu.Target>
<StyledToolElement title="View Mode">
<Flex align="center" gap={3}>
View Mode <CgChevronDown />
</Flex>
</StyledToolElement>
</Menu.Target>
<Menu.Dropdown>
<SegmentedControl
value={viewMode}
onChange={setViewMode}
data={[
{ value: ViewMode.Graph, label: "Graph" },
{ value: ViewMode.Tree, label: "Tree" },
]}
/>
</Menu.Dropdown>
</Menu>
<StyledToolElement title="Import File" onClick={() => setVisible("import")(true)}>
Import
</StyledToolElement>
<ViewMenu />
<Menu shadow="md">
<Menu shadow="md" withArrow>
<Menu.Target>
<StyledToolElement>
<Flex align="center" gap={3}>
@ -348,31 +392,170 @@ export const Tools: React.FC<{ isWidget?: boolean }> = ({ isWidget = false }) =>
</MediaQuery>
)}
<Group spacing="xs" position="right" w="100%" noWrap>
<StyledToolElement title="Zoom Out" onClick={zoomOut}>
<AiOutlineMinus size="18" />
</StyledToolElement>
<StyledToolElement title="Zoom In" onClick={zoomIn}>
<AiOutlinePlus size="18" />
</StyledToolElement>
<SearchInput />
{!isWidget && (
<StyledToolElement title="Save as Image" onClick={() => setVisible("download")(true)}>
<FiDownload size="18" />
</StyledToolElement>
)}
<StyledToolElement title="Center Canvas" onClick={centerView}>
<MdCenterFocusWeak size="18" />
</StyledToolElement>
<SearchInput />
<StyledToolElement title="Fullscreen" $hide={isWidget} onClick={fullscreenBrowser}>
<AiOutlineFullscreen size="18" />
</StyledToolElement>
<StyledToolElement
title="Settings"
$hide={isWidget}
onClick={() => setVisible("settings")(true)}
>
<VscSettingsGear size="18" />
</StyledToolElement>
{!isWidget && (
<Menu shadow="md" trigger="click" closeOnItemClick={false} withArrow>
<Menu.Target>
<StyledToolElement>
<Flex gap={4}>
{Math.round(zoomFactor * 100)}%
<CgChevronDown />
</Flex>
</StyledToolElement>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item>
<Input
type="number"
value={Math.round(tempZoomValue * 100)}
onChange={e => setTempZoomValue(e.currentTarget.valueAsNumber / 100)}
onKeyDown={getHotkeyHandler([["Enter", () => setZoomFactor(tempZoomValue)]])}
size="xs"
rightSection="%"
/>
</Menu.Item>
<Menu.Item rightSection="+" onClick={zoomIn}>
<Text size="xs">Zoom in</Text>
</Menu.Item>
<Menu.Item rightSection="-" onClick={zoomOut}>
<Text size="xs">Zoom out</Text>
</Menu.Item>
<Menu.Item rightSection="⇧ 1" onClick={centerView}>
<Text size="xs">Zoom to fit</Text>
</Menu.Item>
<Menu.Item onClick={() => setZoomFactor(50 / 100)}>
<Text size="xs">Zoom to %50</Text>
</Menu.Item>
<Menu.Item rightSection="⇧ 0" onClick={() => setZoomFactor(100 / 100)}>
<Text size="xs">Zoom to %100</Text>
</Menu.Item>
<Menu.Item onClick={() => setZoomFactor(200 / 100)}>
<Text size="xs">Zoom to %200</Text>
</Menu.Item>
<Menu.Divider />
<Menu.Item
icon={<BsCheck2 opacity={rulersEnabled ? 100 : 0} />}
onClick={() => toggleRulers(!rulersEnabled)}
>
<Text size="xs">Rulers</Text>
</Menu.Item>
<Menu.Item
icon={<BsCheck2 opacity={gesturesEnabled ? 100 : 0} />}
onClick={() => toggleGestures(!gesturesEnabled)}
>
<Text size="xs">Trackpad Gestures</Text>
</Menu.Item>
<Menu.Item
icon={<BsCheck2 opacity={childrenCountVisible ? 100 : 0} />}
onClick={() => toggleChildrenCount(!childrenCountVisible)}
>
<Text size="xs">Item Count</Text>
</Menu.Item>
<Menu.Item
icon={<BsCheck2 opacity={imagePreviewEnabled ? 100 : 0} />}
onClick={() => toggleImagePreview(!imagePreviewEnabled)}
>
<Text size="xs">Image Link Preview</Text>
</Menu.Item>
<Menu.Item
icon={<BsCheck2 opacity={collapseButtonVisible ? 100 : 0} />}
onClick={() => toggleCollapseButton(!collapseButtonVisible)}
>
<Text size="xs">Show Expand/Collapse</Text>
</Menu.Item>
</Menu.Dropdown>
</Menu>
)}
{!isWidget && (
<Menu shadow="md" trigger="click" closeOnItemClick={false} withArrow>
<Menu.Target>
<StyledToolElement>
<Avatar
color="grape"
variant="filled"
size={20}
radius="xl"
src={user?.user_metadata.avatar_url}
alt={user?.user_metadata.name}
>
{user?.user_metadata.name[0]}
</Avatar>
</StyledToolElement>
</Menu.Target>
<Menu.Dropdown>
{user ? (
<Menu.Item
icon={
<Avatar color="grape" alt={user.user_metadata.name} size={20} radius="xl">
{user?.user_metadata.name[0]}
</Avatar>
}
onClick={() => setVisible("account")(true)}
closeMenuOnClick
>
<Text size="xs">Account</Text>
</Menu.Item>
) : (
<Link href="/sign-in">
<Menu.Item icon={<VscSignIn />}>
<Text size="xs">Sign in</Text>
</Menu.Item>
</Link>
)}
{!premium && (
<Menu.Item
icon={<MdOutlineWorkspacePremium color="red" />}
onClick={() => setVisible("premium")(true)}
closeMenuOnClick
>
<Text
variant="gradient"
fw="bold"
gradient={{ from: "orange", to: "red" }}
size="xs"
>
Get Premium
</Text>
</Menu.Item>
)}
<Menu.Item
icon={<BsCheck2 opacity={darkmodeEnabled ? 100 : 0} />}
onClick={() => toggleDarkMode(!darkmodeEnabled)}
>
<Text size="xs">Dark Mode</Text>
</Menu.Item>
{user && (
<>
<Menu.Divider />
<Menu.Item
icon={<VscFeedback />}
onClick={() => setVisible("review")(true)}
closeMenuOnClick
>
<Text size="xs">Feedback</Text>
</Menu.Item>
<Menu.Item icon={<VscSignOut />} onClick={() => logout()} closeMenuOnClick>
<Text size="xs">Log out</Text>
</Menu.Item>
</>
)}
</Menu.Dropdown>
</Menu>
)}
{!isWidget && (
<StyledToolElement title="Fullscreen" $hide={isWidget} onClick={fullscreenBrowser}>
<AiOutlineFullscreen size="18" />
</StyledToolElement>
)}
</Group>
</StyledTools>
);

View File

@ -1,87 +1,145 @@
import React from "react";
import styled from "styled-components";
import { JSONGraph } from "jsongraph-react";
import useGraph from "src/store/useGraph";
import useJC from "src/store/useJC";
import styled, { DefaultTheme, useTheme } from "styled-components";
import { Menu, Text } from "@mantine/core";
import { JSONTree } from "react-json-tree";
import { Graph } from "src/components/Graph";
import { TextRenderer } from "src/components/Graph/CustomNode/TextRenderer";
import { firaMono } from "src/constants/fonts";
import { ViewMode } from "src/enums/viewMode.enum";
import useConfig from "src/store/useConfig";
import useJson from "src/store/useJson";
import useModal from "src/store/useModal";
import useStored from "src/store/useStored";
import { NodeData } from "src/types/graph";
const StyledLiveEditor = styled.div`
position: relative;
height: 100%;
background: ${({ theme }) => theme.GRID_BG_COLOR};
overflow: auto;
& > ul {
margin-top: 0 !important;
padding: 12px !important;
font-family: ${firaMono.style.fontFamily};
font-size: 14px;
font-weight: 500;
}
`;
const LiveEditor: React.FC = () => {
const lightmode = useStored(state => state.lightmode);
type TextColorFn = {
theme: DefaultTheme;
$value?: string | unknown;
};
function getValueColor({ $value, theme }: TextColorFn) {
if ($value && !Number.isNaN(+$value)) return theme.NODE_COLORS.INTEGER;
if ($value === "true") return theme.NODE_COLORS.BOOL.TRUE;
if ($value === "false") return theme.NODE_COLORS.BOOL.FALSE;
if ($value === "null") return theme.NODE_COLORS.NULL;
// default
return theme.NODE_COLORS.NODE_VALUE;
}
function getLabelColor({ $type, theme }: { $type?: string; theme: DefaultTheme }) {
if ($type === "Object") return theme.NODE_COLORS.PARENT_OBJ;
if ($type === "Array") return theme.NODE_COLORS.PARENT_ARR;
return theme.NODE_COLORS.PARENT_OBJ;
}
const View = () => {
const theme = useTheme();
const json = useJson(state => state.json);
const setJCRef = useJC(state => state.setJCRef);
const graphRef = useJC(state => state.graphRef);
const setSelectedNode = useGraph(state => state.setSelectedNode);
const setVisible = useModal(state => state.setVisible);
const viewMode = useConfig(state => state.viewMode);
console.log(graphRef);
if (viewMode === ViewMode.Graph) return <Graph />;
const handleNodeClick = React.useCallback(
(data: NodeData) => {
if (setSelectedNode) setSelectedNode(data);
setVisible("node")(true);
},
[setSelectedNode, setVisible]
);
if (viewMode === ViewMode.Tree)
return (
<JSONTree
data={JSON.parse(json)}
labelRenderer={(keyPath, nodeType, expanded, expandable) => {
return (
<span
style={{
color: getLabelColor({
theme,
$type: nodeType,
}),
}}
>
{keyPath[0]}:
</span>
);
}}
valueRenderer={(valueAsString, value) => {
return (
<span
style={{
color: getValueColor({
theme,
$value: valueAsString,
}),
}}
>
<TextRenderer>{JSON.stringify(value)}</TextRenderer>
</span>
);
}}
theme={{
extend: {
overflow: "scroll",
height: "100%",
scheme: "monokai",
author: "wimer hazenberg (http://www.monokai.nl)",
base00: theme.GRID_BG_COLOR,
},
}}
/>
);
};
const style = React.useMemo(
() => ({
width: "100%",
height: "100%",
}),
[]
);
const handleLayoutChange = React.useCallback(() => {
// graphRef shouldn't be null here
console.log(graphRef);
setTimeout(graphRef?.centerView, 100);
}, [graphRef]);
const layout = React.useMemo(
() => ({
touchGestures: true,
theme: lightmode ? "light" : ("dark" as any),
}),
[lightmode]
);
const LiveEditor: React.FC = () => {
const viewMode = useConfig(state => state.viewMode);
const [contextOpened, setContextOpened] = React.useState(false);
const [contextPosition, setContextPosition] = React.useState({
x: 0,
y: 0,
});
return (
<StyledLiveEditor>
<JSONGraph
ref={setJCRef}
json={json}
style={style}
onNodeClick={handleNodeClick}
onLayoutChange={handleLayoutChange}
layout={layout}
/>
<StyledLiveEditor
onContextMenuCapture={e => {
e.preventDefault();
setContextOpened(true);
setContextPosition({ x: e.pageX, y: e.pageY });
}}
onClick={() => setContextOpened(false)}
>
<div
style={{
position: "fixed",
top: contextPosition.y,
left: contextPosition.x,
zIndex: 100,
}}
>
<Menu opened={false} shadow="sm">
<Menu.Dropdown>
<Menu.Item>
<Text size="xs">Download as Image</Text>
</Menu.Item>
<Menu.Item>
<Text size="xs">Zoom to Fit</Text>
</Menu.Item>
<Menu.Item>
<Text size="xs">Rotate</Text>
</Menu.Item>
</Menu.Dropdown>
</Menu>
</div>
<View />
</StyledLiveEditor>
);
};
export default LiveEditor;
// import React from "react";
// import styled from "styled-components";
// import { Graph } from "src/components/Graph";
// const StyledLiveEditor = styled.div`
// position: relative;
// height: 100%;
// `;
// const LiveEditor: React.FC = () => {
// return (
// <StyledLiveEditor>
// <Graph />
// </StyledLiveEditor>
// );
// };
// export default LiveEditor;

View File

@ -28,7 +28,7 @@ import { AiOutlineLink } from "react-icons/ai";
import { FaTrash } from "react-icons/fa";
import { MdFileOpen } from "react-icons/md";
import { VscAdd, VscEdit, VscLock, VscUnlock } from "react-icons/vsc";
import { FileFormat } from "src/enums/file";
import { FileFormat } from "src/enums/file.enum";
import { documentSvc } from "src/services/document.service";
import useFile, { File } from "src/store/useFile";
import useUser from "src/store/useUser";

View File

@ -3,12 +3,12 @@ import { Stack, Modal, Button, ModalProps, Text, Anchor, Group } from "@mantine/
import Editor from "@monaco-editor/react";
import { VscLinkExternal } from "react-icons/vsc";
import useJsonQuery from "src/hooks/useJsonQuery";
import useStored from "src/store/useStored";
import useConfig from "src/store/useConfig";
export const JQModal: React.FC<ModalProps> = ({ opened, onClose }) => {
const { updateJson } = useJsonQuery();
const [query, setQuery] = React.useState("");
const lightmode = useStored(state => (state.lightmode ? "light" : "vs-dark"));
const darkmodeEnabled = useConfig(state => (state.darkmodeEnabled ? "vs-dark" : "light"));
const onApply = () => {
updateJson(query);
@ -27,7 +27,7 @@ export const JQModal: React.FC<ModalProps> = ({ opened, onClose }) => {
</Text>
<Editor
value={query ?? ""}
theme={lightmode}
theme={darkmodeEnabled}
onChange={e => setQuery(e!)}
height={300}
language="markdown"

View File

@ -6,10 +6,10 @@ import vsDark from "prism-react-renderer/themes/vsDark";
import vsLight from "prism-react-renderer/themes/vsLight";
import { VscLock } from "react-icons/vsc";
import { isIframe } from "src/lib/utils/widget";
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 useStored from "src/store/useStored";
import useUser from "src/store/useUser";
const dataToString = (data: any) => {
@ -49,7 +49,7 @@ export const NodeModal: React.FC<ModalProps> = ({ opened, onClose }) => {
const isPremium = useUser(state => state.premium);
const editContents = useFile(state => state.editContents);
const setVisible = useModal(state => state.setVisible);
const lightmode = useStored(state => (state.lightmode ? "light" : "vs-dark"));
const darkmodeEnabled = useConfig(state => (state.darkmodeEnabled ? "vs-dark" : "light"));
const nodeData = useGraph(state => dataToString(state.selectedNode?.text));
const path = useGraph(state => state.selectedNode?.path);
const isParent = useGraph(state => state.selectedNode?.data?.isParent);
@ -90,7 +90,7 @@ export const NodeModal: React.FC<ModalProps> = ({ opened, onClose }) => {
</Text>
{editMode ? (
<Editor
theme={lightmode}
theme={darkmodeEnabled}
defaultValue={nodeData}
onChange={e => setValue(e!)}
height={200}

View File

@ -3,9 +3,9 @@ import { Stack, Modal, Button, ModalProps, Text, Anchor, Group } from "@mantine/
import Editor from "@monaco-editor/react";
import { toast } from "react-hot-toast";
import { VscLock } from "react-icons/vsc";
import useConfig from "src/store/useConfig";
import useFile from "src/store/useFile";
import useModal from "src/store/useModal";
import useStored from "src/store/useStored";
import useUser from "src/store/useUser";
export const SchemaModal: React.FC<ModalProps> = ({ opened, onClose }) => {
@ -13,7 +13,7 @@ export const SchemaModal: React.FC<ModalProps> = ({ opened, onClose }) => {
const showPremiumModal = useModal(state => state.setVisible("premium"));
const setJsonSchema = useFile(state => state.setJsonSchema);
const [schema, setSchema] = React.useState("");
const lightmode = useStored(state => (state.lightmode ? "light" : "vs-dark"));
const darkmodeEnabled = useConfig(state => (state.darkmodeEnabled ? "vs-dark" : "light"));
const onApply = () => {
if (!isPremium) return showPremiumModal(true);
@ -47,7 +47,7 @@ export const SchemaModal: React.FC<ModalProps> = ({ opened, onClose }) => {
</Text>
<Editor
value={schema ?? ""}
theme={lightmode}
theme={darkmodeEnabled}
onChange={e => setSchema(e!)}
height={300}
language="json"

View File

@ -1,93 +0,0 @@
import React from "react";
import { Modal, Group, Switch, Stack, ModalProps } from "@mantine/core";
// import { VscLock } from "react-icons/vsc";
// import useToggleHide from "src/hooks/useToggleHide";
// import useGraph from "src/store/useGraph";
// import useModal from "src/store/useModal";
// import useUser from "src/store/useUser";
import useStored from "src/store/useStored";
export const SettingsModal: React.FC<ModalProps> = ({ opened, onClose }) => {
// const { validateHiddenNodes } = useToggleHide();
// const setVisible = useModal(state => state.setVisible);
// const toggleCollapseAll = useGraph(state => state.toggleCollapseAll);
// const collapseAll = useGraph(state => state.collapseAll);
// const isPremium = useUser(state => state.premium);
const setLightTheme = useStored(state => state.setLightTheme);
const toggleHideCollapse = useStored(state => state.toggleHideCollapse);
const toggleChildrenCount = useStored(state => state.toggleChildrenCount);
const toggleImagePreview = useStored(state => state.toggleImagePreview);
const toggleGestures = useStored(state => state.toggleGestures);
const hideCollapse = useStored(state => state.hideCollapse);
const childrenCount = useStored(state => state.childrenCount);
const imagePreview = useStored(state => state.imagePreview);
const lightmode = useStored(state => state.lightmode);
const gesturesEnabled = useStored(state => state.gesturesEnabled);
return (
<Modal title="Settings" opened={opened} onClose={onClose} centered>
<Group py="sm">
<Stack>
<Switch
label="Enable Trackpad Gestures"
size="md"
color="teal"
onChange={e => toggleGestures(e.currentTarget.checked)}
checked={gesturesEnabled}
/>
<Switch
label="Live Image Preview"
size="md"
color="teal"
onChange={e => toggleImagePreview(e.currentTarget.checked)}
checked={imagePreview}
/>
<Switch
label="Display Collapse/Expand Button"
size="md"
color="teal"
onChange={e => toggleHideCollapse(e.currentTarget.checked)}
checked={hideCollapse}
/>
<Switch
label="Display Children Count"
size="md"
color="teal"
onChange={e => toggleChildrenCount(e.currentTarget.checked)}
checked={childrenCount}
/>
{/* <Switch
label={
<Flex align="center" gap="xs">
Collapse All by Default
<Badge size="xs" color="violet" variant="outline" radius="sm">
35% Faster
</Badge>
</Flex>
}
size="md"
color="violet"
onChange={e => {
if (isPremium) {
toggleCollapseAll(e.currentTarget.checked);
return validateHiddenNodes();
}
setVisible("premium")(true);
onClose();
}}
checked={collapseAll}
offLabel={isPremium ? null : <VscLock size="12" />}
/> */}
<Switch
label="Light Theme"
size="md"
color="teal"
onChange={e => setLightTheme(e.currentTarget.checked)}
checked={lightmode}
/>
</Stack>
</Group>
</Modal>
);
};

View File

@ -4,7 +4,6 @@ export { DownloadModal } from "./DownloadModal";
export { ImportModal } from "./ImportModal";
export { AccountModal } from "./AccountModal";
export { NodeModal } from "./NodeModal";
export { SettingsModal } from "./SettingsModal";
export { ShareModal } from "./ShareModal";
export { LoginModal } from "./LoginModal";
export { PremiumModal } from "./PremiumModal";
@ -22,7 +21,6 @@ type Modal =
| "import"
| "account"
| "node"
| "settings"
| "share"
| "login"
| "premium"

View File

@ -1,3 +1,4 @@
// eslint-disable
export enum FileFormat {
"JSON" = "json",
"YAML" = "yaml",

View File

@ -0,0 +1,4 @@
export enum ViewMode {
Graph = "graph",
Tree = "tree",
}

View File

@ -1,10 +1,10 @@
import React from "react";
import { useDebouncedValue } from "@mantine/hooks";
import { searchQuery, cleanupHighlight, highlightMatchedNodes } from "src/lib/utils/graph/search";
import useJC from "src/store/useJC";
import useGraph from "src/store/useGraph";
export const useFocusNode = () => {
const viewPort = useJC(state => state.graphRef?.viewPort);
const viewPort = useGraph(state => state.viewPort);
const [selectedNode, setSelectedNode] = React.useState(0);
const [nodeCount, setNodeCount] = React.useState(0);
const [value, setValue] = React.useState("");

View File

@ -4,7 +4,7 @@ import { MantineProvider, MantineThemeOverride } from "@mantine/core";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { monaSans } from "src/constants/fonts";
import { lightTheme, darkTheme } from "src/constants/theme";
import useStored from "src/store/useStored";
import useConfig from "src/store/useConfig";
const queryClient = new QueryClient({
defaultOptions: {
@ -52,13 +52,13 @@ const mantineTheme: MantineThemeOverride = {
};
export const EditorWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const lightmode = useStored(state => state.lightmode);
const darkmodeEnabled = useConfig(state => state.darkmodeEnabled);
return (
<ThemeProvider theme={lightmode ? lightTheme : darkTheme}>
<ThemeProvider theme={darkmodeEnabled ? darkTheme : lightTheme}>
<MantineProvider
theme={{
colorScheme: lightmode ? "light" : "dark",
colorScheme: darkmodeEnabled ? "dark" : "light",
...mantineTheme,
}}
withCSSVariables

View File

@ -11,7 +11,6 @@ const modalComponents: ModalComponent[] = [
{ key: "import", component: Modals.ImportModal },
{ key: "clear", component: Modals.ClearModal },
{ key: "download", component: Modals.DownloadModal },
{ key: "settings", component: Modals.SettingsModal },
{ key: "cloud", component: Modals.CloudModal },
{ key: "account", component: Modals.AccountModal },
{ key: "premium", component: Modals.PremiumModal },

View File

@ -1,6 +1,6 @@
import { firaMono } from "src/constants/fonts";
import useConfig from "src/store/useConfig";
import useGraph from "src/store/useGraph";
import useStored from "src/store/useStored";
type Text = string | [string, string][];
type Size = { width: number; height: number };
@ -54,8 +54,8 @@ setInterval(() => sizeCache.clear(), 120_000);
export const calculateNodeSize = (text: Text, isParent = false) => {
const { foldNodes } = useGraph.getState();
const { imagePreview } = useStored.getState();
const isImage = isContentImage(text) && imagePreview;
const { imagePreviewEnabled } = useConfig.getState();
const isImage = isContentImage(text) && imagePreviewEnabled;
const cacheKey = [text, isParent, foldNodes].toString();

View File

@ -3,7 +3,7 @@ import { csv2json, json2csv } from "json-2-csv";
import { parse } from "jsonc-parser";
import jxon from "jxon";
import toml from "toml";
import { FileFormat } from "src/enums/file";
import { FileFormat } from "src/enums/file.enum";
const keyExists = (obj: object, key: string) => {
if (!obj || (typeof obj !== "object" && !Array.isArray(obj))) {

View File

@ -1,6 +1,6 @@
import { PostgrestSingleResponse } from "@supabase/supabase-js";
import toast from "react-hot-toast";
import { FileFormat } from "src/enums/file";
import { FileFormat } from "src/enums/file.enum";
import { supabase } from "src/lib/api/supabase";
import { File } from "src/store/useFile";
import useUser from "src/store/useUser";

50
src/store/useConfig.ts Normal file
View File

@ -0,0 +1,50 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
import { ViewMode } from "src/enums/viewMode.enum";
import useGraph from "./useGraph";
const initialStates = {
darkmodeEnabled: false,
collapseButtonVisible: true,
childrenCountVisible: true,
imagePreviewEnabled: true,
liveTransformEnabled: true,
gesturesEnabled: false,
rulersEnabled: true,
viewMode: ViewMode.Graph,
};
export interface ConfigActions {
toggleDarkMode: (value: boolean) => void;
toggleCollapseButton: (value: boolean) => void;
toggleChildrenCount: (value: boolean) => void;
toggleImagePreview: (value: boolean) => void;
toggleLiveTransform: (value: boolean) => void;
toggleGestures: (value: boolean) => void;
toggleRulers: (value: boolean) => void;
setViewMode: (value: ViewMode) => void;
}
const useConfig = create(
persist<typeof initialStates & ConfigActions>(
set => ({
...initialStates,
toggleRulers: rulersEnabled => set({ rulersEnabled }),
toggleGestures: gesturesEnabled => set({ gesturesEnabled }),
toggleLiveTransform: liveTransformEnabled => set({ liveTransformEnabled }),
toggleDarkMode: darkmodeEnabled => set({ darkmodeEnabled }),
toggleCollapseButton: collapseButtonVisible => set({ collapseButtonVisible }),
toggleChildrenCount: childrenCountVisible => set({ childrenCountVisible }),
toggleImagePreview: imagePreviewEnabled => {
set({ imagePreviewEnabled });
useGraph.getState().setGraph();
},
setViewMode: viewMode => set({ viewMode }),
}),
{
name: "config",
}
)
);
export default useConfig;

View File

@ -5,13 +5,13 @@ import ReactGA from "react-ga4";
import { toast } from "react-hot-toast";
import { create } from "zustand";
import { defaultJson } from "src/constants/data";
import { FileFormat } from "src/enums/file";
import { FileFormat } from "src/enums/file.enum";
import { contentToJson, jsonToContent } from "src/lib/utils/json/jsonAdapter";
import { isIframe } from "src/lib/utils/widget";
import { documentSvc } from "src/services/document.service";
import useConfig from "./useConfig";
import useGraph from "./useGraph";
import useJson from "./useJson";
import useStored from "./useStored";
import useUser from "./useUser";
type SetContents = {
@ -125,7 +125,7 @@ const useFile = create<FileStates & JsonActions>()((set, get) => ({
const isFetchURL = window.location.href.includes("?");
const json = await contentToJson(get().contents, get().format);
if (!useStored.getState().liveTransform && skipUpdate) return;
if (!useConfig.getState().liveTransformEnabled && skipUpdate) return;
if (get().hasChanges && contents && contents.length < 80_000 && !isIframe() && !isFetchURL) {
sessionStorage.setItem("content", contents);

View File

@ -61,6 +61,7 @@ interface GraphActions {
zoomOut: () => void;
centerView: () => void;
clearGraph: () => void;
setZoomFactor: (zoomFactor: number) => void;
}
const useGraph = create<Graph & GraphActions>((set, get) => ({
@ -193,6 +194,10 @@ const useGraph = create<Graph & GraphActions>((set, get) => ({
elementExtraMarginForZoom: 100,
});
},
setZoomFactor: zoomFactor => {
const viewPort = get().viewPort;
viewPort?.camera?.recenter(viewPort.centerX, viewPort.centerY, zoomFactor);
},
zoomIn: () => {
const viewPort = get().viewPort;
viewPort?.camera?.recenter(viewPort.centerX, viewPort.centerY, viewPort.zoomFactor + 0.1);

View File

@ -1,19 +0,0 @@
import { GraphRef } from "jsongraph-react";
import { create } from "zustand";
interface JCActions {
setJCRef: (graphRef: GraphRef) => void;
}
const initialStates = {
graphRef: null as unknown as GraphRef,
};
export type JCStates = typeof initialStates;
const useJC = create<JCStates & JCActions>()(set => ({
...initialStates,
setJCRef: graphRef => set({ graphRef }),
}));
export default useJC;

View File

@ -18,7 +18,6 @@ const initialStates: ModalState = {
import: false,
account: false,
node: false,
settings: false,
share: false,
login: false,
premium: false,

View File

@ -1,46 +0,0 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
import useGraph from "./useGraph";
const initialStates = {
lightmode: false,
hideCollapse: true,
childrenCount: true,
imagePreview: true,
liveTransform: true,
gesturesEnabled: false,
};
export interface ConfigActions {
setLightTheme: (theme: boolean) => void;
toggleHideCollapse: (value: boolean) => void;
toggleChildrenCount: (value: boolean) => void;
toggleImagePreview: (value: boolean) => void;
toggleLiveTransform: (value: boolean) => void;
toggleGestures: (value: boolean) => void;
}
const useStored = create(
persist<typeof initialStates & ConfigActions>(
set => ({
...initialStates,
toggleGestures: gesturesEnabled => set({ gesturesEnabled }),
toggleLiveTransform: liveTransform => set({ liveTransform }),
setLightTheme: (value: boolean) =>
set({
lightmode: value,
}),
toggleHideCollapse: (value: boolean) => set({ hideCollapse: value }),
toggleChildrenCount: (value: boolean) => set({ childrenCount: value }),
toggleImagePreview: (value: boolean) => {
set({ imagePreview: value });
useGraph.getState().setGraph();
},
}),
{
name: "config",
}
)
);
export default useStored;

705
yarn.lock

File diff suppressed because it is too large Load Diff