diff --git a/.eslintrc.json b/.eslintrc.json index b3f118044a0..94186d2e0a2 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -22,5 +22,6 @@ ] }, "extends": ["next/core-web-vitals", "prettier"], - "plugins": ["prettier", "unused-imports"] + "plugins": ["prettier", "unused-imports"], + "ignorePatterns": ["src/enums"] } diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 9b6d724ccc5..802c4492627 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [16.x] + node-version: [18.x] # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ steps: @@ -18,7 +18,7 @@ jobs: uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} - cache: 'npm' - - run: npm install - - run: npm run lint - - run: npm run build + cache: 'yarn' + - run: yarn install + - run: yarn lint + - run: yarn build diff --git a/.prettierignore b/.prettierignore index 3501bb3ac1d..69dd4500587 100644 --- a/.prettierignore +++ b/.prettierignore @@ -4,4 +4,4 @@ node_modules/ out public *-lock.json -tsconfig.json +tsconfig.json \ No newline at end of file diff --git a/LICENSE b/LICENSE index f288702d2fa..85caea99dc4 100644 --- a/LICENSE +++ b/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 the "copyright" line and a pointer to where the full notice is found. - - Copyright (C) + 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: - Copyright (C) + 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. diff --git a/next.config.js b/next.config.js index 4710d1973bf..2fe158e373b 100644 --- a/next.config.js +++ b/next.config.js @@ -13,6 +13,11 @@ const config = { compiler: { styledComponents: true, }, + webpack: config => { + config.resolve.fallback = { fs: false }; + + return config; + }, }; const bundleAnalyzerConfig = withBundleAnalyzer(config); diff --git a/package.json b/package.json index e3fe1489f81..70d5bdd7fb3 100644 --- a/package.json +++ b/package.json @@ -2,13 +2,14 @@ "name": "json-crack", "private": true, "version": "v3.0.0", + "license": "GPL-3.0", "author": "https://github.com/AykutSarac", "homepage": "https://jsoncrack.com", "scripts": { "dev": "next dev", "build": "next build", "start": "next start", - "lint": "tsc && eslint src && prettier --check src", + "lint": "tsc --project tsconfig.json && eslint src && prettier --check src", "lint:fix": "eslint --fix src & prettier --write src", "analyze": "ANALYZE=true npm run build" }, @@ -25,52 +26,53 @@ "@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", - "json-2-csv": "^4.1.0", + "jq-web": "^0.5.1", + "json-2-csv": "^5.0.1", + "json-to-ts": "^1.7.0", "jsonc-parser": "^3.2.0", "jsonwebtoken": "^9.0.2", "jxon": "^2.0.0-beta.5", "lodash.debounce": "^4.0.8", "lodash.get": "^4.4.2", "lodash.set": "^4.3.2", + "maketypes": "^1.1.2", "next": "13.4.12", "react": "^18.2.0", "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" } diff --git a/src/components/Graph/CustomNode/TextNode.tsx b/src/components/Graph/CustomNode/TextNode.tsx index 70c48bb806d..f18160ada20 100644 --- a/src/components/Graph/CustomNode/TextNode.tsx +++ b/src/components/Graph/CustomNode/TextNode.tsx @@ -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 = ({ 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) => { @@ -80,16 +80,16 @@ const Node: React.FC = ({ node, x, y, hasCollapse = false }) => data-x={x} data-y={y} data-key={JSON.stringify(text)} - $hasCollapse={isParent && hideCollapse} + $hasCollapse={isParent && collapseButtonVisible} > {value} - {isParent && childrenCount > 0 && showChildrenCount && ( + {isParent && childrenCount > 0 && childrenCountVisible && ( ({childrenCount}) )} - {isParent && hasCollapse && hideCollapse && ( + {isParent && hasCollapse && collapseButtonVisible && ( {isExpanded ? : } diff --git a/src/components/Graph/CustomNode/TextRenderer.tsx b/src/components/Graph/CustomNode/TextRenderer.tsx index 340804d8486..35c5fa97ed0 100644 --- a/src/components/Graph/CustomNode/TextRenderer.tsx +++ b/src/components/Graph/CustomNode/TextRenderer.tsx @@ -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 {children}; } - if (isColorFormat(children.replaceAll('"', ""))) { + if (isColorFormat(children?.replaceAll('"', ""))) { return ( - - {children.replaceAll('"', "")} + + {children?.replaceAll('"', "")} ); } diff --git a/src/components/Graph/CustomNode/index.tsx b/src/components/Graph/CustomNode/index.tsx index c94e88ce9c5..02718db39d2 100644 --- a/src/components/Graph/CustomNode/index.tsx +++ b/src/components/Graph/CustomNode/index.tsx @@ -3,7 +3,7 @@ import dynamic from "next/dynamic"; import { NodeProps } from "reaflow"; import useGraph from "src/store/useGraph"; import useModal from "src/store/useModal"; -import { NodeData } from "src/types/models"; +import { NodeData } from "src/types/graph"; import { ObjectNode } from "./ObjectNode"; import { TextNode } from "./TextNode"; diff --git a/src/components/Graph/index.tsx b/src/components/Graph/index.tsx index c737cf872a6..4d5f313352c 100644 --- a/src/components/Graph/index.tsx +++ b/src/components/Graph/index.tsx @@ -1,16 +1,18 @@ 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/models"; +import { NodeData } from "src/types/graph"; import { CustomEdge } from "./CustomEdge"; import { ErrorView } from "./ErrorView"; import { PremiumView } from "./PremiumView"; @@ -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 ; + if (viewType === "error") { + return ; + } + + 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 ; @@ -180,6 +197,7 @@ export const Graph = ({ isWidget = false }: GraphProps) => { onContextMenu={e => e.preventDefault()} onClick={blurOnClick} key={String(gesturesEnabled)} + $showRulers={rulersEnabled} {...bindLongPress()} > { 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(() => { diff --git a/src/constants/theme.ts b/src/constants/theme.ts index dd92624aef1..5c01a05f113 100644 --- a/src/constants/theme.ts +++ b/src/constants/theme.ts @@ -17,6 +17,21 @@ const fixedColors = { TEXT_DANGER: "#db662e", }; +const promptInputColors = { + dark: { + PROMPT_BG: "#072719", + PROMPT_PLACEHOLDER_COLOR: "#15593A", + PROMPT_TEXT_COLOR: "#3DCF8E", + PROMPT_BORDER_COLOR: "#0C3924", + }, + light: { + PROMPT_BG: "#d3ede1", + PROMPT_PLACEHOLDER_COLOR: "#77c2a1", + PROMPT_TEXT_COLOR: "#289b67", + PROMPT_BORDER_COLOR: "#8ad7b3", + }, +}; + const nodeColors = { dark: { NODE_COLORS: { @@ -55,6 +70,7 @@ const nodeColors = { export const darkTheme = { ...fixedColors, ...nodeColors.dark, + ...promptInputColors.dark, BLACK_SECONDARY: "#23272A", SILVER_DARK: "#4D4D4D", NODE_KEY: "#FAA81A", @@ -81,6 +97,7 @@ export const darkTheme = { export const lightTheme = { ...fixedColors, ...nodeColors.light, + ...promptInputColors.light, BLACK_SECONDARY: "#F2F2F2", SILVER_DARK: "#CCCCCC", NODE_KEY: "#DC3790", diff --git a/src/containers/Editor/BottomBar.tsx b/src/containers/Editor/BottomBar.tsx index a7df52c8514..0b80988d34a 100644 --- a/src/containers/Editor/BottomBar.tsx +++ b/src/containers/Editor/BottomBar.tsx @@ -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 { saveToCloud, updateJson } from "src/services/json"; +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; @@ -64,7 +58,7 @@ const StyledRight = styled.div` gap: 4px; `; -const StyledBottomBarItem = styled.button<{ bg?: string }>` +const StyledBottomBarItem = styled.button<{ $bg?: string }>` display: flex; align-items: center; gap: 4px; @@ -75,7 +69,7 @@ const StyledBottomBarItem = styled.button<{ bg?: string }>` font-size: 12px; font-weight: 400; color: ${({ theme }) => theme.INTERACTIVE_NORMAL}; - background: ${({ bg }) => bg}; + background: ${({ $bg }) => $bg}; white-space: nowrap; text-overflow: ellipsis; overflow: hidden; @@ -91,23 +85,20 @@ const StyledBottomBarItem = styled.button<{ bg?: string }>` } `; -const StyledImg = styled.img<{ $light: boolean }>` - filter: ${({ $light }) => $light && "invert(100%)"}; -`; - 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); @@ -115,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]); @@ -131,7 +124,7 @@ export const BottomBar = () => { setIsUpdating(true); toast.loading("Saving document...", { id: "fileSave" }); - const { data, error } = await saveToCloud({ + const { data, error } = await documentSvc.upsert({ id: query?.json, contents: getContents(), format: getFormat(), @@ -170,7 +163,7 @@ export const BottomBar = () => { if (!query.json) return handleSaveJson(); setIsUpdating(true); - const { data: updatedJsonData, error } = await updateJson(query.json as string, { + const { data: updatedJsonData, error } = await documentSvc.update(query.json as string, { private: !isPrivate, }); @@ -195,20 +188,10 @@ export const BottomBar = () => { )} - - - - - {user?.user_metadata.name ?? "Login"} - - + + - {!premium && ( - setVisible("premium")(true)}> - - Upgrade to Premium - - )} + {fileName && ( setVisible("cloud")(true)}> @@ -239,8 +222,8 @@ export const BottomBar = () => { {(data?.owner_email === user?.email || (!data && user)) && ( - {hasChanges ? : } - {hasChanges ? (query?.json ? "Unsaved Changes" : "Create Document") : "Saved"} + {hasChanges || !user ? : } + {hasChanges || !user ? (query?.json ? "Unsaved Changes" : "Save to Cloud") : "Saved"} )} {data?.owner_email === user?.email && ( @@ -256,7 +239,7 @@ export const BottomBar = () => { Share - {liveTransform ? ( + {liveTransformEnabled ? ( toggleLiveTransform(false)}> Live Transform @@ -267,7 +250,7 @@ export const BottomBar = () => { Manual Transform )} - {!liveTransform && ( + {!liveTransformEnabled && ( setContents({})}> Transform diff --git a/src/containers/Editor/JsonEditor/index.tsx b/src/containers/Editor/JsonEditor/index.tsx index 63ea2575c27..5c2d3808872 100644 --- a/src/containers/Editor/JsonEditor/index.tsx +++ b/src/containers/Editor/JsonEditor/index.tsx @@ -1,6 +1,14 @@ import React from "react"; import styled from "styled-components"; +import { ActionIcon, Input, Loader, Tooltip } from "@mantine/core"; +import { getHotkeyHandler, useSessionStorage } from "@mantine/hooks"; +import { toast } from "react-hot-toast"; +import { GoDependabot } from "react-icons/go"; +import { VscClose, VscQuestion } from "react-icons/vsc"; import { MonacoEditor } from "src/components/MonacoEditor"; +import useJsonQuery from "src/hooks/useJsonQuery"; +import { supabase } from "src/lib/api/supabase"; +import useUser from "src/store/useUser"; const StyledEditorWrapper = styled.div` display: flex; @@ -9,9 +17,113 @@ const StyledEditorWrapper = styled.div` user-select: none; `; +function removeWhitespaces(inputString: string) { + // Remove extra whitespaces and newlines + const compactString = inputString.replace(/\s+/g, "").replaceAll("interface", "interface "); + return compactString; +} + +const StyledPromptInput = styled(Input)` + .mantine-Input-input { + font-weight: 500; + background: ${({ theme }) => theme.PROMPT_BG}; + color: ${({ theme }) => theme.PROMPT_TEXT_COLOR}; + border: none; + border-bottom: 1px solid ${({ theme }) => theme.PROMPT_BORDER_COLOR}; + + :focus-within { + border-color: #15593a; + } + } + + svg { + color: ${({ theme }) => theme.PROMPT_TEXT_COLOR}; + } + + ::placeholder { + color: ${({ theme }) => theme.PROMPT_PLACEHOLDER_COLOR}; + } +`; + +const PromptInput = () => { + const { updateJson, getJsonType } = useJsonQuery(); + const premium = useUser(state => state.premium); + const [completing, setCompleting] = React.useState(false); + const [prompt, setPrompt] = React.useState(""); + const [promptVisible, setPromptVisible] = useSessionStorage({ + key: "promptVisible", + defaultValue: true, + }); + + const onSubmit = async () => { + try { + setCompleting(true); + + const resp = await supabase.functions.invoke("jq", { + body: { + query: prompt, + jsonModel: removeWhitespaces(getJsonType()), + }, + }); + + updateJson(resp.data.choices[0].message?.content || ""); + } catch (error) { + toast.error("An error occured while parsing result."); + } finally { + setCompleting(false); + } + }; + + if (!promptVisible) return null; + + return ( + +
+ setPrompt(e.currentTarget.value)} + rightSectionWidth={60} + maxLength={200} + disabled={!premium || completing} + onKeyDown={getHotkeyHandler([["Enter", onSubmit]])} + onSubmit={onSubmit} + rightSection={ + <> + + + + + + setPromptVisible(false)}> + + + + } + icon={completing ? : } + radius={0} + /> +
+
+ ); +}; + export const JsonEditor: React.FC = () => { return ( + {/* */} ); diff --git a/src/containers/Editor/LiveEditor/Tools.tsx b/src/containers/Editor/LiveEditor/Tools.tsx index 305694186e3..d3b927ae62d 100644 --- a/src/containers/Editor/LiveEditor/Tools.tsx +++ b/src/containers/Editor/LiveEditor/Tools.tsx @@ -1,35 +1,52 @@ 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.enum"; +import { ViewMode } from "src/enums/viewMode.enum"; import useToggleHide from "src/hooks/useToggleHide"; 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 useJson from "src/store/useJson"; import useModal from "src/store/useModal"; -import { FileFormat } from "src/types/models"; +import useUser from "src/store/useUser"; export const StyledTools = styled.div` position: relative; @@ -95,16 +112,12 @@ const ViewMenu = () => { const [coreKey, setCoreKey] = React.useState("CTRL"); const toggleFold = useGraph(state => state.toggleFold); 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 toggleFullscreen = useGraph(state => state.toggleFullscreen); const focusFirstNode = useGraph(state => state.focusFirstNode); const foldNodes = useGraph(state => state.foldNodes); const graphCollapsed = useGraph(state => state.graphCollapsed); - const direction = useGraph(state => state.direction); - const fullscreen = useGraph(state => state.fullscreen); - - const toggleEditor = () => toggleFullscreen(!fullscreen); const toggleFoldNodes = () => { toggleFold(!foldNodes); @@ -112,21 +125,19 @@ const ViewMenu = () => { }; const toggleDirection = () => { - const nextDirection = getNextDirection(direction); - - setDirection(nextDirection); + const nextDirection = getNextDirection(direction || "RIGHT"); + if (setDirection) setDirection(nextDirection); }; const toggleExpandCollapseGraph = () => { if (graphCollapsed) expandGraph(); else collapseGraph(); - toast(`${graphCollapsed ? "Expanded" : "Collapsed"} graph.`); validateHiddenNodes(); + toast(`${graphCollapsed ? "Expanded" : "Collapsed"} graph.`); }; useHotkeys([ - ["mod+shift+e", toggleEditor], ["mod+shift+d", toggleDirection], ["mod+shift+f", toggleFoldNodes], ["mod+shift+c", toggleExpandCollapseGraph], @@ -146,7 +157,7 @@ const ViewMenu = () => { }, []); return ( - + @@ -155,25 +166,6 @@ const ViewMenu = () => { - { - toggleEditor(); - ReactGA.event({ - action: "toggle_hide_editor", - category: "User", - label: "Tools", - }); - }} - icon={fullscreen ? : } - rightSection={ - - {coreKey} Shift E - - } - > - {fullscreen ? "Show" : "Hide"} Editor - { @@ -184,7 +176,7 @@ const ViewMenu = () => { label: "Tools", }); }} - icon={} + icon={} rightSection={ {coreKey} Shift D @@ -240,16 +232,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 = useGraph(state => state.centerView); const zoomIn = useGraph(state => state.zoomIn); const zoomOut = useGraph(state => state.zoomOut); - const centerView = useGraph(state => state.centerView); + 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() @@ -305,11 +331,30 @@ export const Tools: React.FC<{ isWidget?: boolean }> = ({ isWidget = false }) => { value: FileFormat.CSV, label: "CSV" }, ]} /> + + + + + View Mode + + + + + + + setVisible("import")(true)}> Import - + @@ -331,6 +376,13 @@ export const Tools: React.FC<{ isWidget?: boolean }> = ({ isWidget = false }) => > Decode JWT + } + onClick={() => setVisible("type")(true)} + > + Generate Type + setVisible("cloud")(true)}> @@ -343,31 +395,168 @@ export const Tools: React.FC<{ isWidget?: boolean }> = ({ isWidget = false }) => )} - - - - - - + + {!isWidget && ( setVisible("download")(true)}> )} - - - - - - - - setVisible("settings")(true)} - > - - + + {!isWidget && ( + + + + + {Math.round(zoomFactor * 100)}% + + + + + + + setTempZoomValue(e.currentTarget.valueAsNumber / 100)} + onKeyDown={getHotkeyHandler([["Enter", () => setZoomFactor(tempZoomValue)]])} + size="xs" + rightSection="%" + /> + + + Zoom in + + + Zoom out + + + Zoom to fit + + setZoomFactor(50 / 100)}> + Zoom to %50 + + setZoomFactor(100 / 100)}> + Zoom to %100 + + setZoomFactor(200 / 100)}> + Zoom to %200 + + + } + onClick={() => toggleRulers(!rulersEnabled)} + > + Rulers + + } + onClick={() => toggleGestures(!gesturesEnabled)} + > + Trackpad Gestures + + } + onClick={() => toggleChildrenCount(!childrenCountVisible)} + > + Item Count + + } + onClick={() => toggleImagePreview(!imagePreviewEnabled)} + > + Image Link Preview + + } + onClick={() => toggleCollapseButton(!collapseButtonVisible)} + > + Show Expand/Collapse + + } + onClick={() => toggleDarkMode(!darkmodeEnabled)} + > + Dark Mode + + + + )} + + {!isWidget && ( + + + + + {user?.user_metadata.name[0]} + + + + + {user ? ( + + } + onClick={() => setVisible("account")(true)} + closeMenuOnClick + > + Account + + ) : ( + + }> + Sign in + + + )} + {!premium && ( + } + onClick={() => setVisible("premium")(true)} + closeMenuOnClick + > + + Get Premium + + + )} + {user && ( + <> + + } + onClick={() => setVisible("review")(true)} + closeMenuOnClick + > + Feedback + + } onClick={() => logout()} closeMenuOnClick> + Log out + + + )} + + + )} + + {!isWidget && ( + + + + )} ); diff --git a/src/containers/Editor/LiveEditor/index.tsx b/src/containers/Editor/LiveEditor/index.tsx index a01ffe962e4..922ae320b98 100644 --- a/src/containers/Editor/LiveEditor/index.tsx +++ b/src/containers/Editor/LiveEditor/index.tsx @@ -1,16 +1,143 @@ import React from "react"; -import styled from "styled-components"; +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"; 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; + } `; +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 viewMode = useConfig(state => state.viewMode); + + if (viewMode === ViewMode.Graph) return ; + + if (viewMode === ViewMode.Tree) + return ( + { + return ( + + {keyPath[0]}: + + ); + }} + valueRenderer={(valueAsString, value) => { + return ( + + {JSON.stringify(value)} + + ); + }} + theme={{ + extend: { + overflow: "scroll", + height: "100%", + scheme: "monokai", + author: "wimer hazenberg (http://www.monokai.nl)", + base00: theme.GRID_BG_COLOR, + }, + }} + /> + ); +}; + 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 ( - - + { + e.preventDefault(); + setContextOpened(true); + setContextPosition({ x: e.pageX, y: e.pageY }); + }} + onClick={() => setContextOpened(false)} + > +
+ + + + Download as Image + + + Zoom to Fit + + + Rotate + + + +
+ +
); }; diff --git a/src/containers/Modals/ClearModal/index.tsx b/src/containers/Modals/ClearModal/index.tsx index c4937d38ca9..452c4b35c86 100644 --- a/src/containers/Modals/ClearModal/index.tsx +++ b/src/containers/Modals/ClearModal/index.tsx @@ -1,7 +1,7 @@ import React from "react"; import { useRouter } from "next/router"; import { Modal, Group, Button, Text, Divider, ModalProps } from "@mantine/core"; -import { deleteJson } from "src/services/json"; +import { documentSvc } from "src/services/document.service"; import useJson from "src/store/useJson"; export const ClearModal: React.FC = ({ opened, onClose }) => { @@ -13,7 +13,7 @@ export const ClearModal: React.FC = ({ opened, onClose }) => { onClose(); if (typeof query.json === "string") { - deleteJson(query.json); + documentSvc.delete(query.json); replace("/editor"); } }; diff --git a/src/containers/Modals/CloudModal/index.tsx b/src/containers/Modals/CloudModal/index.tsx index d29e873b866..3a42f521fa6 100644 --- a/src/containers/Modals/CloudModal/index.tsx +++ b/src/containers/Modals/CloudModal/index.tsx @@ -28,10 +28,10 @@ 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 { deleteJson, getAllJson, saveToCloud, updateJson } from "src/services/json"; +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"; -import { FileFormat } from "src/types/models"; dayjs.extend(relativeTime); @@ -52,7 +52,7 @@ const UpdateNameModal: React.FC<{ const onSubmit = () => { if (!file) return; toast - .promise(updateJson(file.id, { name }), { + .promise(documentSvc.update(file.id, { name }), { loading: "Updating document...", error: "Error occurred while updating document!", success: `Renamed document to ${name}`, @@ -92,7 +92,7 @@ export const CloudModal: React.FC = ({ opened, onClose }) => { const getFormat = useFile(state => state.getFormat); const [currentFile, setCurrentFile] = React.useState(null); const { isReady, query, replace } = useRouter(); - const { data, refetch } = useQuery(["allJson", query], () => getAllJson(), { + const { data, refetch } = useQuery(["allJson", query], () => documentSvc.getAll(), { enabled: isReady && opened, }); @@ -104,7 +104,10 @@ export const CloudModal: React.FC = ({ opened, onClose }) => { const onCreate = async () => { try { toast.loading("Saving document...", { id: "fileSave" }); - const { data, error } = await saveToCloud({ contents: getContents(), format: getFormat() }); + const { data, error } = await documentSvc.upsert({ + contents: getContents(), + format: getFormat(), + }); if (error) throw error; @@ -121,7 +124,7 @@ export const CloudModal: React.FC = ({ opened, onClose }) => { const onDeleteClick = React.useCallback( (file: File) => { toast - .promise(deleteJson(file.id), { + .promise(documentSvc.delete(file.id), { loading: "Deleting file...", error: "An error occurred while deleting the file!", success: `Deleted ${file.name}!`, diff --git a/src/containers/Modals/JQModal/index.tsx b/src/containers/Modals/JQModal/index.tsx index c63fdbbc09f..eb19bd7d660 100644 --- a/src/containers/Modals/JQModal/index.tsx +++ b/src/containers/Modals/JQModal/index.tsx @@ -1,27 +1,17 @@ import React from "react"; import { Stack, Modal, Button, ModalProps, Text, Anchor, Group } from "@mantine/core"; import Editor from "@monaco-editor/react"; -import jq from "jq-in-the-browser"; -import { toast } from "react-hot-toast"; import { VscLinkExternal } from "react-icons/vsc"; -import useFile from "src/store/useFile"; -import useJson from "src/store/useJson"; -import useStored from "src/store/useStored"; +import useJsonQuery from "src/hooks/useJsonQuery"; +import useConfig from "src/store/useConfig"; export const JQModal: React.FC = ({ opened, onClose }) => { + const { updateJson } = useJsonQuery(); const [query, setQuery] = React.useState(""); - const lightmode = useStored(state => (state.lightmode ? "light" : "vs-dark")); - const getJson = useJson(state => state.getJson); - const setContents = useFile(state => state.setContents); + const darkmodeEnabled = useConfig(state => (state.darkmodeEnabled ? "vs-dark" : "light")); const onApply = () => { - try { - const res = jq(query)(JSON.parse(getJson())) as object; - setContents({ contents: JSON.stringify(res, null, 2) }); - onClose(); - } catch (error) { - toast.error("Invalid JQ"); - } + updateJson(query); }; return ( @@ -37,7 +27,7 @@ export const JQModal: React.FC = ({ opened, onClose }) => { setQuery(e!)} height={300} language="markdown" diff --git a/src/containers/Modals/NodeModal/index.tsx b/src/containers/Modals/NodeModal/index.tsx index 75ed7f0705c..c9f201b07ab 100644 --- a/src/containers/Modals/NodeModal/index.tsx +++ b/src/containers/Modals/NodeModal/index.tsx @@ -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 = ({ 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 = ({ opened, onClose }) => { {editMode ? ( setValue(e!)} height={200} diff --git a/src/containers/Modals/SchemaModal/index.tsx b/src/containers/Modals/SchemaModal/index.tsx index 597e2959a49..2376798c84c 100644 --- a/src/containers/Modals/SchemaModal/index.tsx +++ b/src/containers/Modals/SchemaModal/index.tsx @@ -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 = ({ opened, onClose }) => { @@ -13,7 +13,7 @@ export const SchemaModal: React.FC = ({ 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 = ({ opened, onClose }) => { setSchema(e!)} height={300} language="json" diff --git a/src/containers/Modals/SettingsModal/index.tsx b/src/containers/Modals/SettingsModal/index.tsx deleted file mode 100644 index c8da45e92d2..00000000000 --- a/src/containers/Modals/SettingsModal/index.tsx +++ /dev/null @@ -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 = ({ 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 ( - - - - toggleGestures(e.currentTarget.checked)} - checked={gesturesEnabled} - /> - toggleImagePreview(e.currentTarget.checked)} - checked={imagePreview} - /> - toggleHideCollapse(e.currentTarget.checked)} - checked={hideCollapse} - /> - toggleChildrenCount(e.currentTarget.checked)} - checked={childrenCount} - /> - {/* - Collapse All by Default - - 35% Faster - - - } - size="md" - color="violet" - onChange={e => { - if (isPremium) { - toggleCollapseAll(e.currentTarget.checked); - return validateHiddenNodes(); - } - setVisible("premium")(true); - onClose(); - }} - checked={collapseAll} - offLabel={isPremium ? null : } - /> */} - setLightTheme(e.currentTarget.checked)} - checked={lightmode} - /> - - - - ); -}; diff --git a/src/containers/Modals/TypeModal/index.tsx b/src/containers/Modals/TypeModal/index.tsx new file mode 100644 index 00000000000..d43cc6a991d --- /dev/null +++ b/src/containers/Modals/TypeModal/index.tsx @@ -0,0 +1,45 @@ +import React from "react"; +import { Stack, Modal, ModalProps, ScrollArea, Select } from "@mantine/core"; +import { Prism } from "@mantine/prism"; +import vsDark from "prism-react-renderer/themes/vsDark"; +import vsLight from "prism-react-renderer/themes/vsLight"; +import useJsonQuery from "src/hooks/useJsonQuery"; + +const typeOptions = [ + { + label: "TypeScript", + value: "typescript", + }, +]; + +export const TypeModal: React.FC = ({ opened, onClose }) => { + const { getJsonType } = useJsonQuery(); + const [type, setType] = React.useState(""); + + React.useEffect(() => { + if (opened) { + setType(getJsonType()); + } + }, [getJsonType, opened]); + + return ( + + +