├── public
├── .nojekyll
├── CNAME
├── favicon.ico
├── assets
│ ├── 192.jpg
│ ├── 512.jpg
│ ├── mesh.webp
│ ├── jsoncrack.png
│ ├── preview
│ │ ├── 1.png
│ │ ├── 1.webp
│ │ ├── 2.png
│ │ ├── 2.webp
│ │ ├── 3.png
│ │ ├── 3.webp
│ │ ├── 4.png
│ │ ├── 4.webp
│ │ ├── 5.png
│ │ ├── 5.webp
│ │ ├── 6.png
│ │ ├── 6.webp
│ │ ├── 7.png
│ │ ├── 7.webp
│ │ ├── 8.png
│ │ ├── 8.webp
│ │ └── free.webp
│ ├── compare
│ │ ├── free.webp
│ │ └── pro.webp
│ ├── og
│ │ └── affiliates.png
│ ├── features
│ │ ├── edit.webp
│ │ ├── search.webp
│ │ └── compare.webp
│ ├── todiagram_logo.png
│ ├── premium-divider.svg
│ ├── steps-divider-round.svg
│ ├── cursor.svg
│ └── logo.svg
├── robots.txt
├── sitemap.txt
└── manifest.json
├── .env
├── sentry.edge.config.ts
├── sentry.server.config.ts
├── .prettierignore
├── src
├── enums
│ ├── viewMode.enum.ts
│ └── file.enum.ts
├── assets
│ └── fonts
│ │ └── Mona-Sans.woff2
├── lib
│ └── utils
│ │ ├── helpers.ts
│ │ ├── search.ts
│ │ └── jsonAdapter.ts
├── types
│ ├── styled.d.ts
│ └── graph.ts
├── containers
│ ├── Editor
│ │ ├── components
│ │ │ ├── views
│ │ │ │ ├── GraphView
│ │ │ │ │ ├── lib
│ │ │ │ │ │ ├── utils
│ │ │ │ │ │ │ ├── addEdgeToGraph.ts
│ │ │ │ │ │ │ ├── getChildrenEdges.ts
│ │ │ │ │ │ │ ├── addNodeToGraph.ts
│ │ │ │ │ │ │ ├── getOutgoers.ts
│ │ │ │ │ │ │ ├── getNodePath.ts
│ │ │ │ │ │ │ └── calculateNodeSize.ts
│ │ │ │ │ │ └── jsonParser.ts
│ │ │ │ │ ├── CustomEdge
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── CustomNode
│ │ │ │ │ │ ├── ObjectNode.tsx
│ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ ├── TextRenderer.tsx
│ │ │ │ │ │ ├── styles.tsx
│ │ │ │ │ │ └── TextNode.tsx
│ │ │ │ │ └── NotSupported.tsx
│ │ │ │ └── TreeView
│ │ │ │ │ ├── Label.tsx
│ │ │ │ │ ├── index.tsx
│ │ │ │ │ └── Value.tsx
│ │ │ ├── LiveEditor.tsx
│ │ │ ├── FullscreenDropzone.tsx
│ │ │ ├── TextEditor.tsx
│ │ │ └── BottomBar.tsx
│ │ └── index.tsx
│ ├── Toolbar
│ │ ├── Logo.tsx
│ │ ├── styles.ts
│ │ ├── AccountMenu.tsx
│ │ ├── SearchInput.tsx
│ │ ├── FileMenu.tsx
│ │ ├── ZoomMenu.tsx
│ │ ├── OptionsMenu.tsx
│ │ ├── index.tsx
│ │ └── ToolsMenu.tsx
│ ├── Modals
│ │ ├── index.ts
│ │ ├── JWTModal
│ │ │ └── index.tsx
│ │ ├── JQModal
│ │ │ └── index.tsx
│ │ ├── JPathModal
│ │ │ └── index.tsx
│ │ ├── NodeModal
│ │ │ └── index.tsx
│ │ ├── UpgradeModal
│ │ │ └── index.tsx
│ │ ├── TypeModal
│ │ │ └── index.tsx
│ │ ├── ImportModal
│ │ │ └── index.tsx
│ │ ├── SchemaModal
│ │ │ └── index.tsx
│ │ └── DownloadModal
│ │ │ └── index.tsx
│ └── Landing
│ │ ├── HeroPreview.tsx
│ │ ├── FAQ.tsx
│ │ ├── SeePremium.tsx
│ │ ├── PremiumPreview.tsx
│ │ ├── HeroSection.tsx
│ │ ├── Features.tsx
│ │ └── Section1.tsx
├── store
│ ├── useModal.ts
│ ├── useJson.ts
│ └── useConfig.ts
├── layout
│ ├── Layout.tsx
│ ├── ModalController.tsx
│ ├── JsonCrackLogo.tsx
│ ├── ExternalMode.tsx
│ ├── Navbar.tsx
│ ├── Footer.tsx
│ └── AuthLayout.tsx
├── constants
│ ├── seo.ts
│ ├── globalStyle.ts
│ └── theme.ts
├── pages
│ ├── 404.tsx
│ ├── _error.tsx
│ ├── _document.tsx
│ ├── legal
│ │ ├── terms.tsx
│ │ └── privacy.tsx
│ ├── _app.tsx
│ ├── widget.tsx
│ ├── editor.tsx
│ └── index.tsx
├── hooks
│ ├── useJsonQuery.ts
│ ├── useToggleHide.ts
│ └── useFocusNode.ts
└── data
│ └── faq.json
├── next-env.d.ts
├── docker-compose.yml
├── .dockerignore
├── default.conf
├── .prettierrc
├── Dockerfile
├── tsconfig.json
├── .gitignore
├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── feature_request.yml
│ └── bug_report.yml
└── workflows
│ ├── pull-request.yml
│ └── deploy.yml
├── sentry.client.config.ts
├── .eslintrc.json
├── next.config.js
├── package.json
├── LICENSE
└── CONTRIBUTING.md
/public/.nojekyll:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/CNAME:
--------------------------------------------------------------------------------
1 | jsoncrack.com
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | NEXT_PUBLIC_GA_MEASUREMENT_ID=G-JKZEHMJBMH
2 |
--------------------------------------------------------------------------------
/sentry.edge.config.ts:
--------------------------------------------------------------------------------
1 | {
2 | /* empty */
3 | }
4 |
--------------------------------------------------------------------------------
/sentry.server.config.ts:
--------------------------------------------------------------------------------
1 | {
2 | /* empty */
3 | }
4 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliuq/jsoncrack.com/main/public/favicon.ico
--------------------------------------------------------------------------------
/public/assets/192.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliuq/jsoncrack.com/main/public/assets/192.jpg
--------------------------------------------------------------------------------
/public/assets/512.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliuq/jsoncrack.com/main/public/assets/512.jpg
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | .github
2 | .next
3 | node_modules/
4 | out
5 | public
6 | *-lock.json
7 | tsconfig.json
--------------------------------------------------------------------------------
/public/assets/mesh.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliuq/jsoncrack.com/main/public/assets/mesh.webp
--------------------------------------------------------------------------------
/src/enums/viewMode.enum.ts:
--------------------------------------------------------------------------------
1 | export enum ViewMode {
2 | Graph = "graph",
3 | Tree = "tree",
4 | }
5 |
--------------------------------------------------------------------------------
/public/assets/jsoncrack.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliuq/jsoncrack.com/main/public/assets/jsoncrack.png
--------------------------------------------------------------------------------
/public/assets/preview/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliuq/jsoncrack.com/main/public/assets/preview/1.png
--------------------------------------------------------------------------------
/public/assets/preview/1.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliuq/jsoncrack.com/main/public/assets/preview/1.webp
--------------------------------------------------------------------------------
/public/assets/preview/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliuq/jsoncrack.com/main/public/assets/preview/2.png
--------------------------------------------------------------------------------
/public/assets/preview/2.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliuq/jsoncrack.com/main/public/assets/preview/2.webp
--------------------------------------------------------------------------------
/public/assets/preview/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliuq/jsoncrack.com/main/public/assets/preview/3.png
--------------------------------------------------------------------------------
/public/assets/preview/3.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliuq/jsoncrack.com/main/public/assets/preview/3.webp
--------------------------------------------------------------------------------
/public/assets/preview/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliuq/jsoncrack.com/main/public/assets/preview/4.png
--------------------------------------------------------------------------------
/public/assets/preview/4.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliuq/jsoncrack.com/main/public/assets/preview/4.webp
--------------------------------------------------------------------------------
/public/assets/preview/5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliuq/jsoncrack.com/main/public/assets/preview/5.png
--------------------------------------------------------------------------------
/public/assets/preview/5.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliuq/jsoncrack.com/main/public/assets/preview/5.webp
--------------------------------------------------------------------------------
/public/assets/preview/6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliuq/jsoncrack.com/main/public/assets/preview/6.png
--------------------------------------------------------------------------------
/public/assets/preview/6.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliuq/jsoncrack.com/main/public/assets/preview/6.webp
--------------------------------------------------------------------------------
/public/assets/preview/7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliuq/jsoncrack.com/main/public/assets/preview/7.png
--------------------------------------------------------------------------------
/public/assets/preview/7.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliuq/jsoncrack.com/main/public/assets/preview/7.webp
--------------------------------------------------------------------------------
/public/assets/preview/8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliuq/jsoncrack.com/main/public/assets/preview/8.png
--------------------------------------------------------------------------------
/public/assets/preview/8.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliuq/jsoncrack.com/main/public/assets/preview/8.webp
--------------------------------------------------------------------------------
/public/assets/compare/free.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliuq/jsoncrack.com/main/public/assets/compare/free.webp
--------------------------------------------------------------------------------
/public/assets/compare/pro.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliuq/jsoncrack.com/main/public/assets/compare/pro.webp
--------------------------------------------------------------------------------
/public/assets/og/affiliates.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliuq/jsoncrack.com/main/public/assets/og/affiliates.png
--------------------------------------------------------------------------------
/public/assets/preview/free.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliuq/jsoncrack.com/main/public/assets/preview/free.webp
--------------------------------------------------------------------------------
/public/assets/features/edit.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliuq/jsoncrack.com/main/public/assets/features/edit.webp
--------------------------------------------------------------------------------
/public/assets/features/search.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliuq/jsoncrack.com/main/public/assets/features/search.webp
--------------------------------------------------------------------------------
/public/assets/todiagram_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliuq/jsoncrack.com/main/public/assets/todiagram_logo.png
--------------------------------------------------------------------------------
/src/assets/fonts/Mona-Sans.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliuq/jsoncrack.com/main/src/assets/fonts/Mona-Sans.woff2
--------------------------------------------------------------------------------
/public/assets/features/compare.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliuq/jsoncrack.com/main/public/assets/features/compare.webp
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Allow: /
3 |
4 | User-agent: *
5 | Disallow: /widget
6 |
7 | Sitemap: https://jsoncrack.com/sitemap.txt
8 |
--------------------------------------------------------------------------------
/src/enums/file.enum.ts:
--------------------------------------------------------------------------------
1 | export enum FileFormat {
2 | "JSON" = "json",
3 | "YAML" = "yaml",
4 | "XML" = "xml",
5 | "TOML" = "toml",
6 | "CSV" = "csv",
7 | }
8 |
--------------------------------------------------------------------------------
/src/lib/utils/helpers.ts:
--------------------------------------------------------------------------------
1 | export function isIframe() {
2 | try {
3 | return window.self !== window.top;
4 | } catch (e) {
5 | return true;
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/public/sitemap.txt:
--------------------------------------------------------------------------------
1 | https://jsoncrack.com
2 | https://jsoncrack.com/editor
3 | https://jsoncrack.com/docs
4 | https://jsoncrack.com/widget
5 | https://jsoncrack.com/legal/terms
6 | https://jsoncrack.com/legal/privacy
7 |
--------------------------------------------------------------------------------
/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/basic-features/typescript for more information.
6 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.9'
2 | services:
3 | jsoncrack:
4 | image: jsoncrack
5 | container_name: jsoncrack
6 | build:
7 | context: .
8 | dockerfile: Dockerfile
9 | ports:
10 | - "8888:8080"
--------------------------------------------------------------------------------
/src/types/styled.d.ts:
--------------------------------------------------------------------------------
1 | import "styled-components";
2 | import type theme from "src/constants/theme";
3 |
4 | type CustomTheme = typeof theme;
5 |
6 | declare module "styled-components" {
7 | export interface DefaultTheme extends CustomTheme {}
8 | }
9 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | # Ignore node modules as they will be installed in the Dockerfile
2 | node_modules/
3 |
4 | # Ignore local configuration and cache files
5 | .cache/
6 | .config/
7 | .DS_Store
8 |
9 | # Ignore logs
10 | *.log
11 |
12 | # Ignore test and development files
13 | *.md
14 | *.test.js
15 |
--------------------------------------------------------------------------------
/src/containers/Editor/components/views/GraphView/lib/utils/addEdgeToGraph.ts:
--------------------------------------------------------------------------------
1 | import type { Graph } from "../jsonParser";
2 |
3 | export const addEdgeToGraph = (graph: Graph, from: string, to: string) => {
4 | const newEdge = {
5 | id: `e${from}-${to}`,
6 | from: from,
7 | to: to,
8 | };
9 |
10 | graph.edges.push(newEdge);
11 | };
12 |
--------------------------------------------------------------------------------
/default.conf:
--------------------------------------------------------------------------------
1 | server {
2 | listen 8080;
3 | root /app;
4 | include /etc/nginx/mime.types;
5 |
6 | location /editor {
7 | try_files $uri /editor.html;
8 | }
9 |
10 | location /widget {
11 | try_files $uri /widget.html;
12 | }
13 |
14 | location /docs {
15 | try_files $uri /docs.html;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/containers/Toolbar/Logo.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { JSONCrackLogo } from "src/layout/JsonCrackLogo";
3 | import { StyledToolElement } from "./styles";
4 |
5 | export const Logo = () => {
6 | return (
7 |
8 |
9 |
10 | );
11 | };
12 |
--------------------------------------------------------------------------------
/src/containers/Editor/components/views/GraphView/CustomEdge/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import type { EdgeProps } from "reaflow";
3 | import { Edge } from "reaflow";
4 |
5 | const CustomEdgeWrapper = (props: EdgeProps) => {
6 | return ;
7 | };
8 |
9 | export const CustomEdge = React.memo(CustomEdgeWrapper);
10 |
--------------------------------------------------------------------------------
/public/assets/premium-divider.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/assets/steps-divider-round.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/containers/Editor/components/views/GraphView/lib/utils/getChildrenEdges.ts:
--------------------------------------------------------------------------------
1 | import type { NodeData, EdgeData } from "src/types/graph";
2 |
3 | export const getChildrenEdges = (nodes: NodeData[], edges: EdgeData[]): EdgeData[] => {
4 | const nodeIds = nodes.map(node => node.id);
5 |
6 | return edges.filter(
7 | edge => nodeIds.includes(edge.from as string) || nodeIds.includes(edge.to as string)
8 | );
9 | };
10 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "es5",
3 | "singleQuote": false,
4 | "semi": true,
5 | "printWidth": 100,
6 | "arrowParens": "avoid",
7 | "importOrder": [
8 | "^(react/(.*)$)|^(react$)",
9 | "^(next/(.*)$)|^(next$)",
10 | "^@mantine/core",
11 | "^@mantine",
12 | "styled",
13 | "",
14 | "^src/(.*)$",
15 | "^[./]"
16 | ],
17 | "importOrderParserPlugins": ["typescript", "jsx", "decorators-legacy"],
18 | "plugins": ["@trivago/prettier-plugin-sort-imports"]
19 | }
20 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # Builder
2 | FROM node:18-alpine as builder
3 | # Reference :: https://pnpm.io/docker
4 | ENV PNPM_HOME="/pnpm"
5 | ENV PATH="$PNPM_HOME:$PATH"
6 | RUN corepack enable
7 | WORKDIR /src
8 |
9 | # Cache dependencies first
10 | COPY package.json pnpm-lock.yaml ./
11 | RUN pnpm install
12 |
13 | # Copy other files and build
14 | COPY . /src/
15 | RUN pnpm build
16 |
17 | # App
18 | FROM nginxinc/nginx-unprivileged
19 | COPY --chown=nginx:nginx --from=builder /src/out /app
20 | COPY default.conf /etc/nginx/conf.d/default.conf
21 |
--------------------------------------------------------------------------------
/src/types/graph.ts:
--------------------------------------------------------------------------------
1 | import type { NodeType } from "jsonc-parser";
2 |
3 | export interface NodeData {
4 | id: string;
5 | text: string | [string, string][];
6 | width: number;
7 | height: number;
8 | path?: string;
9 | data: {
10 | type: NodeType;
11 | isParent: boolean;
12 | isEmpty: boolean;
13 | childrenCount: number;
14 | };
15 | }
16 |
17 | export interface EdgeData {
18 | id: string;
19 | from: string;
20 | to: string;
21 | }
22 |
23 | export type LayoutDirection = "LEFT" | "RIGHT" | "DOWN" | "UP";
24 |
--------------------------------------------------------------------------------
/src/containers/Modals/index.ts:
--------------------------------------------------------------------------------
1 | export { DownloadModal } from "./DownloadModal";
2 | export { ImportModal } from "./ImportModal";
3 | export { NodeModal } from "./NodeModal";
4 | export { UpgradeModal } from "./UpgradeModal";
5 | export { JWTModal } from "./JWTModal";
6 | export { SchemaModal } from "./SchemaModal";
7 | export { JQModal } from "./JQModal";
8 | export { TypeModal } from "./TypeModal";
9 | export { JPathModal } from "./JPathModal";
10 |
11 | type Modal =
12 | | "download"
13 | | "import"
14 | | "node"
15 | | "upgrade"
16 | | "jwt"
17 | | "schema"
18 | | "jq"
19 | | "type"
20 | | "jpath";
21 |
22 | export type { Modal };
23 |
--------------------------------------------------------------------------------
/public/assets/cursor.svg:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "target": "ES6",
5 | "lib": ["dom", "dom.iterable", "esnext"],
6 | "allowJs": true,
7 | "skipLibCheck": true,
8 | "strict": true,
9 | "forceConsistentCasingInFileNames": true,
10 | "noEmit": true,
11 | "esModuleInterop": true,
12 | "module": "esnext",
13 | "moduleResolution": "node",
14 | "resolveJsonModule": true,
15 | "isolatedModules": true,
16 | "jsx": "preserve",
17 | "incremental": true,
18 | "noImplicitAny": false,
19 | "typeRoots": ["types"]
20 | },
21 | "include": ["src", "next-env.d.ts",],
22 | "exclude": ["node_modules"]
23 | }
24 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 |
25 | # local env files
26 | .env.local
27 | .env.development.local
28 | .env.test.local
29 | .env.production.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 |
37 | # PWA workers
38 | **/public/workbox-*.js
39 | **/public/sw.js
40 | **/public/fallback-*.js
41 | # Sentry Auth Token
42 | .sentryclirc
43 |
--------------------------------------------------------------------------------
/src/containers/Landing/HeroPreview.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Container, Image } from "@mantine/core";
3 |
4 | export const HeroPreview = () => {
5 | return (
6 |
7 |
21 |
22 | );
23 | };
24 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "JSON Crack",
3 | "short_name": "JSON Crack",
4 | "description": "JSON Crack Editor is a tool for visualizing into graphs, analyzing, editing, formatting, querying, transforming and validating JSON, CSV, YAML, XML, and more.",
5 | "theme_color": "#36393e",
6 | "background_color": "#36393e",
7 | "display": "standalone",
8 | "orientation": "landscape",
9 | "scope": "/editor",
10 | "start_url": "/editor",
11 | "icons": [
12 | {
13 | "src": "assets/192.jpg",
14 | "sizes": "192x192",
15 | "type": "image/png"
16 | },
17 |
18 | {
19 | "src": "assets/512.jpg",
20 | "sizes": "512x512",
21 | "type": "image/png"
22 | }
23 | ]
24 | }
25 |
--------------------------------------------------------------------------------
/src/store/useModal.ts:
--------------------------------------------------------------------------------
1 | import { create } from "zustand";
2 | import type { Modal } from "src/containers/Modals";
3 |
4 | type ModalState = {
5 | [key in Modal]: boolean;
6 | };
7 |
8 | interface ModalActions {
9 | setVisible: (modal: Modal) => (visible: boolean) => void;
10 | }
11 |
12 | const initialStates: ModalState = {
13 | download: false,
14 | import: false,
15 | node: false,
16 | upgrade: false,
17 | jwt: false,
18 | schema: false,
19 | jq: false,
20 | type: false,
21 | jpath: false,
22 | };
23 |
24 | const useModal = create()(set => ({
25 | ...initialStates,
26 | setVisible: modal => visible => set({ [modal]: visible }),
27 | }));
28 |
29 | export default useModal;
30 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: AykutSarac
4 | patreon: # patreon name
5 | open_collective: # opencollective name
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
14 |
--------------------------------------------------------------------------------
/sentry.client.config.ts:
--------------------------------------------------------------------------------
1 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/
2 | import * as Sentry from "@sentry/nextjs";
3 |
4 | if (process.env.NODE_ENV === "production") {
5 | Sentry.init({
6 | dsn: "https://d3345591295d4dd1b8c579b62003d939@o1284435.ingest.sentry.io/6495191",
7 | tracesSampleRate: 0.2,
8 | debug: false,
9 | release: `${process.env.SENTRY_RELEASE || "production"}`,
10 | allowUrls: ["https://jsoncrack.com/editor"],
11 | replaysOnErrorSampleRate: 1.0,
12 | replaysSessionSampleRate: 0.1,
13 | integrations: [
14 | new Sentry.BrowserTracing(),
15 | new Sentry.Replay({
16 | maskAllText: true,
17 | blockAllMedia: true,
18 | }),
19 | ],
20 | });
21 | }
22 |
--------------------------------------------------------------------------------
/src/lib/utils/search.ts:
--------------------------------------------------------------------------------
1 | export const searchQuery = (param: string) => {
2 | return document.querySelectorAll(param);
3 | };
4 |
5 | export const cleanupHighlight = () => {
6 | const nodes = document.querySelectorAll("foreignObject.searched, .highlight");
7 |
8 | nodes.forEach(node => {
9 | node.classList.remove("highlight", "searched");
10 | });
11 | };
12 |
13 | export const highlightMatchedNodes = (nodes: NodeListOf, selectedNode: number) => {
14 | nodes.forEach(node => {
15 | const foreignObject = node.parentElement?.closest("foreignObject");
16 |
17 | if (foreignObject) {
18 | foreignObject.classList.add("searched");
19 | }
20 | });
21 |
22 | nodes[selectedNode].classList.add("highlight");
23 | };
24 |
--------------------------------------------------------------------------------
/src/containers/Toolbar/styles.ts:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | export const StyledToolElement = styled.button<{ $hide?: boolean; $highlight?: boolean }>`
4 | display: ${({ $hide }) => ($hide ? "none" : "flex")};
5 | align-items: center;
6 | gap: 4px;
7 | place-content: center;
8 | font-size: 12px;
9 | background: ${({ $highlight }) =>
10 | $highlight ? "linear-gradient(rgba(0, 0, 0, 0.1) 0 0)" : "none"};
11 | color: ${({ theme }) => theme.INTERACTIVE_NORMAL};
12 | padding: 6px;
13 | border-radius: 3px;
14 | white-space: nowrap;
15 |
16 | &:hover {
17 | background-image: linear-gradient(rgba(0, 0, 0, 0.1) 0 0);
18 | }
19 |
20 | &:hover {
21 | color: ${({ theme }) => theme.INTERACTIVE_HOVER};
22 | opacity: 1;
23 | box-shadow: none;
24 | }
25 | `;
26 |
--------------------------------------------------------------------------------
/src/store/useJson.ts:
--------------------------------------------------------------------------------
1 | import { create } from "zustand";
2 | import useGraph from "src/containers/Editor/components/views/GraphView/stores/useGraph";
3 |
4 | interface JsonActions {
5 | setJson: (json: string) => void;
6 | getJson: () => string;
7 | clear: () => void;
8 | }
9 |
10 | const initialStates = {
11 | json: "",
12 | loading: true,
13 | };
14 |
15 | export type JsonStates = typeof initialStates;
16 |
17 | const useJson = create()((set, get) => ({
18 | ...initialStates,
19 | getJson: () => get().json,
20 | setJson: json => {
21 | set({ json, loading: false });
22 | useGraph.getState().setGraph(json);
23 | },
24 | clear: () => {
25 | set({ json: "", loading: false });
26 | useGraph.getState().clearGraph();
27 | },
28 | }));
29 |
30 | export default useJson;
31 |
--------------------------------------------------------------------------------
/src/layout/Layout.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Inter } from "next/font/google";
3 | import styled, { ThemeProvider } from "styled-components";
4 | import { lightTheme } from "src/constants/theme";
5 | import { Footer } from "./Footer";
6 | import { Navbar } from "./Navbar";
7 |
8 | const inter = Inter({
9 | subsets: ["latin-ext"],
10 | });
11 |
12 | const StyledLayoutWrapper = styled.div`
13 | background: #fff;
14 | /* background-image: radial-gradient(#ededed 2px, #ffffff 2px); */
15 | /* background-size: 40px 40px; */
16 | font-family: ${inter.style.fontFamily};
17 | `;
18 |
19 | const Layout = ({ children }: React.PropsWithChildren) => {
20 | return (
21 |
22 |
23 |
24 | {children}
25 |
26 |
27 |
28 | );
29 | };
30 |
31 | export default Layout;
32 |
--------------------------------------------------------------------------------
/src/constants/seo.ts:
--------------------------------------------------------------------------------
1 | import type { NextSeoProps } from "next-seo";
2 |
3 | export const SEO: NextSeoProps = {
4 | title: "JSON Crack | Transform your data into interactive graphs",
5 | description:
6 | "JSON Crack Editor is a tool for visualizing into graphs, analyzing, editing, formatting, querying, transforming and validating JSON, CSV, YAML, XML, and more.",
7 | themeColor: "#36393E",
8 | openGraph: {
9 | type: "website",
10 | images: [
11 | {
12 | url: "https://jsoncrack.com/assets/jsoncrack.png",
13 | width: 1200,
14 | height: 627,
15 | },
16 | ],
17 | },
18 | twitter: {
19 | handle: "@jsoncrack",
20 | cardType: "summary_large_image",
21 | },
22 | additionalLinkTags: [
23 | {
24 | rel: "manifest",
25 | href: "/manifest.json",
26 | },
27 | {
28 | rel: "icon",
29 | href: "/favicon.ico",
30 | sizes: "48x48",
31 | },
32 | ],
33 | };
34 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.yml:
--------------------------------------------------------------------------------
1 | name: Feature request
2 | description: Request a new feature
3 | labels: [feature]
4 | body:
5 | - type: textarea
6 | id: description
7 | attributes:
8 | label: Feature
9 | description: A clear and concise description of what the problem is, or what feature you want to be implemented.
10 | placeholder: I'm always frustrated when..., Discord has recently released..., A good addition would be...
11 | validations:
12 | required: true
13 | - type: textarea
14 | id: alternatives
15 | attributes:
16 | label: Alternative solutions or implementations
17 | description: A clear and concise description of any alternative solutions or features you have considered.
18 | - type: textarea
19 | id: additional-context
20 | attributes:
21 | label: Other context
22 | description: Any other context, screenshots, or file uploads that help us understand your feature request.
23 |
--------------------------------------------------------------------------------
/src/constants/globalStyle.ts:
--------------------------------------------------------------------------------
1 | import { createGlobalStyle } from "styled-components";
2 |
3 | const GlobalStyle = createGlobalStyle`
4 | html, body {
5 | background: #ffffff;
6 | overscroll-behavior: none;
7 | -webkit-font-smoothing: subpixel-antialiased !important;
8 | }
9 |
10 | *,
11 | *::before,
12 | *::after {
13 | box-sizing: border-box;
14 | margin: 0;
15 | padding: 0;
16 | scroll-behavior: smooth !important;
17 | -webkit-tap-highlight-color: transparent;
18 | -webkit-font-smoothing: never;
19 | }
20 |
21 | .hide {
22 | display: none;
23 | }
24 |
25 | svg {
26 | vertical-align: text-top;
27 | }
28 |
29 | a {
30 | color: unset;
31 | text-decoration: none;
32 | }
33 |
34 | button {
35 | border: none;
36 | outline: none;
37 | background: transparent;
38 | width: fit-content;
39 | margin: 0;
40 | padding: 0;
41 | cursor: pointer;
42 | }
43 | `;
44 |
45 | export default GlobalStyle;
46 |
--------------------------------------------------------------------------------
/src/containers/Editor/components/views/TreeView/Label.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import type { DefaultTheme } from "styled-components";
3 | import { styled } from "styled-components";
4 | import type { KeyPath } from "react-json-tree";
5 |
6 | interface LabelProps {
7 | keyPath: KeyPath;
8 | nodeType: string;
9 | }
10 |
11 | function getLabelColor({ $type, theme }: { $type?: string; theme: DefaultTheme }) {
12 | if ($type === "Object") return theme.NODE_COLORS.PARENT_OBJ;
13 | if ($type === "Array") return theme.NODE_COLORS.PARENT_ARR;
14 | return theme.NODE_COLORS.PARENT_OBJ;
15 | }
16 |
17 | const StyledLabel = styled.span<{ $nodeType?: string }>`
18 | color: ${({ theme, $nodeType }) => getLabelColor({ theme, $type: $nodeType })};
19 |
20 | &:hover {
21 | filter: brightness(1.5);
22 | transition: filter 0.2s ease-in-out;
23 | }
24 | `;
25 |
26 | export const Label = ({ keyPath, nodeType }: LabelProps) => {
27 | return {keyPath[0]}:;
28 | };
29 |
--------------------------------------------------------------------------------
/src/containers/Editor/components/views/GraphView/lib/utils/addNodeToGraph.ts:
--------------------------------------------------------------------------------
1 | import type { NodeType } from "jsonc-parser";
2 | import type { Graph } from "src/containers/Editor/components/views/GraphView/lib/jsonParser";
3 | import { calculateNodeSize } from "src/containers/Editor/components/views/GraphView/lib/utils/calculateNodeSize";
4 |
5 | type Props = {
6 | graph: Graph;
7 | text: string | [string, string][];
8 | isEmpty?: boolean;
9 | type?: NodeType;
10 | };
11 |
12 | export const addNodeToGraph = ({ graph, text, type = "null", isEmpty = false }: Props) => {
13 | const id = String(graph.nodes.length + 1);
14 | const isParent = type === "array" || type === "object";
15 | const { width, height } = calculateNodeSize(text, isParent);
16 |
17 | const node = {
18 | id,
19 | text,
20 | width,
21 | height,
22 | data: {
23 | type,
24 | isParent,
25 | isEmpty,
26 | childrenCount: isParent ? 1 : 0,
27 | },
28 | };
29 |
30 | graph.nodes.push(node);
31 |
32 | return id;
33 | };
34 |
--------------------------------------------------------------------------------
/src/containers/Editor/components/views/TreeView/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useTheme } from "styled-components";
3 | import { JSONTree } from "react-json-tree";
4 | import useJson from "src/store/useJson";
5 | import { Label } from "./Label";
6 | import { Value } from "./Value";
7 |
8 | export const TreeView = () => {
9 | const theme = useTheme();
10 | const json = useJson(state => state.json);
11 |
12 | return (
13 | <>
14 | }
18 | labelRenderer={(keyPath, nodeType) => }
19 | theme={{
20 | extend: {
21 | overflow: "scroll",
22 | height: "100%",
23 | scheme: "monokai",
24 | author: "wimer hazenberg (http://www.monokai.nl)",
25 | base00: theme.GRID_BG_COLOR,
26 | },
27 | }}
28 | />
29 | >
30 | );
31 | };
32 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "rules": {
3 | "@next/next/no-img-element": "off",
4 | "@typescript-eslint/consistent-type-imports": "error",
5 | "unused-imports/no-unused-imports": "error",
6 | "@typescript-eslint/no-explicit-any": "off",
7 | "prettier/prettier": "error",
8 | "space-in-parens": "error",
9 | "no-empty": "error",
10 | "no-multiple-empty-lines": "error",
11 | "no-irregular-whitespace": "error",
12 | "strict": ["error", "never"],
13 | "linebreak-style": ["error", "unix"],
14 | "quotes": ["error", "double", { "avoidEscape": true }],
15 | "semi": ["error", "always"],
16 | "prefer-const": "error",
17 | "space-before-function-paren": [
18 | "error",
19 | {
20 | "anonymous": "always",
21 | "named": "never",
22 | "asyncArrow": "always"
23 | }
24 | ]
25 | },
26 | "extends": ["next/core-web-vitals", "prettier", "plugin:@typescript-eslint/recommended"],
27 | "plugins": ["prettier", "unused-imports"],
28 | "ignorePatterns": ["src/enums", "next.config.js"]
29 | }
30 |
--------------------------------------------------------------------------------
/src/containers/Toolbar/AccountMenu.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Menu, Avatar, Text } from "@mantine/core";
3 | import { FaRegCircleUser } from "react-icons/fa6";
4 | import { VscSignIn } from "react-icons/vsc";
5 | import { StyledToolElement } from "./styles";
6 |
7 | export const AccountMenu = () => {
8 | return (
9 |
28 | );
29 | };
30 |
--------------------------------------------------------------------------------
/src/pages/404.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Link from "next/link";
3 | import { Button, Stack, Text, Title } from "@mantine/core";
4 | import { NextSeo } from "next-seo";
5 | import { SEO } from "src/constants/seo";
6 | import Layout from "src/layout/Layout";
7 |
8 | const NotFound = () => {
9 | return (
10 |
11 |
12 |
13 |
14 | 404
15 |
16 | Nothing to see here
17 |
18 | Page you are trying to open does not exist. You may have mistyped the address, or the page
19 | has been moved to another URL. If you think this is an error contact support.
20 |
21 |
22 |
25 |
26 |
27 |
28 | );
29 | };
30 |
31 | export default NotFound;
32 |
--------------------------------------------------------------------------------
/src/containers/Toolbar/SearchInput.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Flex, Text, TextInput } from "@mantine/core";
3 | import { getHotkeyHandler } from "@mantine/hooks";
4 | import { AiOutlineSearch } from "react-icons/ai";
5 | import { useFocusNode } from "src/hooks/useFocusNode";
6 |
7 | export const SearchInput = () => {
8 | const [searchValue, setValue, skip, nodeCount, currentNode] = useFocusNode();
9 |
10 | return (
11 | setValue(e.currentTarget.value)}
18 | placeholder="Search Node"
19 | autoComplete="off"
20 | autoCorrect="off"
21 | onKeyDown={getHotkeyHandler([["Enter", skip]])}
22 | leftSection={}
23 | rightSection={
24 | searchValue && (
25 |
26 |
27 | {searchValue && `${nodeCount}/${nodeCount > 0 ? currentNode + 1 : "0"}`}
28 |
29 |
30 | )
31 | }
32 | />
33 | );
34 | };
35 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | const { withSentryConfig } = require("@sentry/nextjs");
2 | const withBundleAnalyzer = require("@next/bundle-analyzer")({
3 | enabled: process.env.ANALYZE === "true",
4 | });
5 |
6 | /**
7 | * @type {import('next').NextConfig}
8 | */
9 | const config = {
10 | output: "export",
11 | reactStrictMode: false,
12 | productionBrowserSourceMaps: true,
13 | compiler: {
14 | styledComponents: true,
15 | },
16 | webpack: config => {
17 | config.resolve.fallback = { fs: false };
18 | config.output.webassemblyModuleFilename = "static/wasm/[modulehash].wasm";
19 | config.experiments = { asyncWebAssembly: true };
20 |
21 | return config;
22 | },
23 | };
24 |
25 | const bundleAnalyzerConfig = withBundleAnalyzer(config);
26 |
27 | const sentryConfig = withSentryConfig(
28 | config,
29 | {
30 | silent: true,
31 | org: "aykut-sarac",
32 | project: "json-crack",
33 | },
34 | {
35 | widenClientFileUpload: true,
36 | hideSourceMaps: true,
37 | disableLogger: true,
38 | disableServerWebpackPlugin: true,
39 | }
40 | );
41 |
42 | module.exports = process.env.ANALYZE === "true" ? bundleAnalyzerConfig : sentryConfig;
43 |
--------------------------------------------------------------------------------
/src/pages/_error.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useRouter } from "next/router";
3 | import { Button, Stack, Text, Title } from "@mantine/core";
4 | import { NextSeo } from "next-seo";
5 | import { SEO } from "src/constants/seo";
6 | import Layout from "src/layout/Layout";
7 |
8 | const Custom500 = () => {
9 | const router = useRouter();
10 |
11 | return (
12 |
13 |
14 |
15 |
16 | 500
17 |
18 | Something bad just happened...
19 |
20 | Our servers could not handle your request. Don't worry, our development team was
21 | already notified. Try refreshing the page.
22 |
23 |
26 |
27 |
28 | );
29 | };
30 |
31 | export default Custom500;
32 |
--------------------------------------------------------------------------------
/src/containers/Editor/components/views/GraphView/lib/utils/getOutgoers.ts:
--------------------------------------------------------------------------------
1 | import type { NodeData, EdgeData } from "src/types/graph";
2 |
3 | type Outgoers = [NodeData[], string[]];
4 |
5 | export const getOutgoers = (
6 | nodeId: string,
7 | nodes: NodeData[],
8 | edges: EdgeData[],
9 | parent: string[] = []
10 | ): Outgoers => {
11 | const outgoerNodes: NodeData[] = [];
12 | const matchingNodes: string[] = [];
13 |
14 | if (parent.includes(nodeId)) {
15 | const initialParentNode = nodes.find(n => n.id === nodeId);
16 |
17 | if (initialParentNode) outgoerNodes.push(initialParentNode);
18 | }
19 |
20 | const findOutgoers = (currentNodeId: string) => {
21 | const outgoerIds = edges.filter(e => e.from === currentNodeId).map(e => e.to);
22 | const nodeList = nodes.filter(n => {
23 | if (parent.includes(n.id) && !matchingNodes.includes(n.id)) matchingNodes.push(n.id);
24 | return outgoerIds.includes(n.id) && !parent.includes(n.id);
25 | });
26 |
27 | outgoerNodes.push(...nodeList);
28 | nodeList.forEach(node => findOutgoers(node.id));
29 | };
30 |
31 | findOutgoers(nodeId);
32 | return [outgoerNodes, matchingNodes];
33 | };
34 |
--------------------------------------------------------------------------------
/src/hooks/useJsonQuery.ts:
--------------------------------------------------------------------------------
1 | import toast from "react-hot-toast";
2 | import useFile from "src/store/useFile";
3 | import useJson from "src/store/useJson";
4 |
5 | const useJsonQuery = () => {
6 | const getJson = useJson(state => state.getJson);
7 | const setContents = useFile(state => state.setContents);
8 |
9 | const transformer = async ({ value }) => {
10 | const { run } = await import("json_typegen_wasm");
11 | return run("Root", value, JSON.stringify({ output_mode: "typescript/typealias" }));
12 | };
13 |
14 | const updateJson = async (query: string, cb?: () => void) => {
15 | try {
16 | const jq = await import("jq-web");
17 | const res = await jq.promised.json(JSON.parse(getJson()), query);
18 |
19 | setContents({ contents: JSON.stringify(res, null, 2) });
20 | cb?.();
21 | } catch (error) {
22 | console.error(error);
23 | toast.error("Unable to process the request.");
24 | }
25 | };
26 |
27 | const getJsonType = async () => {
28 | const types = await transformer({ value: getJson() });
29 | return types;
30 | };
31 |
32 | return { updateJson, getJsonType };
33 | };
34 |
35 | export default useJsonQuery;
36 |
--------------------------------------------------------------------------------
/.github/workflows/pull-request.yml:
--------------------------------------------------------------------------------
1 | name: Verify Pull Request
2 |
3 | on:
4 | pull_request:
5 | branches: [ "main" ]
6 |
7 | jobs:
8 | cache-and-install:
9 | runs-on: ubuntu-latest
10 |
11 | steps:
12 | - name: Checkout
13 | uses: actions/checkout@v3
14 |
15 | - name: Install Node.js
16 | uses: actions/setup-node@v3
17 | with:
18 | node-version: 20
19 |
20 | - uses: pnpm/action-setup@v2
21 | name: Install pnpm
22 | with:
23 | version: 8
24 | run_install: false
25 |
26 | - name: Get pnpm store directory
27 | shell: bash
28 | run: |
29 | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
30 |
31 | - uses: actions/cache@v3
32 | name: Setup pnpm cache
33 | with:
34 | path: ${{ env.STORE_PATH }}
35 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
36 | restore-keys: |
37 | ${{ runner.os }}-pnpm-store-
38 |
39 | - name: Install dependencies
40 | run: pnpm install
41 |
42 | - name: Lint
43 | run: pnpm run lint
44 |
45 | - name: Build
46 | run: pnpm run build
47 |
48 |
--------------------------------------------------------------------------------
/src/containers/Editor/components/views/TreeView/Value.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import type { DefaultTheme } from "styled-components";
3 | import { useTheme } from "styled-components";
4 | import { TextRenderer } from "src/containers/Editor/components/views/GraphView/CustomNode/TextRenderer";
5 |
6 | type TextColorFn = {
7 | theme: DefaultTheme;
8 | $value?: string | unknown;
9 | };
10 |
11 | function getValueColor({ $value, theme }: TextColorFn) {
12 | if ($value && !Number.isNaN(+$value)) return theme.NODE_COLORS.INTEGER;
13 | if ($value === "true") return theme.NODE_COLORS.BOOL.TRUE;
14 | if ($value === "false") return theme.NODE_COLORS.BOOL.FALSE;
15 | if ($value === "null") return theme.NODE_COLORS.NULL;
16 |
17 | // default
18 | return theme.NODE_COLORS.NODE_VALUE;
19 | }
20 |
21 | interface ValueProps {
22 | valueAsString: unknown;
23 | value: unknown;
24 | }
25 |
26 | export const Value = (props: ValueProps) => {
27 | const theme = useTheme();
28 | const { valueAsString, value } = props;
29 |
30 | return (
31 |
39 | {JSON.stringify(value)}
40 |
41 | );
42 | };
43 |
--------------------------------------------------------------------------------
/src/pages/_document.tsx:
--------------------------------------------------------------------------------
1 | import type { DocumentContext, DocumentInitialProps } from "next/document";
2 | import Document, { Html, Head, Main, NextScript } from "next/document";
3 | import { ColorSchemeScript } from "@mantine/core";
4 | import { ServerStyleSheet } from "styled-components";
5 |
6 | class MyDocument extends Document {
7 | static async getInitialProps(ctx: DocumentContext): Promise {
8 | const sheet = new ServerStyleSheet();
9 | const originalRenderPage = ctx.renderPage;
10 |
11 | try {
12 | ctx.renderPage = () =>
13 | originalRenderPage({
14 | enhanceApp: App => props => sheet.collectStyles(),
15 | });
16 |
17 | const initialProps = await Document.getInitialProps(ctx);
18 |
19 | return {
20 | ...initialProps,
21 | styles: (
22 | <>
23 | {initialProps.styles}
24 | {sheet.getStyleElement()}
25 | >
26 | ),
27 | };
28 | } finally {
29 | sheet.seal();
30 | }
31 | }
32 |
33 | render() {
34 | return (
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | );
45 | }
46 | }
47 |
48 | export default MyDocument;
49 |
--------------------------------------------------------------------------------
/src/containers/Editor/components/LiveEditor.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 | import { GraphView } from "src/containers/Editor/components/views/GraphView";
4 | import { TreeView } from "src/containers/Editor/components/views/TreeView";
5 | import { ViewMode } from "src/enums/viewMode.enum";
6 | import useConfig from "src/store/useConfig";
7 |
8 | const StyledLiveEditor = styled.div`
9 | position: relative;
10 | height: 100%;
11 | background: ${({ theme }) => theme.GRID_BG_COLOR};
12 | overflow: auto;
13 | cursor: url("/assets/cursor.svg"), auto;
14 |
15 | & > ul {
16 | margin-top: 0 !important;
17 | padding: 12px !important;
18 | font-family: monospace;
19 | font-size: 14px;
20 | font-weight: 500;
21 | }
22 |
23 | .tab-group {
24 | position: absolute;
25 | top: 10px;
26 | left: 10px;
27 | z-index: 2;
28 | }
29 | `;
30 |
31 | const View = () => {
32 | const viewMode = useConfig(state => state.viewMode);
33 |
34 | if (viewMode === ViewMode.Graph) return ;
35 | if (viewMode === ViewMode.Tree) return ;
36 | return null;
37 | };
38 |
39 | const LiveEditor = () => {
40 | return (
41 | e.preventDefault()}>
42 |
43 |
44 | );
45 | };
46 |
47 | export default LiveEditor;
48 |
--------------------------------------------------------------------------------
/src/hooks/useToggleHide.ts:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import useGraph from "src/containers/Editor/components/views/GraphView/stores/useGraph";
3 |
4 | const useToggleHide = () => {
5 | const getCollapsedNodeIds = useGraph(state => state.getCollapsedNodeIds);
6 | const getCollapsedEdgeIds = useGraph(state => state.getCollapsedEdgeIds);
7 |
8 | React.useEffect(() => {
9 | validateHiddenNodes(getCollapsedNodeIds(), getCollapsedEdgeIds());
10 | }, [getCollapsedEdgeIds, getCollapsedNodeIds]);
11 |
12 | return {
13 | validateHiddenNodes: () => validateHiddenNodes(getCollapsedNodeIds(), getCollapsedEdgeIds()),
14 | };
15 | };
16 |
17 | function validateHiddenNodes(collapsedNodeIs: string[], collapsedEdgeIds: string[]) {
18 | const nodeList = collapsedNodeIs.map(id => `[id$="node-${id}"]`);
19 | const edgeList = collapsedEdgeIds.map(id => `[class$="edge-${id}"]`);
20 | const hiddenItems = document.body.querySelectorAll(".hide");
21 |
22 | hiddenItems.forEach(item => item.classList.remove("hide"));
23 |
24 | if (nodeList.length) {
25 | const selectedNodes = document.body.querySelectorAll(nodeList.join(","));
26 |
27 | selectedNodes.forEach(node => node.classList.add("hide"));
28 | }
29 |
30 | if (edgeList.length) {
31 | const selectedEdges = document.body.querySelectorAll(edgeList.join(","));
32 |
33 | selectedEdges.forEach(edge => edge.classList.add("hide"));
34 | }
35 | }
36 |
37 | export default useToggleHide;
38 |
--------------------------------------------------------------------------------
/src/layout/ModalController.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import type { ModalProps } from "@mantine/core";
3 | import * as Modals from "src/containers/Modals";
4 | import type { Modal } from "src/containers/Modals";
5 | import useModal from "src/store/useModal";
6 |
7 | type ModalComponent = { key: Modal; component: React.FC };
8 |
9 | const modalComponents: ModalComponent[] = [
10 | { key: "import", component: Modals.ImportModal },
11 | { key: "download", component: Modals.DownloadModal },
12 | { key: "upgrade", component: Modals.UpgradeModal },
13 | { key: "jwt", component: Modals.JWTModal },
14 | { key: "node", component: Modals.NodeModal },
15 | { key: "schema", component: Modals.SchemaModal },
16 | { key: "jq", component: Modals.JQModal },
17 | { key: "type", component: Modals.TypeModal },
18 | { key: "jpath", component: Modals.JPathModal },
19 | ];
20 |
21 | const ModalController = () => {
22 | const setVisible = useModal(state => state.setVisible);
23 | const modalStates = useModal(state => modalComponents.map(modal => state[modal.key]));
24 |
25 | return (
26 | <>
27 | {modalComponents.map(({ key, component }, index) => {
28 | const ModalComponent = component;
29 | const opened = modalStates[index];
30 |
31 | return setVisible(key)(false)} />;
32 | })}
33 | >
34 | );
35 | };
36 |
37 | export default ModalController;
38 |
--------------------------------------------------------------------------------
/src/containers/Modals/JWTModal/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import type { ModalProps } from "@mantine/core";
3 | import { Modal, Button, Textarea, Group } from "@mantine/core";
4 | import { decode } from "jsonwebtoken";
5 | import { event as gaEvent } from "nextjs-google-analytics";
6 | import useFile from "src/store/useFile";
7 |
8 | export const JWTModal = ({ opened, onClose }: ModalProps) => {
9 | const setContents = useFile(state => state.setContents);
10 | const [token, setToken] = React.useState("");
11 |
12 | const resolve = () => {
13 | if (!token) return;
14 | const json = decode(token);
15 | setContents({ contents: JSON.stringify(json, null, 2) });
16 |
17 | gaEvent("resolve_jwt");
18 | setToken("");
19 | onClose();
20 | };
21 |
22 | return (
23 |
24 |
38 | );
39 | };
40 |
--------------------------------------------------------------------------------
/src/containers/Landing/FAQ.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Container, Title, Accordion } from "@mantine/core";
3 | import Questions from "src/data/faq.json";
4 |
5 | export const FAQ = () => {
6 | return (
7 |
8 |
20 | Frequently Asked Questions
21 |
22 |
43 | {Questions.map(({ title, content }) => (
44 |
45 | {title}
46 | {content}
47 |
48 | ))}
49 |
50 |
51 | );
52 | };
53 |
--------------------------------------------------------------------------------
/src/containers/Modals/JQModal/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import type { ModalProps } from "@mantine/core";
3 | import { Stack, Modal, Button, Text, Anchor, Group, TextInput } from "@mantine/core";
4 | import { VscLinkExternal } from "react-icons/vsc";
5 | import useJsonQuery from "src/hooks/useJsonQuery";
6 |
7 | export const JQModal = ({ opened, onClose }: ModalProps) => {
8 | const { updateJson } = useJsonQuery();
9 | const [query, setQuery] = React.useState("");
10 |
11 | return (
12 |
13 |
14 |
15 | jq is a lightweight and flexible command-line JSON processor. JSON Crack uses simplified
16 | version of jq, not all features are supported.
17 |
18 |
24 | Read documentation.
25 |
26 |
27 | setQuery(e.currentTarget.value)}
32 | />
33 |
34 |
35 |
36 |
37 |
38 | );
39 | };
40 |
--------------------------------------------------------------------------------
/src/containers/Editor/components/views/GraphView/CustomNode/ObjectNode.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import type { CustomNodeProps } from "src/containers/Editor/components/views/GraphView/CustomNode";
3 | import { TextRenderer } from "./TextRenderer";
4 | import * as Styled from "./styles";
5 |
6 | type Value = [string, string];
7 |
8 | type RowProps = {
9 | val: Value;
10 | x: number;
11 | y: number;
12 | index: number;
13 | };
14 |
15 | const Row = ({ val, x, y, index }: RowProps) => {
16 | const key = JSON.stringify(val);
17 | const rowKey = JSON.stringify(val[0]).replaceAll('"', "");
18 | const rowValue = JSON.stringify(val[1]);
19 |
20 | return (
21 |
22 | {rowKey}:
23 | {rowValue}
24 |
25 | );
26 | };
27 |
28 | const Node = ({ node, x, y }: CustomNodeProps) => (
29 |
30 | {(node.text as Value[]).map((val, idx) => (
31 |
32 | ))}
33 |
34 | );
35 |
36 | function propsAreEqual(prev: CustomNodeProps, next: CustomNodeProps) {
37 | return String(prev.node.text) === String(next.node.text) && prev.node.width === next.node.width;
38 | }
39 |
40 | export const ObjectNode = React.memo(Node, propsAreEqual);
41 |
--------------------------------------------------------------------------------
/src/containers/Toolbar/FileMenu.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Flex, Menu } from "@mantine/core";
3 | import { event as gaEvent } from "nextjs-google-analytics";
4 | import { CgChevronDown } from "react-icons/cg";
5 | import useFile from "src/store/useFile";
6 | import useModal from "src/store/useModal";
7 | import { StyledToolElement } from "./styles";
8 |
9 | export const FileMenu = () => {
10 | const setVisible = useModal(state => state.setVisible);
11 | const getContents = useFile(state => state.getContents);
12 | const getFormat = useFile(state => state.getFormat);
13 |
14 | const handleSave = () => {
15 | const a = document.createElement("a");
16 | const file = new Blob([getContents()], { type: "text/plain" });
17 |
18 | a.href = window.URL.createObjectURL(file);
19 | a.download = `jsoncrack.${getFormat()}`;
20 | a.click();
21 |
22 | gaEvent("save_file", { label: getFormat() });
23 | };
24 |
25 | return (
26 |
44 | );
45 | };
46 |
--------------------------------------------------------------------------------
/src/pages/legal/terms.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Box, Container, Paper, Stack, Text, Title } from "@mantine/core";
3 | import { NextSeo } from "next-seo";
4 | import { SEO } from "src/constants/seo";
5 | import terms from "src/data/terms.json";
6 | import Layout from "src/layout/Layout";
7 |
8 | const Terms = () => {
9 | return (
10 |
11 |
17 |
18 |
19 |
20 | Terms of Service
21 |
22 |
23 | Last updated: Aug 11, 2024
24 |
25 |
26 |
27 | {Object.keys(terms).map((term, index) => (
28 |
29 |
30 | {term}
31 |
32 | {terms[term].map(term => (
33 |
34 | {term}
35 |
36 | ))}
37 |
38 | ))}
39 |
40 |
41 |
42 |
43 | );
44 | };
45 |
46 | export default Terms;
47 |
--------------------------------------------------------------------------------
/src/containers/Editor/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import dynamic from "next/dynamic";
3 | import styled from "styled-components";
4 | import { Allotment } from "allotment";
5 | import "allotment/dist/style.css";
6 | import useGraph from "src/containers/Editor/components/views/GraphView/stores/useGraph";
7 | import { FullscreenDropzone } from "./components/FullscreenDropzone";
8 |
9 | export const StyledEditor = styled(Allotment)`
10 | position: relative !important;
11 | display: flex;
12 | background: ${({ theme }) => theme.BACKGROUND_SECONDARY};
13 | height: calc(100vh - 67px);
14 |
15 | @media only screen and (max-width: 320px) {
16 | height: 100vh;
17 | }
18 | `;
19 |
20 | const TextEditor = dynamic(() => import("src/containers/Editor/components/TextEditor"), {
21 | ssr: false,
22 | });
23 |
24 | const LiveEditor = dynamic(() => import("src/containers/Editor/components/LiveEditor"), {
25 | ssr: false,
26 | });
27 |
28 | export const Editor = () => {
29 | const fullscreen = useGraph(state => state.fullscreen);
30 |
31 | return (
32 | <>
33 |
34 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | >
48 | );
49 | };
50 |
--------------------------------------------------------------------------------
/src/pages/legal/privacy.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Box, Container, Paper, Stack, Text, Title } from "@mantine/core";
3 | import { NextSeo } from "next-seo";
4 | import { SEO } from "src/constants/seo";
5 | import privacy from "src/data/privacy.json";
6 | import Layout from "src/layout/Layout";
7 |
8 | const Privacy = () => {
9 | return (
10 |
11 |
17 |
18 |
19 |
20 | Privacy Policy
21 |
22 |
23 | Last updated: May 21, 2024
24 |
25 |
26 |
27 | {Object.keys(privacy).map((term, index) => (
28 |
29 |
30 | {term}
31 |
32 | {privacy[term].map(term => (
33 |
34 | {term}
35 |
36 | ))}
37 |
38 | ))}
39 |
40 |
41 |
42 |
43 | );
44 | };
45 |
46 | export default Privacy;
47 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.yml:
--------------------------------------------------------------------------------
1 | name: Bug report
2 | description: Create a report to help us improve
3 | title: "[BUG]: "
4 | labels: bug
5 | assignees: AykutSarac
6 | body:
7 | - type: textarea
8 | id: description
9 | attributes:
10 | label: Issue description
11 | description: |
12 | Describe the issue in as much detail as possible.
13 |
14 | Tip: You can attach images or log files by clicking this area to highlight it and then dragging files into it.
15 | placeholder: |
16 | Steps to reproduce with below code sample:
17 | 1. do thing
18 | 2. click...
19 | 3. observe behavior
20 | 4. see error logs below
21 | validations:
22 | required: true
23 | - type: textarea
24 | id: media
25 | attributes:
26 | label: Media & Screenshots
27 | description: Include screenshots or video of reproduction as much as possible
28 | - type: textarea
29 | id: os
30 | attributes:
31 | label: Operating system
32 | description: Which OS does your application run on?
33 | value: |
34 | - OS: [e.g. iOS]:
35 | - Browser [e.g. chrome, safari]:
36 |
37 | - Any other details...
38 | - type: dropdown
39 | id: priority
40 | attributes:
41 | label: Priority this issue should have
42 | description: Please be realistic. If you need to elaborate on your reasoning, please use the Issue description field above.
43 | options:
44 | - Low (slightly annoying)
45 | - Medium (should be fixed soon)
46 | - High (immediate attention needed)
47 | validations:
48 | required: true
49 |
--------------------------------------------------------------------------------
/src/hooks/useFocusNode.ts:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useDebouncedValue } from "@mantine/hooks";
3 | import { event as gaEvent } from "nextjs-google-analytics";
4 | import useGraph from "src/containers/Editor/components/views/GraphView/stores/useGraph";
5 | import { searchQuery, cleanupHighlight, highlightMatchedNodes } from "src/lib/utils/search";
6 |
7 | export const useFocusNode = () => {
8 | const viewPort = useGraph(state => state.viewPort);
9 | const [selectedNode, setSelectedNode] = React.useState(0);
10 | const [nodeCount, setNodeCount] = React.useState(0);
11 | const [value, setValue] = React.useState("");
12 | const [debouncedValue] = useDebouncedValue(value, 600);
13 |
14 | const skip = () => setSelectedNode(current => (current + 1) % nodeCount);
15 |
16 | React.useEffect(() => {
17 | if (!value) {
18 | cleanupHighlight();
19 | setSelectedNode(0);
20 | setNodeCount(0);
21 | return;
22 | }
23 |
24 | if (!viewPort || !debouncedValue) return;
25 | const matchedNodes: NodeListOf = searchQuery(`span[data-key*='${debouncedValue}' i]`);
26 | const matchedNode: Element | null = matchedNodes[selectedNode] || null;
27 |
28 | cleanupHighlight();
29 |
30 | if (matchedNode && matchedNode.parentElement) {
31 | highlightMatchedNodes(matchedNodes, selectedNode);
32 | setNodeCount(matchedNodes.length);
33 |
34 | viewPort?.camera.centerFitElementIntoView(matchedNode.parentElement, {
35 | elementExtraMarginForZoom: 400,
36 | });
37 | } else {
38 | setSelectedNode(0);
39 | setNodeCount(0);
40 | }
41 |
42 | gaEvent("search_graph");
43 | }, [selectedNode, debouncedValue, value, viewPort]);
44 |
45 | return [value, setValue, skip, nodeCount, selectedNode] as const;
46 | };
47 |
--------------------------------------------------------------------------------
/src/containers/Editor/components/views/GraphView/CustomNode/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import type { NodeProps } from "reaflow";
3 | import { Node } from "reaflow";
4 | import useGraph from "src/containers/Editor/components/views/GraphView/stores/useGraph";
5 | import useModal from "src/store/useModal";
6 | import type { NodeData } from "src/types/graph";
7 | import { ObjectNode } from "./ObjectNode";
8 | import { TextNode } from "./TextNode";
9 |
10 | export interface CustomNodeProps {
11 | node: NodeData;
12 | x: number;
13 | y: number;
14 | hasCollapse?: boolean;
15 | }
16 |
17 | const rootProps = {
18 | rx: 50,
19 | ry: 50,
20 | };
21 |
22 | const CustomNodeWrapper = (nodeProps: NodeProps) => {
23 | const data = nodeProps.properties.data;
24 | const setSelectedNode = useGraph(state => state.setSelectedNode);
25 | const setVisible = useModal(state => state.setVisible);
26 |
27 | const handleNodeClick = React.useCallback(
28 | (_: React.MouseEvent, data: NodeData) => {
29 | if (setSelectedNode) setSelectedNode(data);
30 | setVisible("node")(true);
31 | },
32 | [setSelectedNode, setVisible]
33 | );
34 |
35 | return (
36 |
43 | {({ node, x, y }) => {
44 | if (Array.isArray(nodeProps.properties.text)) {
45 | if (data?.isEmpty) return null;
46 | return ;
47 | }
48 |
49 | return ;
50 | }}
51 |
52 | );
53 | };
54 |
55 | export const CustomNode = React.memo(CustomNodeWrapper);
56 |
--------------------------------------------------------------------------------
/src/containers/Editor/components/views/GraphView/lib/utils/getNodePath.ts:
--------------------------------------------------------------------------------
1 | import type { NodeData, EdgeData } from "src/types/graph";
2 |
3 | export function getNodePath(nodes: NodeData[], edges: EdgeData[], nodeId: string) {
4 | // eslint-disable-next-line @typescript-eslint/no-var-requires
5 | const { getParentsForNodeId } = require("reaflow");
6 |
7 | let resolvedPath = "";
8 | const parentIds = getParentsForNodeId(nodes, edges, nodeId).map(n => n.id);
9 | const path = parentIds.reverse().concat(nodeId);
10 | const rootArrayElementIds = ["1"];
11 | const edgesMap = new Map();
12 |
13 | for (const edge of edges) {
14 | if (!edgesMap.has(edge.from!)) {
15 | edgesMap.set(edge.from!, []);
16 | }
17 | edgesMap.get(edge.from!).push(edge.to);
18 | }
19 |
20 | for (let i = 1; i < edges.length; i++) {
21 | const curNodeId = edges[i].from!;
22 |
23 | if (rootArrayElementIds.includes(curNodeId)) continue;
24 | if (!edgesMap.has(curNodeId)) {
25 | rootArrayElementIds.push(curNodeId);
26 | }
27 | }
28 |
29 | if (rootArrayElementIds.length > 1) {
30 | resolvedPath += `Root[${rootArrayElementIds.findIndex(id => id === path[0])}]`;
31 | } else {
32 | resolvedPath += "{Root}";
33 | }
34 |
35 | for (let i = 1; i < path.length; i++) {
36 | const curId = path[i];
37 | const curNode = nodes[+curId - 1];
38 |
39 | if (!curNode) break;
40 | if (curNode.data?.type === "array") {
41 | resolvedPath += `.${curNode.text}`;
42 |
43 | if (i !== path.length - 1) {
44 | const toNodeId = path[i + 1];
45 | const idx = edgesMap.get(curId).indexOf(toNodeId);
46 |
47 | resolvedPath += `[${idx}]`;
48 | }
49 | }
50 |
51 | if (curNode.data?.type === "object") {
52 | resolvedPath += `.${curNode.text}`;
53 | }
54 | }
55 |
56 | return resolvedPath;
57 | }
58 |
--------------------------------------------------------------------------------
/src/store/useConfig.ts:
--------------------------------------------------------------------------------
1 | import { create } from "zustand";
2 | import { persist } from "zustand/middleware";
3 | import { ViewMode } from "src/enums/viewMode.enum";
4 | import useGraph from "../containers/Editor/components/views/GraphView/stores/useGraph";
5 |
6 | const initialStates = {
7 | darkmodeEnabled: false,
8 | collapseButtonVisible: true,
9 | childrenCountVisible: true,
10 | imagePreviewEnabled: true,
11 | liveTransformEnabled: true,
12 | gesturesEnabled: false,
13 | rulersEnabled: true,
14 | viewMode: ViewMode.Graph,
15 | };
16 |
17 | export interface ConfigActions {
18 | toggleDarkMode: (value: boolean) => void;
19 | toggleCollapseButton: (value: boolean) => void;
20 | toggleChildrenCount: (value: boolean) => void;
21 | toggleImagePreview: (value: boolean) => void;
22 | toggleLiveTransform: (value: boolean) => void;
23 | toggleGestures: (value: boolean) => void;
24 | toggleRulers: (value: boolean) => void;
25 | setViewMode: (value: ViewMode) => void;
26 | }
27 |
28 | const useConfig = create(
29 | persist(
30 | set => ({
31 | ...initialStates,
32 | toggleRulers: rulersEnabled => set({ rulersEnabled }),
33 | toggleGestures: gesturesEnabled => set({ gesturesEnabled }),
34 | toggleLiveTransform: liveTransformEnabled => set({ liveTransformEnabled }),
35 | toggleDarkMode: darkmodeEnabled => set({ darkmodeEnabled }),
36 | toggleCollapseButton: collapseButtonVisible => set({ collapseButtonVisible }),
37 | toggleChildrenCount: childrenCountVisible => set({ childrenCountVisible }),
38 | toggleImagePreview: imagePreviewEnabled => {
39 | set({ imagePreviewEnabled });
40 | useGraph.getState().setGraph();
41 | },
42 | setViewMode: viewMode => set({ viewMode }),
43 | }),
44 | {
45 | name: "config",
46 | }
47 | )
48 | );
49 |
50 | export default useConfig;
51 |
--------------------------------------------------------------------------------
/src/data/faq.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "title": "What is JSON Crack and what does it do?",
4 | "content": "JSON Crack is a versatile online tool designed to visualize and analyze various data formats, including JSON, YAML, CSV, XML and more. It transforms complex data into intuitive graphs and tree views, making it ideal for developers, data analysts, and anyone working with structured data."
5 | },
6 | {
7 | "title": "Is JSON Crack free?",
8 | "content": "Yes, JSON Crack is a free-forever open source online tool. For advanced features you may use ToDiagram.com"
9 | },
10 | {
11 | "title": "Is my data secure?",
12 | "content": "Absolutely! JSON Crack prioritizes your data privacy. When you paste or import your data into the editor, it's processed only on your browser to create the visualization without going into our servers. Your data remains completely private and is never stored anywhere unless you choose to upload it manually."
13 | },
14 | {
15 | "title": "Can I convert JSON to other formats using JSON Crack?",
16 | "content": "Yes, JSON Crack offers robust data conversion capabilities. You can easily convert JSON to YAML, XML to JSON, CSV to JSON and other popular formats."
17 | },
18 | {
19 | "title": "What kind of data formats are supported?",
20 | "content": "A wide range of data formats are supported including JSON, YAML, XML, CSV, and TOML."
21 | },
22 | {
23 | "title": "What size of data can I visualize?",
24 | "content": "It supports approximately 300 KB. It might vary depending on the complexity of the data and your hardware."
25 | },
26 | {
27 | "title": "Can I export the generated graphs?",
28 | "content": "Yes, you can export the generated graphs as PNG, JPEG, or SVG files."
29 | },
30 | {
31 | "title": "How to use VS Code extension?",
32 | "content": "You can use the VS Code extension to visualize JSON data directly in your editor. Install the extension from the VS Code marketplace and follow the instructions at extension's page."
33 | }
34 | ]
35 |
--------------------------------------------------------------------------------
/src/layout/JsonCrackLogo.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import localFont from "next/font/local";
3 | import Link from "next/link";
4 | import { Image } from "@mantine/core";
5 | import styled from "styled-components";
6 |
7 | const monaSans = localFont({
8 | src: "../assets/fonts/Mona-Sans.woff2",
9 | variable: "--mona-sans",
10 | display: "swap",
11 | fallback: ["Futura, Helvetica, sans-serif", "Tahoma, Verdana, sans-serif"],
12 | });
13 |
14 | const StyledLogoWrapper = styled.div`
15 | display: flex;
16 | align-items: center;
17 | gap: 8px;
18 | color: white;
19 | mix-blend-mode: difference;
20 | `;
21 |
22 | const StyledTitle = styled.span<{ fontSize: string }>`
23 | font-weight: 800;
24 | margin: 0;
25 | font-family: ${monaSans.style.fontFamily} !important;
26 | font-size: ${({ fontSize }) => fontSize};
27 | white-space: nowrap;
28 | z-index: 10;
29 | vertical-align: middle;
30 | `;
31 |
32 | interface LogoProps extends React.ComponentPropsWithoutRef<"div"> {
33 | fontSize?: string;
34 | hideLogo?: boolean;
35 | hideText?: boolean;
36 | }
37 |
38 | export const JSONCrackLogo = ({ fontSize = "1.2rem", hideText, hideLogo, ...props }: LogoProps) => {
39 | const [isIframe, setIsIframe] = React.useState(false);
40 |
41 | useEffect(() => {
42 | setIsIframe(window !== undefined && window.location.href.includes("widget"));
43 | }, []);
44 |
45 | return (
46 |
47 |
48 | {!hideLogo && (
49 |
57 | )}
58 | {!hideText && (
59 |
60 | JSON CRACK
61 |
62 | )}
63 |
64 |
65 | );
66 | };
67 |
--------------------------------------------------------------------------------
/src/containers/Editor/components/views/GraphView/CustomNode/TextRenderer.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { ColorSwatch } from "@mantine/core";
3 | import styled from "styled-components";
4 |
5 | const StyledRow = styled.span`
6 | display: inline-flex;
7 | align-items: center;
8 | overflow: hidden;
9 | gap: 4px;
10 | vertical-align: middle;
11 | `;
12 |
13 | const isURL = (word: string) => {
14 | const urlPattern =
15 | /^(http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)?[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?$/gm;
16 |
17 | return word.match(urlPattern);
18 | };
19 |
20 | const Linkify = (text: string) => {
21 | const addMarkup = (word: string) => {
22 | return isURL(word)
23 | ? `${word}`
24 | : word;
25 | };
26 |
27 | const words = text.split(" ");
28 | const formatedWords = words.map(w => addMarkup(w));
29 | const html = formatedWords.join(" ");
30 | return ;
31 | };
32 |
33 | interface TextRendererProps {
34 | children: string;
35 | }
36 |
37 | export const TextRenderer = ({ children }: TextRendererProps) => {
38 | const text = children?.replaceAll('"', "");
39 |
40 | if (isURL(text)) return Linkify(text);
41 |
42 | if (isColorFormat(text)) {
43 | return (
44 |
45 |
46 | {text}
47 |
48 | );
49 | }
50 | return <>{children}>;
51 | };
52 |
53 | function isColorFormat(colorString: string) {
54 | const hexCodeRegex = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/;
55 | const rgbRegex = /^rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/;
56 | const rgbaRegex = /^rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(0|1|0\.\d+)\s*\)$/;
57 |
58 | return (
59 | hexCodeRegex.test(colorString) || rgbRegex.test(colorString) || rgbaRegex.test(colorString)
60 | );
61 | }
62 |
--------------------------------------------------------------------------------
/src/containers/Editor/components/FullscreenDropzone.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Group, Text } from "@mantine/core";
3 | import { Dropzone } from "@mantine/dropzone";
4 | import toast from "react-hot-toast";
5 | import { VscCircleSlash, VscFiles } from "react-icons/vsc";
6 | import { FileFormat } from "src/enums/file.enum";
7 | import useFile from "src/store/useFile";
8 |
9 | export const FullscreenDropzone = () => {
10 | const setContents = useFile(state => state.setContents);
11 |
12 | return (
13 | toast.error(`Unable to load file ${files[0].file.name}`)}
24 | onDrop={async e => {
25 | const fileContent = await e[0].text();
26 | let fileExtension = e[0].name.split(".").pop() as FileFormat | undefined;
27 | if (!fileExtension) fileExtension = FileFormat.JSON;
28 | setContents({ contents: fileContent, format: fileExtension, hasChanges: false });
29 | }}
30 | >
31 |
39 |
40 |
41 |
42 | Upload to JSON Crack
43 |
44 |
45 | (Max file size: 4 MB)
46 |
47 |
48 |
49 |
50 |
51 | Invalid file
52 |
53 |
54 | Allowed formats are JSON, YAML, CSV, XML, TOML
55 |
56 |
57 |
58 |
59 | );
60 | };
61 |
--------------------------------------------------------------------------------
/src/containers/Modals/JPathModal/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import type { ModalProps } from "@mantine/core";
3 | import { Stack, Modal, Button, Text, Anchor, Group, TextInput } from "@mantine/core";
4 | import { JSONPath } from "jsonpath-plus";
5 | import { event as gaEvent } from "nextjs-google-analytics";
6 | import toast from "react-hot-toast";
7 | import { VscLinkExternal } from "react-icons/vsc";
8 | import useFile from "src/store/useFile";
9 | import useJson from "src/store/useJson";
10 |
11 | export const JPathModal = ({ opened, onClose }: ModalProps) => {
12 | const getJson = useJson(state => state.getJson);
13 | const setContents = useFile(state => state.setContents);
14 | const [query, setQuery] = React.useState("");
15 |
16 | const evaluteJsonPath = () => {
17 | try {
18 | const json = getJson();
19 | const result = JSONPath({ path: query, json: JSON.parse(json) });
20 |
21 | setContents({ contents: JSON.stringify(result, null, 2) });
22 | gaEvent("run_json_path");
23 | onClose();
24 | } catch (error) {
25 | if (error instanceof Error) toast.error(error.message);
26 | }
27 | };
28 |
29 | return (
30 |
31 |
32 |
33 | JsonPath expressions always refer to a JSON structure in the same way as XPath expression
34 | are used in combination with an XML document. The "root member object" in
35 | JsonPath is always referred to as $ regardless if it is an object or array.
36 |
37 |
42 | Read documentation.
43 |
44 |
45 | setQuery(e.currentTarget.value)}
48 | placeholder="Enter JSON Path..."
49 | data-autofocus
50 | />
51 |
52 |
55 |
56 |
57 |
58 | );
59 | };
60 |
--------------------------------------------------------------------------------
/src/containers/Modals/NodeModal/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import type { ModalProps } from "@mantine/core";
3 | import { Modal, Stack, Text, ScrollArea, Button } from "@mantine/core";
4 | import { CodeHighlight } from "@mantine/code-highlight";
5 | import { event as gaEvent } from "nextjs-google-analytics";
6 | import { VscLock } from "react-icons/vsc";
7 | import useGraph from "src/containers/Editor/components/views/GraphView/stores/useGraph";
8 | import useModal from "src/store/useModal";
9 |
10 | const dataToString = (data: any) => {
11 | const text = Array.isArray(data) ? Object.fromEntries(data) : data;
12 | const replacer = (_: string, v: string) => {
13 | if (typeof v === "string") return v.replaceAll('"', "");
14 | return v;
15 | };
16 |
17 | return JSON.stringify(text, replacer, 2);
18 | };
19 |
20 | export const NodeModal = ({ opened, onClose }: ModalProps) => {
21 | const setVisible = useModal(state => state.setVisible);
22 | const nodeData = useGraph(state => dataToString(state.selectedNode?.text));
23 | const path = useGraph(state => state.selectedNode?.path || "");
24 |
25 | return (
26 |
27 |
28 |
29 |
30 | Content
31 |
32 |
33 |
34 |
35 |
36 |
45 |
46 | JSON Path
47 |
48 |
49 |
58 |
59 |
60 |
61 | );
62 | };
63 |
--------------------------------------------------------------------------------
/src/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import type { AppProps } from "next/app";
3 | import { createTheme, MantineProvider } from "@mantine/core";
4 | import "@mantine/core/styles.css";
5 | import "@mantine/code-highlight/styles.css";
6 | import { ThemeProvider } from "styled-components";
7 | import { NextSeo } from "next-seo";
8 | import { GoogleAnalytics } from "nextjs-google-analytics";
9 | import { Toaster } from "react-hot-toast";
10 | import GlobalStyle from "src/constants/globalStyle";
11 | import { SEO } from "src/constants/seo";
12 | import { lightTheme } from "src/constants/theme";
13 |
14 | const theme = createTheme({
15 | autoContrast: true,
16 | fontSmoothing: false,
17 | respectReducedMotion: true,
18 | cursorType: "pointer",
19 | fontFamily:
20 | 'system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji"',
21 | defaultGradient: {
22 | from: "#388cdb",
23 | to: "#0f037f",
24 | deg: 180,
25 | },
26 | primaryShade: 8,
27 | colors: {
28 | brightBlue: [
29 | "#e6f2ff",
30 | "#cee1ff",
31 | "#9bc0ff",
32 | "#649dff",
33 | "#3980fe",
34 | "#1d6dfe",
35 | "#0964ff",
36 | "#0054e4",
37 | "#004acc",
38 | "#003fb5",
39 | ],
40 | },
41 | radius: {
42 | lg: "12px",
43 | },
44 | components: {
45 | Button: {
46 | defaultProps: {
47 | fw: 500,
48 | },
49 | },
50 | },
51 | });
52 |
53 | const IS_PROD = process.env.NODE_ENV === "production";
54 |
55 | function JsonCrack({ Component, pageProps }: AppProps) {
56 | return (
57 | <>
58 |
59 |
60 |
61 |
76 |
77 | {IS_PROD && }
78 |
79 |
80 |
81 | >
82 | );
83 | }
84 |
85 | export default JsonCrack;
86 |
--------------------------------------------------------------------------------
/src/lib/utils/jsonAdapter.ts:
--------------------------------------------------------------------------------
1 | import { FileFormat } from "src/enums/file.enum";
2 |
3 | const keyExists = (obj: object, key: string) => {
4 | if (!obj || (typeof obj !== "object" && !Array.isArray(obj))) {
5 | return false;
6 | } else if (obj.hasOwnProperty(key)) {
7 | return obj[key];
8 | } else if (Array.isArray(obj)) {
9 | for (let i = 0; i < obj.length; i++) {
10 | const result = keyExists(obj[i], key);
11 |
12 | if (result) {
13 | return result;
14 | }
15 | }
16 | } else {
17 | for (const k in obj) {
18 | const result = keyExists(obj[k], key);
19 |
20 | if (result) {
21 | return result;
22 | }
23 | }
24 | }
25 |
26 | return false;
27 | };
28 |
29 | const contentToJson = async (value: string, format = FileFormat.JSON): Promise