mirror of
https://github.com/AykutSarac/jsoncrack.com.git
synced 2025-01-27 15:22:56 +08:00
commit
1940987759
@ -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
|
@ -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
|
@ -2,7 +2,7 @@
|
||||
"trailingComma": "es5",
|
||||
"singleQuote": false,
|
||||
"semi": true,
|
||||
"printWidth": 85,
|
||||
"printWidth": 100,
|
||||
"arrowParens": "avoid",
|
||||
"importOrder": [
|
||||
"^(react/(.*)$)|^(react$)",
|
||||
|
@ -9,7 +9,7 @@ const withPWA = require("next-pwa")({
|
||||
* @type {import('next').NextConfig}
|
||||
*/
|
||||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
reactStrictMode: false,
|
||||
};
|
||||
|
||||
module.exports = withPWA(nextConfig);
|
||||
|
14
package.json
14
package.json
@ -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 |
BIN
public/assets/Mona-Sans.woff2
Normal file
BIN
public/assets/Mona-Sans.woff2
Normal file
Binary file not shown.
BIN
public/assets/icon.png
Normal file
BIN
public/assets/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 44 KiB |
13
src/api/altogic.ts
Normal file
13
src/api/altogic.ts
Normal 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 };
|
@ -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>
|
||||
|
@ -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);
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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}>
|
||||
|
84
src/components/Footer/index.tsx
Normal file
84
src/components/Footer/index.tsx
Normal 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>
|
||||
);
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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;
|
||||
`;
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
28
src/components/Spinner/index.tsx
Normal file
28
src/components/Spinner/index.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
))}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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 * {
|
||||
|
@ -1,6 +1,7 @@
|
||||
const fixedColors = {
|
||||
CRIMSON: "#DC143C",
|
||||
BLURPLE: "#5865F2",
|
||||
PURPLE: "#9036AF",
|
||||
FULL_WHITE: "#FFFFFF",
|
||||
BLACK: "#202225",
|
||||
BLACK_DARK: "#2C2F33",
|
||||
|
184
src/containers/Editor/BottomBar.tsx
Normal file
184
src/containers/Editor/BottomBar.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
|
@ -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} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -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'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'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't have to be difficult to use. That's why
|
||||
we'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'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'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's Monaco Editor which is also used by VS Code, easily
|
||||
edit your JSON and directly view through the graphs. Also there's a JSON
|
||||
validator above of it to make sure there is no type error.
|
||||
With Microsoft's Monaco Editor which is also used by VS Code, easily edit your JSON and
|
||||
directly view through the graphs. Also there'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>
|
||||
— 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's why we'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've made our app
|
||||
freely available to anyone who wants to use it, and we welcome contributions from anyone
|
||||
who's interested in helping to improve it. Whether you're a developer, a data
|
||||
scientist, or just someone who's passionate about open source, we'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'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'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>
|
||||
);
|
||||
|
@ -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;
|
||||
|
51
src/containers/ModalController/index.tsx
Normal file
51
src/containers/ModalController/index.tsx
Normal 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")} />
|
||||
</>
|
||||
);
|
||||
};
|
143
src/containers/Modals/AccountModal/index.tsx
Normal file
143
src/containers/Modals/AccountModal/index.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -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
|
||||
|
270
src/containers/Modals/CloudModal/index.tsx
Normal file
270
src/containers/Modals/CloudModal/index.tsx
Normal 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't guarantee protection of your data.
|
||||
</StyledInfoText>
|
||||
</Modal.Controls>
|
||||
</StyledModal>
|
||||
);
|
||||
};
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -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's Goals</Modal.Header>
|
||||
<Modal.Content>
|
||||
<StyledTitle>OUR GOAL</StyledTitle>
|
||||
<b>JSON Crack'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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
|
26
src/containers/Modals/LoginModal/index.tsx
Normal file
26
src/containers/Modals/LoginModal/index.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -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!");
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
128
src/containers/PricingCards/index.tsx
Normal file
128
src/containers/PricingCards/index.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -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);
|
||||
|
||||
|
33
src/hooks/useHideNodes.tsx
Normal file
33
src/hooks/useHideNodes.tsx
Normal 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;
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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"
|
||||
/>
|
||||
|
@ -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
186
src/pages/docs.tsx
Normal 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">
|
||||
< 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 "json" 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 "json" and "options" 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'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'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;
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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
35
src/pages/pricing.tsx
Normal 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>
|
||||
< Go Back
|
||||
</Button>
|
||||
<StyledHeroSection>
|
||||
<img src="assets/icon.png" alt="json crack" width="400" />
|
||||
<h1>Premium</h1>
|
||||
</StyledHeroSection>
|
||||
<PricingCards />
|
||||
</StyledPageWrapper>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Pricing;
|
@ -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
35
src/services/db/json.tsx
Normal 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 };
|
@ -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;
|
@ -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
105
src/store/useJson.tsx
Normal 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
38
src/store/useModal.tsx
Normal 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;
|
@ -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
59
src/store/useUser.tsx
Normal 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
56
src/typings/altogic.ts
Normal 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;
|
||||
}
|
6
src/typings/types.d.ts
vendored
6
src/typings/types.d.ts
vendored
@ -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;
|
||||
|
@ -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)
|
||||
);
|
||||
};
|
||||
|
@ -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));
|
||||
};
|
||||
|
@ -1,10 +0,0 @@
|
||||
import { parse } from "jsonc-parser";
|
||||
|
||||
export const isValidJson = (str: string) => {
|
||||
try {
|
||||
parse(str);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
return str;
|
||||
};
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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
341
yarn.lock
@ -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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user