mirror of
https://github.com/AykutSarac/jsoncrack.com.git
synced 2025-01-27 15:22:56 +08:00
feat: update ui
This commit is contained in:
parent
4895d235f7
commit
6ab9338709
@ -22,5 +22,6 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"extends": ["next/core-web-vitals", "prettier"],
|
"extends": ["next/core-web-vitals", "prettier"],
|
||||||
"plugins": ["prettier", "unused-imports"]
|
"plugins": ["prettier", "unused-imports"],
|
||||||
|
"ignorePatterns": ["src/enums"]
|
||||||
}
|
}
|
||||||
|
6
LICENSE
6
LICENSE
@ -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
|
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.
|
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.>
|
JSON Crack
|
||||||
Copyright (C) <year> <name of author>
|
Copyright (C) 2023 Aykut Saraç
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
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
|
If the program does terminal interaction, make it output a short
|
||||||
notice like this when it starts in an interactive mode:
|
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 program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||||
This is free software, and you are welcome to redistribute it
|
This is free software, and you are welcome to redistribute it
|
||||||
under certain conditions; type `show c' for details.
|
under certain conditions; type `show c' for details.
|
||||||
|
36
package.json
36
package.json
@ -2,6 +2,7 @@
|
|||||||
"name": "json-crack",
|
"name": "json-crack",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "v3.0.0",
|
"version": "v3.0.0",
|
||||||
|
"license": "GPL-3.0",
|
||||||
"author": "https://github.com/AykutSarac",
|
"author": "https://github.com/AykutSarac",
|
||||||
"homepage": "https://jsoncrack.com",
|
"homepage": "https://jsoncrack.com",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@ -19,22 +20,21 @@
|
|||||||
"@mantine/hooks": "^6.0.17",
|
"@mantine/hooks": "^6.0.17",
|
||||||
"@mantine/next": "^6.0.21",
|
"@mantine/next": "^6.0.21",
|
||||||
"@mantine/prism": "^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",
|
"@sentry/nextjs": "^7.72.0",
|
||||||
"@supabase/auth-helpers-nextjs": "^0.8.1",
|
"@supabase/auth-helpers-nextjs": "^0.8.1",
|
||||||
"@supabase/auth-helpers-react": "^0.4.2",
|
"@supabase/auth-helpers-react": "^0.4.2",
|
||||||
"@supabase/supabase-js": "^2.36.0",
|
"@supabase/supabase-js": "^2.36.0",
|
||||||
"@tanstack/react-query": "^4.35.3",
|
"@tanstack/react-query": "^4.35.3",
|
||||||
"allotment": "^1.19.2",
|
"allotment": "^1.19.3",
|
||||||
"axios": "^1.5.0",
|
"axios": "^1.5.0",
|
||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
"html-to-image": "^1.11.11",
|
"html-to-image": "^1.11.11",
|
||||||
"jq-in-the-browser": "^0.7.2",
|
"jq-in-the-browser": "^0.7.2",
|
||||||
"jq-web": "^0.5.1",
|
"jq-web": "^0.5.1",
|
||||||
"json-2-csv": "^4.1.0",
|
"json-2-csv": "^5.0.1",
|
||||||
"json-to-ts": "^1.7.0",
|
"json-to-ts": "^1.7.0",
|
||||||
"jsonc-parser": "^3.2.0",
|
"jsonc-parser": "^3.2.0",
|
||||||
"jsongraph-react": "^0.0.12",
|
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"jxon": "^2.0.0-beta.5",
|
"jxon": "^2.0.0-beta.5",
|
||||||
"lodash.debounce": "^4.0.8",
|
"lodash.debounce": "^4.0.8",
|
||||||
@ -46,35 +46,33 @@
|
|||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-ga4": "^2.1.0",
|
"react-ga4": "^2.1.0",
|
||||||
"react-hot-toast": "^2.4.1",
|
"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-linkify-it": "^1.0.7",
|
||||||
"react-simple-typewriter": "^5.0.1",
|
"react-simple-typewriter": "^5.0.1",
|
||||||
"react-zoomable-ui": "^0.11.0",
|
"react-zoomable-ui": "^0.11.0",
|
||||||
"reaflow": "5.2.6",
|
"reaflow": "5.2.8",
|
||||||
"styled-components": "^6.0.8",
|
"styled-components": "^6.1.1",
|
||||||
"toml": "^3.0.0",
|
"toml": "^3.0.0",
|
||||||
"use-long-press": "^3.1.5",
|
"use-long-press": "^3.1.5",
|
||||||
"zustand": "^4.4.0"
|
"zustand": "^4.4.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@next/bundle-analyzer": "^13.4.12",
|
"@next/bundle-analyzer": "^13.4.12",
|
||||||
"@testing-library/react": "^14.0.0",
|
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
||||||
"@trivago/prettier-plugin-sort-imports": "^4.2.0",
|
"@types/jsonwebtoken": "^9.0.5",
|
||||||
"@types/jsonwebtoken": "^9.0.3",
|
"@types/jxon": "^2.0.5",
|
||||||
"@types/jxon": "^2.0.2",
|
"@types/lodash.debounce": "^4.0.9",
|
||||||
"@types/lodash.debounce": "^4.0.7",
|
"@types/lodash.get": "^4.4.9",
|
||||||
"@types/lodash.get": "^4.4.7",
|
"@types/lodash.set": "^4.3.9",
|
||||||
"@types/lodash.set": "^4.3.7",
|
|
||||||
"@types/node": "^20.4.7",
|
"@types/node": "^20.4.7",
|
||||||
"@types/react": "18.2.18",
|
"@types/react": "18.2.18",
|
||||||
"@types/react-color": "^3.0.6",
|
"eslint": "8.56.0",
|
||||||
"@types/react-syntax-highlighter": "^15.5.7",
|
|
||||||
"eslint": "8.46.0",
|
|
||||||
"eslint-config-next": "13.4.12",
|
"eslint-config-next": "13.4.12",
|
||||||
"eslint-config-prettier": "^8.10.0",
|
"eslint-config-prettier": "^8.10.0",
|
||||||
"eslint-plugin-prettier": "^5.0.0",
|
"eslint-plugin-prettier": "^5.0.0",
|
||||||
"eslint-plugin-unused-imports": "^3.0.0",
|
"eslint-plugin-unused-imports": "^3.0.0",
|
||||||
"prettier": "^3.0.1",
|
"prettier": "^3.1.1",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"typescript": "5.1.6"
|
"typescript": "5.1.6"
|
||||||
}
|
}
|
||||||
|
@ -4,8 +4,8 @@ import { MdLink, MdLinkOff } from "react-icons/md";
|
|||||||
import { CustomNodeProps } from "src/components/Graph/CustomNode";
|
import { CustomNodeProps } from "src/components/Graph/CustomNode";
|
||||||
import useToggleHide from "src/hooks/useToggleHide";
|
import useToggleHide from "src/hooks/useToggleHide";
|
||||||
import { isContentImage } from "src/lib/utils/graph/calculateNodeSize";
|
import { isContentImage } from "src/lib/utils/graph/calculateNodeSize";
|
||||||
|
import useConfig from "src/store/useConfig";
|
||||||
import useGraph from "src/store/useGraph";
|
import useGraph from "src/store/useGraph";
|
||||||
import useStored from "src/store/useStored";
|
|
||||||
import { TextRenderer } from "./TextRenderer";
|
import { TextRenderer } from "./TextRenderer";
|
||||||
import * as Styled from "./styles";
|
import * as Styled from "./styles";
|
||||||
|
|
||||||
@ -52,13 +52,13 @@ const Node: React.FC<CustomNodeProps> = ({ node, x, y, hasCollapse = false }) =>
|
|||||||
data: { isParent, childrenCount, type },
|
data: { isParent, childrenCount, type },
|
||||||
} = node;
|
} = node;
|
||||||
const { validateHiddenNodes } = useToggleHide();
|
const { validateHiddenNodes } = useToggleHide();
|
||||||
const hideCollapse = useStored(state => state.hideCollapse);
|
const collapseButtonVisible = useConfig(state => state.collapseButtonVisible);
|
||||||
const showChildrenCount = useStored(state => state.childrenCount);
|
const childrenCountVisible = useConfig(state => state.childrenCountVisible);
|
||||||
const imagePreview = useStored(state => state.imagePreview);
|
const imagePreviewEnabled = useConfig(state => state.imagePreviewEnabled);
|
||||||
const expandNodes = useGraph(state => state.expandNodes);
|
const expandNodes = useGraph(state => state.expandNodes);
|
||||||
const collapseNodes = useGraph(state => state.collapseNodes);
|
const collapseNodes = useGraph(state => state.collapseNodes);
|
||||||
const isExpanded = useGraph(state => state.collapsedParents.includes(id));
|
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 value = JSON.stringify(text).replaceAll('"', "");
|
||||||
|
|
||||||
const handleExpand = (e: React.MouseEvent<HTMLButtonElement>) => {
|
const handleExpand = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
@ -80,16 +80,16 @@ const Node: React.FC<CustomNodeProps> = ({ node, x, y, hasCollapse = false }) =>
|
|||||||
data-x={x}
|
data-x={x}
|
||||||
data-y={y}
|
data-y={y}
|
||||||
data-key={JSON.stringify(text)}
|
data-key={JSON.stringify(text)}
|
||||||
$hasCollapse={isParent && hideCollapse}
|
$hasCollapse={isParent && collapseButtonVisible}
|
||||||
>
|
>
|
||||||
<Styled.StyledKey $value={value} $parent={isParent} $type={type}>
|
<Styled.StyledKey $value={value} $parent={isParent} $type={type}>
|
||||||
<TextRenderer>{value}</TextRenderer>
|
<TextRenderer>{value}</TextRenderer>
|
||||||
</Styled.StyledKey>
|
</Styled.StyledKey>
|
||||||
{isParent && childrenCount > 0 && showChildrenCount && (
|
{isParent && childrenCount > 0 && childrenCountVisible && (
|
||||||
<Styled.StyledChildrenCount>({childrenCount})</Styled.StyledChildrenCount>
|
<Styled.StyledChildrenCount>({childrenCount})</Styled.StyledChildrenCount>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isParent && hasCollapse && hideCollapse && (
|
{isParent && hasCollapse && collapseButtonVisible && (
|
||||||
<StyledExpand aria-label="Expand" onClick={handleExpand}>
|
<StyledExpand aria-label="Expand" onClick={handleExpand}>
|
||||||
{isExpanded ? <MdLinkOff size={18} /> : <MdLink size={18} />}
|
{isExpanded ? <MdLinkOff size={18} /> : <MdLink size={18} />}
|
||||||
</StyledExpand>
|
</StyledExpand>
|
||||||
|
@ -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;
|
/(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 }) => {
|
export const TextRenderer: React.FC<{ children: string }> = ({ children }) => {
|
||||||
if (isURL.test(children.replaceAll('"', ""))) {
|
if (isURL.test(children?.replaceAll('"', ""))) {
|
||||||
return <Styled.StyledLinkItUrl>{children}</Styled.StyledLinkItUrl>;
|
return <Styled.StyledLinkItUrl>{children}</Styled.StyledLinkItUrl>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isColorFormat(children.replaceAll('"', ""))) {
|
if (isColorFormat(children?.replaceAll('"', ""))) {
|
||||||
return (
|
return (
|
||||||
<StyledRow>
|
<StyledRow>
|
||||||
<ColorSwatch radius={4} h={12} w={12} color={children.replaceAll('"', "")} />
|
<ColorSwatch radius={4} h={12} w={12} mr={8} color={children?.replaceAll('"', "")} />
|
||||||
{children.replaceAll('"', "")}
|
{children?.replaceAll('"', "")}
|
||||||
</StyledRow>
|
</StyledRow>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,16 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
import { toast } from "react-hot-toast";
|
||||||
import { Space } from "react-zoomable-ui";
|
import { Space } from "react-zoomable-ui";
|
||||||
import { ElkRoot } from "reaflow/dist/layout/useLayout";
|
import { ElkRoot } from "reaflow/dist/layout/useLayout";
|
||||||
import { useLongPress } from "use-long-press";
|
import { useLongPress } from "use-long-press";
|
||||||
import { CustomNode } from "src/components/Graph/CustomNode";
|
import { CustomNode } from "src/components/Graph/CustomNode";
|
||||||
|
import { ViewMode } from "src/enums/viewMode.enum";
|
||||||
import useToggleHide from "src/hooks/useToggleHide";
|
import useToggleHide from "src/hooks/useToggleHide";
|
||||||
import { Loading } from "src/layout/Loading";
|
import { Loading } from "src/layout/Loading";
|
||||||
|
import useConfig from "src/store/useConfig";
|
||||||
import useGraph from "src/store/useGraph";
|
import useGraph from "src/store/useGraph";
|
||||||
import useStored from "src/store/useStored";
|
|
||||||
import useUser from "src/store/useUser";
|
import useUser from "src/store/useUser";
|
||||||
import { NodeData } from "src/types/graph";
|
import { NodeData } from "src/types/graph";
|
||||||
import { CustomEdge } from "./CustomEdge";
|
import { CustomEdge } from "./CustomEdge";
|
||||||
@ -23,7 +25,7 @@ interface GraphProps {
|
|||||||
isWidget?: boolean;
|
isWidget?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StyledEditorWrapper = styled.div<{ $widget: boolean }>`
|
const StyledEditorWrapper = styled.div<{ $widget: boolean; $showRulers: boolean }>`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: ${({ $widget }) => ($widget ? "calc(100vh - 36px)" : "calc(100vh - 63px)")};
|
height: ${({ $widget }) => ($widget ? "calc(100vh - 36px)" : "calc(100vh - 63px)")};
|
||||||
@ -33,6 +35,9 @@ const StyledEditorWrapper = styled.div<{ $widget: boolean }>`
|
|||||||
--line-color-2: ${({ theme }) => theme.GRID_COLOR_SECONDARY};
|
--line-color-2: ${({ theme }) => theme.GRID_COLOR_SECONDARY};
|
||||||
|
|
||||||
background-color: var(--bg-color);
|
background-color: var(--bg-color);
|
||||||
|
${({ $showRulers }) =>
|
||||||
|
$showRulers &&
|
||||||
|
`
|
||||||
background-image: linear-gradient(var(--line-color-1) 1.5px, transparent 1.5px),
|
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(90deg, var(--line-color-1) 1.5px, transparent 1.5px),
|
||||||
linear-gradient(var(--line-color-2) 1px, transparent 1px),
|
linear-gradient(var(--line-color-2) 1px, transparent 1px),
|
||||||
@ -47,6 +52,7 @@ const StyledEditorWrapper = styled.div<{ $widget: boolean }>`
|
|||||||
100px 100px,
|
100px 100px,
|
||||||
20px 20px,
|
20px 20px,
|
||||||
20px 20px;
|
20px 20px;
|
||||||
|
`};
|
||||||
|
|
||||||
:active {
|
:active {
|
||||||
cursor: move;
|
cursor: move;
|
||||||
@ -76,7 +82,8 @@ const layoutOptions = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const PREMIUM_LIMIT = 200;
|
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 GraphCanvas = ({ isWidget }: GraphProps) => {
|
||||||
const { validateHiddenNodes } = useToggleHide();
|
const { validateHiddenNodes } = useToggleHide();
|
||||||
@ -138,6 +145,7 @@ const GraphCanvas = ({ isWidget }: GraphProps) => {
|
|||||||
|
|
||||||
function getViewType(nodes: NodeData[]) {
|
function getViewType(nodes: NodeData[]) {
|
||||||
if (nodes.length > ERROR_LIMIT) return "error";
|
if (nodes.length > ERROR_LIMIT) return "error";
|
||||||
|
if (nodes.length > ERROR_LIMIT_TREE) return "tree";
|
||||||
if (nodes.length > PREMIUM_LIMIT) return "premium";
|
if (nodes.length > PREMIUM_LIMIT) return "premium";
|
||||||
return "graph";
|
return "graph";
|
||||||
}
|
}
|
||||||
@ -147,7 +155,9 @@ export const Graph = ({ isWidget = false }: GraphProps) => {
|
|||||||
const loading = useGraph(state => state.loading);
|
const loading = useGraph(state => state.loading);
|
||||||
const isPremium = useUser(state => state.premium);
|
const isPremium = useUser(state => state.premium);
|
||||||
const viewType = useGraph(state => getViewType(state.nodes));
|
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 callback = React.useCallback(() => {
|
||||||
const canvas = document.querySelector(".jsoncrack-canvas") as HTMLDivElement | null;
|
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 ("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 (viewType === "premium" && !isWidget) {
|
||||||
if (!isPremium) return <PremiumView />;
|
if (!isPremium) return <PremiumView />;
|
||||||
@ -180,6 +197,7 @@ export const Graph = ({ isWidget = false }: GraphProps) => {
|
|||||||
onContextMenu={e => e.preventDefault()}
|
onContextMenu={e => e.preventDefault()}
|
||||||
onClick={blurOnClick}
|
onClick={blurOnClick}
|
||||||
key={String(gesturesEnabled)}
|
key={String(gesturesEnabled)}
|
||||||
|
$showRulers={rulersEnabled}
|
||||||
{...bindLongPress()}
|
{...bindLongPress()}
|
||||||
>
|
>
|
||||||
<Space
|
<Space
|
||||||
|
@ -2,8 +2,8 @@ import React from "react";
|
|||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import Editor, { loader, useMonaco } from "@monaco-editor/react";
|
import Editor, { loader, useMonaco } from "@monaco-editor/react";
|
||||||
import { Loading } from "src/layout/Loading";
|
import { Loading } from "src/layout/Loading";
|
||||||
|
import useConfig from "src/store/useConfig";
|
||||||
import useFile from "src/store/useFile";
|
import useFile from "src/store/useFile";
|
||||||
import useStored from "src/store/useStored";
|
|
||||||
|
|
||||||
loader.config({
|
loader.config({
|
||||||
paths: {
|
paths: {
|
||||||
@ -33,7 +33,7 @@ export const MonacoEditor = () => {
|
|||||||
const setError = useFile(state => state.setError);
|
const setError = useFile(state => state.setError);
|
||||||
const jsonSchema = useFile(state => state.jsonSchema);
|
const jsonSchema = useFile(state => state.jsonSchema);
|
||||||
const getHasChanges = useFile(state => state.getHasChanges);
|
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);
|
const fileType = useFile(state => state.format);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
@ -11,22 +11,15 @@ import {
|
|||||||
AiOutlineLock,
|
AiOutlineLock,
|
||||||
AiOutlineUnlock,
|
AiOutlineUnlock,
|
||||||
} from "react-icons/ai";
|
} from "react-icons/ai";
|
||||||
|
import { BiSolidDockLeft } from "react-icons/bi";
|
||||||
import { MdOutlineCheckCircleOutline } from "react-icons/md";
|
import { MdOutlineCheckCircleOutline } from "react-icons/md";
|
||||||
import { TbTransform } from "react-icons/tb";
|
import { TbTransform } from "react-icons/tb";
|
||||||
import {
|
import { VscError, VscFeedback, VscSourceControl, VscSync, VscSyncIgnored } from "react-icons/vsc";
|
||||||
VscAccount,
|
|
||||||
VscError,
|
|
||||||
VscFeedback,
|
|
||||||
VscSourceControl,
|
|
||||||
VscSync,
|
|
||||||
VscSyncIgnored,
|
|
||||||
VscWorkspaceTrusted,
|
|
||||||
} from "react-icons/vsc";
|
|
||||||
import { documentSvc } from "src/services/document.service";
|
import { documentSvc } from "src/services/document.service";
|
||||||
|
import useConfig from "src/store/useConfig";
|
||||||
import useFile from "src/store/useFile";
|
import useFile from "src/store/useFile";
|
||||||
import useGraph from "src/store/useGraph";
|
import useGraph from "src/store/useGraph";
|
||||||
import useModal from "src/store/useModal";
|
import useModal from "src/store/useModal";
|
||||||
import useStored from "src/store/useStored";
|
|
||||||
import useUser from "src/store/useUser";
|
import useUser from "src/store/useUser";
|
||||||
|
|
||||||
const StyledBottomBar = styled.div`
|
const StyledBottomBar = styled.div`
|
||||||
@ -51,6 +44,7 @@ const StyledLeft = styled.div`
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: left;
|
justify-content: left;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
|
padding-left: 8px;
|
||||||
|
|
||||||
@media screen and (max-width: 480px) {
|
@media screen and (max-width: 480px) {
|
||||||
display: none;
|
display: none;
|
||||||
@ -95,15 +89,16 @@ export const BottomBar = () => {
|
|||||||
const { query, replace } = useRouter();
|
const { query, replace } = useRouter();
|
||||||
const data = useFile(state => state.fileData);
|
const data = useFile(state => state.fileData);
|
||||||
const user = useUser(state => state.user);
|
const user = useUser(state => state.user);
|
||||||
const premium = useUser(state => state.premium);
|
const toggleLiveTransform = useConfig(state => state.toggleLiveTransform);
|
||||||
const toggleLiveTransform = useStored(state => state.toggleLiveTransform);
|
const liveTransformEnabled = useConfig(state => state.liveTransformEnabled);
|
||||||
const liveTransform = useStored(state => state.liveTransform);
|
|
||||||
const hasChanges = useFile(state => state.hasChanges);
|
const hasChanges = useFile(state => state.hasChanges);
|
||||||
const error = useFile(state => state.error);
|
const error = useFile(state => state.error);
|
||||||
const getContents = useFile(state => state.getContents);
|
const getContents = useFile(state => state.getContents);
|
||||||
const setContents = useFile(state => state.setContents);
|
const setContents = useFile(state => state.setContents);
|
||||||
const nodeCount = useGraph(state => state.nodes.length);
|
const nodeCount = useGraph(state => state.nodes.length);
|
||||||
const fileName = useFile(state => state.fileData?.name);
|
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 setVisible = useModal(state => state.setVisible);
|
||||||
const setHasChanges = useFile(state => state.setHasChanges);
|
const setHasChanges = useFile(state => state.setHasChanges);
|
||||||
@ -111,6 +106,8 @@ export const BottomBar = () => {
|
|||||||
const [isPrivate, setIsPrivate] = React.useState(false);
|
const [isPrivate, setIsPrivate] = React.useState(false);
|
||||||
const [isUpdating, setIsUpdating] = React.useState(false);
|
const [isUpdating, setIsUpdating] = React.useState(false);
|
||||||
|
|
||||||
|
const toggleEditor = () => toggleFullscreen(!fullscreen);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
setIsPrivate(data?.private ?? true);
|
setIsPrivate(data?.private ?? true);
|
||||||
}, [data]);
|
}, [data]);
|
||||||
@ -191,20 +188,10 @@ export const BottomBar = () => {
|
|||||||
</Head>
|
</Head>
|
||||||
)}
|
)}
|
||||||
<StyledLeft>
|
<StyledLeft>
|
||||||
<StyledBottomBarItem $bg="#1864AB" onClick={handleLoginClick}>
|
<StyledBottomBarItem onClick={toggleEditor}>
|
||||||
<Flex align="center" gap={5} px={5}>
|
<BiSolidDockLeft />
|
||||||
<VscAccount color="white" />
|
|
||||||
<Text maw={120} c="white" truncate="end">
|
|
||||||
{user?.user_metadata.name ?? "Login"}
|
|
||||||
</Text>
|
|
||||||
</Flex>
|
|
||||||
</StyledBottomBarItem>
|
</StyledBottomBarItem>
|
||||||
{!premium && (
|
|
||||||
<StyledBottomBarItem onClick={() => setVisible("premium")(true)}>
|
|
||||||
<VscWorkspaceTrusted />
|
|
||||||
Upgrade to Premium
|
|
||||||
</StyledBottomBarItem>
|
|
||||||
)}
|
|
||||||
{fileName && (
|
{fileName && (
|
||||||
<StyledBottomBarItem onClick={() => setVisible("cloud")(true)}>
|
<StyledBottomBarItem onClick={() => setVisible("cloud")(true)}>
|
||||||
<VscSourceControl />
|
<VscSourceControl />
|
||||||
@ -236,7 +223,7 @@ export const BottomBar = () => {
|
|||||||
{(data?.owner_email === user?.email || (!data && user)) && (
|
{(data?.owner_email === user?.email || (!data && user)) && (
|
||||||
<StyledBottomBarItem onClick={handleSaveJson} disabled={isUpdating || error}>
|
<StyledBottomBarItem onClick={handleSaveJson} disabled={isUpdating || error}>
|
||||||
{hasChanges ? <AiOutlineCloudUpload /> : <AiOutlineCloudSync />}
|
{hasChanges ? <AiOutlineCloudUpload /> : <AiOutlineCloudSync />}
|
||||||
{hasChanges ? (query?.json ? "Unsaved Changes" : "Create Document") : "Saved"}
|
{hasChanges || !user ? (query?.json ? "Unsaved Changes" : "Save to Cloud") : "Saved"}
|
||||||
</StyledBottomBarItem>
|
</StyledBottomBarItem>
|
||||||
)}
|
)}
|
||||||
{data?.owner_email === user?.email && (
|
{data?.owner_email === user?.email && (
|
||||||
@ -252,7 +239,7 @@ export const BottomBar = () => {
|
|||||||
<AiOutlineLink />
|
<AiOutlineLink />
|
||||||
Share
|
Share
|
||||||
</StyledBottomBarItem>
|
</StyledBottomBarItem>
|
||||||
{liveTransform ? (
|
{liveTransformEnabled ? (
|
||||||
<StyledBottomBarItem onClick={() => toggleLiveTransform(false)}>
|
<StyledBottomBarItem onClick={() => toggleLiveTransform(false)}>
|
||||||
<VscSync />
|
<VscSync />
|
||||||
<Text>Live Transform</Text>
|
<Text>Live Transform</Text>
|
||||||
@ -263,7 +250,7 @@ export const BottomBar = () => {
|
|||||||
<Text>Manual Transform</Text>
|
<Text>Manual Transform</Text>
|
||||||
</StyledBottomBarItem>
|
</StyledBottomBarItem>
|
||||||
)}
|
)}
|
||||||
{!liveTransform && (
|
{!liveTransformEnabled && (
|
||||||
<StyledBottomBarItem onClick={() => setContents({})}>
|
<StyledBottomBarItem onClick={() => setContents({})}>
|
||||||
<TbTransform />
|
<TbTransform />
|
||||||
Transform
|
Transform
|
||||||
|
@ -123,7 +123,7 @@ const PromptInput = () => {
|
|||||||
export const JsonEditor: React.FC = () => {
|
export const JsonEditor: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<StyledEditorWrapper>
|
<StyledEditorWrapper>
|
||||||
<PromptInput />
|
{/* <PromptInput /> */}
|
||||||
<MonacoEditor />
|
<MonacoEditor />
|
||||||
</StyledEditorWrapper>
|
</StyledEditorWrapper>
|
||||||
);
|
);
|
||||||
|
@ -1,36 +1,51 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { Flex, Group, MediaQuery, Menu, Select, Text } from "@mantine/core";
|
import {
|
||||||
import { useHotkeys } from "@mantine/hooks";
|
Avatar,
|
||||||
|
Flex,
|
||||||
|
Group,
|
||||||
|
Input,
|
||||||
|
MediaQuery,
|
||||||
|
Menu,
|
||||||
|
SegmentedControl,
|
||||||
|
Select,
|
||||||
|
Text,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { getHotkeyHandler, useHotkeys } from "@mantine/hooks";
|
||||||
import ReactGA from "react-ga4";
|
import ReactGA from "react-ga4";
|
||||||
import toast from "react-hot-toast";
|
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 { CgArrowsMergeAltH, CgArrowsShrinkH, CgChevronDown } from "react-icons/cg";
|
||||||
import { FiDownload } from "react-icons/fi";
|
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 { SiJsonwebtokens } from "react-icons/si";
|
||||||
import { TiFlowMerge } from "react-icons/ti";
|
import { TiFlowMerge } from "react-icons/ti";
|
||||||
import {
|
import {
|
||||||
VscCollapseAll,
|
VscCollapseAll,
|
||||||
VscExpandAll,
|
VscExpandAll,
|
||||||
VscJson,
|
VscJson,
|
||||||
VscLayoutSidebarLeft,
|
|
||||||
VscLayoutSidebarLeftOff,
|
|
||||||
VscSettingsGear,
|
|
||||||
VscTarget,
|
VscTarget,
|
||||||
VscSearchFuzzy,
|
VscSearchFuzzy,
|
||||||
VscGroupByRefType,
|
VscGroupByRefType,
|
||||||
|
VscSignOut,
|
||||||
|
VscFeedback,
|
||||||
|
VscSignIn,
|
||||||
} from "react-icons/vsc";
|
} from "react-icons/vsc";
|
||||||
import { SearchInput } from "src/components/SearchInput";
|
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 { JSONCrackLogo } from "src/layout/JsonCrackLogo";
|
||||||
import { getNextDirection } from "src/lib/utils/graph/getNextDirection";
|
import { getNextDirection } from "src/lib/utils/graph/getNextDirection";
|
||||||
import { isIframe } from "src/lib/utils/widget";
|
import { isIframe } from "src/lib/utils/widget";
|
||||||
|
import useConfig from "src/store/useConfig";
|
||||||
import useFile from "src/store/useFile";
|
import useFile from "src/store/useFile";
|
||||||
import useGraph from "src/store/useGraph";
|
import useGraph from "src/store/useGraph";
|
||||||
import useJC from "src/store/useJC";
|
|
||||||
import useJson from "src/store/useJson";
|
import useJson from "src/store/useJson";
|
||||||
import useModal from "src/store/useModal";
|
import useModal from "src/store/useModal";
|
||||||
|
import useUser from "src/store/useUser";
|
||||||
|
|
||||||
export const StyledTools = styled.div`
|
export const StyledTools = styled.div`
|
||||||
position: relative;
|
position: relative;
|
||||||
@ -94,17 +109,13 @@ function fullscreenBrowser() {
|
|||||||
const ViewMenu = () => {
|
const ViewMenu = () => {
|
||||||
const [coreKey, setCoreKey] = React.useState("CTRL");
|
const [coreKey, setCoreKey] = React.useState("CTRL");
|
||||||
const toggleFold = useGraph(state => state.toggleFold);
|
const toggleFold = useGraph(state => state.toggleFold);
|
||||||
const setDirection = useJC(state => state.graphRef?.setDirection);
|
const setDirection = useGraph(state => state.setDirection);
|
||||||
const direction = useJC(state => state.graphRef?.direction);
|
const direction = useGraph(state => state.direction);
|
||||||
const expandGraph = useJC(state => state.graphRef?.expandGraph);
|
const expandGraph = useGraph(state => state.expandGraph);
|
||||||
const collapseGraph = useJC(state => state.graphRef?.collapseGraph);
|
const collapseGraph = useGraph(state => state.collapseGraph);
|
||||||
const toggleFullscreen = useGraph(state => state.toggleFullscreen);
|
const focusFirstNode = useGraph(state => state.focusFirstNode);
|
||||||
const focusFirstNode = useJC(state => state.graphRef?.focusFirstNode);
|
|
||||||
const foldNodes = useGraph(state => state.foldNodes);
|
const foldNodes = useGraph(state => state.foldNodes);
|
||||||
const graphCollapsed = useJC(state => state.graphRef?.graphCollapsed);
|
const graphCollapsed = useGraph(state => state.graphCollapsed);
|
||||||
const fullscreen = useGraph(state => state.fullscreen);
|
|
||||||
|
|
||||||
const toggleEditor = () => toggleFullscreen(!fullscreen);
|
|
||||||
|
|
||||||
const toggleFoldNodes = () => {
|
const toggleFoldNodes = () => {
|
||||||
toggleFold(!foldNodes);
|
toggleFold(!foldNodes);
|
||||||
@ -124,7 +135,6 @@ const ViewMenu = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useHotkeys([
|
useHotkeys([
|
||||||
["mod+shift+e", toggleEditor],
|
|
||||||
["mod+shift+d", toggleDirection],
|
["mod+shift+d", toggleDirection],
|
||||||
["mod+shift+f", toggleFoldNodes],
|
["mod+shift+f", toggleFoldNodes],
|
||||||
["mod+shift+c", toggleExpandCollapseGraph],
|
["mod+shift+c", toggleExpandCollapseGraph],
|
||||||
@ -144,7 +154,7 @@ const ViewMenu = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu shadow="md" closeOnItemClick={false}>
|
<Menu shadow="md" closeOnItemClick={false} withArrow>
|
||||||
<Menu.Target>
|
<Menu.Target>
|
||||||
<StyledToolElement>
|
<StyledToolElement>
|
||||||
<Flex align="center" gap={3}>
|
<Flex align="center" gap={3}>
|
||||||
@ -153,25 +163,6 @@ const ViewMenu = () => {
|
|||||||
</StyledToolElement>
|
</StyledToolElement>
|
||||||
</Menu.Target>
|
</Menu.Target>
|
||||||
<Menu.Dropdown>
|
<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
|
<Menu.Item
|
||||||
fz={12}
|
fz={12}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -238,16 +229,50 @@ const ViewMenu = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const Tools: React.FC<{ isWidget?: boolean }> = ({ isWidget = false }) => {
|
export const Tools: React.FC<{ isWidget?: boolean }> = ({ isWidget = false }) => {
|
||||||
|
const { push } = useRouter();
|
||||||
|
|
||||||
const getJson = useJson(state => state.getJson);
|
const getJson = useJson(state => state.getJson);
|
||||||
const setVisible = useModal(state => state.setVisible);
|
const setVisible = useModal(state => state.setVisible);
|
||||||
const centerView = useJC(state => state.graphRef?.centerView);
|
const centerView = useGraph(state => state.centerView);
|
||||||
const zoomIn = useJC(state => state.graphRef?.zoomIn);
|
const zoomIn = useGraph(state => state.zoomIn);
|
||||||
const zoomOut = useJC(state => state.graphRef?.zoomOut);
|
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 setFormat = useFile(state => state.setFormat);
|
||||||
const format = useFile(state => state.format);
|
const format = useFile(state => state.format);
|
||||||
|
|
||||||
|
const [tempZoomValue, setTempZoomValue] = React.useState(zoomFactor);
|
||||||
const [logoURL, setLogoURL] = React.useState("CTRL");
|
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(() => {
|
React.useEffect(() => {
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
const url = !isIframe()
|
const url = !isIframe()
|
||||||
@ -303,11 +328,30 @@ export const Tools: React.FC<{ isWidget?: boolean }> = ({ isWidget = false }) =>
|
|||||||
{ value: FileFormat.CSV, label: "CSV" },
|
{ 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)}>
|
<StyledToolElement title="Import File" onClick={() => setVisible("import")(true)}>
|
||||||
Import
|
Import
|
||||||
</StyledToolElement>
|
</StyledToolElement>
|
||||||
<ViewMenu />
|
<ViewMenu />
|
||||||
<Menu shadow="md">
|
<Menu shadow="md" withArrow>
|
||||||
<Menu.Target>
|
<Menu.Target>
|
||||||
<StyledToolElement>
|
<StyledToolElement>
|
||||||
<Flex align="center" gap={3}>
|
<Flex align="center" gap={3}>
|
||||||
@ -348,31 +392,170 @@ export const Tools: React.FC<{ isWidget?: boolean }> = ({ isWidget = false }) =>
|
|||||||
</MediaQuery>
|
</MediaQuery>
|
||||||
)}
|
)}
|
||||||
<Group spacing="xs" position="right" w="100%" noWrap>
|
<Group spacing="xs" position="right" w="100%" noWrap>
|
||||||
<StyledToolElement title="Zoom Out" onClick={zoomOut}>
|
<SearchInput />
|
||||||
<AiOutlineMinus size="18" />
|
|
||||||
</StyledToolElement>
|
|
||||||
<StyledToolElement title="Zoom In" onClick={zoomIn}>
|
|
||||||
<AiOutlinePlus size="18" />
|
|
||||||
</StyledToolElement>
|
|
||||||
{!isWidget && (
|
{!isWidget && (
|
||||||
<StyledToolElement title="Save as Image" onClick={() => setVisible("download")(true)}>
|
<StyledToolElement title="Save as Image" onClick={() => setVisible("download")(true)}>
|
||||||
<FiDownload size="18" />
|
<FiDownload size="18" />
|
||||||
</StyledToolElement>
|
</StyledToolElement>
|
||||||
)}
|
)}
|
||||||
<StyledToolElement title="Center Canvas" onClick={centerView}>
|
|
||||||
<MdCenterFocusWeak size="18" />
|
{!isWidget && (
|
||||||
|
<Menu shadow="md" trigger="click" closeOnItemClick={false} withArrow>
|
||||||
|
<Menu.Target>
|
||||||
|
<StyledToolElement>
|
||||||
|
<Flex gap={4}>
|
||||||
|
{Math.round(zoomFactor * 100)}%
|
||||||
|
<CgChevronDown />
|
||||||
|
</Flex>
|
||||||
</StyledToolElement>
|
</StyledToolElement>
|
||||||
<SearchInput />
|
</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}>
|
<StyledToolElement title="Fullscreen" $hide={isWidget} onClick={fullscreenBrowser}>
|
||||||
<AiOutlineFullscreen size="18" />
|
<AiOutlineFullscreen size="18" />
|
||||||
</StyledToolElement>
|
</StyledToolElement>
|
||||||
<StyledToolElement
|
)}
|
||||||
title="Settings"
|
|
||||||
$hide={isWidget}
|
|
||||||
onClick={() => setVisible("settings")(true)}
|
|
||||||
>
|
|
||||||
<VscSettingsGear size="18" />
|
|
||||||
</StyledToolElement>
|
|
||||||
</Group>
|
</Group>
|
||||||
</StyledTools>
|
</StyledTools>
|
||||||
);
|
);
|
||||||
|
@ -1,87 +1,145 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import styled from "styled-components";
|
import styled, { DefaultTheme, useTheme } from "styled-components";
|
||||||
import { JSONGraph } from "jsongraph-react";
|
import { Menu, Text } from "@mantine/core";
|
||||||
import useGraph from "src/store/useGraph";
|
import { JSONTree } from "react-json-tree";
|
||||||
import useJC from "src/store/useJC";
|
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 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`
|
const StyledLiveEditor = styled.div`
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 100%;
|
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 = () => {
|
type TextColorFn = {
|
||||||
const lightmode = useStored(state => state.lightmode);
|
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 json = useJson(state => state.json);
|
||||||
const setJCRef = useJC(state => state.setJCRef);
|
const viewMode = useConfig(state => state.viewMode);
|
||||||
const graphRef = useJC(state => state.graphRef);
|
|
||||||
const setSelectedNode = useGraph(state => state.setSelectedNode);
|
|
||||||
const setVisible = useModal(state => state.setVisible);
|
|
||||||
|
|
||||||
console.log(graphRef);
|
if (viewMode === ViewMode.Graph) return <Graph />;
|
||||||
|
|
||||||
const handleNodeClick = React.useCallback(
|
if (viewMode === ViewMode.Tree)
|
||||||
(data: NodeData) => {
|
return (
|
||||||
if (setSelectedNode) setSelectedNode(data);
|
<JSONTree
|
||||||
setVisible("node")(true);
|
data={JSON.parse(json)}
|
||||||
},
|
labelRenderer={(keyPath, nodeType, expanded, expandable) => {
|
||||||
[setSelectedNode, setVisible]
|
return (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
color: getLabelColor({
|
||||||
|
theme,
|
||||||
|
$type: nodeType,
|
||||||
|
}),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{keyPath[0]}:
|
||||||
|
</span>
|
||||||
);
|
);
|
||||||
|
}}
|
||||||
const style = React.useMemo(
|
valueRenderer={(valueAsString, value) => {
|
||||||
() => ({
|
return (
|
||||||
width: "100%",
|
<span
|
||||||
|
style={{
|
||||||
|
color: getValueColor({
|
||||||
|
theme,
|
||||||
|
$value: valueAsString,
|
||||||
|
}),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TextRenderer>{JSON.stringify(value)}</TextRenderer>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
theme={{
|
||||||
|
extend: {
|
||||||
|
overflow: "scroll",
|
||||||
height: "100%",
|
height: "100%",
|
||||||
}),
|
scheme: "monokai",
|
||||||
[]
|
author: "wimer hazenberg (http://www.monokai.nl)",
|
||||||
|
base00: theme.GRID_BG_COLOR,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const handleLayoutChange = React.useCallback(() => {
|
const LiveEditor: React.FC = () => {
|
||||||
// graphRef shouldn't be null here
|
const viewMode = useConfig(state => state.viewMode);
|
||||||
console.log(graphRef);
|
const [contextOpened, setContextOpened] = React.useState(false);
|
||||||
setTimeout(graphRef?.centerView, 100);
|
const [contextPosition, setContextPosition] = React.useState({
|
||||||
}, [graphRef]);
|
x: 0,
|
||||||
|
y: 0,
|
||||||
const layout = React.useMemo(
|
});
|
||||||
() => ({
|
|
||||||
touchGestures: true,
|
|
||||||
theme: lightmode ? "light" : ("dark" as any),
|
|
||||||
}),
|
|
||||||
[lightmode]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledLiveEditor>
|
<StyledLiveEditor
|
||||||
<JSONGraph
|
onContextMenuCapture={e => {
|
||||||
ref={setJCRef}
|
e.preventDefault();
|
||||||
json={json}
|
setContextOpened(true);
|
||||||
style={style}
|
setContextPosition({ x: e.pageX, y: e.pageY });
|
||||||
onNodeClick={handleNodeClick}
|
}}
|
||||||
onLayoutChange={handleLayoutChange}
|
onClick={() => setContextOpened(false)}
|
||||||
layout={layout}
|
>
|
||||||
/>
|
<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>
|
</StyledLiveEditor>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default LiveEditor;
|
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;
|
|
||||||
|
@ -28,7 +28,7 @@ import { AiOutlineLink } from "react-icons/ai";
|
|||||||
import { FaTrash } from "react-icons/fa";
|
import { FaTrash } from "react-icons/fa";
|
||||||
import { MdFileOpen } from "react-icons/md";
|
import { MdFileOpen } from "react-icons/md";
|
||||||
import { VscAdd, VscEdit, VscLock, VscUnlock } from "react-icons/vsc";
|
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 { documentSvc } from "src/services/document.service";
|
||||||
import useFile, { File } from "src/store/useFile";
|
import useFile, { File } from "src/store/useFile";
|
||||||
import useUser from "src/store/useUser";
|
import useUser from "src/store/useUser";
|
||||||
|
@ -3,12 +3,12 @@ import { Stack, Modal, Button, ModalProps, Text, Anchor, Group } from "@mantine/
|
|||||||
import Editor from "@monaco-editor/react";
|
import Editor from "@monaco-editor/react";
|
||||||
import { VscLinkExternal } from "react-icons/vsc";
|
import { VscLinkExternal } from "react-icons/vsc";
|
||||||
import useJsonQuery from "src/hooks/useJsonQuery";
|
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 }) => {
|
export const JQModal: React.FC<ModalProps> = ({ opened, onClose }) => {
|
||||||
const { updateJson } = useJsonQuery();
|
const { updateJson } = useJsonQuery();
|
||||||
const [query, setQuery] = React.useState("");
|
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 = () => {
|
const onApply = () => {
|
||||||
updateJson(query);
|
updateJson(query);
|
||||||
@ -27,7 +27,7 @@ export const JQModal: React.FC<ModalProps> = ({ opened, onClose }) => {
|
|||||||
</Text>
|
</Text>
|
||||||
<Editor
|
<Editor
|
||||||
value={query ?? ""}
|
value={query ?? ""}
|
||||||
theme={lightmode}
|
theme={darkmodeEnabled}
|
||||||
onChange={e => setQuery(e!)}
|
onChange={e => setQuery(e!)}
|
||||||
height={300}
|
height={300}
|
||||||
language="markdown"
|
language="markdown"
|
||||||
|
@ -6,10 +6,10 @@ import vsDark from "prism-react-renderer/themes/vsDark";
|
|||||||
import vsLight from "prism-react-renderer/themes/vsLight";
|
import vsLight from "prism-react-renderer/themes/vsLight";
|
||||||
import { VscLock } from "react-icons/vsc";
|
import { VscLock } from "react-icons/vsc";
|
||||||
import { isIframe } from "src/lib/utils/widget";
|
import { isIframe } from "src/lib/utils/widget";
|
||||||
|
import useConfig from "src/store/useConfig";
|
||||||
import useFile from "src/store/useFile";
|
import useFile from "src/store/useFile";
|
||||||
import useGraph from "src/store/useGraph";
|
import useGraph from "src/store/useGraph";
|
||||||
import useModal from "src/store/useModal";
|
import useModal from "src/store/useModal";
|
||||||
import useStored from "src/store/useStored";
|
|
||||||
import useUser from "src/store/useUser";
|
import useUser from "src/store/useUser";
|
||||||
|
|
||||||
const dataToString = (data: any) => {
|
const dataToString = (data: any) => {
|
||||||
@ -49,7 +49,7 @@ export const NodeModal: React.FC<ModalProps> = ({ opened, onClose }) => {
|
|||||||
const isPremium = useUser(state => state.premium);
|
const isPremium = useUser(state => state.premium);
|
||||||
const editContents = useFile(state => state.editContents);
|
const editContents = useFile(state => state.editContents);
|
||||||
const setVisible = useModal(state => state.setVisible);
|
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 nodeData = useGraph(state => dataToString(state.selectedNode?.text));
|
||||||
const path = useGraph(state => state.selectedNode?.path);
|
const path = useGraph(state => state.selectedNode?.path);
|
||||||
const isParent = useGraph(state => state.selectedNode?.data?.isParent);
|
const isParent = useGraph(state => state.selectedNode?.data?.isParent);
|
||||||
@ -90,7 +90,7 @@ export const NodeModal: React.FC<ModalProps> = ({ opened, onClose }) => {
|
|||||||
</Text>
|
</Text>
|
||||||
{editMode ? (
|
{editMode ? (
|
||||||
<Editor
|
<Editor
|
||||||
theme={lightmode}
|
theme={darkmodeEnabled}
|
||||||
defaultValue={nodeData}
|
defaultValue={nodeData}
|
||||||
onChange={e => setValue(e!)}
|
onChange={e => setValue(e!)}
|
||||||
height={200}
|
height={200}
|
||||||
|
@ -3,9 +3,9 @@ import { Stack, Modal, Button, ModalProps, Text, Anchor, Group } from "@mantine/
|
|||||||
import Editor from "@monaco-editor/react";
|
import Editor from "@monaco-editor/react";
|
||||||
import { toast } from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
import { VscLock } from "react-icons/vsc";
|
import { VscLock } from "react-icons/vsc";
|
||||||
|
import useConfig from "src/store/useConfig";
|
||||||
import useFile from "src/store/useFile";
|
import useFile from "src/store/useFile";
|
||||||
import useModal from "src/store/useModal";
|
import useModal from "src/store/useModal";
|
||||||
import useStored from "src/store/useStored";
|
|
||||||
import useUser from "src/store/useUser";
|
import useUser from "src/store/useUser";
|
||||||
|
|
||||||
export const SchemaModal: React.FC<ModalProps> = ({ opened, onClose }) => {
|
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 showPremiumModal = useModal(state => state.setVisible("premium"));
|
||||||
const setJsonSchema = useFile(state => state.setJsonSchema);
|
const setJsonSchema = useFile(state => state.setJsonSchema);
|
||||||
const [schema, setSchema] = React.useState("");
|
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 = () => {
|
const onApply = () => {
|
||||||
if (!isPremium) return showPremiumModal(true);
|
if (!isPremium) return showPremiumModal(true);
|
||||||
@ -47,7 +47,7 @@ export const SchemaModal: React.FC<ModalProps> = ({ opened, onClose }) => {
|
|||||||
</Text>
|
</Text>
|
||||||
<Editor
|
<Editor
|
||||||
value={schema ?? ""}
|
value={schema ?? ""}
|
||||||
theme={lightmode}
|
theme={darkmodeEnabled}
|
||||||
onChange={e => setSchema(e!)}
|
onChange={e => setSchema(e!)}
|
||||||
height={300}
|
height={300}
|
||||||
language="json"
|
language="json"
|
||||||
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
@ -4,7 +4,6 @@ export { DownloadModal } from "./DownloadModal";
|
|||||||
export { ImportModal } from "./ImportModal";
|
export { ImportModal } from "./ImportModal";
|
||||||
export { AccountModal } from "./AccountModal";
|
export { AccountModal } from "./AccountModal";
|
||||||
export { NodeModal } from "./NodeModal";
|
export { NodeModal } from "./NodeModal";
|
||||||
export { SettingsModal } from "./SettingsModal";
|
|
||||||
export { ShareModal } from "./ShareModal";
|
export { ShareModal } from "./ShareModal";
|
||||||
export { LoginModal } from "./LoginModal";
|
export { LoginModal } from "./LoginModal";
|
||||||
export { PremiumModal } from "./PremiumModal";
|
export { PremiumModal } from "./PremiumModal";
|
||||||
@ -22,7 +21,6 @@ type Modal =
|
|||||||
| "import"
|
| "import"
|
||||||
| "account"
|
| "account"
|
||||||
| "node"
|
| "node"
|
||||||
| "settings"
|
|
||||||
| "share"
|
| "share"
|
||||||
| "login"
|
| "login"
|
||||||
| "premium"
|
| "premium"
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
// eslint-disable
|
||||||
export enum FileFormat {
|
export enum FileFormat {
|
||||||
"JSON" = "json",
|
"JSON" = "json",
|
||||||
"YAML" = "yaml",
|
"YAML" = "yaml",
|
4
src/enums/viewMode.enum.ts
Normal file
4
src/enums/viewMode.enum.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export enum ViewMode {
|
||||||
|
Graph = "graph",
|
||||||
|
Tree = "tree",
|
||||||
|
}
|
@ -1,10 +1,10 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { useDebouncedValue } from "@mantine/hooks";
|
import { useDebouncedValue } from "@mantine/hooks";
|
||||||
import { searchQuery, cleanupHighlight, highlightMatchedNodes } from "src/lib/utils/graph/search";
|
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 = () => {
|
export const useFocusNode = () => {
|
||||||
const viewPort = useJC(state => state.graphRef?.viewPort);
|
const viewPort = useGraph(state => state.viewPort);
|
||||||
const [selectedNode, setSelectedNode] = React.useState(0);
|
const [selectedNode, setSelectedNode] = React.useState(0);
|
||||||
const [nodeCount, setNodeCount] = React.useState(0);
|
const [nodeCount, setNodeCount] = React.useState(0);
|
||||||
const [value, setValue] = React.useState("");
|
const [value, setValue] = React.useState("");
|
||||||
|
@ -4,7 +4,7 @@ import { MantineProvider, MantineThemeOverride } from "@mantine/core";
|
|||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { monaSans } from "src/constants/fonts";
|
import { monaSans } from "src/constants/fonts";
|
||||||
import { lightTheme, darkTheme } from "src/constants/theme";
|
import { lightTheme, darkTheme } from "src/constants/theme";
|
||||||
import useStored from "src/store/useStored";
|
import useConfig from "src/store/useConfig";
|
||||||
|
|
||||||
const queryClient = new QueryClient({
|
const queryClient = new QueryClient({
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
@ -52,13 +52,13 @@ const mantineTheme: MantineThemeOverride = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const EditorWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
export const EditorWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||||
const lightmode = useStored(state => state.lightmode);
|
const darkmodeEnabled = useConfig(state => state.darkmodeEnabled);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider theme={lightmode ? lightTheme : darkTheme}>
|
<ThemeProvider theme={darkmodeEnabled ? darkTheme : lightTheme}>
|
||||||
<MantineProvider
|
<MantineProvider
|
||||||
theme={{
|
theme={{
|
||||||
colorScheme: lightmode ? "light" : "dark",
|
colorScheme: darkmodeEnabled ? "dark" : "light",
|
||||||
...mantineTheme,
|
...mantineTheme,
|
||||||
}}
|
}}
|
||||||
withCSSVariables
|
withCSSVariables
|
||||||
|
@ -11,7 +11,6 @@ const modalComponents: ModalComponent[] = [
|
|||||||
{ key: "import", component: Modals.ImportModal },
|
{ key: "import", component: Modals.ImportModal },
|
||||||
{ key: "clear", component: Modals.ClearModal },
|
{ key: "clear", component: Modals.ClearModal },
|
||||||
{ key: "download", component: Modals.DownloadModal },
|
{ key: "download", component: Modals.DownloadModal },
|
||||||
{ key: "settings", component: Modals.SettingsModal },
|
|
||||||
{ key: "cloud", component: Modals.CloudModal },
|
{ key: "cloud", component: Modals.CloudModal },
|
||||||
{ key: "account", component: Modals.AccountModal },
|
{ key: "account", component: Modals.AccountModal },
|
||||||
{ key: "premium", component: Modals.PremiumModal },
|
{ key: "premium", component: Modals.PremiumModal },
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { firaMono } from "src/constants/fonts";
|
import { firaMono } from "src/constants/fonts";
|
||||||
|
import useConfig from "src/store/useConfig";
|
||||||
import useGraph from "src/store/useGraph";
|
import useGraph from "src/store/useGraph";
|
||||||
import useStored from "src/store/useStored";
|
|
||||||
|
|
||||||
type Text = string | [string, string][];
|
type Text = string | [string, string][];
|
||||||
type Size = { width: number; height: number };
|
type Size = { width: number; height: number };
|
||||||
@ -54,8 +54,8 @@ setInterval(() => sizeCache.clear(), 120_000);
|
|||||||
|
|
||||||
export const calculateNodeSize = (text: Text, isParent = false) => {
|
export const calculateNodeSize = (text: Text, isParent = false) => {
|
||||||
const { foldNodes } = useGraph.getState();
|
const { foldNodes } = useGraph.getState();
|
||||||
const { imagePreview } = useStored.getState();
|
const { imagePreviewEnabled } = useConfig.getState();
|
||||||
const isImage = isContentImage(text) && imagePreview;
|
const isImage = isContentImage(text) && imagePreviewEnabled;
|
||||||
|
|
||||||
const cacheKey = [text, isParent, foldNodes].toString();
|
const cacheKey = [text, isParent, foldNodes].toString();
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ import { csv2json, json2csv } from "json-2-csv";
|
|||||||
import { parse } from "jsonc-parser";
|
import { parse } from "jsonc-parser";
|
||||||
import jxon from "jxon";
|
import jxon from "jxon";
|
||||||
import toml from "toml";
|
import toml from "toml";
|
||||||
import { FileFormat } from "src/enums/file";
|
import { FileFormat } from "src/enums/file.enum";
|
||||||
|
|
||||||
const keyExists = (obj: object, key: string) => {
|
const keyExists = (obj: object, key: string) => {
|
||||||
if (!obj || (typeof obj !== "object" && !Array.isArray(obj))) {
|
if (!obj || (typeof obj !== "object" && !Array.isArray(obj))) {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { PostgrestSingleResponse } from "@supabase/supabase-js";
|
import { PostgrestSingleResponse } from "@supabase/supabase-js";
|
||||||
import toast from "react-hot-toast";
|
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 { supabase } from "src/lib/api/supabase";
|
||||||
import { File } from "src/store/useFile";
|
import { File } from "src/store/useFile";
|
||||||
import useUser from "src/store/useUser";
|
import useUser from "src/store/useUser";
|
||||||
|
50
src/store/useConfig.ts
Normal file
50
src/store/useConfig.ts
Normal 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;
|
@ -5,13 +5,13 @@ import ReactGA from "react-ga4";
|
|||||||
import { toast } from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import { defaultJson } from "src/constants/data";
|
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 { contentToJson, jsonToContent } from "src/lib/utils/json/jsonAdapter";
|
||||||
import { isIframe } from "src/lib/utils/widget";
|
import { isIframe } from "src/lib/utils/widget";
|
||||||
import { documentSvc } from "src/services/document.service";
|
import { documentSvc } from "src/services/document.service";
|
||||||
|
import useConfig from "./useConfig";
|
||||||
import useGraph from "./useGraph";
|
import useGraph from "./useGraph";
|
||||||
import useJson from "./useJson";
|
import useJson from "./useJson";
|
||||||
import useStored from "./useStored";
|
|
||||||
import useUser from "./useUser";
|
import useUser from "./useUser";
|
||||||
|
|
||||||
type SetContents = {
|
type SetContents = {
|
||||||
@ -125,7 +125,7 @@ const useFile = create<FileStates & JsonActions>()((set, get) => ({
|
|||||||
const isFetchURL = window.location.href.includes("?");
|
const isFetchURL = window.location.href.includes("?");
|
||||||
const json = await contentToJson(get().contents, get().format);
|
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) {
|
if (get().hasChanges && contents && contents.length < 80_000 && !isIframe() && !isFetchURL) {
|
||||||
sessionStorage.setItem("content", contents);
|
sessionStorage.setItem("content", contents);
|
||||||
|
@ -61,6 +61,7 @@ interface GraphActions {
|
|||||||
zoomOut: () => void;
|
zoomOut: () => void;
|
||||||
centerView: () => void;
|
centerView: () => void;
|
||||||
clearGraph: () => void;
|
clearGraph: () => void;
|
||||||
|
setZoomFactor: (zoomFactor: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const useGraph = create<Graph & GraphActions>((set, get) => ({
|
const useGraph = create<Graph & GraphActions>((set, get) => ({
|
||||||
@ -193,6 +194,10 @@ const useGraph = create<Graph & GraphActions>((set, get) => ({
|
|||||||
elementExtraMarginForZoom: 100,
|
elementExtraMarginForZoom: 100,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
setZoomFactor: zoomFactor => {
|
||||||
|
const viewPort = get().viewPort;
|
||||||
|
viewPort?.camera?.recenter(viewPort.centerX, viewPort.centerY, zoomFactor);
|
||||||
|
},
|
||||||
zoomIn: () => {
|
zoomIn: () => {
|
||||||
const viewPort = get().viewPort;
|
const viewPort = get().viewPort;
|
||||||
viewPort?.camera?.recenter(viewPort.centerX, viewPort.centerY, viewPort.zoomFactor + 0.1);
|
viewPort?.camera?.recenter(viewPort.centerX, viewPort.centerY, viewPort.zoomFactor + 0.1);
|
||||||
|
@ -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;
|
|
@ -18,7 +18,6 @@ const initialStates: ModalState = {
|
|||||||
import: false,
|
import: false,
|
||||||
account: false,
|
account: false,
|
||||||
node: false,
|
node: false,
|
||||||
settings: false,
|
|
||||||
share: false,
|
share: false,
|
||||||
login: false,
|
login: false,
|
||||||
premium: false,
|
premium: false,
|
||||||
|
@ -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;
|
|
Loading…
x
Reference in New Issue
Block a user