Merge pull request #280 from AykutSarac/mona-sans

Cloud updates
This commit is contained in:
Aykut Saraç 2022-12-30 18:15:46 +03:00 committed by GitHub
commit 1940987759
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
74 changed files with 2608 additions and 1139 deletions

View File

@ -1 +1,3 @@
NEXT_PUBLIC_BASE_URL=http://localhost:3000
NEXT_PUBLIC_BASE_URL=http://localhost:3000
NEXT_PUBLIC_ALTOGIC_ENV_URL=https://jsoncrack.c5-na.altogic.com
NEXT_PUBLIC_ALTOGIC_CLIENT_KEY=f1e92022789f4ccf91273a345ab2bdf8

View File

@ -1 +1,3 @@
NEXT_PUBLIC_BASE_URL=https://jsoncrack.com
NEXT_PUBLIC_BASE_URL=https://jsoncrack.com
NEXT_PUBLIC_ALTOGIC_ENV_URL=https://jsoncrack.c5-na.altogic.com
NEXT_PUBLIC_ALTOGIC_CLIENT_KEY=f1e92022789f4ccf91273a345ab2bdf8

View File

@ -2,7 +2,7 @@
"trailingComma": "es5",
"singleQuote": false,
"semi": true,
"printWidth": 85,
"printWidth": 100,
"arrowParens": "avoid",
"importOrder": [
"^(react/(.*)$)|^(react$)",

View File

@ -9,7 +9,7 @@ const withPWA = require("next-pwa")({
* @type {import('next').NextConfig}
*/
const nextConfig = {
reactStrictMode: true,
reactStrictMode: false,
};
module.exports = withPWA(nextConfig);

View File

@ -1,7 +1,7 @@
{
"name": "json-crack",
"private": true,
"version": "v2.2.0",
"version": "v2.5.0",
"author": "https://github.com/AykutSarac",
"homepage": "https://jsoncrack.com",
"scripts": {
@ -14,11 +14,17 @@
},
"dependencies": {
"@monaco-editor/react": "^4.4.6",
"@react-oauth/google": "^0.4.0",
"@sentry/nextjs": "^7.16.0",
"@tanstack/react-query": "^4.19.1",
"allotment": "^1.17.0",
"compress-json": "^2.1.2",
"altogic": "^2.3.8",
"axios": "^1.1.3",
"dayjs": "^1.11.6",
"html-to-image": "^1.10.8",
"jsonc-parser": "^3.2.0",
"lodash.debounce": "^4.0.8",
"lz-string": "^1.4.4",
"next": "^12.3.1",
"next-transpile-modules": "^9.1.0",
"react": "^18.2.0",
@ -28,6 +34,7 @@
"react-icons": "^4.6.0",
"react-in-viewport": "^1.0.0-alpha.28",
"react-linkify-it": "^1.0.7",
"react-syntax-highlighter": "^15.5.0",
"react-zoom-pan-pinch": "^2.1.3",
"reaflow": "^5.0.7",
"styled-components": "^5.3.6",
@ -36,9 +43,12 @@
"devDependencies": {
"@testing-library/react": "^13.3.0",
"@trivago/prettier-plugin-sort-imports": "^3.3.0",
"@types/lodash.debounce": "^4.0.7",
"@types/lz-string": "^1.3.34",
"@types/node": "^18.7.21",
"@types/react": "18.0.21",
"@types/react-color": "^3.0.6",
"@types/react-syntax-highlighter": "^15.5.5",
"@types/styled-components": "^5.1.26",
"eslint": "8.24.0",
"eslint-config-next": "12.3.1",

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

BIN
public/assets/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

13
src/api/altogic.ts Normal file
View File

@ -0,0 +1,13 @@
import { APIError, createClient } from "altogic";
let envUrl = process.env.NEXT_PUBLIC_ALTOGIC_ENV_URL as string;
let clientKey = process.env.NEXT_PUBLIC_ALTOGIC_CLIENT_KEY as string;
const altogic = createClient(envUrl, clientKey);
export interface AltogicResponse<T> {
data: T;
errors: APIError | null;
}
export { altogic };

View File

@ -4,6 +4,7 @@ import styled, { DefaultTheme } from "styled-components";
enum ButtonType {
PRIMARY = "PRIMARY",
SECONDARY = "BLURPLE",
TERTIARY = "PURPLE",
DANGER = "DANGER",
SUCCESS = "SEAGREEN",
WARNING = "ORANGE",
@ -16,7 +17,7 @@ interface ButtonProps {
type ConditionalProps =
| ({
link?: boolean;
link: boolean;
} & React.ComponentPropsWithoutRef<"a">)
| ({
link?: never;
@ -29,19 +30,20 @@ function getButtonStatus(status: keyof typeof ButtonType, theme: DefaultTheme) {
const StyledButton = styled.button<{
status: keyof typeof ButtonType;
block: boolean;
link: boolean;
}>`
display: flex;
display: inline-flex;
align-items: center;
background: ${({ status, theme }) => getButtonStatus(status, theme)};
color: #ffffff;
padding: 8px 16px;
min-width: 60px;
padding: ${({ link }) => (link ? "2px 16px" : "8px 16px")};
min-width: 70px;
min-height: 32px;
border-radius: 3px;
font-family: "Mona Sans";
font-size: 14px;
font-weight: 500;
font-family: "Catamaran", sans-serif;
width: ${({ block }) => (block ? "100%" : "fit-content")};
width: ${({ block }) => (block ? "-webkit-fill-available" : "fit-content")};
height: 40px;
background-image: none;
@ -73,6 +75,7 @@ const StyledButtonContent = styled.div`
gap: 8px;
white-space: nowrap;
text-overflow: ellipsis;
font-weight: 600;
`;
export const Button: React.FC<ButtonProps & ConditionalProps> = ({
@ -84,10 +87,11 @@ export const Button: React.FC<ButtonProps & ConditionalProps> = ({
}) => {
return (
<StyledButton
as={link ? "a" : "button"}
type="button"
as={link ? "a" : "button"}
status={status ?? "PRIMARY"}
block={block}
link={link}
{...props}
>
<StyledButtonContent>{children}</StyledButtonContent>

View File

@ -1,7 +1,7 @@
import React from "react";
// import { useInViewport } from "react-in-viewport";
import { CustomNodeProps } from "src/components/CustomNode";
import useConfig from "src/store/useConfig";
import useGraph from "src/store/useGraph";
import * as Styled from "./styles";
const inViewport = true;
@ -9,28 +9,16 @@ const inViewport = true;
const ObjectNode: React.FC<CustomNodeProps> = ({ node, x, y }) => {
const { text, width, height, data } = node;
const ref = React.useRef(null);
const performanceMode = useConfig(state => state.performanceMode);
const performanceMode = useGraph(state => state.performanceMode);
// const { inViewport } = useInViewport(ref);
if (data.isEmpty) return null;
return (
<Styled.StyledForeignObject
width={width}
height={height}
x={0}
y={0}
ref={ref}
isObject
>
<Styled.StyledForeignObject width={width} height={height} x={0} y={0} ref={ref} isObject>
{(!performanceMode || inViewport) &&
text.map((val, idx) => (
<Styled.StyledRow
data-key={JSON.stringify(val[1])}
data-x={x}
data-y={y}
key={idx}
>
<Styled.StyledRow data-key={JSON.stringify(val[1])} data-x={x} data-y={y} key={idx}>
<Styled.StyledKey objectKey>
{JSON.stringify(val[0]).replaceAll('"', "")}:{" "}
</Styled.StyledKey>
@ -42,10 +30,7 @@ const ObjectNode: React.FC<CustomNodeProps> = ({ node, x, y }) => {
};
function propsAreEqual(prev: CustomNodeProps, next: CustomNodeProps) {
return (
String(prev.node.text) === String(next.node.text) &&
prev.node.width === next.node.width
);
return String(prev.node.text) === String(next.node.text) && prev.node.width === next.node.width;
}
export default React.memo(ObjectNode, propsAreEqual);

View File

@ -2,7 +2,6 @@ import React from "react";
import { MdLink, MdLinkOff } from "react-icons/md";
// import { useInViewport } from "react-in-viewport";
import { CustomNodeProps } from "src/components/CustomNode";
import useConfig from "src/store/useConfig";
import useGraph from "src/store/useGraph";
import useStored from "src/store/useStored";
import styled from "styled-components";
@ -28,27 +27,34 @@ const StyledExpand = styled.button`
const StyledTextNodeWrapper = styled.div<{ hasCollapse: boolean }>`
display: flex;
justify-content: ${({ hasCollapse }) =>
hasCollapse ? "space-between" : "center"};
justify-content: ${({ hasCollapse }) => (hasCollapse ? "space-between" : "center")};
align-items: center;
height: 100%;
width: 100%;
`;
const TextNode: React.FC<CustomNodeProps> = ({
node,
x,
y,
hasCollapse = false,
}) => {
const StyledImageWrapper = styled.div`
padding: 5px;
`;
const StyledImage = styled.img`
border-radius: 2px;
object-fit: contain;
background: ${({ theme }) => theme.BACKGROUND_MODIFIER_ACCENT};
`;
const TextNode: React.FC<CustomNodeProps> = ({ node, x, y, hasCollapse = false }) => {
const { id, text, width, height, data } = node;
const ref = React.useRef(null);
const hideCollapse = useStored(state => state.hideCollapse);
const hideChildrenCount = useStored(state => state.hideChildrenCount);
const childrenCount = useStored(state => state.childrenCount);
const imagePreview = useStored(state => state.imagePreview);
const expandNodes = useGraph(state => state.expandNodes);
const collapseNodes = useGraph(state => state.collapseNodes);
const isExpanded = useGraph(state => state.collapsedParents.includes(id));
const performanceMode = useConfig(state => state.performanceMode);
const performanceMode = useGraph(state => state.performanceMode);
const isImage =
!Array.isArray(text) && /(https?:\/\/.*\.(?:png|jpg|gif))/i.test(text) && imagePreview;
// const { inViewport } = useInViewport(ref);
const handleExpand = (e: React.MouseEvent<HTMLButtonElement>) => {
@ -64,36 +70,38 @@ const TextNode: React.FC<CustomNodeProps> = ({
height={height}
x={0}
y={0}
hideCollapse={hideCollapse}
hasCollapse={data.parent && hasCollapse}
ref={ref}
>
<StyledTextNodeWrapper hasCollapse={data.parent && !hideCollapse}>
{(!performanceMode || inViewport) && (
<Styled.StyledKey
data-x={x}
data-y={y}
data-key={JSON.stringify(text)}
parent={data.parent}
>
<Styled.StyledLinkItUrl>
{JSON.stringify(text).replaceAll('"', "")}
</Styled.StyledLinkItUrl>
</Styled.StyledKey>
)}
{isImage ? (
<StyledImageWrapper>
<StyledImage src={text} width="70" height="70" />
</StyledImageWrapper>
) : (
<StyledTextNodeWrapper hasCollapse={data.parent && hideCollapse}>
{(!performanceMode || inViewport) && (
<Styled.StyledKey
data-x={x}
data-y={y}
data-key={JSON.stringify(text)}
parent={data.parent}
>
<Styled.StyledLinkItUrl>
{JSON.stringify(text).replaceAll('"', "")}
</Styled.StyledLinkItUrl>
</Styled.StyledKey>
)}
{data.parent && data.childrenCount > 0 && childrenCount && (
<Styled.StyledChildrenCount>({data.childrenCount})</Styled.StyledChildrenCount>
)}
{data.parent && data.childrenCount > 0 && !hideChildrenCount && (
<Styled.StyledChildrenCount>
({data.childrenCount})
</Styled.StyledChildrenCount>
)}
{inViewport && data.parent && hasCollapse && !hideCollapse && (
<StyledExpand onClick={handleExpand}>
{isExpanded ? <MdLinkOff size={18} /> : <MdLink size={18} />}
</StyledExpand>
)}
</StyledTextNodeWrapper>
{inViewport && data.parent && hasCollapse && hideCollapse && (
<StyledExpand onClick={handleExpand}>
{isExpanded ? <MdLinkOff size={18} /> : <MdLink size={18} />}
</StyledExpand>
)}
</StyledTextNodeWrapper>
)}
</Styled.StyledForeignObject>
);
};

View File

@ -28,12 +28,7 @@ export const CustomNode = (nodeProps: NodeProps) => {
}
return (
<TextNode
node={node as NodeData}
hasCollapse={data.childrenCount > 0}
x={x}
y={y}
/>
<TextNode node={node as NodeData} hasCollapse={data.childrenCount > 0} x={x} y={y} />
);
}}
</Node>

View File

@ -16,7 +16,6 @@ export const StyledLinkItUrl = styled(LinkItUrl)`
export const StyledForeignObject = styled.foreignObject<{
hasCollapse?: boolean;
hideCollapse?: boolean;
isObject?: boolean;
}>`
text-align: ${({ isObject }) => !isObject && "center"};
@ -53,11 +52,7 @@ export const StyledForeignObject = styled.foreignObject<{
}
`;
function getKeyColor(
theme: DefaultTheme,
parent: "array" | "object" | false,
objectKey: boolean
) {
function getKeyColor(theme: DefaultTheme, parent: "array" | "object" | false, objectKey: boolean) {
if (parent) {
if (parent === "array") return theme.NODE_COLORS.PARENT_ARR;
return theme.NODE_COLORS.PARENT_OBJ;
@ -74,8 +69,7 @@ export const StyledKey = styled.span<{
display: inline;
flex: 1;
font-weight: 500;
color: ${({ theme, objectKey = false, parent = false }) =>
getKeyColor(theme, parent, objectKey)};
color: ${({ theme, objectKey = false, parent = false }) => getKeyColor(theme, parent, objectKey)};
font-size: ${({ parent }) => parent && "14px"};
overflow: hidden;
text-overflow: ellipsis;

View File

@ -1,5 +1,6 @@
import React from "react";
import { MdReportGmailerrorred, MdOutlineCheckCircleOutline } from "react-icons/md";
import useJson from "src/store/useJson";
import styled from "styled-components";
const StyledErrorWrapper = styled.div`
@ -40,7 +41,9 @@ const StyledError = styled.pre`
white-space: pre-line;
`;
export const ErrorContainer = ({ hasError }: { hasError: boolean }) => {
export const ErrorContainer = () => {
const hasError = useJson(state => state.hasError);
return (
<StyledErrorWrapper>
<StyledErrorExpand error={hasError}>

View File

@ -0,0 +1,84 @@
import Link from "next/link";
import { FaGithub, FaLinkedin, FaTwitter } from "react-icons/fa";
import styled from "styled-components";
import pkg from "../../../package.json";
export const StyledFooter = styled.footer`
display: flex;
flex-direction: row;
justify-content: space-between;
width: 80%;
margin: 0 auto;
padding: 30px 3%;
border-top: 1px solid #b4b4b4;
opacity: 0.7;
`;
export const StyledFooterText = styled.p`
display: flex;
flex-direction: column;
gap: 20px;
color: #b4b4b4;
`;
export const StyledNavLink = styled.a`
display: flex;
justify-content: center;
align-items: center;
font-size: 1rem;
cursor: pointer;
transition: color 0.2s;
&:hover {
font-weight: 500;
color: ${({ theme }) => theme.ORANGE};
}
`;
export const StyledIconLinks = styled.div`
display: flex;
gap: 20px;
`;
export const Footer = () => (
<StyledFooter>
<StyledFooterText>
<Link href="/">
<a>
<img width="120" src="assets/icon.png" alt="icon" loading="lazy" />
</a>
</Link>
<span>
© {new Date().getFullYear()} JSON Crack - {pkg.version}
</span>
</StyledFooterText>
<StyledIconLinks>
<StyledNavLink
href="https://github.com/AykutSarac/jsoncrack.com"
rel="external"
target="_blank"
aria-label="github"
>
<FaGithub size={26} />
</StyledNavLink>
<StyledNavLink
href="https://www.linkedin.com/in/aykutsarac/"
rel="me"
target="_blank"
aria-label="linkedin"
>
<FaLinkedin size={26} />
</StyledNavLink>
<StyledNavLink
href="https://twitter.com/jsoncrack"
rel="me"
target="_blank"
aria-label="twitter"
>
<FaTwitter size={26} />
</StyledNavLink>
</StyledIconLinks>
</StyledFooter>
);

View File

@ -26,17 +26,12 @@ const StyledInfo = styled.p`
export const ErrorView = () => (
<StyledErrorView>
<img
src="/assets/undraw_qa_engineers_dg-5-p.svg"
width="200"
height="200"
alt="oops"
/>
<img src="/assets/undraw_qa_engineers_dg-5-p.svg" width="200" height="200" alt="oops" />
<StyledTitle>JSON Crack is unable to handle this file!</StyledTitle>
<StyledInfo>
We apologize for the problem you encountered. We are doing our best as an Open
Source community to improve our service. Unfortunately, JSON Crack is currently
unable to handle such a large file.
We apologize for the problem you encountered. We are doing our best as an Open Source
community to improve our service. Unfortunately, JSON Crack is currently unable to handle such
a large file.
</StyledInfo>
</StyledErrorView>
);

View File

@ -1,12 +1,7 @@
import React from "react";
import {
ReactZoomPanPinchRef,
TransformComponent,
TransformWrapper,
} from "react-zoom-pan-pinch";
import { ReactZoomPanPinchRef, TransformComponent, TransformWrapper } from "react-zoom-pan-pinch";
import { Canvas, Edge, ElkRoot } from "reaflow";
import { CustomNode } from "src/components/CustomNode";
import useConfig from "src/store/useConfig";
import useGraph from "src/store/useGraph";
import styled from "styled-components";
import { Loading } from "../Loading";
@ -41,14 +36,10 @@ const StyledEditorWrapper = styled.div<{ isWidget: boolean }>`
}
`;
const GraphComponent = ({
isWidget = false,
openModal,
setSelectedNode,
}: GraphProps) => {
const GraphComponent = ({ isWidget = false, openModal, setSelectedNode }: GraphProps) => {
const setLoading = useGraph(state => state.setLoading);
const setConfig = useConfig(state => state.setConfig);
const centerView = useConfig(state => state.centerView);
const setZoomPanPinch = useGraph(state => state.setZoomPanPinch);
const centerView = useGraph(state => state.centerView);
const loading = useGraph(state => state.loading);
const direction = useGraph(state => state.direction);
@ -70,58 +61,35 @@ const GraphComponent = ({
const onInit = React.useCallback(
(ref: ReactZoomPanPinchRef) => {
setConfig("zoomPanPinch", ref);
setZoomPanPinch(ref);
},
[setConfig]
[setZoomPanPinch]
);
const onLayoutChange = React.useCallback(
(layout: ElkRoot) => {
if (layout.width && layout.height) {
const areaSize = layout.width * layout.height;
const changeRatio = Math.abs(
(areaSize * 100) / (size.width * size.height) - 100
);
const changeRatio = Math.abs((areaSize * 100) / (size.width * size.height) - 100);
setSize({ width: layout.width + 400, height: layout.height + 400 });
setSize({
width: (layout.width as number) + 400,
height: (layout.height as number) + 400,
});
requestAnimationFrame(() => {
setTimeout(() => {
setLoading(false);
setTimeout(() => (changeRatio > 75 || isWidget) && centerView(), 0);
}, 0);
setTimeout(() => {
if (changeRatio > 70 || isWidget) centerView();
});
});
});
}
},
[size.width, size.height, setLoading, isWidget, centerView]
[centerView, isWidget, setLoading, size.height, size.width]
);
// const onLayoutChange = React.useCallback(
// (layout: ElkRoot) => {
// if (layout.width && layout.height) {
// const areaSize = layout.width * layout.height;
// const changeRatio = Math.abs(
// (areaSize * 100) / (size.width * size.height) - 100
// );
// const MIN_SCALE = Math.round((400_000 / areaSize) * 100) / 100;
// const scale = MIN_SCALE > 2 ? 1 : MIN_SCALE <= 0 ? 0.1 : MIN_SCALE;
// setMinScale(scale);
// setSize({ width: layout.width + 400, height: layout.height + 400 });
// requestAnimationFrame(() => {
// setTimeout(() => {
// setLoading(false);
// setTimeout(() => (changeRatio > 50 || isWidget) && centerView(), 0);
// }, 0);
// });
// }
// },
// [centerView, isWidget, setLoading, size.height, size.width]
// );
const onCanvasClick = React.useCallback(() => {
const input = document.querySelector("input:focus") as HTMLInputElement;
if (input) input.blur();
@ -131,7 +99,7 @@ const GraphComponent = ({
return (
<StyledEditorWrapper isWidget={isWidget} onContextMenu={e => e.preventDefault()}>
{loading && <Loading message="Painting graph..." />}
<Loading message="Painting graph..." loading={loading} />
<TransformWrapper
maxScale={2}
minScale={0.05}
@ -141,9 +109,7 @@ const GraphComponent = ({
doubleClick={{ disabled: true }}
onInit={onInit}
onPanning={ref => ref.instance.wrapperComponent?.classList.add("dragging")}
onPanningStop={ref =>
ref.instance.wrapperComponent?.classList.remove("dragging")
}
onPanningStop={ref => ref.instance.wrapperComponent?.classList.remove("dragging")}
>
<TransformComponent
wrapperStyle={{
@ -170,9 +136,7 @@ const GraphComponent = ({
fit={true}
key={direction}
node={props => <CustomNode {...props} onClick={handleNodeClick} />}
edge={props => (
<Edge {...props} containerClassName={`edge-${props.id}`} />
)}
edge={props => <Edge {...props} containerClassName={`edge-${props.id}`} />}
/>
</TransformComponent>
</TransformWrapper>

View File

@ -2,6 +2,7 @@ import React from "react";
import styled, { keyframes } from "styled-components";
interface LoadingProps {
loading?: boolean;
message?: string;
}
@ -32,7 +33,7 @@ const StyledLoading = styled.div`
`;
const StyledLogo = styled.h2`
font-weight: 600;
font-weight: 800;
font-size: 56px;
pointer-events: none;
margin-bottom: 10px;
@ -48,13 +49,15 @@ const StyledMessage = styled.div`
font-weight: 500;
`;
export const Loading: React.FC<LoadingProps> = ({ message }) => (
<StyledLoading>
<StyledLogo>
<StyledText>JSON</StyledText> Crack
</StyledLogo>
<StyledMessage>
{message ?? "Preparing the environment for you..."}
</StyledMessage>
</StyledLoading>
);
export const Loading: React.FC<LoadingProps> = ({ loading = true, message }) => {
if (!loading) return null;
return (
<StyledLoading>
<StyledLogo>
<StyledText>JSON</StyledText> Crack
</StyledLogo>
<StyledMessage>{message ?? "Preparing the environment for you..."}</StyledMessage>
</StyledLoading>
);
};

View File

@ -17,7 +17,8 @@ type ModalTypes = {
export interface ModalProps {
visible: boolean;
setVisible: React.Dispatch<React.SetStateAction<boolean>>;
setVisible: React.Dispatch<React.SetStateAction<boolean>> | ((visible: boolean) => void);
size?: "sm" | "md" | "lg";
}
const Header: ReactComponent = ({ children }) => {
@ -51,10 +52,11 @@ const Modal: React.FC<React.PropsWithChildren<ModalProps>> & ModalTypes = ({
children,
visible,
setVisible,
size = "sm",
}) => {
const onClick = (e: React.SyntheticEvent<HTMLDivElement>) => {
if (e.currentTarget === e.target) {
setVisible(v => !v);
setVisible(false);
}
};
@ -62,7 +64,7 @@ const Modal: React.FC<React.PropsWithChildren<ModalProps>> & ModalTypes = ({
return (
<Styled.ModalWrapper onClick={onClick}>
<Styled.ModalInnerWrapper>{children}</Styled.ModalInnerWrapper>
<Styled.ModalInnerWrapper size={size}>{children}</Styled.ModalInnerWrapper>
</Styled.ModalWrapper>
);
};

View File

@ -22,9 +22,9 @@ export const ModalWrapper = styled.div`
}
`;
export const ModalInnerWrapper = styled.div`
export const ModalInnerWrapper = styled.div<{ size: "sm" | "md" | "lg" }>`
min-width: 440px;
max-width: 490px;
max-width: ${({ size }) => (size === "sm" ? "490px" : size === "md" ? "50%" : "90%")};
width: fit-content;
animation: ${appearAnimation} 220ms ease-in-out;
line-height: 20px;
@ -36,9 +36,12 @@ export const ModalInnerWrapper = styled.div`
`;
export const Title = styled.h2`
display: flex;
align-items: center;
gap: 5px;
color: ${({ theme }) => theme.INTERACTIVE_ACTIVE};
font-size: 20px !important;
margin: 0;
margin: 0 !important;
`;
export const HeaderWrapper = styled.div`
@ -52,13 +55,14 @@ export const ContentWrapper = styled.div`
background: ${({ theme }) => theme.MODAL_BACKGROUND};
padding: 16px;
overflow: hidden auto;
max-height: 500px;
`;
export const ControlsWrapper = styled.div`
display: flex;
flex-direction: row-reverse;
background: ${({ theme }) => theme.BACKGROUND_SECONDARY};
padding: 16px;
padding: 12px;
border-radius: 0 0 5px 5px;
gap: 10px;
`;

View File

@ -1,11 +1,9 @@
import React from "react";
import Editor, { loader, Monaco } from "@monaco-editor/react";
import { parse } from "jsonc-parser";
import debounce from "lodash.debounce";
import { Loading } from "src/components/Loading";
import useConfig from "src/store/useConfig";
import useGraph from "src/store/useGraph";
import useJson from "src/store/useJson";
import useStored from "src/store/useStored";
import { parser } from "src/utils/jsonParser";
import styled from "styled-components";
loader.config({
@ -28,63 +26,78 @@ const StyledWrapper = styled.div`
grid-template-rows: minmax(0, 1fr);
`;
function handleEditorWillMount(monaco: Monaco) {
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
allowComments: true,
comments: "ignore",
});
}
export const MonacoEditor = () => {
const json = useJson(state => state.json);
const setJson = useJson(state => state.setJson);
const setError = useJson(state => state.setError);
const [loaded, setLoaded] = React.useState(false);
const [value, setValue] = React.useState<string | undefined>(json);
export const MonacoEditor = ({
setHasError,
}: {
setHasError: (value: boolean) => void;
}) => {
const [value, setValue] = React.useState<string | undefined>("");
const setJson = useConfig(state => state.setJson);
const setGraphValue = useGraph(state => state.setGraphValue);
const json = useConfig(state => state.json);
const foldNodes = useConfig(state => state.foldNodes);
const hasError = useJson(state => state.hasError);
const getHasChanges = useJson(state => state.getHasChanges);
const lightmode = useStored(state => (state.lightmode ? "light" : "vs-dark"));
React.useEffect(() => {
const { nodes, edges } = parser(json, foldNodes);
const handleEditorWillMount = React.useCallback(
(monaco: Monaco) => {
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
allowComments: true,
comments: "ignore",
});
setGraphValue("nodes", nodes);
setGraphValue("edges", edges);
setValue(json);
}, [foldNodes, json, setGraphValue]);
monaco.editor.onDidChangeMarkers(([uri]) => {
const markers = monaco.editor.getModelMarkers({ resource: uri });
setError(!!markers.length);
});
},
[setError]
);
const debouncedSetJson = React.useMemo(
() =>
debounce(value => {
if (hasError) return;
setJson(value || "[]");
}, 1200),
[hasError, setJson]
);
React.useEffect(() => {
const formatTimer = setTimeout(() => {
if (!value) {
setHasError(false);
return setJson("{}");
if ((value || !hasError) && loaded) debouncedSetJson(value);
setLoaded(true);
return () => debouncedSetJson.cancel();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [debouncedSetJson, hasError, value]);
React.useEffect(() => {
const beforeunload = (e: BeforeUnloadEvent) => {
if (getHasChanges()) {
const confirmationMessage =
"Unsaved changes, if you leave before saving your changes will be lost";
(e || window.event).returnValue = confirmationMessage; //Gecko + IE
return confirmationMessage;
}
};
const errors = [];
const parsedJSON = JSON.stringify(parse(value, errors), null, 2);
if (errors.length) return setHasError(true);
window.addEventListener("beforeunload", beforeunload);
setJson(parsedJSON);
setHasError(false);
}, 1200);
return () => clearTimeout(formatTimer);
}, [value, setJson, setHasError]);
return () => {
window.removeEventListener("beforeunload", beforeunload);
};
}, [getHasChanges]);
return (
<StyledWrapper>
<Editor
height="100%"
defaultLanguage="json"
value={value}
value={json}
theme={lightmode}
options={editorOptions}
onChange={setValue}
loading={<Loading message="Loading Editor..." />}
beforeMount={handleEditorWillMount}
defaultLanguage="json"
height="100%"
/>
</StyledWrapper>
);

View File

@ -76,11 +76,7 @@ export const SearchInput: React.FC = () => {
placeholder="Search Node"
/>
<StyledSearchButton type="reset" aria-label="search" onClick={handleClear}>
{content.value ? (
<IoCloseSharp size={18} />
) : (
<AiOutlineSearch size={18} />
)}
{content.value ? <IoCloseSharp size={18} /> : <AiOutlineSearch size={18} />}
</StyledSearchButton>
</StyledForm>
</StyledInputWrapper>

View File

@ -1,31 +1,23 @@
import React from "react";
import Link from "next/link";
import toast from "react-hot-toast";
import {
AiOutlineDelete,
AiFillGithub,
AiOutlineTwitter,
AiOutlineSave,
AiOutlineFileAdd,
AiOutlineLink,
AiOutlineEdit,
} from "react-icons/ai";
import { AiOutlineDelete, AiOutlineSave, AiOutlineFileAdd, AiOutlineEdit } from "react-icons/ai";
import { CgArrowsMergeAltH, CgArrowsShrinkH } from "react-icons/cg";
import { FiDownload } from "react-icons/fi";
import { HiHeart } from "react-icons/hi";
import { MdCenterFocusWeak } from "react-icons/md";
import { TiFlowMerge } from "react-icons/ti";
import { VscCollapseAll, VscExpandAll } from "react-icons/vsc";
import {
VscAccount,
VscCloud,
VscCollapseAll,
VscExpandAll,
VscSettingsGear,
} from "react-icons/vsc";
import { Tooltip } from "src/components/Tooltip";
import { ClearModal } from "src/containers/Modals/ClearModal";
import { DownloadModal } from "src/containers/Modals/DownloadModal";
import { ImportModal } from "src/containers/Modals/ImportModal";
import { ShareModal } from "src/containers/Modals/ShareModal";
import useConfig from "src/store/useConfig";
import useGraph from "src/store/useGraph";
import useJson from "src/store/useJson";
import useModal from "src/store/useModal";
import { getNextDirection } from "src/utils/getNextDirection";
import styled from "styled-components";
import shallow from "zustand/shallow";
const StyledSidebar = styled.div`
display: flex;
@ -48,7 +40,7 @@ const StyledElement = styled.button`
display: flex;
justify-content: center;
text-align: center;
font-size: 26px;
font-size: 24px;
font-weight: 600;
width: fit-content;
color: ${({ theme }) => theme.SIDEBAR_ICONS};
@ -78,8 +70,7 @@ const StyledElement = styled.button`
`;
const StyledText = styled.span<{ secondary?: boolean }>`
color: ${({ theme, secondary }) =>
secondary ? theme.INTERACTIVE_HOVER : theme.ORANGE};
color: ${({ theme, secondary }) => (secondary ? theme.INTERACTIVE_HOVER : theme.ORANGE)};
`;
const StyledFlowIcon = styled(TiFlowMerge)<{ rotate: number }>`
@ -140,28 +131,34 @@ function rotateLayout(direction: "LEFT" | "RIGHT" | "DOWN" | "UP") {
return 360;
}
export const Sidebar: React.FC = () => {
const [uploadVisible, setUploadVisible] = React.useState(false);
const [clearVisible, setClearVisible] = React.useState(false);
const [shareVisible, setShareVisible] = React.useState(false);
const [isDownloadVisible, setDownloadVisible] = React.useState(false);
const SidebarButton: React.FC<{
onClick: () => void;
deviceDisplay?: "desktop" | "mobile";
title: string;
component: React.ReactNode;
}> = ({ onClick, deviceDisplay, title, component }) => {
return (
<Tooltip className={deviceDisplay} title={title}>
<StyledElement onClick={onClick}>{component}</StyledElement>
</Tooltip>
);
};
const getJson = useConfig(state => state.getJson);
export const Sidebar: React.FC = () => {
const setVisible = useModal(state => state.setVisible);
const setDirection = useGraph(state => state.setDirection);
const setConfig = useConfig(state => state.setConfig);
const centerView = useConfig(state => state.centerView);
const getJson = useJson(state => state.getJson);
const collapseGraph = useGraph(state => state.collapseGraph);
const expandGraph = useGraph(state => state.expandGraph);
const centerView = useGraph(state => state.centerView);
const toggleFold = useGraph(state => state.toggleFold);
const toggleFullscreen = useGraph(state => state.toggleFullscreen);
const [graphCollapsed, direction] = useGraph(state => [
state.graphCollapsed,
state.direction,
]);
const [foldNodes, hideEditor] = useConfig(
state => [state.foldNodes, state.hideEditor],
shallow
);
const direction = useGraph(state => state.direction);
const foldNodes = useGraph(state => state.foldNodes);
const fullscreen = useGraph(state => state.fullscreen);
const graphCollapsed = useGraph(state => state.graphCollapsed);
const handleSave = () => {
const a = document.createElement("a");
@ -173,7 +170,7 @@ export const Sidebar: React.FC = () => {
};
const toggleFoldNodes = () => {
setConfig("foldNodes", !foldNodes);
toggleFold(!foldNodes);
toast(`${foldNodes ? "Unfolded" : "Folded"} nodes`);
};
@ -192,96 +189,90 @@ export const Sidebar: React.FC = () => {
return (
<StyledSidebar>
<StyledTopWrapper>
<Link passHref href="/">
<StyledElement as={StyledLogo}>
<StyledText>J</StyledText>
<StyledText secondary>C</StyledText>
</StyledElement>
</Link>
<Tooltip className="mobile" title="Edit JSON">
<StyledElement onClick={() => setConfig("hideEditor", !hideEditor)}>
<AiOutlineEdit />
</StyledElement>
</Tooltip>
<Tooltip title="Import File">
<StyledElement onClick={() => setUploadVisible(true)}>
<AiOutlineFileAdd />
</StyledElement>
</Tooltip>
<Tooltip title="Rotate Layout">
<StyledElement onClick={toggleDirection}>
<StyledFlowIcon rotate={rotateLayout(direction)} />
</StyledElement>
</Tooltip>
<Tooltip className="mobile" title="Center View">
<StyledElement onClick={centerView}>
<MdCenterFocusWeak />
</StyledElement>
</Tooltip>
<Tooltip
className="desktop"
<StyledElement href="/" as={StyledLogo}>
<StyledText>J</StyledText>
<StyledText secondary>C</StyledText>
</StyledElement>
<SidebarButton
title="Edit JSON"
deviceDisplay="mobile"
onClick={() => toggleFullscreen(!fullscreen)}
component={<AiOutlineEdit />}
/>
<SidebarButton
title="Import File"
onClick={() => setVisible("import")(true)}
component={<AiOutlineFileAdd />}
/>
<SidebarButton
title="Rotate Layout"
onClick={toggleDirection}
component={<StyledFlowIcon rotate={rotateLayout(direction)} />}
/>
<SidebarButton
title="Center View"
deviceDisplay="mobile"
onClick={centerView}
component={<MdCenterFocusWeak />}
/>
<SidebarButton
title={foldNodes ? "Unfold Nodes" : "Fold Nodes"}
>
<StyledElement onClick={toggleFoldNodes}>
{foldNodes ? <CgArrowsShrinkH /> : <CgArrowsMergeAltH />}
</StyledElement>
</Tooltip>
<Tooltip
className="desktop"
deviceDisplay="desktop"
onClick={toggleFoldNodes}
component={foldNodes ? <CgArrowsShrinkH /> : <CgArrowsMergeAltH />}
/>
<SidebarButton
title={graphCollapsed ? "Expand Graph" : "Collapse Graph"}
>
<StyledElement onClick={toggleExpandCollapseGraph}>
{graphCollapsed ? <VscExpandAll /> : <VscCollapseAll />}
</StyledElement>
</Tooltip>
<Tooltip className="desktop" title="Save JSON">
<StyledElement onClick={handleSave}>
<AiOutlineSave />
</StyledElement>
</Tooltip>
<Tooltip className="mobile" title="Download Image">
<StyledElement onClick={() => setDownloadVisible(true)}>
<FiDownload />
</StyledElement>
</Tooltip>
<Tooltip title="Clear JSON">
<StyledElement onClick={() => setClearVisible(true)}>
<AiOutlineDelete />
</StyledElement>
</Tooltip>
<Tooltip className="desktop" title="Share">
<StyledElement onClick={() => setShareVisible(true)}>
<AiOutlineLink />
</StyledElement>
</Tooltip>
deviceDisplay="desktop"
onClick={toggleExpandCollapseGraph}
component={graphCollapsed ? <VscExpandAll /> : <VscCollapseAll />}
/>
<SidebarButton
title="Download JSON"
deviceDisplay="desktop"
onClick={handleSave}
component={<AiOutlineSave />}
/>
<SidebarButton
title="Download Image"
deviceDisplay="mobile"
onClick={() => setVisible("download")(true)}
component={<FiDownload />}
/>
<SidebarButton
title="Delete JSON"
onClick={() => setVisible("clear")(true)}
component={<AiOutlineDelete />}
/>
<SidebarButton
title="View Cloud"
deviceDisplay="desktop"
onClick={() => setVisible("cloud")(true)}
component={<VscCloud />}
/>
</StyledTopWrapper>
<StyledBottomWrapper>
<StyledElement>
<Link href="https://twitter.com/jsoncrack">
<a aria-label="Twitter" rel="me" target="_blank">
<AiOutlineTwitter />
</a>
</Link>
</StyledElement>
<StyledElement>
<Link href="https://github.com/AykutSarac/jsoncrack.com">
<a aria-label="GitHub" rel="me" target="_blank">
<AiFillGithub />
</a>
</Link>
</StyledElement>
<StyledElement>
<Link href="https://github.com/sponsors/AykutSarac">
<a aria-label="GitHub Sponsors" rel="me" target="_blank">
<HiHeart />
</a>
</Link>
</StyledElement>
<SidebarButton
title="Account"
onClick={() => setVisible("account")(true)}
component={<VscAccount />}
/>
<SidebarButton
title="Settings"
onClick={() => setVisible("settings")(true)}
component={<VscSettingsGear />}
/>
</StyledBottomWrapper>
<ImportModal visible={uploadVisible} setVisible={setUploadVisible} />
<ClearModal visible={clearVisible} setVisible={setClearVisible} />
<ShareModal visible={shareVisible} setVisible={setShareVisible} />
<DownloadModal visible={isDownloadVisible} setVisible={setDownloadVisible} />
</StyledSidebar>
);
};

View File

@ -0,0 +1,28 @@
import React from "react";
import { CgSpinner } from "react-icons/cg";
import styled, { keyframes } from "styled-components";
const rotateAnimation = keyframes`
to { transform: rotate(360deg); }
`;
const StyledSpinnerWrapper = styled.div`
display: flex;
align-items: center;
padding: 25px;
justify-content: center;
width: 100%;
height: 100%;
svg {
animation: ${rotateAnimation} 1s linear infinite;
}
`;
export const Spinner = () => {
return (
<StyledSpinnerWrapper>
<CgSpinner size={40} />
</StyledSpinnerWrapper>
);
};

View File

@ -24,7 +24,7 @@ async function getSponsors() {
const StyledSponsorsWrapper = styled.ul`
display: flex;
width: 100%;
width: 70%;
margin: 0;
padding: 0;
list-style: none;
@ -60,8 +60,7 @@ const StyledSponsor = styled.li<{ handle: string }>`
transform: translateY(-110%);
border-width: 5px;
border-style: solid;
border-color: ${({ theme }) => theme.BACKGROUND_PRIMARY} transparent
transparent transparent;
border-color: ${({ theme }) => theme.BACKGROUND_PRIMARY} transparent transparent transparent;
}
}
@ -86,13 +85,7 @@ export const Sponsors = () => {
{sponsors.users.map(user => (
<StyledSponsor handle={user.handle} key={user.handle}>
<a href={user.profile} target="_blank" rel="noreferrer">
<img
src={user.avatar}
alt={user.handle}
width="40"
height="40"
loading="lazy"
/>
<img src={user.avatar} alt={user.handle} width="40" height="40" loading="lazy" />
</a>
</StyledSponsor>
))}

View File

@ -26,9 +26,10 @@ const StyledSupportButton = styled.a`
transition: all 0.5s;
overflow: hidden;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.07), 0 2px 4px rgba(0, 0, 0, 0.07),
0 4px 8px rgba(0, 0, 0, 0.07), 0 8px 16px rgba(0, 0, 0, 0.07),
0 16px 32px rgba(0, 0, 0, 0.07), 0 32px 64px rgba(0, 0, 0, 0.07);
0 4px 8px rgba(0, 0, 0, 0.07), 0 8px 16px rgba(0, 0, 0, 0.07), 0 16px 32px rgba(0, 0, 0, 0.07),
0 32px 64px rgba(0, 0, 0, 0.07);
opacity: 0.7;
box-sizing: content-box !important;
&:hover {
width: 180px;
@ -43,14 +44,8 @@ const StyledSupportButton = styled.a`
`;
export const SupportButton = () => {
if (location.pathname.includes("widget")) return null;
return (
<StyledSupportButton
href="https://github.com/sponsors/AykutSarac"
target="_blank"
rel="me"
>
<StyledSupportButton href="https://github.com/sponsors/AykutSarac" target="_blank" rel="me">
<StyledText>Support JSON Crack</StyledText>
<HiHeart size={25} />
</StyledSupportButton>

View File

@ -5,14 +5,9 @@ interface TooltipProps extends React.ComponentPropsWithoutRef<"div"> {
title?: string;
}
const StyledTooltipWrapper = styled.div`
position: relative;
width: fit-content;
height: 100%;
`;
const StyledTooltip = styled.div<{ visible: boolean }>`
const StyledTooltip = styled.div`
position: absolute;
display: none;
top: 0;
right: 0;
transform: translate(calc(100% + 15px), 25%);
@ -20,15 +15,15 @@ const StyledTooltip = styled.div<{ visible: boolean }>`
background: ${({ theme }) => theme.BACKGROUND_PRIMARY};
color: ${({ theme }) => theme.TEXT_NORMAL};
border-radius: 5px;
padding: 4px 8px;
display: ${({ visible }) => (visible ? "initial" : "none")};
padding: 6px 8px;
white-space: nowrap;
font-family: "Mona Sans";
font-size: 16px;
user-select: none;
font-weight: 500;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.07), 0 2px 4px rgba(0, 0, 0, 0.07),
0 4px 8px rgba(0, 0, 0, 0.07), 0 8px 16px rgba(0, 0, 0, 0.07),
0 16px 32px rgba(0, 0, 0, 0.07), 0 32px 64px rgba(0, 0, 0, 0.07);
0 4px 8px rgba(0, 0, 0, 0.07), 0 8px 16px rgba(0, 0, 0, 0.07), 0 16px 32px rgba(0, 0, 0, 0.07),
0 32px 64px rgba(0, 0, 0, 0.07);
&::after {
content: "";
@ -38,8 +33,7 @@ const StyledTooltip = styled.div<{ visible: boolean }>`
transform: translate(-90%, 50%);
border-width: 8px;
border-style: solid;
border-color: transparent ${({ theme }) => theme.BACKGROUND_PRIMARY} transparent
transparent;
border-color: transparent ${({ theme }) => theme.BACKGROUND_PRIMARY} transparent transparent;
}
@media only screen and (max-width: 768px) {
@ -47,25 +41,23 @@ const StyledTooltip = styled.div<{ visible: boolean }>`
}
`;
const StyledChildren = styled.div``;
const StyledTooltipWrapper = styled.div`
position: relative;
width: fit-content;
height: 100%;
&:hover ${StyledTooltip} {
display: initial;
}
`;
export const Tooltip: React.FC<React.PropsWithChildren<TooltipProps>> = ({
children,
title,
...props
}) => {
const [visible, setVisible] = React.useState(false);
return (
<StyledTooltipWrapper {...props}>
{title && <StyledTooltip visible={visible}>{title}</StyledTooltip>}
<StyledChildren
onMouseEnter={() => setVisible(true)}
onMouseLeave={() => setVisible(false)}
>
{children}
</StyledChildren>
</StyledTooltipWrapper>
);
};
}) => (
<StyledTooltipWrapper {...props}>
{title && <StyledTooltip>{title}</StyledTooltip>}
<div>{children}</div>
</StyledTooltipWrapper>
);

View File

@ -1,32 +1,45 @@
import { createGlobalStyle } from "styled-components";
const GlobalStyle = createGlobalStyle`
@font-face {
font-family: 'Mona Sans';
src:
url('assets/Mona-Sans.woff2') format('woff2 supports variations'),
url('assets/Mona-Sans.woff2') format('woff2-variations');
font-weight: 200 900;
font-stretch: 75% 125%;
}
svg {
vertical-align: top;
}
h1, h2, h3, h4, p {
font-family: 'Mona Sans';
}
html, body {
margin: 0;
padding: 0;
box-sizing: border-box;
color: ${({ theme }) => theme.FULL_WHITE};
font-family: 'Catamaran', sans-serif;
font-family: 'Mona Sans';
font-weight: 400;
font-size: 16px;
scroll-behavior: smooth;
height: 100%;
background-color: #000000;
opacity: 1;
background-image: radial-gradient(#414141 0.5px, #000000 0.5px);
background-size: 10px 10px;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100%25' height='100%25' viewBox='0 0 800 800'%3E%3Cg fill-opacity='0.3'%3E%3Ccircle fill='%23000000' cx='400' cy='400' r='600'/%3E%3Ccircle fill='%23110718' cx='400' cy='400' r='500'/%3E%3Ccircle fill='%23220e30' cx='400' cy='400' r='400'/%3E%3Ccircle fill='%23331447' cx='400' cy='400' r='300'/%3E%3Ccircle fill='%23441b5f' cx='400' cy='400' r='200'/%3E%3Ccircle fill='%23552277' cx='400' cy='400' r='100'/%3E%3C/g%3E%3C/svg%3E");
background-attachment: fixed;
background-size: cover;
@media only screen and (min-width: 768px) {
background-color: #000000;
opacity: 1;
background-image: radial-gradient(#414141 0.5px, #000000 0.5px);
background-size: 15px 15px;
@media only screen and (max-width: 768px) {
background-position: right;
}
}
* {
-webkit-tap-highlight-color: transparent;
scroll-behavior: smooth;
}
.hide {
@ -42,6 +55,7 @@ const GlobalStyle = createGlobalStyle`
}
button {
font-family: 'Mona Sans';
border: none;
outline: none;
background: transparent;
@ -49,6 +63,7 @@ const GlobalStyle = createGlobalStyle`
margin: 0;
padding: 0;
cursor: pointer;
font-weight: 800;
}
#carbonads * {

View File

@ -1,6 +1,7 @@
const fixedColors = {
CRIMSON: "#DC143C",
BLURPLE: "#5865F2",
PURPLE: "#9036AF",
FULL_WHITE: "#FFFFFF",
BLACK: "#202225",
BLACK_DARK: "#2C2F33",

View File

@ -0,0 +1,184 @@
import React from "react";
import { useRouter } from "next/router";
import toast from "react-hot-toast";
import {
AiOutlineCloudSync,
AiOutlineCloudUpload,
AiOutlineLink,
AiOutlineLock,
AiOutlineUnlock,
} from "react-icons/ai";
import { VscAccount } from "react-icons/vsc";
import { saveJson, updateJson } from "src/services/db/json";
import useJson from "src/store/useJson";
import useModal from "src/store/useModal";
import useStored from "src/store/useStored";
import useUser from "src/store/useUser";
import styled from "styled-components";
const StyledBottomBar = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
border-top: 1px solid ${({ theme }) => theme.BACKGROUND_MODIFIER_ACCENT};
background: ${({ theme }) => theme.BACKGROUND_TERTIARY};
max-height: 28px;
height: 28px;
padding: 0 6px;
`;
const StyledLeft = styled.div`
display: flex;
align-items: center;
justify-content: left;
gap: 4px;
`;
const StyledRight = styled.div`
display: flex;
align-items: center;
justify-content: right;
gap: 4px;
`;
const StyledBottomBarItem = styled.button`
display: flex;
align-items: center;
gap: 4px;
width: fit-content;
margin: 0;
height: 28px;
padding: 4px;
font-size: 12px;
font-weight: 400;
color: ${({ theme }) => theme.INTERACTIVE_NORMAL};
&:hover:not(&:disabled) {
background-image: linear-gradient(rgba(0, 0, 0, 0.1) 0 0);
color: ${({ theme }) => theme.INTERACTIVE_HOVER};
}
&:disabled {
opacity: 0.4;
cursor: progress;
}
`;
const StyledImg = styled.img<{ light: boolean }>`
filter: ${({ light }) => light && "invert(100%)"};
`;
export const BottomBar = () => {
const { replace, query } = useRouter();
const data = useJson(state => state.data);
const user = useUser(state => state.user);
const lightmode = useStored(state => state.lightmode);
const hasChanges = useJson(state => state.hasChanges);
const getJson = useJson(state => state.getJson);
const setVisible = useModal(state => state.setVisible);
const setHasChanges = useJson(state => state.setHasChanges);
const [isPrivate, setIsPrivate] = React.useState(false);
const [isUpdating, setIsUpdating] = React.useState(false);
React.useEffect(() => {
setIsPrivate(data?.private ?? false);
}, [data]);
const handleSaveJson = React.useCallback(async () => {
if (!user) return setVisible("login")(true);
if (hasChanges) {
try {
setIsUpdating(true);
toast.loading("Saving JSON...", { id: "jsonSave" });
const res = await saveJson({ id: query.json as string, data: getJson() });
if (res.errors && res.errors.items.length > 0) throw res.errors;
if (res.data._id) replace({ query: { json: res.data._id } });
toast.success("JSON saved to cloud", { id: "jsonSave" });
setHasChanges(false);
} catch (error: any) {
if (error?.items?.length > 0) {
return toast.error(error.items[0].message, { id: "jsonSave", duration: 5000 });
}
toast.error("Failed to save JSON!", { id: "jsonSave" });
} finally {
setIsUpdating(false);
}
}
}, [getJson, hasChanges, query.json, replace, setHasChanges, setVisible, user]);
const handleLoginClick = () => {
if (user) return setVisible("account")(true);
else setVisible("login")(true);
};
const setPrivate = async () => {
try {
if (!query.json) return handleSaveJson();
if (!isPrivate && user?.type === 0) {
return window.open("https://jsoncrack.com/pricing", "_blank");
}
setIsUpdating(true);
const res = await updateJson(query.json as string, { private: !isPrivate });
if (!res.errors?.items.length) {
setIsPrivate(res.data.private);
toast.success(`Document set to ${isPrivate ? "public" : "private"}.`);
} else throw res.errors;
} catch (error) {
toast.error("An error occured while updating document!");
} finally {
setIsUpdating(false);
}
};
return (
<StyledBottomBar>
<StyledLeft>
<StyledBottomBarItem onClick={handleLoginClick}>
<VscAccount />
{user ? user.name : "Login"}
</StyledBottomBarItem>
<StyledBottomBarItem onClick={handleSaveJson} disabled={isUpdating}>
{hasChanges ? <AiOutlineCloudUpload /> : <AiOutlineCloudSync />}
{hasChanges ? "Unsaved Changes" : "Saved"}
</StyledBottomBarItem>
{data && (
<>
{typeof data.private !== "undefined" && (
<StyledBottomBarItem onClick={setPrivate} disabled={isUpdating}>
{isPrivate ? <AiOutlineLock /> : <AiOutlineUnlock />}
{isPrivate ? "Private" : "Public"}
</StyledBottomBarItem>
)}
<StyledBottomBarItem onClick={() => setVisible("share")(true)}>
<AiOutlineLink />
Share
</StyledBottomBarItem>
</>
)}
</StyledLeft>
<StyledRight>
<a
href="https://www.altogic.com/?utm_source=jsoncrack&utm_medium=referral&utm_campaign=sponsorship"
rel="sponsored noreferrer"
target="_blank"
>
<StyledBottomBarItem>
Powered by
<StyledImg
height="20"
src="https://www.altogic.com/img/logo_dark.svg"
alt="powered by altogic"
light={lightmode}
/>
</StyledBottomBarItem>
</a>
</StyledRight>
</StyledBottomBar>
);
};

View File

@ -11,12 +11,10 @@ const StyledEditorWrapper = styled.div`
user-select: none;
`;
export const JsonEditor: React.FC = () => {
const [hasError, setHasError] = React.useState(false);
return (
<StyledEditorWrapper>
<ErrorContainer hasError={hasError} />
<MonacoEditor setHasError={setHasError} />
<ErrorContainer />
<MonacoEditor />
</StyledEditorWrapper>
);
};

View File

@ -7,11 +7,10 @@ export const GraphCanvas = ({ isWidget = false }: { isWidget?: boolean }) => {
const [isModalVisible, setModalVisible] = React.useState(false);
const [selectedNode, setSelectedNode] = React.useState<[string, string][]>([]);
const openModal = React.useCallback(() => setModalVisible(true), []);
const collapsedNodes = useGraph(state => state.collapsedNodes);
const collapsedEdges = useGraph(state => state.collapsedEdges);
const loading = useGraph(state => state.loading);
const openModal = React.useCallback(() => setModalVisible(true), []);
React.useEffect(() => {
const nodeList = collapsedNodes.map(id => `[id$="node-${id}"]`);
@ -22,27 +21,23 @@ export const GraphCanvas = ({ isWidget = false }: { isWidget?: boolean }) => {
if (nodeList.length) {
const selectedNodes = document.querySelectorAll(nodeList.join(","));
const selectedEdges = document.querySelectorAll(edgeList.join(","));
selectedNodes.forEach(node => node.classList.add("hide"));
}
if (edgeList.length) {
const selectedEdges = document.querySelectorAll(edgeList.join(","));
selectedEdges.forEach(edge => edge.classList.add("hide"));
}
}, [collapsedNodes, collapsedEdges, loading]);
}, [collapsedNodes, collapsedEdges]);
return (
<>
<Graph
openModal={openModal}
setSelectedNode={setSelectedNode}
isWidget={isWidget}
<Graph openModal={openModal} setSelectedNode={setSelectedNode} isWidget={isWidget} />
<NodeModal
selectedNode={selectedNode}
visible={isModalVisible}
closeModal={() => setModalVisible(false)}
/>
{!isWidget && (
<NodeModal
selectedNode={selectedNode}
visible={isModalVisible}
closeModal={() => setModalVisible(false)}
/>
)}
</>
);
};

View File

@ -3,7 +3,7 @@ import dynamic from "next/dynamic";
import { Allotment } from "allotment";
import "allotment/dist/style.css";
import { JsonEditor } from "src/containers/Editor/JsonEditor";
import useConfig from "src/store/useConfig";
import useGraph from "src/store/useGraph";
import styled from "styled-components";
export const StyledEditor = styled(Allotment)`
@ -17,25 +17,25 @@ const LiveEditor = dynamic(() => import("src/containers/Editor/LiveEditor"), {
});
const Panes: React.FC = () => {
const hideEditor = useConfig(state => state.hideEditor);
const setConfig = useConfig(state => state.setConfig);
const isMobile = window.innerWidth <= 768;
const fullscreen = useGraph(state => state.fullscreen);
const toggleFullscreen = useGraph(state => state.toggleFullscreen);
const isMobile = React.useMemo(() => window.innerWidth <= 768, []);
React.useEffect(() => {
if (isMobile) setConfig("hideEditor", true);
}, [isMobile, setConfig]);
if (isMobile) toggleFullscreen(true);
}, [isMobile, toggleFullscreen]);
return (
<StyledEditor proportionalLayout={false} vertical={isMobile}>
<Allotment.Pane
preferredSize={isMobile ? "100%" : 400}
minSize={hideEditor ? 0 : 300}
minSize={fullscreen ? 0 : 300}
maxSize={isMobile ? Infinity : 800}
visible={!hideEditor}
visible={!fullscreen}
>
<JsonEditor />
</Allotment.Pane>
<Allotment.Pane minSize={0} maxSize={isMobile && !hideEditor ? 0 : Infinity}>
<Allotment.Pane minSize={0} maxSize={isMobile && !fullscreen ? 0 : Infinity}>
<LiveEditor />
</Allotment.Pane>
</StyledEditor>

View File

@ -2,12 +2,10 @@ import React from "react";
import { AiOutlineFullscreen, AiOutlineMinus, AiOutlinePlus } from "react-icons/ai";
import { FiDownload } from "react-icons/fi";
import { MdCenterFocusWeak } from "react-icons/md";
import { TbSettings } from "react-icons/tb";
import { SearchInput } from "src/components/SearchInput";
import { SettingsModal } from "src/containers/Modals/SettingsModal";
import useConfig from "src/store/useConfig";
import useGraph from "src/store/useGraph";
import useModal from "src/store/useModal";
import styled from "styled-components";
import { DownloadModal } from "../Modals/DownloadModal";
export const StyledTools = styled.div`
position: relative;
@ -48,16 +46,15 @@ const StyledToolElement = styled.button`
`;
export const Tools: React.FC = () => {
const [settingsVisible, setSettingsVisible] = React.useState(false);
const [isDownloadVisible, setDownloadVisible] = React.useState(false);
const setVisible = useModal(state => state.setVisible);
const hideEditor = useConfig(state => state.hideEditor);
const setConfig = useConfig(state => state.setConfig);
const fullscreen = useGraph(state => state.fullscreen);
const toggleFullscreen = useGraph(state => state.toggleFullscreen);
const zoomIn = useConfig(state => state.zoomIn);
const zoomOut = useConfig(state => state.zoomOut);
const centerView = useConfig(state => state.centerView);
const toggleEditor = () => setConfig("hideEditor", !hideEditor);
const zoomIn = useGraph(state => state.zoomIn);
const zoomOut = useGraph(state => state.zoomOut);
const centerView = useGraph(state => state.centerView);
const toggleEditor = () => toggleFullscreen(!fullscreen);
return (
<>
@ -65,17 +62,8 @@ export const Tools: React.FC = () => {
<StyledToolElement aria-label="fullscreen" onClick={toggleEditor}>
<AiOutlineFullscreen />
</StyledToolElement>
<StyledToolElement
aria-label="settings"
onClick={() => setSettingsVisible(true)}
>
<TbSettings />
</StyledToolElement>
<SearchInput />
<StyledToolElement
aria-label="save"
onClick={() => setDownloadVisible(true)}
>
<StyledToolElement aria-label="save" onClick={() => setVisible("download")(true)}>
<FiDownload />
</StyledToolElement>
<StyledToolElement aria-label="center canvas" onClick={centerView}>
@ -88,8 +76,6 @@ export const Tools: React.FC = () => {
<AiOutlinePlus />
</StyledToolElement>
</StyledTools>
<DownloadModal visible={isDownloadVisible} setVisible={setDownloadVisible} />
<SettingsModal visible={settingsVisible} setVisible={setSettingsVisible} />
</>
);
};

View File

@ -2,20 +2,22 @@ import React from "react";
import Head from "next/head";
import Link from "next/link";
import Script from "next/script";
import { FaGithub, FaHeart, FaLinkedin, FaTwitter } from "react-icons/fa";
import { AiOutlineRight } from "react-icons/ai";
import {
HiCursorClick,
HiLightningBolt,
HiOutlineDownload,
HiOutlineSearchCircle,
} from "react-icons/hi";
import { IoRocketSharp } from "react-icons/io5";
import { SiVisualstudiocode } from "react-icons/si";
import { CarbonAds } from "src/components/CarbonAds";
import { Footer } from "src/components/Footer";
import { Producthunt } from "src/components/Producthunt";
import { Sponsors } from "src/components/Sponsors";
import { defaultJson } from "src/constants/data";
import { GoalsModal } from "src/containers/Modals/GoalsModal";
import pkg from "../../../package.json";
import { SupportButton } from "src/components/SupportButton";
import { baseURL } from "src/constants/data";
import { PricingCards } from "../PricingCards";
import * as Styles from "./styles";
const Navbar = () => (
@ -34,6 +36,9 @@ const Navbar = () => (
>
GitHub
</Styles.StyledNavLink>
<Link href="docs" passHref>
<Styles.StyledNavLink>Documentation</Styles.StyledNavLink>
</Link>
</Styles.StyledNavbar>
);
@ -43,23 +48,22 @@ const HeroSection = () => {
return (
<Styles.StyledHeroSection id="main">
<Styles.StyledTitle>
<Styles.StyledGradientText>JSON</Styles.StyledGradientText> Crack
<Styles.StyledGradientText>JSON</Styles.StyledGradientText> CRACK
</Styles.StyledTitle>
<Styles.StyledSubTitle>
Seamlessly visualize your JSON data{" "}
<Styles.StyledHighlightedText>instantly</Styles.StyledHighlightedText> into
graphs.
<Styles.StyledHighlightedText>instantly</Styles.StyledHighlightedText> into graphs.
</Styles.StyledSubTitle>
<Styles.StyledMinorTitle>Paste - Import - Fetch!</Styles.StyledMinorTitle>
<Styles.StyledButton rel="prefetch" href="/editor" link>
<Styles.StyledButton href="/editor" link>
GO TO EDITOR
<AiOutlineRight strokeWidth="80" />
</Styles.StyledButton>
<Styles.StyledButtonWrapper>
<Styles.StyledSponsorButton onClick={() => setModalVisible(true)}>
Help JSON Crack&apos;s Goals
<FaHeart />
<Styles.StyledSponsorButton href="/pricing" link>
GET PREMIUM
<IoRocketSharp />
</Styles.StyledSponsorButton>
<Styles.StyledSponsorButton
href="https://marketplace.visualstudio.com/items?itemName=AykutSarac.jsoncrack-vscode"
@ -69,7 +73,6 @@ const HeroSection = () => {
GET IT ON VS CODE
<SiVisualstudiocode />
</Styles.StyledSponsorButton>
<GoalsModal visible={isModalVisible} setVisible={setModalVisible} />
</Styles.StyledButtonWrapper>
</Styles.StyledHeroSection>
);
@ -96,9 +99,9 @@ const FeaturesSection = () => (
</Styles.StyledCardIcon>
<Styles.StyledCardTitle>EASY-TO-USE</Styles.StyledCardTitle>
<Styles.StyledCardDescription>
Don&apos;t even bother to update your schema to view your JSON into graphs;
directly paste, import or fetch! JSON Crack helps you to visualize without
any additional values and save your time.
We believe that powerful software doesn&apos;t have to be difficult to use. That&apos;s why
we&apos;ve designed our app to be as intuitive and easy-to-use as possible. You can quickly
and easily load your JSON data and start exploring and analyzing it right away!
</Styles.StyledCardDescription>
</Styles.StyledSectionCard>
@ -108,9 +111,9 @@ const FeaturesSection = () => (
</Styles.StyledCardIcon>
<Styles.StyledCardTitle>SEARCH</Styles.StyledCardTitle>
<Styles.StyledCardDescription>
Have a huge file of values, keys or arrays? Worry no more, type in the
keyword you are looking for into search input and it will take you to each
node with matching result highlighting the line to understand better!
Have a huge file of values, keys or arrays? Worry no more, type in the keyword you are
looking for into search input and it will take you to each node with matching result
highlighting the line to understand better!
</Styles.StyledCardDescription>
</Styles.StyledSectionCard>
@ -120,9 +123,9 @@ const FeaturesSection = () => (
</Styles.StyledCardIcon>
<Styles.StyledCardTitle>DOWNLOAD</Styles.StyledCardTitle>
<Styles.StyledCardDescription>
Download the graph to your local machine and use it wherever you want, to
your blogs, website or make it a poster and paste to the wall. Who
wouldn&apos;t want to see a JSON Crack graph onto their wall, eh?
Download the graph to your local machine and use it wherever you want, to your blogs,
website or make it a poster and paste to the wall. Who wouldn&apos;t want to see a JSON
Crack graph onto their wall, eh?
</Styles.StyledCardDescription>
</Styles.StyledSectionCard>
@ -132,9 +135,9 @@ const FeaturesSection = () => (
</Styles.StyledCardIcon>
<Styles.StyledCardTitle>LIVE</Styles.StyledCardTitle>
<Styles.StyledCardDescription>
With Microsoft&apos;s Monaco Editor which is also used by VS Code, easily
edit your JSON and directly view through the graphs. Also there&apos;s a JSON
validator above of it to make sure there is no type error.
With Microsoft&apos;s Monaco Editor which is also used by VS Code, easily edit your JSON and
directly view through the graphs. Also there&apos;s a JSON validator above of it to make
sure there is no type error.
</Styles.StyledCardDescription>
</Styles.StyledSectionCard>
</Styles.StyledFeaturesSection>
@ -143,40 +146,37 @@ const FeaturesSection = () => (
const GitHubSection = () => (
<Styles.StyledSection id="github" reverse>
<Styles.StyledTwitterQuote>
<blockquote
className="twitter-tweet"
data-lang="en"
data-dnt="true"
data-theme="light"
>
<blockquote className="twitter-tweet" data-lang="en" data-dnt="true" data-theme="light">
<p lang="en" dir="ltr">
Looking to understand or explore some JSON? Just paste or upload to
visualize it as a graph with{" "}
<a href="https://t.co/HlKSrhKryJ">https://t.co/HlKSrhKryJ</a> 😍 <br />
Looking to understand or explore some JSON? Just paste or upload to visualize it as a
graph with <a href="https://t.co/HlKSrhKryJ">https://t.co/HlKSrhKryJ</a> 😍 <br />
<br />
Thanks to{" "}
<a href="https://twitter.com/aykutsarach?ref_src=twsrc%5Etfw">
Thanks to <a href="https://twitter.com/aykutsarach?ref_src=twsrc%5Etfw">
@aykutsarach
</a>
! <a href="https://t.co/0LyPUL8Ezz">pic.twitter.com/0LyPUL8Ezz</a>
</a>! <a href="https://t.co/0LyPUL8Ezz">pic.twitter.com/0LyPUL8Ezz</a>
</p>
&mdash; GitHub (@github){" "}
<a href="https://twitter.com/github/status/1519363257794015233?ref_src=twsrc%5Etfw">
April 27, 2022
</a>
</blockquote>{" "}
<Script
async
src="https://platform.twitter.com/widgets.js"
charSet="utf-8"
></Script>
<Script async src="https://platform.twitter.com/widgets.js" charSet="utf-8"></Script>
</Styles.StyledTwitterQuote>
<Styles.StyledSectionArea>
<Styles.StyledSubTitle>Open Source Community</Styles.StyledSubTitle>
<Styles.StyledMinorTitle>
Join the Open Source Community by suggesting new ideas, support by
contributing; implementing new features, fixing bugs and doing changes minor
to major!
At JSON Crack, we believe in the power of open source software and the communities that
support it. That&apos;s why we&apos;re proud to be part of the open source community and to
contribute to the development and growth of open source tools and technologies.
<br />
<br /> As part of our commitment to the open source community, we&apos;ve made our app
freely available to anyone who wants to use it, and we welcome contributions from anyone
who&apos;s interested in helping to improve it. Whether you&apos;re a developer, a data
scientist, or just someone who&apos;s passionate about open source, we&apos;d love to have
you join our community and help us make JSON Crack the best it can be.
<br />
<br /> So why not join us and become part of the JSON Crack open source community today? We
can&apos;t wait to see what we can accomplish together!
</Styles.StyledMinorTitle>
<Styles.StyledButton
href="https://github.com/AykutSarac/jsoncrack.com"
@ -194,30 +194,38 @@ const EmbedSection = () => (
<Styles.StyledSectionArea>
<Styles.StyledSubTitle>Embed Into Your Website</Styles.StyledSubTitle>
<Styles.StyledMinorTitle>
Easily embed the JSON Crack graph into your website to showcase your
visitors, blog readers or anybody else!
JSON Crack provides users with the necessary code to embed the app into a website easily
using an iframe. This code can be easily copied and pasted into the desired location on the
website, allowing users to quickly and easily integrate JSON Crack into existing workflows.
<br />
<br /> Once the app is embedded, users can use it to view and analyze JSON data directly on
the website. This can be useful for a variety of purposes, such as quickly checking the
structure of a JSON file or verifying the data contained within it. JSON Crack&apos;s
intuitive interface makes it easy to navigate and understand even complex JSON data, making
it a valuable tool for anyone working with JSON.
</Styles.StyledMinorTitle>
<Styles.StyledButton
href="https://jsoncrack.com/embed"
status="SECONDARY"
link
>
<Styles.StyledButton href="/docs" status="SECONDARY" link>
LEARN TO EMBED
</Styles.StyledButton>
</Styles.StyledSectionArea>
<div>
<Styles.StyledIframge
src="https://jsoncrack.com/widget"
src={`${baseURL}/widget`}
onLoad={e => {
const frame = e.currentTarget.contentWindow;
setTimeout(() => {
frame?.postMessage(
{
json: defaultJson,
json: JSON.stringify({
"random images": [
"https://random.imagecdn.app/50/50?.png",
"https://random.imagecdn.app/80/80?.png",
"https://random.imagecdn.app/100/100?.png",
],
}),
options: {
theme: "dark",
direction: "DOWN"
}
},
},
"*"
);
@ -263,42 +271,6 @@ const SponsorSection = () => (
</Styles.StyledSponsorSection>
);
const Footer = () => (
<Styles.StyledFooter>
<Styles.StyledFooterText>
© 2022 JSON Crack - {pkg.version}
</Styles.StyledFooterText>
<Styles.StyledIconLinks>
<Styles.StyledNavLink
href="https://github.com/AykutSarac/jsoncrack.com"
rel="external"
target="_blank"
aria-label="github"
>
<FaGithub size={26} />
</Styles.StyledNavLink>
<Styles.StyledNavLink
href="https://www.linkedin.com/in/aykutsarac/"
rel="me"
target="_blank"
aria-label="linkedin"
>
<FaLinkedin size={26} />
</Styles.StyledNavLink>
<Styles.StyledNavLink
href="https://twitter.com/jsoncrack"
rel="me"
target="_blank"
aria-label="twitter"
>
<FaTwitter size={26} />
</Styles.StyledNavLink>
</Styles.StyledIconLinks>
</Styles.StyledFooter>
);
const Home: React.FC = () => {
return (
<Styles.StyledHome>
@ -311,8 +283,10 @@ const Home: React.FC = () => {
<FeaturesSection />
<GitHubSection />
<EmbedSection />
<PricingCards />
<SupportSection />
<SponsorSection />
<SupportButton />
<Footer />
</Styles.StyledHome>
);

View File

@ -4,6 +4,10 @@ import styled from "styled-components";
export const StyledButtonWrapper = styled.div`
display: flex;
gap: 18px;
@media only screen and (max-width: 768px) {
display: none;
}
`;
export const StyledTwitterQuote = styled.div`
@ -70,13 +74,7 @@ export const StyledHome = styled.div`
export const StyledGradientText = styled.span`
background: #ffb76b;
background: linear-gradient(
to right,
#ffb76b 0%,
#ffa73d 30%,
#ff7c00 60%,
#ff7f04 100%
);
background: linear-gradient(to right, #ffb76b 0%, #ffa73d 30%, #ff7c00 60%, #ff7f04 100%);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
@ -105,6 +103,10 @@ export const StyledHeroSection = styled.section`
gap: 1.5em;
min-height: 40vh;
padding: 0 3%;
h2 {
margin-bottom: 25px;
}
`;
export const StyledNavLink = styled.a`
@ -122,12 +124,12 @@ export const StyledNavLink = styled.a`
`;
export const StyledTitle = styled.h1`
font-size: 5rem;
font-weight: 900;
margin: 0;
font-size: min(10vw, 64px);
@media only screen and (max-width: 768px) {
font-size: 3rem;
font-size: 2.5rem;
}
`;
@ -139,14 +141,14 @@ export const StyledSubTitle = styled.h2`
margin: 0;
@media only screen and (max-width: 768px) {
font-size: 1.75rem;
font-size: 1.5rem;
}
`;
export const StyledMinorTitle = styled.p`
color: #b4b4b4;
text-align: center;
font-size: 1.25rem;
font-size: 1rem;
margin: 0;
letter-spacing: 1.2px;
@ -158,11 +160,12 @@ export const StyledMinorTitle = styled.p`
export const StyledButton = styled(Button)`
background: ${({ status }) => !status && "#a13cc2"};
padding: 12px 24px;
height: 46px;
div {
font-family: "Roboto", sans-serif;
font-family: "Mona Sans";
font-weight: 700;
font-size: 16px;
font-size: 1rem;
}
`;
@ -189,28 +192,38 @@ export const StyledSponsorButton = styled(Button)<{ isBlue?: boolean }>`
color: white;
}
}
@media only screen and (max-width: 768px) {
display: ${({ isBlue }) => isBlue && "none"};
}
`;
export const StyledFeaturesSection = styled.section`
display: flex;
max-width: 85%;
display: grid;
margin: 0 auto;
gap: 2rem;
padding: 50px 3%;
max-width: 60%;
justify-content: center;
grid-template-columns: repeat(2, 40%);
grid-template-rows: repeat(2, 1fr);
grid-column-gap: 60px;
grid-row-gap: 60px;
@media only screen and (max-width: 768px) {
flex-direction: column;
max-width: 80%;
display: flex;
}
`;
export const StyledSectionCard = styled.div`
text-align: center;
flex: 1;
border: 1px solid ${({ theme }) => theme.BACKGROUND_MODIFIER_ACCENT};
background: rgb(48, 0, 65);
background: linear-gradient(
138deg,
rgba(48, 0, 65, 0.8870141806722689) 0%,
rgba(72, 12, 84, 0.40802258403361347) 33%,
rgba(65, 8, 92, 0.6012998949579832) 100%
);
border-radius: 6px;
padding: 16px;
`;
export const StyledCardTitle = styled.div`
@ -255,7 +268,7 @@ export const StyledSection = styled.section<{ reverse?: boolean }>`
width: 100%;
}
@media only screen and (max-width: 768px) {
@media only screen and (max-width: 1200px) {
flex-direction: ${({ reverse }) => (reverse ? "column-reverse" : "column")};
max-width: 80%;
}
@ -278,8 +291,7 @@ export const StyledSectionArea = styled.div`
width: 100%;
align-items: center;
h2,
p {
h2 {
text-align: center;
}
}
@ -297,7 +309,7 @@ export const StyledSponsorSection = styled.section`
padding: 50px 3%;
@media only screen and (max-width: 768px) {
max-width: 80%;
max-width: 90%;
}
`;
@ -327,7 +339,6 @@ export const StyledPreviewSection = styled.section`
@media only screen and (max-width: 768px) {
display: none;
max-width: 80%;
}
`;
@ -338,30 +349,6 @@ export const StyledImage = styled.img`
filter: drop-shadow(0px 0px 12px rgba(255, 255, 255, 0.6));
`;
export const StyledFooter = styled.footer`
display: flex;
flex-direction: row;
justify-content: space-between;
width: 80%;
margin: 0 auto;
padding: 30px 3%;
border-top: 1px solid #b4b4b4;
opacity: 0.7;
`;
export const StyledFooterText = styled.p`
color: #b4b4b4;
`;
export const StyledIconLinks = styled.div`
display: flex;
gap: 20px;
${StyledNavLink} {
color: unset;
}
`;
export const StyledHighlightedText = styled.span`
text-decoration: underline;
text-decoration-style: dashed;

View File

@ -0,0 +1,51 @@
import React from "react";
import { AccountModal } from "src/containers/Modals/AccountModal";
import { ClearModal } from "src/containers/Modals/ClearModal";
import { CloudModal } from "src/containers/Modals/CloudModal";
import { DownloadModal } from "src/containers/Modals/DownloadModal";
import { ImportModal } from "src/containers/Modals/ImportModal";
import { LoginModal } from "src/containers/Modals/LoginModal";
import { SettingsModal } from "src/containers/Modals/SettingsModal";
import { ShareModal } from "src/containers/Modals/ShareModal";
import useModal from "src/store/useModal";
import shallow from "zustand/shallow";
export const ModalController = () => {
const setVisible = useModal(state => state.setVisible);
const [
importModal,
clearModal,
downloadModal,
settingsModal,
cloudModal,
accountModal,
loginModal,
shareModal,
] = useModal(
state => [
state.import,
state.clear,
state.download,
state.settings,
state.cloud,
state.account,
state.login,
state.share,
],
shallow
);
return (
<>
<ImportModal visible={importModal} setVisible={setVisible("import")} />
<ClearModal visible={clearModal} setVisible={setVisible("clear")} />
<DownloadModal visible={downloadModal} setVisible={setVisible("download")} />
<SettingsModal visible={settingsModal} setVisible={setVisible("settings")} />
<CloudModal visible={cloudModal} setVisible={setVisible("cloud")} />
<AccountModal visible={accountModal} setVisible={setVisible("account")} />
<LoginModal visible={loginModal} setVisible={setVisible("login")} />
<ShareModal visible={shareModal} setVisible={setVisible("share")} />
</>
);
};

View File

@ -0,0 +1,143 @@
import React from "react";
import { IoRocketSharp } from "react-icons/io5";
import { MdVerified } from "react-icons/md";
import { Button } from "src/components/Button";
import { Modal, ModalProps } from "src/components/Modal";
import useUser from "src/store/useUser";
import styled from "styled-components";
const StyledTitle = styled.p`
display: flex;
align-items: center;
color: ${({ theme }) => theme.TEXT_POSITIVE};
flex: 1;
font-weight: 700;
margin-top: 0;
&::after {
background: ${({ theme }) => theme.TEXT_POSITIVE};
height: 1px;
content: "";
-webkit-box-flex: 1;
-ms-flex: 1 1 auto;
flex: 1 1 auto;
margin-left: 4px;
opacity: 0.6;
}
`;
const StyledAccountWrapper = styled.div`
display: flex;
flex-wrap: wrap;
gap: 20px;
background: ${({ theme }) => theme.BACKGROUND_TERTIARY};
padding: 16px;
border-radius: 6px;
button {
flex-basis: 100%;
}
`;
const StyledAvatar = styled.img`
border-radius: 100%;
`;
const StyledContainer = styled.div`
display: flex;
flex-direction: column;
gap: 8px;
padding: 12px 0;
font-size: 12px;
line-height: 16px;
font-weight: 600;
color: ${({ theme }) => theme.INTERACTIVE_NORMAL};
& > div {
font-weight: 400;
font-size: 14px;
color: ${({ theme }) => theme.INTERACTIVE_ACTIVE};
}
`;
const AccountView: React.FC<Pick<ModalProps, "setVisible">> = ({ setVisible }) => {
const user = useUser(state => state.user);
const isPremium = useUser(state => state.isPremium());
const logout = useUser(state => state.logout);
const onImgFail = (e: React.SyntheticEvent<HTMLImageElement>) => {
e.currentTarget.setAttribute("src", `https://ui-avatars.com/api/?name=${user?.name}`);
};
return (
<>
<Modal.Header>Account</Modal.Header>
<Modal.Content>
<StyledTitle>Hello, {user?.name}!</StyledTitle>
<StyledAccountWrapper>
<StyledAvatar
width="60"
height="60"
src={user?.profilePicture}
alt={user?.name}
onError={onImgFail}
/>
<StyledContainer>
USERNAME
<div>{user?.name}</div>
</StyledContainer>
<StyledContainer>
ACCOUNT STATUS
<div>
{isPremium ? "PREMIUM " : "Free"}
{isPremium && <MdVerified />}
</div>
</StyledContainer>
<StyledContainer>
EMAIL
<div>{user?.email}</div>
</StyledContainer>
<StyledContainer>
REGISTRATION
<div>{user?.signUpAt && new Date(user.signUpAt).toDateString()}</div>
</StyledContainer>
{isPremium ? (
<Button
status="DANGER"
block
onClick={() => window.open("https://patreon.com/jsoncrack", "_blank")}
>
<IoRocketSharp />
Cancel Subscription
</Button>
) : (
<Button href="/pricing" status="TERTIARY" block link>
<IoRocketSharp />
UPGRADE TO PREMIUM!
</Button>
)}
</StyledAccountWrapper>
</Modal.Content>
<Modal.Controls setVisible={setVisible}>
<Button
status="DANGER"
onClick={() => {
logout();
setVisible(false);
}}
>
Log Out
</Button>
</Modal.Controls>
</>
);
};
export const AccountModal: React.FC<ModalProps> = ({ setVisible, visible }) => {
return (
<Modal visible={visible} setVisible={setVisible}>
<AccountView setVisible={setVisible} />
</Modal>
);
};

View File

@ -1,22 +1,28 @@
import React from "react";
import toast from "react-hot-toast";
import { useRouter } from "next/router";
import { Button } from "src/components/Button";
import { Modal, ModalProps } from "src/components/Modal";
import useConfig from "src/store/useConfig";
import { deleteJson } from "src/services/db/json";
import useJson from "src/store/useJson";
export const ClearModal: React.FC<ModalProps> = ({ visible, setVisible }) => {
const setJson = useConfig(state => state.setJson);
const setJson = useJson(state => state.setJson);
const { query, replace } = useRouter();
const handleClear = () => {
setJson("{}");
toast.success(`Cleared JSON and removed from memory.`);
setVisible(false);
if (typeof query.json === "string") {
deleteJson(query.json);
replace("/editor");
}
};
return (
<Modal visible={visible} setVisible={setVisible}>
<Modal.Header>Clear JSON</Modal.Header>
<Modal.Content>Are you sure you want to clear JSON?</Modal.Content>
<Modal.Header>Delete JSON</Modal.Header>
<Modal.Content>Are you sure you want to delete JSON?</Modal.Content>
<Modal.Controls setVisible={setVisible}>
<Button status="DANGER" onClick={handleClear}>
Confirm

View File

@ -0,0 +1,270 @@
import React from "react";
import { useRouter } from "next/router";
import { useQuery } from "@tanstack/react-query";
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
import toast from "react-hot-toast";
import {
AiOutlineEdit,
AiOutlineInfoCircle,
AiOutlineLock,
AiOutlinePlus,
AiOutlineUnlock,
} from "react-icons/ai";
import { FaTrash } from "react-icons/fa";
import { IoRocketSharp } from "react-icons/io5";
import { Button } from "src/components/Button";
import { Modal, ModalProps } from "src/components/Modal";
import { Spinner } from "src/components/Spinner";
import { deleteJson, getAllJson, saveJson, updateJson } from "src/services/db/json";
import useJson from "src/store/useJson";
import useUser from "src/store/useUser";
import { Json } from "src/typings/altogic";
import styled from "styled-components";
dayjs.extend(relativeTime);
const StyledModalContent = styled.div`
display: flex;
flex-direction: column;
gap: 14px;
overflow: auto;
`;
const StyledJsonCard = styled.a<{ active?: boolean; create?: boolean }>`
display: ${({ create }) => (create ? "block" : "flex")};
align-items: center;
justify-content: space-between;
background: ${({ theme }) => theme.BLACK_SECONDARY};
border: 2px solid ${({ theme, active }) => (active ? theme.SEAGREEN : theme.BLACK_SECONDARY)};
border-radius: 5px;
overflow: hidden;
flex: 1;
height: 160px;
`;
const StyledInfo = styled.div`
padding: 4px 6px;
`;
const StyledTitle = styled.div`
display: flex;
align-items: center;
gap: 4px;
font-size: 14px;
font-weight: 500;
width: fit-content;
cursor: pointer;
span {
overflow: hidden;
text-overflow: ellipsis;
}
`;
const StyledDetils = styled.div`
display: flex;
align-items: center;
font-size: 12px;
gap: 4px;
`;
const StyledModal = styled(Modal)`
#modal-view {
display: none;
}
`;
const StyledDeleteButton = styled(Button)`
background: transparent;
`;
const StyledCreateWrapper = styled.div`
display: flex;
height: 100%;
gap: 6px;
align-items: center;
justify-content: center;
opacity: 0.6;
height: 45px;
font-size: 14px;
cursor: pointer;
`;
const StyledNameInput = styled.input`
background: transparent;
border: none;
outline: none;
width: 90%;
color: ${({ theme }) => theme.SEAGREEN};
font-weight: 600;
`;
const StyledInfoText = styled.span`
font-size: 10px;
color: ${({ theme }) => theme.INTERACTIVE_NORMAL};
svg {
vertical-align: text-top;
margin-right: 4px;
}
`;
const GraphCard: React.FC<{ data: Json; refetch: () => void; active: boolean }> = ({
data,
refetch,
active,
...props
}) => {
const [editMode, setEditMode] = React.useState(false);
const [name, setName] = React.useState(data.name);
const onSubmit = () => {
toast
.promise(updateJson(data._id, { name }), {
loading: "Updating document...",
error: "Error occured while updating document!",
success: `Renamed document to ${name}`,
})
.then(refetch);
setEditMode(false);
};
const onDeleteClick = (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
toast
.promise(deleteJson(data._id), {
loading: "Deleting JSON file...",
error: "An error occured while deleting the file!",
success: `Deleted ${name}!`,
})
.then(refetch);
};
return (
<StyledJsonCard
href={`?json=${data._id}`}
as={editMode ? "div" : "a"}
active={active}
{...props}
>
<StyledInfo>
{editMode ? (
<form onSubmit={onSubmit}>
<StyledNameInput
value={name}
onChange={e => setName(e.currentTarget.value)}
onClick={e => e.preventDefault()}
autoFocus
/>
<input type="submit" hidden />
</form>
) : (
<StyledTitle
onClick={e => {
e.preventDefault();
setEditMode(true);
}}
>
<span>{name}</span>
<AiOutlineEdit />
</StyledTitle>
)}
<StyledDetils>
{data.private ? <AiOutlineLock /> : <AiOutlineUnlock />}
Last modified {dayjs(data.updatedAt).fromNow()}
</StyledDetils>
</StyledInfo>
<StyledDeleteButton onClick={onDeleteClick}>
<FaTrash />
</StyledDeleteButton>
</StyledJsonCard>
);
};
const CreateCard: React.FC<{ reachedLimit: boolean }> = ({ reachedLimit }) => {
const { replace } = useRouter();
const isPremium = useUser(state => state.isPremium());
const getJson = useJson(state => state.getJson);
const setHasChanges = useJson(state => state.setHasChanges);
const onCreate = async () => {
try {
toast.loading("Saving JSON...", { id: "jsonSave" });
const res = await saveJson({ data: getJson() });
if (res.errors && res.errors.items.length > 0) throw res.errors;
toast.success("JSON saved to cloud", { id: "jsonSave" });
setHasChanges(false);
replace({ query: { json: res.data._id } });
} catch (error: any) {
if (error?.items?.length > 0) {
return toast.error(error.items[0].message, { id: "jsonSave", duration: 7000 });
}
toast.error("Failed to save JSON!", { id: "jsonSave" });
}
};
if (!isPremium && reachedLimit)
return (
<StyledJsonCard href="/pricing" create>
<StyledCreateWrapper>
<IoRocketSharp size="18" />
You reached max limit, upgrade to premium for more!
</StyledCreateWrapper>
</StyledJsonCard>
);
return (
<StyledJsonCard onClick={onCreate} create>
<StyledCreateWrapper>
<AiOutlinePlus size="24" />
Create New JSON
</StyledCreateWrapper>
</StyledJsonCard>
);
};
export const CloudModal: React.FC<ModalProps> = ({ visible, setVisible }) => {
const { isReady, query } = useRouter();
const { data, isFetching, refetch } = useQuery(["allJson", query], () => getAllJson(), {
enabled: isReady && visible,
});
return (
<StyledModal visible={visible} setVisible={setVisible}>
<Modal.Header>Saved On The Cloud</Modal.Header>
<Modal.Content>
<StyledModalContent>
{isFetching ? (
<Spinner />
) : (
<>
<CreateCard reachedLimit={data ? data?.data.result.length > 15 : false} />
{data?.data?.result?.map(json => (
<GraphCard
data={json}
key={json._id}
refetch={refetch}
active={query.json === json._id}
/>
))}
</>
)}
</StyledModalContent>
</Modal.Content>
<Modal.Controls setVisible={setVisible}>
<StyledInfoText>
<AiOutlineInfoCircle />
Cloud Save feature is for ease-of-access only and not recommended to store sensitive data,
we don&apos;t guarantee protection of your data.
</StyledInfoText>
</Modal.Controls>
</StyledModal>
);
};

View File

@ -7,7 +7,7 @@ import { FiCopy, FiDownload } from "react-icons/fi";
import { Button } from "src/components/Button";
import { Input } from "src/components/Input";
import { Modal, ModalProps } from "src/components/Modal";
import useConfig from "src/store/useConfig";
import useGraph from "src/store/useGraph";
import styled from "styled-components";
const ColorPickerStyles: Partial<TwitterPickerStylesProps> = {
@ -93,7 +93,7 @@ const StyledColorIndicator = styled.div<{ color: string }>`
`;
export const DownloadModal: React.FC<ModalProps> = ({ visible, setVisible }) => {
const setConfig = useConfig(state => state.setConfig);
const togglePerfMode = useGraph(state => state.togglePerfMode);
const [fileDetails, setFileDetails] = React.useState({
filename: "jsoncrack.com",
backgroundColor: "transparent",
@ -103,7 +103,7 @@ export const DownloadModal: React.FC<ModalProps> = ({ visible, setVisible }) =>
const clipboardImage = async () => {
try {
toast.loading("Copying to clipboard...", { id: "toastClipboard" });
setConfig("performanceMode", false);
togglePerfMode(false);
const imageElement = document.querySelector("svg[id*='ref']") as HTMLElement;
@ -126,14 +126,14 @@ export const DownloadModal: React.FC<ModalProps> = ({ visible, setVisible }) =>
} finally {
toast.dismiss("toastClipboard");
setVisible(false);
setConfig("performanceMode", true);
togglePerfMode(true);
}
};
const exportAsImage = async () => {
try {
toast.loading("Downloading...", { id: "toastDownload" });
setConfig("performanceMode", false);
togglePerfMode(false);
const imageElement = document.querySelector("svg[id*='ref']") as HTMLElement;
@ -148,7 +148,7 @@ export const DownloadModal: React.FC<ModalProps> = ({ visible, setVisible }) =>
} finally {
toast.dismiss("toastDownload");
setVisible(false);
setConfig("performanceMode", true);
togglePerfMode(true);
}
};

View File

@ -1,76 +0,0 @@
import React from "react";
import { useRouter } from "next/router";
import { FaHeart, FaTwitter } from "react-icons/fa";
import { Button } from "src/components/Button";
import { Modal } from "src/components/Modal";
import styled from "styled-components";
const StyledTitle = styled.p`
display: flex;
align-items: center;
color: ${({ theme }) => theme.TEXT_POSITIVE};
flex: 1;
font-weight: 700;
font-family: "Catamaran", sans-serif;
margin-top: 0;
&::after {
background: ${({ theme }) => theme.TEXT_POSITIVE};
height: 1px;
content: "";
-webkit-box-flex: 1;
-ms-flex: 1 1 auto;
flex: 1 1 auto;
margin-left: 4px;
opacity: 0.6;
}
`;
const ButtonsWrapper = styled.div`
display: flex;
padding: 40px 0 0;
gap: 20px;
`;
export const GoalsModal = ({ visible, setVisible }) => {
const { push } = useRouter();
return (
<Modal visible={visible} setVisible={setVisible}>
<Modal.Header>Help JSON Crack&apos;s Goals</Modal.Header>
<Modal.Content>
<StyledTitle>OUR GOAL</StyledTitle>
<b>JSON Crack&apos;s Goal</b> is to keep the service completely free and open
source for everyone! For the contiunity of our service and keep the new
updates coming we need your support to make that possible
<ButtonsWrapper>
<Button
href="https://github.com/sponsors/AykutSarac"
target="_blank"
rel="me"
status="DANGER"
block
link
>
<FaHeart />
Sponsor
</Button>
<Button
href={`https://twitter.com/intent/tweet?button=&url=${encodeURIComponent(
"https://jsoncrack.com"
)}&text=Looking+to+understand+or+explore+some+JSON?+Just+paste+or+upload+to+visualize+it+as+a+graph+with+JSON+Crack+😍&button=`}
rel="noreferrer"
status="SECONDARY"
block
link
>
<FaTwitter />
Share on Twitter
</Button>
</ButtonsWrapper>
</Modal.Content>
<Modal.Controls setVisible={setVisible} />
</Modal>
);
};

View File

@ -4,7 +4,7 @@ import { AiOutlineUpload } from "react-icons/ai";
import { Button } from "src/components/Button";
import { Input } from "src/components/Input";
import { Modal, ModalProps } from "src/components/Modal";
import useConfig from "src/store/useConfig";
import useJson from "src/store/useJson";
import styled from "styled-components";
const StyledModalContent = styled(Modal.Content)`
@ -33,6 +33,7 @@ const StyledUploadWrapper = styled.label`
`;
const StyledFileName = styled.span`
padding-top: 14px;
color: ${({ theme }) => theme.INTERACTIVE_NORMAL};
`;
@ -42,7 +43,7 @@ const StyledUploadMessage = styled.h3`
`;
export const ImportModal: React.FC<ModalProps> = ({ visible, setVisible }) => {
const setJson = useConfig(state => state.setJson);
const setJson = useJson(state => state.setJson);
const [url, setURL] = React.useState("");
const [jsonFile, setJsonFile] = React.useState<File | null>(null);
@ -58,7 +59,7 @@ export const ImportModal: React.FC<ModalProps> = ({ visible, setVisible }) => {
return fetch(url)
.then(res => res.json())
.then(json => {
setJson(JSON.stringify(json));
setJson(JSON.stringify(json, null, 2));
setVisible(false);
})
.catch(() => toast.error("Failed to fetch JSON!"))
@ -99,11 +100,7 @@ export const ImportModal: React.FC<ModalProps> = ({ visible, setVisible }) => {
</StyledUploadWrapper>
</StyledModalContent>
<Modal.Controls setVisible={setVisible}>
<Button
status="SECONDARY"
onClick={handleImportFile}
disabled={!(jsonFile || url)}
>
<Button status="SECONDARY" onClick={handleImportFile} disabled={!(jsonFile || url)}>
Import
</Button>
</Modal.Controls>

View File

@ -0,0 +1,26 @@
import React from "react";
import { AiOutlineGoogle } from "react-icons/ai";
import { altogic } from "src/api/altogic";
import { Button } from "src/components/Button";
import { Modal, ModalProps } from "src/components/Modal";
export const LoginModal: React.FC<ModalProps> = ({ setVisible, visible }) => {
const handleLoginClick = () => {
altogic.auth.signInWithProvider("google");
};
return (
<Modal visible={visible} setVisible={setVisible}>
<Modal.Header>Login</Modal.Header>
<Modal.Content>
<h2>Welcome Back!</h2>
<p>Login to unlock full potential of JSON Crack!</p>
<Button onClick={handleLoginClick} status="SECONDARY" block>
<AiOutlineGoogle size={24} />
Login with Google
</Button>
</Modal.Content>
<Modal.Controls setVisible={setVisible} />
</Modal>
);
};

View File

@ -26,9 +26,7 @@ const StyledTextarea = styled.textarea`
`;
export const NodeModal = ({ selectedNode, visible, closeModal }: NodeModalProps) => {
const nodeData = Array.isArray(selectedNode)
? Object.fromEntries(selectedNode)
: selectedNode;
const nodeData = Array.isArray(selectedNode) ? Object.fromEntries(selectedNode) : selectedNode;
const handleClipboard = () => {
toast.success("Content copied to clipboard!");

View File

@ -1,5 +1,5 @@
import React from "react";
import { Modal } from "src/components/Modal";
import { Modal, ModalProps } from "src/components/Modal";
import Toggle from "src/components/Toggle";
import useStored from "src/store/useStored";
import styled from "styled-components";
@ -16,18 +16,26 @@ const StyledModalWrapper = styled.div`
gap: 20px;
`;
export const SettingsModal: React.FC<{
visible: boolean;
setVisible: React.Dispatch<React.SetStateAction<boolean>>;
}> = ({ visible, setVisible }) => {
export const SettingsModal: React.FC<ModalProps> = ({ visible, setVisible }) => {
const lightmode = useStored(state => state.lightmode);
const setLightTheme = useStored(state => state.setLightTheme);
const [toggleHideCollapse, hideCollapse] = useStored(
state => [state.toggleHideCollapse, state.hideCollapse],
shallow
);
const [toggleHideChildrenCount, hideChildrenCount] = useStored(
state => [state.toggleHideChildrenCount, state.hideChildrenCount],
const [
toggleHideCollapse,
toggleChildrenCount,
toggleImagePreview,
hideCollapse,
childrenCount,
imagePreview,
] = useStored(
state => [
state.toggleHideCollapse,
state.toggleChildrenCount,
state.toggleImagePreview,
state.hideCollapse,
state.childrenCount,
state.imagePreview,
],
shallow
);
@ -36,20 +44,17 @@ export const SettingsModal: React.FC<{
<Modal.Header>Settings</Modal.Header>
<Modal.Content>
<StyledModalWrapper>
<StyledToggle onChange={toggleImagePreview} checked={imagePreview}>
Live Image Preview
</StyledToggle>
<StyledToggle onChange={toggleHideCollapse} checked={hideCollapse}>
Hide Collapse/Expand Button
Display Collapse/Expand Button
</StyledToggle>
<StyledToggle
onChange={toggleHideChildrenCount}
checked={hideChildrenCount}
>
Hide Children Count
<StyledToggle onChange={toggleChildrenCount} checked={childrenCount}>
Display Children Count
</StyledToggle>
<StyledToggle
onChange={() => setLightTheme(!lightmode)}
checked={lightmode}
>
Enable Light Theme
<StyledToggle onChange={() => setLightTheme(!lightmode)} checked={lightmode}>
Light Theme
</StyledToggle>
</StyledModalWrapper>
</Modal.Content>

View File

@ -1,26 +1,11 @@
import React from "react";
import { useRouter } from "next/router";
import { compress } from "compress-json";
import toast from "react-hot-toast";
import { BiErrorAlt } from "react-icons/bi";
import { Button } from "src/components/Button";
import { Input } from "src/components/Input";
import { Modal, ModalProps } from "src/components/Modal";
import { baseURL } from "src/constants/data";
import useConfig from "src/store/useConfig";
import styled from "styled-components";
const StyledWarning = styled.p``;
const StyledErrorWrapper = styled.div`
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: ${({ theme }) => theme.TEXT_DANGER};
font-weight: 600;
`;
const StyledFlex = styled.div`
display: flex;
gap: 12px;
@ -45,21 +30,8 @@ const StyledContainer = styled.div`
`;
export const ShareModal: React.FC<ModalProps> = ({ visible, setVisible }) => {
const json = useConfig(state => state.json);
const [encodedJson, setEncodedJson] = React.useState("");
const navigate = useRouter();
const embedText = `<iframe id="jsoncrackEmbed" src="${baseURL}/widget></iframe>`;
const shareURL = `${baseURL}/editor?json=${encodedJson}`;
React.useEffect(() => {
if (visible) {
const jsonEncode = compress(JSON.parse(json));
const jsonString = JSON.stringify(jsonEncode);
setEncodedJson(encodeURIComponent(jsonString));
}
}, [json, visible]);
const { push, query } = useRouter();
const shareURL = `https://jsoncrack.com/editor?json=${query.json}`;
const handleShare = (value: string) => {
navigator.clipboard.writeText(value);
@ -67,43 +39,32 @@ export const ShareModal: React.FC<ModalProps> = ({ visible, setVisible }) => {
setVisible(false);
};
const onEmbedClick = () => {
push("/docs");
setVisible(false);
};
return (
<Modal visible={visible} setVisible={setVisible}>
<Modal.Header>Create a Share Link</Modal.Header>
<Modal.Content>
{encodedJson.length > 5000 ? (
<StyledErrorWrapper>
<BiErrorAlt size={60} />
<StyledWarning>
Link size exceeds 5000 characters, unable to generate link for file of
this size!
</StyledWarning>
</StyledErrorWrapper>
) : (
<>
<StyledContainer>
Share Link
<StyledFlex>
<Input value={shareURL} type="url" readOnly />
<Button status="SECONDARY" onClick={() => handleShare(shareURL)}>
Copy
</Button>
</StyledFlex>
</StyledContainer>
<StyledContainer>
Embed into your website
<StyledFlex>
<Button
status="SUCCESS"
onClick={() => navigate.push("/embed")}
block
>
Learn How to Embed
</Button>
</StyledFlex>
</StyledContainer>
</>
)}
<StyledContainer>
Share Link
<StyledFlex>
<Input value={shareURL} type="url" readOnly />
<Button status="SECONDARY" onClick={() => handleShare(shareURL)}>
Copy
</Button>
</StyledFlex>
</StyledContainer>
<StyledContainer>
Embed into your website
<StyledFlex>
<Button status="SUCCESS" onClick={onEmbedClick} block>
Learn How to Embed
</Button>
</StyledFlex>
</StyledContainer>
</Modal.Content>
<Modal.Controls setVisible={setVisible}></Modal.Controls>
</Modal>

View File

@ -0,0 +1,128 @@
import React from "react";
import { Button } from "src/components/Button";
import styled from "styled-components";
const StyledSectionBody = styled.div`
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr;
gap: 50px;
align-items: center;
justify-content: space-between;
background: rgba(181, 116, 214, 0.23);
width: 80%;
margin: 5% auto 0;
border-radius: 6px;
padding: 50px;
@media only screen and (max-width: 768px) {
grid-template-columns: 1fr;
grid-template-rows: 1fr 1fr;
padding: 20px;
}
`;
const StyledPricingCard = styled.div<{ premium?: boolean }>`
padding: 6px;
width: 100%;
height: 100%;
${({ premium }) =>
premium
? `
background: rgba(255, 5, 214, 0.19);
border-radius: 4px;
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
border: 1px solid rgba(255, 5, 214, 0.74);`
: `background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
border: 1px solid rgba(255, 255, 255, 0.3);`};
`;
const StyledPricingCardTitle = styled.h2`
text-align: center;
font-weight: 800;
font-size: 24px;
`;
const StyledPricingCardPrice = styled.h3`
text-align: center;
font-weight: 600;
font-size: 24px;
color: ${({ theme }) => theme.SILVER};
`;
const StyledPricingCardDetails = styled.ul`
color: ${({ theme }) => theme.TEXT_NORMAL};
line-height: 2.3;
padding: 20px;
`;
const StyledPricingCardDetailsItem = styled.li`
font-weight: 500;
@media only screen and (max-width: 768px) {
font-size: 14px;
}
`;
const StyledButton = styled(Button)`
border: 1px solid white;
`;
const StyledPricingSection = styled.section`
margin: 0 auto;
h1 {
text-align: center;
padding-bottom: 25px;
}
`;
export const PricingCards = () => {
return (
<StyledPricingSection>
<h1>Unlock Full Potential of JSON Crack</h1>
<StyledSectionBody>
<StyledPricingCard>
<StyledPricingCardTitle>Free</StyledPricingCardTitle>
<StyledPricingCardDetails>
<StyledPricingCardDetailsItem>Store up to 15 files</StyledPricingCardDetailsItem>
<StyledPricingCardDetailsItem>
Create short-links for saved JSON files
</StyledPricingCardDetailsItem>
<StyledPricingCardDetailsItem>Embed saved JSON instantly</StyledPricingCardDetailsItem>
</StyledPricingCardDetails>
</StyledPricingCard>
<StyledPricingCard premium>
<StyledPricingCardTitle>Premium</StyledPricingCardTitle>
<StyledPricingCardPrice>$5/mo</StyledPricingCardPrice>
<StyledPricingCardDetails>
<StyledPricingCardDetailsItem>
Create and share up to 200 files
</StyledPricingCardDetailsItem>
<StyledPricingCardDetailsItem>Store private JSON</StyledPricingCardDetailsItem>
<StyledPricingCardDetailsItem>
Get access to JSON Crack API to generate JSON remotely
</StyledPricingCardDetailsItem>
<StyledPricingCardDetailsItem>Everything in previous tier</StyledPricingCardDetailsItem>
</StyledPricingCardDetails>
<StyledButton
href="https://www.patreon.com/jsoncrack"
target="_blank"
status="SUCCESS"
block
link
>
GET IT NOW!
</StyledButton>
</StyledPricingCard>
</StyledSectionBody>
</StyledPricingSection>
);
};

View File

@ -1,14 +1,10 @@
import React from "react";
import {
searchQuery,
cleanupHighlight,
highlightMatchedNodes,
} from "src/utils/search";
import useConfig from "../store/useConfig";
import useGraph from "src/store/useGraph";
import { searchQuery, cleanupHighlight, highlightMatchedNodes } from "src/utils/search";
export const useFocusNode = () => {
const setConfig = useConfig(state => state.setConfig);
const zoomPanPinch = useConfig(state => state.zoomPanPinch);
const togglePerfMode = useGraph(state => state.togglePerfMode);
const zoomPanPinch = useGraph(state => state.zoomPanPinch);
const [selectedNode, setSelectedNode] = React.useState(0);
const [content, setContent] = React.useState({
value: "",
@ -18,14 +14,14 @@ export const useFocusNode = () => {
const skip = () => setSelectedNode(current => current + 1);
React.useEffect(() => {
setConfig("performanceMode", !content.value.length);
togglePerfMode(!content.value.length);
const debouncer = setTimeout(() => {
setContent(val => ({ ...val, debounced: content.value }));
}, 800);
return () => clearTimeout(debouncer);
}, [content.value, setConfig]);
}, [content.value, togglePerfMode]);
React.useEffect(() => {
if (!zoomPanPinch) return;
@ -39,18 +35,18 @@ export const useFocusNode = () => {
cleanupHighlight();
if (ref && matchedNode && matchedNode.parentElement) {
const newScale = 1;
const newScale = 0.4;
const x = Number(matchedNode.getAttribute("data-x"));
const y = Number(matchedNode.getAttribute("data-y"));
const newPositionX =
(ref.offsetLeft - x) * newScale +
ref.clientWidth / 2 -
matchedNode.getBoundingClientRect().width / 2;
ref.clientWidth / 10 -
matchedNode.getBoundingClientRect().width / 10;
const newPositionY =
(ref.offsetLeft - y) * newScale +
ref.clientHeight / 2 -
matchedNode.getBoundingClientRect().height / 2;
ref.clientHeight / 10 -
matchedNode.getBoundingClientRect().height / 10;
highlightMatchedNodes(matchedNodes, selectedNode);

View File

@ -0,0 +1,33 @@
import React from "react";
import useGraph from "src/store/useGraph";
const useHideNodes = () => {
const collapsedNodes = useGraph(state => state.collapsedNodes);
const collapsedEdges = useGraph(state => state.collapsedEdges);
const nodeList = React.useMemo(
() => collapsedNodes.map(id => `[id$="node-${id}"]`),
[collapsedNodes]
);
const edgeList = React.useMemo(
() => collapsedEdges.map(id => `[class$="edge-${id}"]`),
[collapsedEdges]
);
const checkNodes = () => {
const hiddenItems = document.querySelectorAll(".hide");
hiddenItems.forEach(item => item.classList.remove("hide"));
if (nodeList.length > 1) {
const selectedNodes = document.querySelectorAll(nodeList.join(","));
const selectedEdges = document.querySelectorAll(edgeList.join(","));
selectedNodes.forEach(node => node.classList.add("hide"));
selectedEdges.forEach(edge => edge.classList.add("hide"));
}
};
return { checkNodes };
};
export default useHideNodes;

View File

@ -1,63 +1,52 @@
import React from "react";
import type { AppProps } from "next/app";
import { useRouter } from "next/router";
import { init } from "@sentry/nextjs";
import { decompress } from "compress-json";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { Toaster } from "react-hot-toast";
import { GoogleAnalytics } from "src/components/GoogleAnalytics";
import { SupportButton } from "src/components/SupportButton";
import GlobalStyle from "src/constants/globalStyle";
import { darkTheme, lightTheme } from "src/constants/theme";
import useConfig from "src/store/useConfig";
import { ModalController } from "src/containers/ModalController";
import useStored from "src/store/useStored";
import { isValidJson } from "src/utils/isValidJson";
import { ThemeProvider } from "styled-components";
if (process.env.NODE_ENV !== "development") {
init({
dsn: "https://d3345591295d4dd1b8c579b62003d939@o1284435.ingest.sentry.io/6495191",
tracesSampleRate: 0.5,
tracesSampleRate: 0.25,
});
}
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
retry: false,
},
},
});
function JsonCrack({ Component, pageProps }: AppProps) {
const { query, pathname } = useRouter();
const [isReady, setReady] = React.useState(false);
const lightmode = useStored(state => state.lightmode);
const setJson = useConfig(state => state.setJson);
const [isRendered, setRendered] = React.useState(false);
React.useEffect(() => {
try {
if (pathname !== "editor") return;
const isJsonValid =
typeof query.json === "string" &&
isValidJson(decodeURIComponent(query.json));
if (isJsonValid) {
const jsonDecoded = decompress(JSON.parse(isJsonValid));
const jsonString = JSON.stringify(jsonDecoded);
setJson(jsonString);
}
} catch (error) {
console.error(error);
}
}, [pathname, query.json, setJson]);
React.useEffect(() => {
setRendered(true);
setReady(true);
}, []);
if (isRendered)
if (isReady)
return (
<>
<QueryClientProvider client={queryClient}>
<GoogleAnalytics />
<ThemeProvider theme={lightmode ? lightTheme : darkTheme}>
<GlobalStyle />
<Component {...pageProps} />
<Toaster
position="bottom-right"
position="top-right"
containerStyle={{
right: 60,
top: 40,
right: 6,
fontSize: 14,
}}
toastOptions={{
style: {
@ -66,9 +55,9 @@ function JsonCrack({ Component, pageProps }: AppProps) {
},
}}
/>
<SupportButton />
<ModalController />
</ThemeProvider>
</>
</QueryClientProvider>
);
}

View File

@ -15,13 +15,9 @@ class MyDocument extends Document {
<link rel="manifest" href="/manifest.json" />
<link rel="icon" href="/favicon.ico" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
<link
rel="preconnect"
href="https://fonts.gstatic.com"
crossOrigin="anonymous"
/>
<link
href="https://fonts.googleapis.com/css2?family=Catamaran:wght@400;500;700&family=Roboto+Mono:wght@500&family=Roboto:wght@400;500;700&display=swap"
href="https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@500&family=Roboto:wght@400;500;700&display=swap"
rel="stylesheet"
crossOrigin="anonymous"
/>

View File

@ -15,7 +15,7 @@ const StyledNotFound = styled.div`
const StyledMessage = styled.h4`
color: ${({ theme }) => theme.FULL_WHITE};
font-size: 25px;
font-weight: 600;
font-weight: 800;
margin: 10px 0;
`;

186
src/pages/docs.tsx Normal file
View File

@ -0,0 +1,186 @@
import React from "react";
import dynamic from "next/dynamic";
import Head from "next/head";
import { materialDark } from "react-syntax-highlighter/dist/cjs/styles/prism";
import { Button } from "src/components/Button";
import { Footer } from "src/components/Footer";
import styled from "styled-components";
const SyntaxHighlighter = dynamic(
() => import("react-syntax-highlighter").then(c => c.PrismAsync),
{
ssr: false,
}
);
const StyledFrame = styled.iframe`
border: none;
width: 100%;
height: 400px;
flex: 1;
`;
const StyledPage = styled.div`
padding: 5%;
`;
const StyledContent = styled.section`
margin-top: 20px;
background: rgba(181, 116, 214, 0.23);
padding: 16px;
border-radius: 6px;
`;
const StyledDescription = styled.div``;
const StyledContentBody = styled.div`
display: flex;
flex-wrap: wrap;
gap: 15px;
line-height: 1.8;
${StyledDescription} {
flex: 1;
}
`;
const StyledHighlight = styled.span<{ link?: boolean; alert?: boolean }>`
text-align: left;
white-space: nowrap;
color: ${({ theme, link, alert }) =>
alert ? theme.DANGER : link ? theme.BLURPLE : theme.TEXT_POSITIVE};
background: ${({ theme }) => theme.BACKGROUND_TERTIARY};
border-radius: 4px;
font-weight: 500;
padding: 4px;
font-size: 14px;
margin: ${({ alert }) => alert && "8px 0"};
`;
const Docs = () => {
return (
<>
<Head>
<title>Creating JSON Crack Embed | JSON Crack</title>
<meta name="description" content="Embedding JSON Crack tutorial into your websites." />
</Head>
<StyledPage>
<Button href="/" link status="SECONDARY">
&lt; Go Back
</Button>
<h1>Documentation</h1>
<StyledContent>
<h2># Fetching from URL</h2>
<StyledContentBody>
<StyledDescription>
By adding <StyledHighlight>?json=https://catfact.ninja/fact</StyledHighlight> query at
the end of iframe src you will be able to fetch from URL at widgets without additional
scripts. This applies to editor page as well, the following link will fetch the url at
the editor:{" "}
<StyledHighlight
as="a"
href="https://jsoncrack.com/editor?json=https://catfact.ninja/fact"
link
>
https://jsoncrack.com/editor?json=https://catfact.ninja/fact
</StyledHighlight>
</StyledDescription>
<StyledFrame
scrolling="no"
title="Untitled"
src="https://codepen.io/AykutSarac/embed/KKBpWVR?default-tab=html%2Cresult"
loading="eager"
>
See the Pen <a href="https://codepen.io/AykutSarac/pen/KKBpWVR">Untitled</a> by Aykut
Saraç (<a href="https://codepen.io/AykutSarac">@AykutSarac</a>) on{" "}
<a href="https://codepen.io">CodePen</a>.
</StyledFrame>
</StyledContentBody>
</StyledContent>
<StyledContent>
<h2># Embed Saved JSON</h2>
<StyledContentBody>
<StyledDescription>
Just like fetching from URL above, you can embed saved public json by adding the json
id to &quot;json&quot; query{" "}
<StyledHighlight>?json=639b65c5a82efc29a24b2de2</StyledHighlight>
</StyledDescription>
<StyledFrame
scrolling="no"
title="Untitled"
src="https://codepen.io/AykutSarac/embed/vYaORgM?default-tab=html%2Cresult"
loading="lazy"
>
See the Pen <a href="https://codepen.io/AykutSarac/pen/vYaORgM">Untitled</a> by Aykut
Saraç (<a href="https://codepen.io/AykutSarac">@AykutSarac</a>) on{" "}
<a href="https://codepen.io">CodePen</a>.
</StyledFrame>
</StyledContentBody>
</StyledContent>
<StyledContent>
<h2># Communicating with API</h2>
<h3> Post Message to Embed</h3>
<StyledContentBody>
<StyledDescription>
Communicating with the embed is possible with{" "}
<StyledHighlight
as="a"
href="https://developer.mozilla.org/en-US/docs/Web/API/MessagePort/postMessage"
link
>
MessagePort
</StyledHighlight>
, you should pass an object consist of &quot;json&quot; and &quot;options&quot; key
where json is a string and options is an object that may contain the following:
<SyntaxHighlighter language="markdown" style={materialDark} showLineNumbers={true}>
{`{\n\ttheme: "light" | "dark",\n\tdirection: "TOP" | "RIGHT" | "DOWN" | "LEFT"\n}`}
</SyntaxHighlighter>
</StyledDescription>
<StyledFrame
scrolling="no"
title="Untitled"
src="https://codepen.io/AykutSarac/embed/rNrVyWP?default-tab=html%2Cresult"
loading="lazy"
>
See the Pen <a href="https://codepen.io/AykutSarac/pen/rNrVyWP">Untitled</a> by Aykut
Saraç (<a href="https://codepen.io/AykutSarac">@AykutSarac</a>) on{" "}
<a href="https://codepen.io">CodePen</a>.
</StyledFrame>
</StyledContentBody>
</StyledContent>
<StyledContent>
<h3> On Page Load</h3>
<StyledContentBody>
<StyledDescription>
<StyledHighlight as="div" alert>
<b>Important!</b> - iframe should be defined before the script tag
</StyledHighlight>
<StyledHighlight as="div" alert>
<b>Note</b> - postMessage should be delayed using setTimeout
</StyledHighlight>
To display JSON on load event, you should post json into iframe using it&apos;s onload
event like in the example. Make sure to use{" "}
<StyledHighlight>setTimeout</StyledHighlight> when loading data and set a time around
500ms otherwise it won&apos;t work.
</StyledDescription>
<StyledFrame
scrolling="no"
title="Untitled"
src="https://codepen.io/AykutSarac/embed/QWBbpqx?default-tab=html%2Cresult"
loading="lazy"
>
See the Pen <a href="https://codepen.io/AykutSarac/pen/QWBbpqx">Untitled</a> by Aykut
Saraç (<a href="https://codepen.io/AykutSarac">@AykutSarac</a>) on{" "}
<a href="https://codepen.io">CodePen</a>.
</StyledFrame>
</StyledContentBody>
</StyledContent>
</StyledPage>
<Footer />
</>
);
};
export default Docs;

View File

@ -1,13 +1,18 @@
import React from "react";
import Head from "next/head";
import { useRouter } from "next/router";
import { Loading } from "src/components/Loading";
import { Sidebar } from "src/components/Sidebar";
import { BottomBar } from "src/containers/Editor/BottomBar";
import Panes from "src/containers/Editor/Panes";
import useJson from "src/store/useJson";
import useUser from "src/store/useUser";
import styled from "styled-components";
export const StyledPageWrapper = styled.div`
display: flex;
flex-direction: row;
height: 100vh;
height: calc(100vh - 28px);
width: 100%;
@media only screen and (max-width: 768px) {
@ -24,14 +29,27 @@ export const StyledEditorWrapper = styled.div`
`;
const EditorPage: React.FC = () => {
const { isReady, query } = useRouter();
const checkSession = useUser(state => state.checkSession);
const fetchJson = useJson(state => state.fetchJson);
const loading = useJson(state => state.loading);
React.useEffect(() => {
// Fetch JSON by query
// Check Session User
if (isReady) {
checkSession();
fetchJson(query.json);
}
}, [checkSession, fetchJson, isReady, query.json]);
if (loading) return <Loading message="Fetching JSON from cloud..." />;
return (
<StyledEditorWrapper>
<Head>
<title>Editor | JSON Crack</title>
<meta
name="description"
content="View your JSON data in graphs instantly."
/>
<meta name="description" content="View your JSON data in graphs instantly." />
</Head>
<StyledPageWrapper>
<Sidebar />
@ -39,6 +57,7 @@ const EditorPage: React.FC = () => {
<Panes />
</StyledEditorWrapper>
</StyledPageWrapper>
<BottomBar />
</StyledEditorWrapper>
);
};

View File

@ -1,35 +0,0 @@
import React from "react";
import Head from "next/head";
import styled from "styled-components";
const StyledPageWrapper = styled.iframe`
height: 100vh;
width: 100%;
border: none;
`;
const Embed = () => {
return (
<>
<Head>
<title>Creating JSON Crack Embed | JSON Crack</title>
<meta
name="description"
content="Embedding JSON Crack tutorial into your websites."
/>
</Head>
<StyledPageWrapper
scrolling="no"
title="Untitled"
src="https://codepen.io/AykutSarac/embed/PoawZYo?default-tab=html%2Cresult"
loading="lazy"
>
See the Pen <a href="https://codepen.io/AykutSarac/pen/PoawZYo">Untitled</a>{" "}
by Aykut Saraç (<a href="https://codepen.io/AykutSarac">@AykutSarac</a>) on{" "}
<a href="https://codepen.io">CodePen</a>.
</StyledPageWrapper>
</>
);
};
export default Embed;

35
src/pages/pricing.tsx Normal file
View File

@ -0,0 +1,35 @@
import React from "react";
import { Button } from "src/components/Button";
import { Footer } from "src/components/Footer";
import { PricingCards } from "src/containers/PricingCards";
import styled from "styled-components";
const StyledPageWrapper = styled.div`
padding: 5%;
`;
const StyledHeroSection = styled.section`
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
`;
const Pricing = () => {
return (
<>
<StyledPageWrapper>
<Button href="/" link>
&lt; Go Back
</Button>
<StyledHeroSection>
<img src="assets/icon.png" alt="json crack" width="400" />
<h1>Premium</h1>
</StyledHeroSection>
<PricingCards />
</StyledPageWrapper>
<Footer />
</>
);
};
export default Pricing;

View File

@ -4,14 +4,16 @@ import { useRouter } from "next/router";
import toast from "react-hot-toast";
import { baseURL } from "src/constants/data";
import { darkTheme, lightTheme } from "src/constants/theme";
import { NodeModal } from "src/containers/Modals/NodeModal";
import useGraph from "src/store/useGraph";
import { parser } from "src/utils/jsonParser";
import useJson from "src/store/useJson";
import styled, { ThemeProvider } from "styled-components";
const Graph = dynamic<any>(() => import("src/components/Graph").then(c => c.Graph), {
ssr: false,
});
const GraphCanvas = dynamic(
() => import("src/containers/Editor/LiveEditor/GraphCanvas").then(c => c.GraphCanvas),
{
ssr: false,
}
);
const StyledAttribute = styled.a`
position: fixed;
@ -45,70 +47,28 @@ interface EmbedMessage {
};
}
const StyledDeprecated = styled.div`
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
height: 100vh;
a {
text-decoration: underline;
}
`;
const WidgetPage = () => {
const { query, push } = useRouter();
const [isModalVisible, setModalVisible] = React.useState(false);
const [selectedNode, setSelectedNode] = React.useState<[string, string][]>([]);
const { query, push, isReady } = useRouter();
const [theme, setTheme] = React.useState("dark");
const collapsedNodes = useGraph(state => state.collapsedNodes);
const collapsedEdges = useGraph(state => state.collapsedEdges);
const loading = useGraph(state => state.loading);
const setGraphValue = useGraph(state => state.setGraphValue);
const openModal = React.useCallback(() => setModalVisible(true), []);
const fetchJson = useJson(state => state.fetchJson);
const setGraph = useGraph(state => state.setGraph);
React.useEffect(() => {
const nodeList = collapsedNodes.map(id => `[id$="node-${id}"]`);
const edgeList = collapsedEdges.map(id => `[class$="edge-${id}"]`);
const hiddenItems = document.querySelectorAll(".hide");
hiddenItems.forEach(item => item.classList.remove("hide"));
if (nodeList.length) {
const selectedNodes = document.querySelectorAll(nodeList.join(","));
const selectedEdges = document.querySelectorAll(edgeList.join(","));
selectedNodes.forEach(node => node.classList.add("hide"));
selectedEdges.forEach(edge => edge.classList.add("hide"));
if (isReady) {
fetchJson(query.json);
if (!inIframe()) push("/");
}
if (!inIframe()) push("/");
}, [collapsedNodes, collapsedEdges, loading, push]);
}, [fetchJson, isReady, push, query.json]);
React.useEffect(() => {
const handler = (event: EmbedMessage) => {
try {
if (!event.data?.json) return;
if (event.data?.options?.theme === "light" || event.data?.options?.theme === "dark") {
setTheme(event.data.options.theme);
}
const { nodes, edges } = parser(event.data.json);
const options = {
direction: "RIGHT",
theme,
...event.data.options,
};
setGraphValue("direction", options.direction);
if (options.theme === "light" || options.theme === "dark")
setTheme(options.theme);
setGraphValue("nodes", nodes);
setGraphValue("edges", edges);
setGraph(event.data.json, event.data.options);
} catch (error) {
console.error(error);
toast.error("Invalid JSON!");
@ -117,27 +77,11 @@ const WidgetPage = () => {
window.addEventListener("message", handler);
return () => window.removeEventListener("message", handler);
}, [setGraphValue, theme]);
if (query.json)
return (
<StyledDeprecated>
<h1> Deprecated </h1>
<br />
<a href="https://jsoncrack.com/embed" target="_blank" rel="noreferrer">
https://jsoncrack.com/embed
</a>
</StyledDeprecated>
);
}, [setGraph, theme]);
return (
<ThemeProvider theme={theme === "dark" ? darkTheme : lightTheme}>
<Graph openModal={openModal} setSelectedNode={setSelectedNode} isWidget />
<NodeModal
selectedNode={selectedNode}
visible={isModalVisible}
closeModal={() => setModalVisible(false)}
/>
<GraphCanvas isWidget />
<StyledAttribute href={`${baseURL}/editor`} target="_blank" rel="noreferrer">
jsoncrack.com
</StyledAttribute>

35
src/services/db/json.tsx Normal file
View File

@ -0,0 +1,35 @@
import { compressToBase64 } from "lz-string";
import { altogic, AltogicResponse } from "src/api/altogic";
import { Json } from "src/typings/altogic";
const saveJson = async ({
id,
data,
}: {
id?: string | null;
data: string;
}): Promise<AltogicResponse<{ _id: string }>> => {
const compressedData = compressToBase64(data);
if (id) {
return await altogic.endpoint.put(`json/${id}`, {
json: compressedData,
});
}
return await altogic.endpoint.post("json", {
json: compressedData,
});
};
const getAllJson = async (): Promise<AltogicResponse<{ result: Json[] }>> =>
await altogic.endpoint.get(`json`);
const updateJson = async (id: string, data: object) =>
await altogic.endpoint.put(`json/${id}`, {
...data,
});
const deleteJson = async (id: string) => await altogic.endpoint.delete(`json/${id}`);
export { saveJson, getAllJson, updateJson, deleteJson };

View File

@ -1,58 +0,0 @@
import { ReactZoomPanPinchRef } from "react-zoom-pan-pinch";
import { defaultJson } from "src/constants/data";
import create from "zustand";
interface ConfigActions {
setJson: (json: string) => void;
setConfig: (key: keyof Config, value: unknown) => void;
getJson: () => string;
zoomIn: () => void;
zoomOut: () => void;
centerView: () => void;
}
const initialStates = {
json: defaultJson,
cursorMode: "move" as "move" | "navigation",
foldNodes: false,
hideEditor: false,
performanceMode: true,
disableLoading: false,
zoomPanPinch: undefined as ReactZoomPanPinchRef | undefined,
};
export type Config = typeof initialStates;
const useConfig = create<Config & ConfigActions>()((set, get) => ({
...initialStates,
getJson: () => get().json,
setJson: (json: string) => set({ json }),
zoomIn: () => {
const zoomPanPinch = get().zoomPanPinch;
if (zoomPanPinch) {
zoomPanPinch.setTransform(
zoomPanPinch?.state.positionX,
zoomPanPinch?.state.positionY,
zoomPanPinch?.state.scale + 0.4
);
}
},
zoomOut: () => {
const zoomPanPinch = get().zoomPanPinch;
if (zoomPanPinch) {
zoomPanPinch.setTransform(
zoomPanPinch?.state.positionX,
zoomPanPinch?.state.positionY,
zoomPanPinch?.state.scale - 0.4
);
}
},
centerView: () => {
const zoomPanPinch = get().zoomPanPinch;
const canvas = document.querySelector(".jsoncrack-canvas") as HTMLElement;
if (zoomPanPinch && canvas) zoomPanPinch.zoomToElement(canvas);
},
setConfig: (setting: keyof Config, value: unknown) => set({ [setting]: value }),
}));
export default useConfig;

View File

@ -1,13 +1,20 @@
import { ReactZoomPanPinchRef } from "react-zoom-pan-pinch";
import { CanvasDirection } from "reaflow";
import { Graph } from "src/components/Graph";
import { getChildrenEdges } from "src/utils/getChildrenEdges";
import { getOutgoers } from "src/utils/getOutgoers";
import { parser } from "src/utils/jsonParser";
import create from "zustand";
import useJson from "./useJson";
const initialStates = {
loading: false,
zoomPanPinch: undefined as ReactZoomPanPinchRef | undefined,
direction: "RIGHT" as CanvasDirection,
loading: true,
graphCollapsed: false,
foldNodes: false,
fullscreen: false,
performanceMode: true,
nodes: [] as NodeData[],
edges: [] as EdgeData[],
collapsedNodes: [] as string[],
@ -18,27 +25,39 @@ const initialStates = {
export type Graph = typeof initialStates;
interface GraphActions {
setGraphValue: (key: keyof Graph, value: any) => void;
setGraph: (json?: string, options?: Partial<Graph>[]) => void;
setLoading: (loading: boolean) => void;
setDirection: (direction: CanvasDirection) => void;
setZoomPanPinch: (ref: ReactZoomPanPinchRef) => void;
expandNodes: (nodeId: string) => void;
collapseNodes: (nodeId: string) => void;
collapseGraph: () => void;
expandGraph: () => void;
toggleFold: (value: boolean) => void;
toggleFullscreen: (value: boolean) => void;
togglePerfMode: (value: boolean) => void;
zoomIn: () => void;
zoomOut: () => void;
centerView: () => void;
}
const useGraph = create<Graph & GraphActions>((set, get) => ({
...initialStates,
setDirection: direction => set({ direction }),
setGraphValue: (key, value) =>
setGraph: (data, options) => {
const { nodes, edges } = parser(data ?? useJson.getState().json);
set({
nodes,
edges,
collapsedParents: [],
collapsedNodes: [],
collapsedEdges: [],
graphCollapsed: false,
loading: true,
[key]: value,
}),
...options,
});
},
setDirection: direction => set({ direction }),
setLoading: loading => set({ loading }),
expandNodes: nodeId => {
const [childrenNodes, matchingNodes] = getOutgoers(
@ -57,18 +76,12 @@ const useGraph = create<Graph & GraphActions>((set, get) => ({
const matchingNodesConnectedToParent = matchingNodes.filter(node =>
nodesConnectedToParent.includes(node)
);
const nodeIds = childrenNodes
.map(node => node.id)
.concat(matchingNodesConnectedToParent);
const nodeIds = childrenNodes.map(node => node.id).concat(matchingNodesConnectedToParent);
const edgeIds = childrenEdges.map(edge => edge.id);
const collapsedParents = get().collapsedParents.filter(cp => cp !== nodeId);
const collapsedNodes = get().collapsedNodes.filter(
nodeId => !nodeIds.includes(nodeId)
);
const collapsedEdges = get().collapsedEdges.filter(
edgeId => !edgeIds.includes(edgeId)
);
const collapsedNodes = get().collapsedNodes.filter(nodeId => !nodeIds.includes(nodeId));
const collapsedEdges = get().collapsedEdges.filter(edgeId => !edgeIds.includes(edgeId));
set({
collapsedParents,
@ -100,19 +113,19 @@ const useGraph = create<Graph & GraphActions>((set, get) => ({
.filter(edge => parentNodesIds.includes(edge.from))
.map(edge => edge.to);
const collapsedParents = get()
.nodes.filter(node => !parentNodesIds.includes(node.id) && node.data.parent)
.map(node => node.id);
const collapsedNodes = get()
.nodes.filter(
node => !parentNodesIds.includes(node.id) && !secondDegreeNodesIds.includes(node.id)
)
.map(node => node.id);
set({
collapsedParents: get()
.nodes.filter(
node => !parentNodesIds.includes(node.id) && node.data.isParent
)
.map(node => node.id),
collapsedNodes: get()
.nodes.filter(
node =>
!parentNodesIds.includes(node.id) &&
!secondDegreeNodesIds.includes(node.id)
)
.map(node => node.id),
collapsedParents,
collapsedNodes,
collapsedEdges: get()
.edges.filter(edge => !parentNodesIds.includes(edge.from))
.map(edge => edge.id),
@ -127,6 +140,39 @@ const useGraph = create<Graph & GraphActions>((set, get) => ({
graphCollapsed: false,
});
},
zoomIn: () => {
const zoomPanPinch = get().zoomPanPinch;
if (zoomPanPinch) {
zoomPanPinch.setTransform(
zoomPanPinch?.state.positionX,
zoomPanPinch?.state.positionY,
zoomPanPinch?.state.scale + 0.4
);
}
},
zoomOut: () => {
const zoomPanPinch = get().zoomPanPinch;
if (zoomPanPinch) {
zoomPanPinch.setTransform(
zoomPanPinch?.state.positionX,
zoomPanPinch?.state.positionY,
zoomPanPinch?.state.scale - 0.4
);
}
},
centerView: () => {
const zoomPanPinch = get().zoomPanPinch;
const canvas = document.querySelector(".jsoncrack-canvas") as HTMLElement;
if (zoomPanPinch && canvas) zoomPanPinch.zoomToElement(canvas);
},
toggleFold: foldNodes => {
set({ foldNodes });
get().setGraph();
},
togglePerfMode: performanceMode => set({ performanceMode }),
toggleFullscreen: fullscreen => set({ fullscreen }),
setZoomPanPinch: zoomPanPinch => set({ zoomPanPinch }),
}));
export default useGraph;

105
src/store/useJson.tsx Normal file
View File

@ -0,0 +1,105 @@
import { decompressFromBase64 } from "lz-string";
import toast from "react-hot-toast";
import { altogic } from "src/api/altogic";
import { defaultJson } from "src/constants/data";
import { saveJson as saveJsonDB } from "src/services/db/json";
import useGraph from "src/store/useGraph";
import { Json } from "src/typings/altogic";
import create from "zustand";
interface JsonActions {
setJson: (json: string) => void;
getJson: () => string;
getHasChanges: () => boolean;
fetchJson: (jsonId: string | string[] | undefined) => void;
setError: (hasError: boolean) => void;
setHasChanges: (hasChanges: boolean) => void;
saveJson: (isNew?: boolean) => Promise<string | undefined>;
}
const initialStates = {
data: null as Json | null,
json: "",
loading: true,
hasChanges: false,
hasError: false,
};
export type JsonStates = typeof initialStates;
const useJson = create<JsonStates & JsonActions>()((set, get) => ({
...initialStates,
getJson: () => get().json,
getHasChanges: () => get().hasChanges,
fetchJson: async jsonId => {
const isURL = new RegExp(
/^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&\/=]*)$/
);
if (typeof jsonId === "string" && isURL.test(jsonId)) {
try {
const res = await fetch(jsonId);
const json = await res.json();
const jsonStr = JSON.stringify(json, null, 2);
useGraph.getState().setGraph(jsonStr);
return set({ json: jsonStr, loading: false });
} catch (error) {
useGraph.getState().setGraph(defaultJson);
set({ json: defaultJson, loading: false });
toast.error("Failed to fetch JSON from URL!");
}
} else if (jsonId) {
const { data, errors } = await altogic.endpoint.get(`json/${jsonId}`, undefined, {
userid: altogic.auth.getUser()?._id,
});
if (!errors) {
const decompressedData = decompressFromBase64(data.json);
if (decompressedData) {
useGraph.getState().setGraph(decompressedData);
return set({
data,
json: decompressedData ?? undefined,
loading: false,
});
}
}
}
useGraph.getState().setGraph(defaultJson);
set({ json: defaultJson, loading: false });
},
setJson: json => {
useGraph.getState().setGraph(json);
set({ json, hasChanges: true });
},
saveJson: async (isNew = true) => {
try {
const url = new URL(window.location.href);
const params = new URLSearchParams(url.search);
const jsonQuery = params.get("json");
toast.loading("Saving JSON...", { id: "jsonSave" });
const res = await saveJsonDB({ id: isNew ? undefined : jsonQuery, data: get().json });
if (res.errors && res.errors.items.length > 0) throw res.errors;
toast.success("JSON saved to cloud", { id: "jsonSave" });
set({ hasChanges: false });
return res.data._id;
} catch (error: any) {
if (error?.items?.length > 0) {
toast.error(error.items[0].message, { id: "jsonSave", duration: 5000 });
return undefined;
}
toast.error("Failed to save JSON!", { id: "jsonSave" });
return undefined;
}
},
setError: (hasError: boolean) => set({ hasError }),
setHasChanges: (hasChanges: boolean) => set({ hasChanges }),
}));
export default useJson;

38
src/store/useModal.tsx Normal file
View File

@ -0,0 +1,38 @@
import create from "zustand";
import useUser from "./useUser";
interface ModalActions {
setVisible: (modal: keyof typeof initialStates) => (visible: boolean) => void;
}
const initialStates = {
clear: false,
cloud: false,
download: false,
goals: false,
import: false,
account: false,
node: false,
settings: false,
share: false,
login: false,
};
type ModalType = keyof typeof initialStates;
const authModals: ModalType[] = ["cloud", "share", "account"];
export type ModalStates = typeof initialStates;
const useModal = create<ModalStates & ModalActions>()(set => ({
...initialStates,
setVisible: modal => visible => {
if (authModals.includes(modal) && !useUser.getState().isAuthenticated) {
return set({ login: true });
}
set({ [modal]: visible });
},
}));
export default useModal;

View File

@ -1,5 +1,6 @@
import create from "zustand";
import { persist } from "zustand/middleware";
import useGraph from "./useGraph";
type Sponsor = {
handle: string;
@ -15,30 +16,29 @@ function getTomorrow() {
return new Date(tomorrow).getTime();
}
export interface Config {
lightmode: boolean;
hideCollapse: boolean;
hideChildrenCount: boolean;
const initialStates = {
lightmode: false,
hideCollapse: true,
childrenCount: true,
imagePreview: true,
sponsors: {
users: Sponsor[];
nextDate: number;
};
users: [] as Sponsor[],
nextDate: Date.now(),
},
};
export interface ConfigActions {
setSponsors: (sponsors: Sponsor[]) => void;
setLightTheme: (theme: boolean) => void;
toggleHideCollapse: (value: boolean) => void;
toggleHideChildrenCount: (value: boolean) => void;
toggleChildrenCount: (value: boolean) => void;
toggleImagePreview: (value: boolean) => void;
}
const useStored = create(
persist<Config>(
persist<typeof initialStates & ConfigActions>(
set => ({
lightmode: false,
hideCollapse: false,
hideChildrenCount: true,
sponsors: {
users: [],
nextDate: Date.now(),
},
...initialStates,
setLightTheme: (value: boolean) =>
set({
lightmode: value,
@ -51,7 +51,11 @@ const useStored = create(
},
}),
toggleHideCollapse: (value: boolean) => set({ hideCollapse: value }),
toggleHideChildrenCount: (value: boolean) => set({ hideChildrenCount: value }),
toggleChildrenCount: (value: boolean) => set({ childrenCount: value }),
toggleImagePreview: (value: boolean) => {
set({ imagePreview: value });
useGraph.getState().setGraph();
},
}),
{
name: "config",

59
src/store/useUser.tsx Normal file
View File

@ -0,0 +1,59 @@
import toast from "react-hot-toast";
import { altogic } from "src/api/altogic";
import { AltogicAuth, User } from "src/typings/altogic";
import create from "zustand";
import useModal from "./useModal";
interface UserActions {
login: (response: AltogicAuth) => void;
logout: () => void;
setUser: (key: keyof typeof initialStates, value: any) => void;
checkSession: () => void;
isPremium: () => boolean;
}
const initialStates = {
isAuthenticated: false,
user: null as User | null,
};
export type UserStates = typeof initialStates;
const useUser = create<UserStates & UserActions>()((set, get) => ({
...initialStates,
setUser: (key, value) => set({ [key]: value }),
isPremium: () => {
const user = get().user;
if (user) return user.type > 0;
return false;
},
logout: () => {
altogic.auth.signOut();
toast.success("Logged out.");
useModal.setState({ account: false });
set(initialStates);
},
login: response => {
set({ user: response.user as any, isAuthenticated: true });
},
checkSession: async () => {
const currentSession = altogic.auth.getSession();
if (currentSession) {
const dbUser = await altogic.auth.getUserFromDB();
altogic.auth.setSession(currentSession);
set({ user: dbUser.user as any, isAuthenticated: true });
} else {
if (!new URLSearchParams(window.location.search).get("access_token")) return;
const data = await altogic.auth.getAuthGrant();
if (!data.errors?.items.length) {
set({ user: data.user as any, isAuthenticated: true });
}
}
},
}));
export default useUser;

56
src/typings/altogic.ts Normal file
View File

@ -0,0 +1,56 @@
export interface User {
_id: string;
provider: string;
providerUserId: string;
email: string;
name: string;
profilePicture: string;
signUpAt: Date;
lastLoginAt: Date;
type: 0 | 1;
}
export interface Json {
_id: string;
createdAt: string;
updatedAt: string;
json: string;
name: string;
private: false;
}
interface Device {
family: string;
major: string;
minor: string;
patch: string;
}
interface Os {
family: string;
major: string;
minor: string;
patch: string;
}
interface UserAgent {
family: string;
major: string;
minor: string;
patch: string;
device: Device;
os: Os;
}
interface Session {
userId: string;
token: string;
creationDtm: Date;
userAgent: UserAgent;
accessGroupKeys: any[];
}
export interface AltogicAuth {
user: User;
session: Session;
}

View File

@ -1,11 +1,5 @@
type CanvasDirection = "LEFT" | "RIGHT" | "DOWN" | "UP";
interface CustomNodeData {
isParent: true;
childrenCount: children.length;
children: NodeData[];
}
interface NodeData<T = any> {
id: string;
disabled?: boolean;

View File

@ -1,11 +1,7 @@
export const getChildrenEdges = (
nodes: NodeData[],
edges: EdgeData[]
): EdgeData[] => {
export const getChildrenEdges = (nodes: NodeData[], edges: EdgeData[]): EdgeData[] => {
const nodeIds = nodes.map(node => node.id);
return edges.filter(
edge =>
nodeIds.includes(edge.from as string) || nodeIds.includes(edge.to as string)
edge => nodeIds.includes(edge.from as string) || nodeIds.includes(edge.to as string)
);
};

View File

@ -15,11 +15,10 @@ export const getOutgoers = (
const runner = (nodeId: string) => {
const outgoerIds = edges.filter(e => e.from === nodeId).map(e => e.to);
const nodeList = nodes.filter(n => {
if (parent.includes(n.id) && !matchingNodes.includes(n.id))
matchingNodes.push(n.id);
if (parent.includes(n.id) && !matchingNodes.includes(n.id)) matchingNodes.push(n.id);
return outgoerIds.includes(n.id) && !parent.includes(n.id);
});
outgoerNodes.push(...nodeList);
nodeList.forEach(node => runner(node.id));
};

View File

@ -1,10 +0,0 @@
import { parse } from "jsonc-parser";
export const isValidJson = (str: string) => {
try {
parse(str);
} catch (e) {
return false;
}
return str;
};

View File

@ -1,38 +1,49 @@
import { Node, parseTree } from "jsonc-parser";
import useGraph from "src/store/useGraph";
import useStored from "src/store/useStored";
const calculateSize = (
text: string | [string, string][],
isParent = false,
isFolded: boolean
) => {
let value = "";
const calculateSize = (text: string | [string, string][], isParent = false) => {
const isFolded = useGraph.getState().foldNodes;
const isImagePreview = useStored.getState().imagePreview;
let lineCounts = 1;
let lineLengths: number[] = [];
if (typeof text === "string") value = text;
else value = text.map(([k, v]) => `${k}: ${v}`).join("\n");
if (typeof text === "string") {
lineLengths.push(text.length);
} else {
lineCounts = text.map(([k, v]) => {
const length = `${k}: ${v}`.length;
const line = length > 150 ? 150 : length;
lineLengths.push(line);
return `${k}: ${v}`;
}).length;
}
const lineCount = value.split("\n");
const lineLengths = lineCount.map(line => line.length);
const longestLine = lineLengths.sort((a, b) => b - a)[0];
const longestLine = Math.max(...lineLengths);
const getWidth = () => {
if (text.length === 0) return 35;
if (Array.isArray(text) && !text.length) return 40;
if (!isFolded) return 35 + longestLine * 8 + (isParent ? 60 : 0);
if (isParent) return 170;
if (!isFolded) return 35 + longestLine * 7.8 + (isParent ? 60 : 0);
if (isParent && isFolded) return 170;
return 200;
};
const getHeight = () => {
if (lineCount.length * 17.8 < 30) return 40;
return (lineCount.length + 1) * 18;
if (lineCounts * 17.8 < 30) return 40;
return (lineCounts + 1) * 18;
};
const isImage =
!Array.isArray(text) && /(https?:\/\/.*\.(?:png|jpg|gif))/i.test(text) && isImagePreview;
return {
width: getWidth(),
height: getHeight(),
width: isImage ? 80 : getWidth(),
height: isImage ? 80 : getHeight(),
};
};
export const parser = (jsonStr: string, isFolded = false) => {
export const parser = (jsonStr: string) => {
try {
let json = parseTree(jsonStr);
let nodes: NodeData[] = [];
@ -98,11 +109,7 @@ export const parser = (jsonStr: string, isFolded = false) => {
if (!children) {
if (value !== undefined) {
if (
parentType === "property" &&
nextType !== "object" &&
nextType !== "array"
) {
if (parentType === "property" && nextType !== "object" && nextType !== "array") {
brothersParentId = myParentId;
if (nextType === undefined) {
// add key and value to brothers node
@ -111,7 +118,7 @@ export const parser = (jsonStr: string, isFolded = false) => {
brotherKey = value;
}
} else if (parentType === "array") {
const { width, height } = calculateSize(String(value), false, isFolded);
const { width, height } = calculateSize(String(value), false);
const nodeFromArrayId = addNodes(String(value), width, height, false);
if (myParentId) {
addEdges(myParentId, nodeFromArrayId);
@ -134,28 +141,22 @@ export const parser = (jsonStr: string, isFolded = false) => {
let findBrothersNode = brothersNodeProps.find(
e =>
e.parentId === brothersParentId &&
e.objectsFromArrayId ===
objectsFromArray[objectsFromArray.length - 1]
e.objectsFromArrayId === objectsFromArray[objectsFromArray.length - 1]
);
if (findBrothersNode) {
let ModifyNodes = [...nodes];
let findNode = nodes.findIndex(e => e.id === findBrothersNode?.id);
if (ModifyNodes[findNode]) {
ModifyNodes[findNode].text =
ModifyNodes[findNode].text.concat(brothersNode);
const { width, height } = calculateSize(
ModifyNodes[findNode].text,
false,
isFolded
);
ModifyNodes[findNode].text = ModifyNodes[findNode].text.concat(brothersNode);
const { width, height } = calculateSize(ModifyNodes[findNode].text, false);
ModifyNodes[findNode].width = width;
ModifyNodes[findNode].height = height;
nodes = [...ModifyNodes];
brothersNode = [];
}
} else {
const { width, height } = calculateSize(brothersNode, false, isFolded);
const { width, height } = calculateSize(brothersNode, false);
const brothersNodeId = addNodes(brothersNode, width, height, false);
brothersNode = [];
@ -177,7 +178,7 @@ export const parser = (jsonStr: string, isFolded = false) => {
}
// add parent node
const { width, height } = calculateSize(parentName, true, isFolded);
const { width, height } = calculateSize(parentName, true);
parentId = addNodes(parentName, width, height, type);
bracketOpen = [...bracketOpen, { id: parentId, type: type }];
parentName = "";
@ -232,28 +233,22 @@ export const parser = (jsonStr: string, isFolded = false) => {
let findBrothersNode = brothersNodeProps.find(
e =>
e.parentId === brothersParentId &&
e.objectsFromArrayId ===
objectsFromArray[objectsFromArray.length - 1]
e.objectsFromArrayId === objectsFromArray[objectsFromArray.length - 1]
);
if (findBrothersNode) {
let ModifyNodes = [...nodes];
let findNode = nodes.findIndex(e => e.id === findBrothersNode?.id);
if (ModifyNodes[findNode]) {
ModifyNodes[findNode].text =
ModifyNodes[findNode].text.concat(brothersNode);
const { width, height } = calculateSize(
ModifyNodes[findNode].text,
false,
isFolded
);
ModifyNodes[findNode].text = ModifyNodes[findNode].text.concat(brothersNode);
const { width, height } = calculateSize(ModifyNodes[findNode].text, false);
ModifyNodes[findNode].width = width;
ModifyNodes[findNode].height = height;
nodes = [...ModifyNodes];
brothersNode = [];
}
} else {
const { width, height } = calculateSize(brothersNode, false, isFolded);
const { width, height } = calculateSize(brothersNode, false);
const brothersNodeId = addNodes(brothersNode, width, height, false);
brothersNode = [];
@ -309,7 +304,7 @@ export const parser = (jsonStr: string, isFolded = false) => {
if (notHaveParent.length > 1) {
if (json.type !== "array") {
const text = "";
const { width, height } = calculateSize(text, false, isFolded);
const { width, height } = calculateSize(text, false);
const emptyId = addNodes(text, width, height, false, true);
notHaveParent.forEach(children => {
addEdges(emptyId, children);
@ -320,11 +315,11 @@ export const parser = (jsonStr: string, isFolded = false) => {
if (nodes.length === 0) {
if (json.type === "array") {
const text = "[]";
const { width, height } = calculateSize(text, false, isFolded);
const { width, height } = calculateSize(text, false);
addNodes(text, width, height, false);
} else {
const text = "{}";
const { width, height } = calculateSize(text, false, isFolded);
const { width, height } = calculateSize(text, false);
addNodes(text, width, height, false);
}
}

View File

@ -11,10 +11,7 @@ export const cleanupHighlight = () => {
});
};
export const highlightMatchedNodes = (
nodes: NodeListOf<Element>,
selectedNode: number
) => {
export const highlightMatchedNodes = (nodes: NodeListOf<Element>, selectedNode: number) => {
nodes?.forEach(node => {
node.parentElement?.closest("foreignObject")?.classList.add("searched");
});

341
yarn.lock
View File

@ -921,6 +921,13 @@
dependencies:
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.3.1":
version "7.20.7"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.7.tgz#fcb41a5a70550e04a7b708037c7c32f7f356d8fd"
integrity sha512-UF0tvkUtxwAgZ5W/KrkHf0Rn0fdnLDU9ScxBrEVNUprE/MzirjK4MJUX1/BVDv00Sv8cljtukVK1aky++X1SjQ==
dependencies:
regenerator-runtime "^0.13.11"
"@babel/template@^7.16.7", "@babel/template@^7.18.10":
version "7.18.10"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.18.10.tgz#6f9134835970d1dbf0835c0d100c9f38de0c5e71"
@ -1291,6 +1298,11 @@
"@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0"
"@react-oauth/google@^0.4.0":
version "0.4.0"
resolved "https://registry.yarnpkg.com/@react-oauth/google/-/google-0.4.0.tgz#ae4fe2724040bd11facdc53aad43a21e9f34b2c9"
integrity sha512-2QxxrKbXXH8bwHSefB56sBgsKs7Bq3Pvv8tVmGJuINGefECsssIUKidTDm5P55T4CV99sCX/GUfxs3l2Ntxo8Q==
"@rollup/plugin-babel@^5.2.0":
version "5.3.1"
resolved "https://registry.yarnpkg.com/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz#04bc0608f4aa4b2e4b1aebf284344d0f68fda283"
@ -1463,6 +1475,11 @@
dependencies:
"@sentry/cli" "^1.74.4"
"@socket.io/component-emitter@~3.1.0":
version "3.1.0"
resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553"
integrity sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==
"@surma/rollup-plugin-off-main-thread@^2.2.3":
version "2.2.3"
resolved "https://registry.yarnpkg.com/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz#ee34985952ca21558ab0d952f00298ad2190c053"
@ -1480,6 +1497,19 @@
dependencies:
tslib "^2.4.0"
"@tanstack/query-core@4.19.1":
version "4.19.1"
resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-4.19.1.tgz#2e92d9e8a50884eb231c5beb4386e131ebe34306"
integrity sha512-Zp0aIose5C8skBzqbVFGk9HJsPtUhRVDVNWIqVzFbGQQgYSeLZMd3Sdb4+EnA5wl1J7X+bre2PJGnQg9x/zHOA==
"@tanstack/react-query@^4.19.1":
version "4.19.1"
resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-4.19.1.tgz#43356dd537127e76d75f5a2769eb23dafd9a3690"
integrity sha512-5dvHvmc0vrWI03AJugzvKfirxCyCLe+qawrWFCXdu8t7dklIhJ7D5ZhgTypv7mMtIpdHPcECtCiT/+V74wCn2A==
dependencies:
"@tanstack/query-core" "4.19.1"
use-sync-external-store "^1.2.0"
"@testing-library/dom@^8.5.0":
version "8.18.1"
resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-8.18.1.tgz#80f91be02bc171fe5a3a7003f88207be31ac2cf3"
@ -1554,6 +1584,13 @@
"@types/minimatch" "*"
"@types/node" "*"
"@types/hast@^2.0.0":
version "2.3.4"
resolved "https://registry.yarnpkg.com/@types/hast/-/hast-2.3.4.tgz#8aa5ef92c117d20d974a82bdfb6a648b08c0bafc"
integrity sha512-wLEm0QvaoawEDoTRwzTXp4b4jpwiJDvR5KMnFnVodm3scufTlBOWRD6N1OBf9TZMhjlNsSfcO5V+7AF4+Vy+9g==
dependencies:
"@types/unist" "*"
"@types/hoist-non-react-statics@*":
version "3.3.1"
resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f"
@ -1572,6 +1609,23 @@
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==
"@types/lodash.debounce@^4.0.7":
version "4.0.7"
resolved "https://registry.yarnpkg.com/@types/lodash.debounce/-/lodash.debounce-4.0.7.tgz#0285879defb7cdb156ae633cecd62d5680eded9f"
integrity sha512-X1T4wMZ+gT000M2/91SYj0d/7JfeNZ9PeeOldSNoE/lunLeQXKvkmIumI29IaKMotU/ln/McOIvgzZcQ/3TrSA==
dependencies:
"@types/lodash" "*"
"@types/lodash@*":
version "4.14.191"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.191.tgz#09511e7f7cba275acd8b419ddac8da9a6a79e2fa"
integrity sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==
"@types/lz-string@^1.3.34":
version "1.3.34"
resolved "https://registry.yarnpkg.com/@types/lz-string/-/lz-string-1.3.34.tgz#69bfadde419314b4a374bf2c8e58659c035ed0a5"
integrity sha512-j6G1e8DULJx3ONf6NdR5JiR2ZY3K3PaaqiEuKYkLQO0Czfi1AzrtjfnfCROyWGeDd5IVMKCwsgSmMip9OWijow==
"@types/minimatch@*":
version "5.1.2"
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-5.1.2.tgz#07508b45797cb81ec3f273011b054cd0755eddca"
@ -1602,6 +1656,13 @@
dependencies:
"@types/react" "*"
"@types/react-syntax-highlighter@^15.5.5":
version "15.5.5"
resolved "https://registry.yarnpkg.com/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.5.tgz#4d3b51f8956195f1f63360ff03f8822c5d74c516"
integrity sha512-QH3JZQXa2usAvJvSsdSUJ4Yu4j8ReuZpgRrEW+XP+Rmosbn425YshW9iGEb/pAARm8496axHhHUPRH3UmTiB6A==
dependencies:
"@types/react" "*"
"@types/react@*", "@types/react@18.0.21":
version "18.0.21"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.21.tgz#b8209e9626bb00a34c76f55482697edd2b43cc67"
@ -1644,6 +1705,11 @@
resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.2.tgz#fc25ad9943bcac11cceb8168db4f275e0e72e756"
integrity sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==
"@types/unist@*":
version "2.0.6"
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d"
integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==
"@typescript-eslint/parser@^5.21.0":
version "5.38.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.38.0.tgz#5a59a1ff41a7b43aacd1bb2db54f6bf1c02b2ff8"
@ -1759,6 +1825,14 @@ allotment@^1.17.0:
lodash.isequal "^4.5.0"
use-resize-observer "^9.0.0"
altogic@^2.3.8:
version "2.3.8"
resolved "https://registry.yarnpkg.com/altogic/-/altogic-2.3.8.tgz#475ca083faec97660d30b838bbddaeeb83da9072"
integrity sha512-KTRSPH/g490sJiIe0qfuJaMsidGFkYSAnPR93Hn9VQ9GyZ+0/KmMIPSV6ctRaCvk/fw06w56IgYQNZDf6VJyxg==
dependencies:
cross-fetch "^3.1.4"
socket.io-client "^4.5.1"
ansi-regex@^2.0.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
@ -1887,6 +1961,11 @@ async@^3.2.3:
resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c"
integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==
asynckit@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
at-least-node@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2"
@ -1897,6 +1976,15 @@ axe-core@^4.4.3:
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.4.3.tgz#11c74d23d5013c0fa5d183796729bc3482bd2f6f"
integrity sha512-32+ub6kkdhhWick/UjvEwRchgoetXqTK14INLqbGm5U2TzBkBNF3nQtLYm8ovxSkQWArjEQvftCKryjZaATu3w==
axios@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.1.3.tgz#8274250dada2edf53814ed7db644b9c2866c1e35"
integrity sha512-00tXVRwKx/FZr/IDVFt4C+f9FYairX517WoGCL6dpOntqLkZofjhu43F/Xl44UOpqa+9sLFDrG/XAnFsUYgkDA==
dependencies:
follow-redirects "^1.15.0"
form-data "^4.0.0"
proxy-from-env "^1.1.0"
axobject-query@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be"
@ -2064,6 +2152,21 @@ chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
character-entities-legacy@^1.0.0:
version "1.1.4"
resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz#94bc1845dce70a5bb9d2ecc748725661293d8fc1"
integrity sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==
character-entities@^1.0.0:
version "1.2.4"
resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-1.2.4.tgz#e12c3939b7eaf4e5b15e7ad4c5e28e1d48c5b16b"
integrity sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==
character-reference-invalid@^1.0.0:
version "1.1.4"
resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz#083329cda0eae272ab3dbbf37e9a382c13af1560"
integrity sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==
classnames@^2.3.0, classnames@^2.3.1:
version "2.3.2"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.2.tgz#351d813bf0137fcc6a76a16b88208d2560a0d924"
@ -2105,6 +2208,18 @@ color-name@~1.1.4:
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
combined-stream@^1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
dependencies:
delayed-stream "~1.0.0"
comma-separated-tokens@^1.0.0:
version "1.0.8"
resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz#632b80b6117867a158f1080ad498b2fbe7e3f5ea"
integrity sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==
commander@^2.20.0:
version "2.20.3"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
@ -2125,11 +2240,6 @@ commondir@^1.0.1:
resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==
compress-json@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/compress-json/-/compress-json-2.1.2.tgz#37e0e7c7480c572fad9ad387fca5a2f36fee6f83"
integrity sha512-91247RD8bKQXzRmXUS4zGT250mhw86+J9X8w2L2SGtRE7g0CvzjOETFaFmsDdaXPWv8T7L9iiM7kdcnnH3BH7w==
concat-map@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
@ -2174,6 +2284,13 @@ create-require@^1.1.0:
resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333"
integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==
cross-fetch@^3.1.4:
version "3.1.5"
resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f"
integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==
dependencies:
node-fetch "2.6.7"
cross-spawn@^7.0.2:
version "7.0.3"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
@ -2224,7 +2341,12 @@ damerau-levenshtein@^1.0.8:
resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7"
integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==
debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4:
dayjs@^1.11.6:
version "1.11.6"
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.6.tgz#2e79a226314ec3ec904e3ee1dd5a4f5e5b1c7afb"
integrity sha512-zZbY5giJAinCG+7AGaw0wIhNZ6J8AhWuSXKvuc1KAyMiRsvGQWqh4L+MomvhdAYjN+lqvVCMq1I41e3YHvXkyQ==
debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2:
version "4.3.4"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
@ -2305,6 +2427,11 @@ del@^4.1.1:
pify "^4.0.1"
rimraf "^2.6.3"
delayed-stream@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
delegates@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
@ -2387,6 +2514,22 @@ emojis-list@^3.0.0:
resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78"
integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==
engine.io-client@~6.2.3:
version "6.2.3"
resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-6.2.3.tgz#a8cbdab003162529db85e9de31575097f6d29458"
integrity sha512-aXPtgF1JS3RuuKcpSrBtimSjYvrbhKW9froICH4s0F3XQWLxsKNxqzG39nnvQZQnva4CMvUK63T7shevxRyYHw==
dependencies:
"@socket.io/component-emitter" "~3.1.0"
debug "~4.3.1"
engine.io-parser "~5.0.3"
ws "~8.2.3"
xmlhttprequest-ssl "~2.0.0"
engine.io-parser@~5.0.3:
version "5.0.4"
resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.0.4.tgz#0b13f704fa9271b3ec4f33112410d8f3f41d0fc0"
integrity sha512-+nVFp+5z1E3HcToEnO7ZIj3g+3k9389DvWtvJZz0T6/eOCPIyyxehFcedoYrZQrp0LgQbD9pPXhpMBKMd5QURg==
enhanced-resolve@^5.10.0:
version "5.10.0"
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.10.0.tgz#0dc579c3bb2a1032e357ac45b8f3a6f3ad4fb1e6"
@ -2753,6 +2896,13 @@ fastq@^1.6.0:
dependencies:
reusify "^1.0.4"
fault@^1.0.0:
version "1.0.4"
resolved "https://registry.yarnpkg.com/fault/-/fault-1.0.4.tgz#eafcfc0a6d214fc94601e170df29954a4f842f13"
integrity sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==
dependencies:
format "^0.2.0"
file-entry-cache@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027"
@ -2812,6 +2962,11 @@ flatted@^3.1.0:
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787"
integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==
follow-redirects@^1.15.0:
version "1.15.2"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13"
integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==
for-each@~0.3.3:
version "0.3.3"
resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e"
@ -2819,6 +2974,20 @@ for-each@~0.3.3:
dependencies:
is-callable "^1.1.3"
form-data@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452"
integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==
dependencies:
asynckit "^0.4.0"
combined-stream "^1.0.8"
mime-types "^2.1.12"
format@^0.2.0:
version "0.2.2"
resolved "https://registry.yarnpkg.com/format/-/format-0.2.2.tgz#d6170107e9efdc4ed30c9dc39016df942b5cb58b"
integrity sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==
framer-motion@^6.2.8:
version "6.5.1"
resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-6.5.1.tgz#802448a16a6eb764124bf36d8cbdfa6dd6b931a7"
@ -3067,11 +3236,32 @@ has@^1.0.3, has@~1.0.3:
dependencies:
function-bind "^1.1.1"
hast-util-parse-selector@^2.0.0:
version "2.2.5"
resolved "https://registry.yarnpkg.com/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz#d57c23f4da16ae3c63b3b6ca4616683313499c3a"
integrity sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==
hastscript@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/hastscript/-/hastscript-6.0.0.tgz#e8768d7eac56c3fdeac8a92830d58e811e5bf640"
integrity sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==
dependencies:
"@types/hast" "^2.0.0"
comma-separated-tokens "^1.0.0"
hast-util-parse-selector "^2.0.0"
property-information "^5.0.0"
space-separated-tokens "^1.0.0"
hey-listen@^1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/hey-listen/-/hey-listen-1.0.8.tgz#8e59561ff724908de1aa924ed6ecc84a56a9aa68"
integrity sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==
highlight.js@^10.4.1, highlight.js@~10.7.0:
version "10.7.3"
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.7.3.tgz#697272e3991356e40c3cac566a74eef681756531"
integrity sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==
hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
@ -3142,6 +3332,19 @@ internal-slot@^1.0.3:
has "^1.0.3"
side-channel "^1.0.4"
is-alphabetical@^1.0.0:
version "1.0.4"
resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-1.0.4.tgz#9e7d6b94916be22153745d184c298cbf986a686d"
integrity sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==
is-alphanumerical@^1.0.0:
version "1.0.4"
resolved "https://registry.yarnpkg.com/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz#7eb9a2431f855f6b1ef1a78e326df515696c4dbf"
integrity sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==
dependencies:
is-alphabetical "^1.0.0"
is-decimal "^1.0.0"
is-arguments@^1.0.4:
version "1.1.1"
resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b"
@ -3184,6 +3387,11 @@ is-date-object@^1.0.1:
dependencies:
has-tostringtag "^1.0.0"
is-decimal@^1.0.0:
version "1.0.4"
resolved "https://registry.yarnpkg.com/is-decimal/-/is-decimal-1.0.4.tgz#65a3a5958a1c5b63a706e1b333d7cd9f630d3fa5"
integrity sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==
is-extglob@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
@ -3208,6 +3416,11 @@ is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3:
dependencies:
is-extglob "^2.1.1"
is-hexadecimal@^1.0.0:
version "1.0.4"
resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz#cc35c97588da4bd49a8eedd6bc4082d44dcb23a7"
integrity sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==
is-module@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591"
@ -3562,6 +3775,14 @@ loose-envify@^1.1.0, loose-envify@^1.4.0:
dependencies:
js-tokens "^3.0.0 || ^4.0.0"
lowlight@^1.17.0:
version "1.20.0"
resolved "https://registry.yarnpkg.com/lowlight/-/lowlight-1.20.0.tgz#ddb197d33462ad0d93bf19d17b6c301aa3941888"
integrity sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==
dependencies:
fault "^1.0.0"
highlight.js "~10.7.0"
lru-cache@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
@ -3621,6 +3842,18 @@ micromatch@^4.0.4:
braces "^3.0.2"
picomatch "^2.3.1"
mime-db@1.52.0:
version "1.52.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
mime-types@^2.1.12:
version "2.1.35"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
dependencies:
mime-db "1.52.0"
minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
@ -3732,7 +3965,7 @@ next@^12.3.1:
"@next/swc-win32-ia32-msvc" "12.3.1"
"@next/swc-win32-x64-msvc" "12.3.1"
node-fetch@^2.6.7:
node-fetch@2.6.7, node-fetch@^2.6.7:
version "2.6.7"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
@ -3896,6 +4129,18 @@ parent-module@^1.0.0:
dependencies:
callsites "^3.0.0"
parse-entities@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-2.0.0.tgz#53c6eb5b9314a1f4ec99fa0fdf7ce01ecda0cbe8"
integrity sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==
dependencies:
character-entities "^1.0.0"
character-entities-legacy "^1.0.0"
character-reference-invalid "^1.0.0"
is-alphanumerical "^1.0.0"
is-decimal "^1.0.0"
is-hexadecimal "^1.0.0"
path-exists@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
@ -4023,6 +4268,16 @@ pretty-format@^27.0.2:
ansi-styles "^5.0.0"
react-is "^17.0.1"
prismjs@^1.27.0:
version "1.29.0"
resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.29.0.tgz#f113555a8fa9b57c35e637bba27509dcf802dd12"
integrity sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==
prismjs@~1.27.0:
version "1.27.0"
resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.27.0.tgz#bb6ee3138a0b438a3653dd4d6ce0cc6510a45057"
integrity sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==
process-nextick-args@~2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
@ -4042,6 +4297,13 @@ prop-types@^15.5.10, prop-types@^15.7.2, prop-types@^15.8.1:
object-assign "^4.1.1"
react-is "^16.13.1"
property-information@^5.0.0:
version "5.6.0"
resolved "https://registry.yarnpkg.com/property-information/-/property-information-5.6.0.tgz#61675545fb23002f245c6540ec46077d4da3ed69"
integrity sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==
dependencies:
xtend "^4.0.0"
proxy-from-env@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
@ -4145,6 +4407,17 @@ react-scrolllock@^5.0.1:
dependencies:
exenv "^1.2.2"
react-syntax-highlighter@^15.5.0:
version "15.5.0"
resolved "https://registry.yarnpkg.com/react-syntax-highlighter/-/react-syntax-highlighter-15.5.0.tgz#4b3eccc2325fa2ec8eff1e2d6c18fa4a9e07ab20"
integrity sha512-+zq2myprEnQmH5yw6Gqc8lD55QHnpKaU8TOcFeC/Lg/MQSs8UknEA0JC4nTZGFAXC2J2Hyj/ijJ7NlabyPi2gg==
dependencies:
"@babel/runtime" "^7.3.1"
highlight.js "^10.4.1"
lowlight "^1.17.0"
prismjs "^1.27.0"
refractor "^3.6.0"
react-use-gesture@^8.0.1:
version "8.0.1"
resolved "https://registry.yarnpkg.com/react-use-gesture/-/react-use-gesture-8.0.1.tgz#4360c0f7c9e26baf9fbe58f63fc9de7ef699c17f"
@ -4210,6 +4483,15 @@ reakeys@^1.2.6:
dependencies:
mousetrap "^1.6.5"
refractor@^3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/refractor/-/refractor-3.6.0.tgz#ac318f5a0715ead790fcfb0c71f4dd83d977935a"
integrity sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==
dependencies:
hastscript "^6.0.0"
parse-entities "^2.0.0"
prismjs "~1.27.0"
regenerate-unicode-properties@^10.1.0:
version "10.1.0"
resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz#7c3192cab6dd24e21cb4461e5ddd7dd24fa8374c"
@ -4222,6 +4504,11 @@ regenerate@^1.4.2:
resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a"
integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==
regenerator-runtime@^0.13.11:
version "0.13.11"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9"
integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==
regenerator-runtime@^0.13.4:
version "0.13.9"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52"
@ -4468,6 +4755,24 @@ slash@^3.0.0:
resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
socket.io-client@^4.5.1:
version "4.5.4"
resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.5.4.tgz#d3cde8a06a6250041ba7390f08d2468ccebc5ac9"
integrity sha512-ZpKteoA06RzkD32IbqILZ+Cnst4xewU7ZYK12aS1mzHftFFjpoMz69IuhP/nL25pJfao/amoPI527KnuhFm01g==
dependencies:
"@socket.io/component-emitter" "~3.1.0"
debug "~4.3.2"
engine.io-client "~6.2.3"
socket.io-parser "~4.2.1"
socket.io-parser@~4.2.1:
version "4.2.1"
resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.1.tgz#01c96efa11ded938dcb21cbe590c26af5eff65e5"
integrity sha512-V4GrkLy+HeF1F/en3SpUaM+7XxYXpuMUWLGde1kSSh5nQMN4hLrbPIkD+otwh6q9R6NOQBN4AMaOZ2zVjui82g==
dependencies:
"@socket.io/component-emitter" "~3.1.0"
debug "~4.3.1"
source-list-map@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34"
@ -4508,6 +4813,11 @@ sourcemap-codec@^1.4.8:
resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4"
integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==
space-separated-tokens@^1.0.0:
version "1.1.5"
resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz#85f32c3d10d9682007e917414ddc5c26d1aa6899"
integrity sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==
state-local@^1.0.6:
version "1.0.7"
resolved "https://registry.yarnpkg.com/state-local/-/state-local-1.0.7.tgz#da50211d07f05748d53009bee46307a37db386d5"
@ -4952,7 +5262,7 @@ use-resize-observer@^9.0.0:
dependencies:
"@juggle/resize-observer" "^3.3.1"
use-sync-external-store@1.2.0:
use-sync-external-store@1.2.0, use-sync-external-store@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"
integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==
@ -5206,6 +5516,21 @@ wrappy@1:
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
ws@~8.2.3:
version "8.2.3"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.2.3.tgz#63a56456db1b04367d0b721a0b80cae6d8becbba"
integrity sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==
xmlhttprequest-ssl@~2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz#91360c86b914e67f44dce769180027c0da618c67"
integrity sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==
xtend@^4.0.0:
version "4.0.2"
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
yallist@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"