├── .dockerignore ├── .env ├── .eslintignore ├── .eslintrc.json ├── .gemini └── settings.json ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── actions │ └── install-dependencies │ │ └── action.yml └── workflows │ ├── build-aliyun-docker-image.yml │ ├── delete-workflow-runs.yml │ ├── e2e-cleanup.yml │ ├── e2e-tests.yml │ └── unit-tests.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .rgignore ├── .vscode └── launch.json ├── Dockerfile ├── GEMINI.md ├── LICENSE ├── README.md ├── __tests__ ├── color.test.ts ├── compare.test.ts ├── date.test.ts ├── fixtures │ ├── compare_data1.txt │ ├── compare_data2.txt │ ├── complex.txt │ ├── currency2region.txt │ ├── json_path.txt │ ├── nest.txt │ ├── region_and_currency.csv │ ├── region_and_currency1.txt │ └── region_and_currency2.txt ├── format.bench.ts ├── format.test.ts ├── jsonpath.test.ts ├── layout.test.ts ├── parse.test.ts ├── parseAndFormat.test.ts ├── table.test.ts ├── tree.test.ts ├── urltojson.test.ts └── utils.ts ├── auto-imports.d.ts ├── components.json ├── e2e ├── helpers │ └── utils.ts └── tests │ ├── commands.spec.ts │ ├── editor.spec.ts │ ├── graph.spec.ts │ ├── home.spec.ts │ ├── sidenav.spec.ts │ ├── statusbar.spec.ts │ ├── table.spec.ts │ └── tutorial.spec.ts ├── messages ├── en.json └── zh.json ├── next.config.mjs ├── package.json ├── playwright.config.ts ├── pnpm-lock.yaml ├── postcss.config.mjs ├── public ├── ads.txt ├── example │ ├── compare.webp │ ├── drag-drop.webp │ ├── graph.webp │ ├── import-csv.webp │ ├── jq.gif │ ├── json-path.webp │ ├── json4u.webp │ ├── nest-parse.webp │ ├── table.webp │ ├── url-to-json.webp │ ├── validate-in-tutorial.webp │ └── validate.webp └── jq │ └── 1.7 │ ├── jq.js │ └── jq.wasm ├── sentry.client.config.ts ├── sentry.edge.config.ts ├── sentry.server.config.ts ├── src ├── app │ ├── (home) │ │ ├── (mdx) │ │ │ ├── MdxPage.tsx │ │ │ ├── changelog │ │ │ │ ├── en.mdx │ │ │ │ ├── page.tsx │ │ │ │ └── zh.mdx │ │ │ ├── layout.tsx │ │ │ ├── privacy │ │ │ │ ├── en.mdx │ │ │ │ ├── page.tsx │ │ │ │ └── zh.mdx │ │ │ ├── terms │ │ │ │ ├── en.mdx │ │ │ │ ├── page.tsx │ │ │ │ └── zh.mdx │ │ │ └── tutorial │ │ │ │ ├── Refer.tsx │ │ │ │ ├── compare │ │ │ │ ├── en.mdx │ │ │ │ ├── page.tsx │ │ │ │ └── zh.mdx │ │ │ │ ├── csv │ │ │ │ ├── en.mdx │ │ │ │ ├── page.tsx │ │ │ │ └── zh.mdx │ │ │ │ ├── en.mdx │ │ │ │ ├── escape │ │ │ │ ├── en.mdx │ │ │ │ ├── page.tsx │ │ │ │ └── zh.mdx │ │ │ │ ├── format │ │ │ │ ├── en.mdx │ │ │ │ ├── page.tsx │ │ │ │ └── zh.mdx │ │ │ │ ├── jq │ │ │ │ ├── en.mdx │ │ │ │ ├── page.tsx │ │ │ │ └── zh.mdx │ │ │ │ ├── json-path │ │ │ │ ├── en.mdx │ │ │ │ ├── page.tsx │ │ │ │ └── zh.mdx │ │ │ │ ├── minify │ │ │ │ ├── en.mdx │ │ │ │ ├── page.tsx │ │ │ │ └── zh.mdx │ │ │ │ ├── page.tsx │ │ │ │ ├── python-dict-to-json │ │ │ │ ├── en.mdx │ │ │ │ ├── page.tsx │ │ │ │ └── zh.mdx │ │ │ │ ├── sort │ │ │ │ ├── en.mdx │ │ │ │ ├── page.tsx │ │ │ │ └── zh.mdx │ │ │ │ ├── url-to-json │ │ │ │ ├── en.mdx │ │ │ │ ├── page.tsx │ │ │ │ └── zh.mdx │ │ │ │ └── zh.mdx │ │ ├── layout.tsx │ │ ├── login-error │ │ │ └── page.tsx │ │ ├── login │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ │ └── page.tsx │ ├── actions.ts │ ├── api │ │ ├── auth │ │ │ └── callback │ │ │ │ └── route.ts │ │ └── billing │ │ │ └── webhook │ │ │ └── route.ts │ ├── apple-icon.png │ ├── editor │ │ ├── layout.tsx │ │ └── page.tsx │ ├── global-error.tsx │ ├── globals.css │ ├── icon.svg │ ├── layout.tsx │ ├── not-found.tsx │ ├── robots.ts │ └── sitemap.ts ├── components │ ├── AccountPanel.tsx │ ├── Background.tsx │ ├── Container.tsx │ ├── LinkButton.tsx │ ├── Loading.tsx │ ├── LoadingButton.tsx │ ├── LogoutButton.tsx │ ├── Section.tsx │ ├── UserAvatar.tsx │ ├── icons │ │ ├── CircleCheck.tsx │ │ ├── CircleX.tsx │ │ ├── GitHub.tsx │ │ ├── Google.tsx │ │ ├── Logo.tsx │ │ ├── Twitter.tsx │ │ └── Weibo.tsx │ └── ui │ │ ├── LoadingIcon.tsx │ │ ├── accordion.tsx │ │ ├── avatar.tsx │ │ ├── badge.tsx │ │ ├── breadcrumb.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── checkbox.tsx │ │ ├── collapsible.tsx │ │ ├── command.tsx │ │ ├── dialog.tsx │ │ ├── form.tsx │ │ ├── input-otp.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── popover.tsx │ │ ├── resizable.tsx │ │ ├── search │ │ ├── CommandSearchInput.tsx │ │ ├── SearchInput.tsx │ │ └── ViewSearchInput.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── sonner.tsx │ │ ├── switch.tsx │ │ ├── tabs.tsx │ │ ├── toggle.tsx │ │ ├── tooltip.tsx │ │ ├── truncate.tsx │ │ └── typography.tsx ├── containers │ ├── editor │ │ ├── components │ │ │ ├── CollapseHint.tsx │ │ │ ├── FullScreenButton.tsx │ │ │ ├── InitialSetup.tsx │ │ │ ├── LeftPanelButtons.tsx │ │ │ ├── RightPanelButtons.tsx │ │ │ ├── StatusBar.tsx │ │ │ ├── ViewTabs.tsx │ │ │ └── index.ts │ │ ├── editor │ │ │ ├── Editor.tsx │ │ │ ├── data.ts │ │ │ └── index.tsx │ │ ├── graph │ │ │ ├── EditableText.tsx │ │ │ ├── Graph.tsx │ │ │ ├── Handle.tsx │ │ │ ├── KV.tsx │ │ │ ├── MouseButton.tsx │ │ │ ├── Node.tsx │ │ │ ├── Popover.tsx │ │ │ ├── Toolbar.tsx │ │ │ ├── useClickNode.ts │ │ │ ├── useViewportChange.ts │ │ │ └── useVirtualGraph.ts │ │ ├── hooks │ │ │ └── useObserveResize.ts │ │ ├── mode │ │ │ ├── InputBox.tsx │ │ │ ├── JqInput.tsx │ │ │ ├── JsonPathInput.tsx │ │ │ ├── ModePanel.tsx │ │ │ └── SwapButton.tsx │ │ ├── panels │ │ │ ├── LeftPanel.tsx │ │ │ ├── MainPanel.tsx │ │ │ └── RightPanel.tsx │ │ ├── sidenav │ │ │ ├── AccountButton.tsx │ │ │ ├── BasePopover.tsx │ │ │ ├── Button.tsx │ │ │ ├── ExportPopover.tsx │ │ │ ├── FileTypeSelect.tsx │ │ │ ├── IconLabel.tsx │ │ │ ├── ImportPopover.tsx │ │ │ ├── LinkButton.tsx │ │ │ ├── PopoverButton.tsx │ │ │ ├── SharePopover.tsx │ │ │ ├── StatisticsPopover.tsx │ │ │ ├── Toggle.tsx │ │ │ └── index.tsx │ │ └── table │ │ │ ├── Cell.tsx │ │ │ ├── Table.tsx │ │ │ ├── useOnResize.ts │ │ │ ├── useRevealNode.ts │ │ │ └── useTableGrid.ts │ ├── landing │ │ ├── FAQ.tsx │ │ ├── Features.tsx │ │ ├── Footer.tsx │ │ ├── Header.tsx │ │ └── HeroTitle.tsx │ ├── login │ │ ├── EmailForm.tsx │ │ ├── OAuthButton.tsx │ │ └── useRedirectTo.ts │ └── pricing │ │ ├── CtaButton.tsx │ │ ├── Description.zh.tsx │ │ ├── Pricing.tsx │ │ ├── PricingOverlay.tsx │ │ ├── index.tsx │ │ └── pricing.module.css ├── global.d.ts ├── i18n │ └── request.tsx ├── instrumentation.ts ├── lib │ ├── color │ │ └── index.ts │ ├── compare │ │ ├── compare.ts │ │ ├── diff.ts │ │ ├── histogram.ts │ │ ├── index.ts │ │ └── myers.ts │ ├── date │ │ └── index.ts │ ├── db │ │ └── config.ts │ ├── editor │ │ ├── cdn.ts │ │ ├── comparer.ts │ │ ├── diffColor.ts │ │ ├── editor.ts │ │ ├── handler │ │ │ ├── hoverProvider.ts │ │ │ └── inlayHintsProvider.ts │ │ └── types.d.ts │ ├── env.ts │ ├── format │ │ ├── pretty.ts │ │ └── text.ts │ ├── graph │ │ ├── actions.ts │ │ ├── layout.ts │ │ ├── style.ts │ │ ├── types.ts │ │ ├── utils.ts │ │ └── virtual.ts │ ├── hooks.ts │ ├── idgen │ │ ├── index.ts │ │ └── pointer.ts │ ├── jq │ │ └── index.ts │ ├── parser │ │ ├── index.ts │ │ ├── node.ts │ │ ├── parse.ts │ │ └── tree.ts │ ├── preview │ │ ├── base64.ts │ │ ├── color.ts │ │ ├── date.ts │ │ ├── image.ts │ │ ├── index.ts │ │ ├── jwt.ts │ │ ├── types.ts │ │ ├── unicode.ts │ │ ├── uri.ts │ │ ├── url.ts │ │ └── utils.ts │ ├── shop │ │ ├── base.ts │ │ ├── subscription.ts │ │ ├── subscriptionInvoice.ts │ │ ├── subscriptionItem.ts │ │ ├── types.ts │ │ └── webhookRequest.ts │ ├── supabase │ │ ├── client.ts │ │ ├── database.types.ts │ │ ├── server.ts │ │ └── table.types.ts │ ├── table │ │ ├── builder.ts │ │ ├── createNode.ts │ │ ├── style.ts │ │ ├── tableNode.ts │ │ ├── tag.ts │ │ ├── types.ts │ │ └── utils.ts │ ├── utils.ts │ └── worker │ │ ├── command │ │ ├── compare.ts │ │ ├── csv.ts │ │ ├── escape.ts │ │ ├── jsonPath.ts │ │ ├── parse.ts │ │ ├── pythonDictToJSON.ts │ │ └── urlToJSON.ts │ │ ├── stores │ │ ├── types.ts │ │ └── viewStore.ts │ │ └── worker.ts ├── mdx-components.tsx ├── middleware.ts └── stores │ ├── editorStore.ts │ ├── hook.tsx │ ├── statusStore.ts │ ├── treeStore.ts │ └── userStore.ts ├── tailwind.config.ts ├── tsconfig.json └── vitest.config.ts /.dockerignore: -------------------------------------------------------------------------------- 1 | __tests__ 2 | Dockerfile 3 | .dockerignore 4 | node_modules 5 | npm-debug.log 6 | LICENSE 7 | README.md 8 | tsconfig.tsbuildinfo 9 | .next 10 | !.next/static 11 | !.next/standalone 12 | .git -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_APP_URL="http://localhost.json4u.com:3000" 2 | NEXT_PUBLIC_FREE_QUOTA='{"graphModeView":30,"tableModeView":30,"textComparison":30,"jqExecutions":30}' 3 | NEXT_PUBLIC_SUPABASE_URL=https://kfuwzghygbtmonplcuou.supabase.co 4 | NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImtmdXd6Z2h5Z2J0bW9ucGxjdW91Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3MjU5NTQ3ODIsImV4cCI6MjA0MTUzMDc4Mn0.c5Eu4Gyy4rpnwa349KrNqWpO-LCcyxKapMFk_5BjKo8 5 | NEXT_PUBLIC_HCAPTCHA_SITE_KEY=c87e1d8c-e81c-4dbc-a540-60246482751a 6 | 7 | SUPABASE_PROJECT_ID=kfuwzghygbtmonplcuou 8 | SUPABASE_KEY=a 9 | LEMONSQUEEZY_STORE_ID=1 10 | LEMONSQUEEZY_SUBSCRIPTION_VARIANT_MAP='{"monthly":1,"yearly":2}' 11 | LEMONSQUEEZY_WEBHOOK_SECRET=a 12 | LEMONSQUEEZY_API_KEY=a 13 | SENTRY_ORG=loggerhead 14 | SENTRY_PROJECT=json4u 15 | SENTRY_DSN=https://d60bd8847a6d8afc72e3de0d9288fa4c@o4506325094236160.ingest.us.sentry.io/4506325157085184 16 | SENTRY_AUTH_TOKEN= -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | src/lib/utils.ts 2 | src/lib/supabase/database.types.ts 3 | src/components/ui/ 4 | sentry.client.config.ts 5 | pnpm-lock.yaml -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "@next/next/no-img-element": "off", 4 | "no-unused-vars": "off", 5 | "unused-imports/no-unused-imports": "error", 6 | "import/no-cycle": "warn", 7 | "no-empty": "error", 8 | "no-multiple-empty-lines": "error", 9 | "no-irregular-whitespace": "error", 10 | "strict": ["error", "never"], 11 | "linebreak-style": ["error", "unix"], 12 | "quotes": ["error", "double", { "avoidEscape": true }], 13 | "prefer-const": "error", 14 | "react-hooks/exhaustive-deps": "off", 15 | // Avoid hardcoded labels in component markup 16 | "react/jsx-no-literals": "warn" 17 | }, 18 | "extends": ["next/core-web-vitals"], 19 | "plugins": ["unused-imports"] 20 | } 21 | -------------------------------------------------------------------------------- /.gemini/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "playwright": { 4 | "command": "npx", 5 | "args": ["@playwright/mcp@latest"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG] " 5 | labels: bug 6 | assignees: loggerhead 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **Steps To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. See error 18 | 19 | **Minimal data for reproduction** 20 | Upload or paste the data needed for reproduction here. 21 | 22 | **Screenshots** 23 | If applicable, add screenshots to help explain your problem. 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[Feature]" 5 | labels: enhancement 6 | assignees: loggerhead 7 | 8 | --- 9 | 10 | **Describe the feature you'd like to request** 11 | A clear and concise description of what you want and what your use case is. 12 | 13 | **Describe similar features you've seen** 14 | A clear and concise description of any similar features you've seen. If you could provide screenshots of them, that would be great. 15 | -------------------------------------------------------------------------------- /.github/actions/install-dependencies/action.yml: -------------------------------------------------------------------------------- 1 | name: Install dependencies 2 | description: Setup node/pnpm + install dependencies 3 | runs: 4 | using: composite 5 | steps: 6 | - name: Install pnpm 7 | uses: pnpm/action-setup@v4 8 | - uses: actions/setup-node@v4 9 | with: 10 | node-version: lts/* 11 | cache: pnpm 12 | - name: Install dependencies 13 | shell: bash 14 | run: pnpm install --frozen-lockfile --prefer-offline -------------------------------------------------------------------------------- /.github/workflows/build-aliyun-docker-image.yml: -------------------------------------------------------------------------------- 1 | name: Build Aliyun Docker Image 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: 6 | - main 7 | 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.ref_name }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v4 18 | - name: Set variables 19 | run: | 20 | VER=$(grep '"version"' package.json | cut -d '"' -f 4) 21 | echo "VERSION=$VER" >> $GITHUB_ENV 22 | - name: Login to ACR 23 | uses: aliyun/acr-login@v1 24 | with: 25 | login-server: https://registry.cn-heyuan.aliyuncs.com 26 | username: "${{ secrets.ACR_REGISTRY_USERNAME }}" 27 | password: "${{ secrets.ACR_REGISTRY_PASSWORD }}" 28 | - name: Build and push image 29 | run: | 30 | IMAGE_NAME=registry.cn-heyuan.aliyuncs.com/json4u/json4u 31 | docker build . \ 32 | --build-arg APP_URL=${{ secrets.ALIYUN_APP_URL }} \ 33 | --build-arg FREE_QUOTA=${{ secrets.ALIYUN_FREE_QUOTA }} \ 34 | --build-arg SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} \ 35 | --tag $IMAGE_NAME:${{ env.VERSION }} 36 | docker tag $IMAGE_NAME:${{ env.VERSION }} $IMAGE_NAME:latest 37 | docker push $IMAGE_NAME:latest 38 | -------------------------------------------------------------------------------- /.github/workflows/e2e-cleanup.yml: -------------------------------------------------------------------------------- 1 | name: 'Delete e2e reports' 2 | 3 | on: 4 | delete: 5 | branches-ignore: [main, gh-pages] 6 | 7 | permissions: 8 | contents: write 9 | issues: write 10 | pull-requests: write 11 | 12 | # ensures that currently running Playwright workflow of deleted branch gets cancelled 13 | concurrency: 14 | group: ${{ github.event.ref }} 15 | cancel-in-progress: true 16 | 17 | jobs: 18 | delete_reports: 19 | name: Delete Reports 20 | runs-on: ubuntu-latest 21 | env: 22 | # Contains all reports for deleted branch 23 | BRANCH_REPORTS_DIR: reports/${{ github.event.ref }} 24 | steps: 25 | - name: Checkout GitHub Pages Branch 26 | uses: actions/checkout@v2 27 | with: 28 | repository: calcom/test-results 29 | ref: gh-pages 30 | token: ${{ secrets.GH_ACCESS_TOKEN }} 31 | - name: Set Git User 32 | # see: https://github.com/actions/checkout/issues/13#issuecomment-724415212 33 | run: | 34 | git config --global user.name "github-actions[bot]" 35 | git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" 36 | - name: Check for workflow reports 37 | run: | 38 | if [ -z "$(ls -A $BRANCH_REPORTS_DIR)" ]; then 39 | echo "BRANCH_REPORTS_EXIST="false"" >> $GITHUB_ENV 40 | else 41 | echo "BRANCH_REPORTS_EXIST="true"" >> $GITHUB_ENV 42 | fi 43 | - name: Delete reports from repo for branch 44 | if: ${{ env.BRANCH_REPORTS_EXIST == 'true' }} 45 | timeout-minutes: 3 46 | run: | 47 | cd $BRANCH_REPORTS_DIR/.. 48 | 49 | rm -rf ${{ github.event.ref }} 50 | git add . 51 | git commit -m "workflow: remove all reports for branch ${{ github.event.ref }}" 52 | 53 | while true; do 54 | git pull --rebase 55 | if [ $? -ne 0 ]; then 56 | echo "Failed to rebase. Please review manually." 57 | exit 1 58 | fi 59 | 60 | git push 61 | if [ $? -eq 0 ]; then 62 | echo "Successfully pushed HTML reports to repo." 63 | exit 0 64 | fi 65 | done -------------------------------------------------------------------------------- /.github/workflows/unit-tests.yml: -------------------------------------------------------------------------------- 1 | name: 'Unit Tests' 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches-ignore: [ gh-pages ] 6 | pull_request: 7 | branches: [main] 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.ref_name }} 11 | cancel-in-progress: true 12 | jobs: 13 | test: 14 | runs-on: ubuntu-latest 15 | permissions: 16 | # Required to checkout the code 17 | contents: read 18 | # Required to put a comment into the pull-request 19 | pull-requests: write 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v4 23 | - name: Install dependencies 24 | uses: ./.github/actions/install-dependencies 25 | - name: 'Test' 26 | run: pnpm test 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | upload-monaco-editor.sh 2 | .trae 3 | e2e/foo.spec.ts 4 | *.report.html 5 | tsconfig.tsbuildinfo 6 | public/sw.js* 7 | .vscode 8 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 9 | runner-results/ 10 | cypress/screenshots/ 11 | cypress/parallel-weights.json 12 | multi-reporter-config.json 13 | .idea/ 14 | dist/ 15 | 16 | # dependencies 17 | /node_modules 18 | /.pnp 19 | .pnp.js 20 | .yarn/install-state.gz 21 | 22 | # testing 23 | coverage/ 24 | auto-imports.d.ts 25 | 26 | # next.js 27 | /.next/ 28 | /out/ 29 | 30 | # production 31 | /build 32 | 33 | # misc 34 | .DS_Store 35 | *.pem 36 | 37 | # debug 38 | npm-debug.log* 39 | yarn-debug.log* 40 | yarn-error.log* 41 | 42 | # local env files 43 | .env.local 44 | .env.development 45 | .env.production 46 | 47 | # vercel 48 | .vercel 49 | 50 | # typescript 51 | *.tsbuildinfo 52 | next-env.d.ts 53 | 54 | # Sentry Config File 55 | .sentryclirc 56 | 57 | # Million Lint 58 | .million 59 | 60 | # Sentry Config File 61 | .env.sentry-build-plugin 62 | node_modules/ 63 | /test-results/ 64 | /playwright-report/ 65 | /blob-report/ 66 | /playwright/.cache/ 67 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | messages/ 2 | .github 3 | .next 4 | node_modules/ 5 | out 6 | public 7 | *-lock.json 8 | src/lib/utils.ts 9 | src/lib/supabase/database.types.ts 10 | src/components/ui/ 11 | sentry.client.config.ts 12 | pnpm-lock.yaml -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "singleQuote": false, 4 | "semi": true, 5 | "printWidth": 120, 6 | "tabWidth": 2, 7 | "arrowParens": "always", 8 | "importOrder": ["^(react/(.*)$)|^(react$)", "^(next/(.*)$)|^(next$)", "", "^src/(.*)$", "^[./]"], 9 | "importOrderParserPlugins": ["typescript", "jsx", "decorators-legacy"], 10 | "plugins": ["@trivago/prettier-plugin-sort-imports"] 11 | } 12 | -------------------------------------------------------------------------------- /.rgignore: -------------------------------------------------------------------------------- 1 | *.txt 2 | public/ 3 | yarn.lock 4 | messages/ 5 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 3 | "version": "0.2.0", 4 | "configurations": [ 5 | { 6 | "type": "node", 7 | "request": "launch", 8 | "name": "Debug Current Test File", 9 | "autoAttachChildProcesses": true, 10 | "skipFiles": [ 11 | "/**", 12 | "**/node_modules/**" 13 | ], 14 | "program": "${workspaceRoot}/node_modules/vitest/vitest.mjs", 15 | "args": [ 16 | "run", 17 | "${relativeFile}" 18 | ], 19 | "smartStep": true, 20 | "console": "integratedTerminal" 21 | } 22 | ] 23 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # build stage 2 | FROM node:20-alpine AS base 3 | 4 | # Install dependencies only when needed 5 | FROM base AS deps 6 | # Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. 7 | RUN apk add --no-cache libc6-compat 8 | WORKDIR /app 9 | 10 | # Install dependencies based on the preferred package manager 11 | COPY package.json pnpm-lock.yaml* ./ 12 | RUN corepack enable pnpm && pnpm i --frozen-lockfile 13 | 14 | # Rebuild the source code only when needed 15 | FROM base AS builder 16 | WORKDIR /app 17 | COPY --from=deps /app/node_modules ./node_modules 18 | COPY . . 19 | 20 | ARG APP_URL=http://localhost.json4u.cn:3000 21 | ARG FREE_QUOTA=99 22 | ARG SENTRY_AUTH_TOKEN= 23 | 24 | ENV SENTRY_AUTH_TOKEN=$SENTRY_AUTH_TOKEN 25 | ENV NEXT_TELEMETRY_DISABLED=1 26 | ENV NODE_ENV=production 27 | ENV NEXT_PUBLIC_APP_URL=$APP_URL 28 | ENV NEXT_PUBLIC_FREE_QUOTA="{\"graphModeView\":$FREE_QUOTA,\"tableModeView\":$FREE_QUOTA,\"textComparison\":$FREE_QUOTA,\"jqExecutions\":$FREE_QUOTA}" 29 | 30 | RUN corepack enable pnpm && pnpm run build 31 | 32 | # Production image, copy all the files and run next 33 | FROM base AS runner 34 | WORKDIR /app 35 | 36 | RUN addgroup --system --gid 1001 nodejs 37 | RUN adduser --system --uid 1001 nextjs 38 | 39 | COPY --from=builder /app/public ./public 40 | 41 | # Set the correct permission for prerender cache 42 | RUN mkdir .next 43 | RUN chown nextjs:nodejs .next 44 | 45 | # Automatically leverage output traces to reduce image size 46 | # https://nextjs.org/docs/advanced-features/output-file-tracing 47 | COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ 48 | COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static 49 | 50 | USER nextjs 51 | 52 | EXPOSE 3000 53 | 54 | ENV PORT=3000 55 | 56 | # server.js is created by next build from the standalone output 57 | # https://nextjs.org/docs/pages/api-reference/next-config-js/output 58 | ENV HOSTNAME="0.0.0.0" 59 | CMD ["node", "server.js"] -------------------------------------------------------------------------------- /__tests__/color.test.ts: -------------------------------------------------------------------------------- 1 | import { convertColor } from "@/lib/color/index"; 2 | 3 | describe("convertColor", () => { 4 | test("converts 3-digit hex color to rgb and hsl", () => { 5 | const result = convertColor("#f00"); 6 | expect(result).toEqual({ 7 | hex: "#ff0000", 8 | rgb: "rgb(255, 0, 0)", 9 | hsl: "hsl(0, 100%, 50%)", 10 | }); 11 | }); 12 | 13 | test("converts 6-digit hex color with alpha channel", () => { 14 | const result = convertColor("#00ff0080"); 15 | expect(result).toEqual({ 16 | hex: "#00ff0080", 17 | rgb: "rgba(0, 255, 0, 0.5)", 18 | hsl: "hsla(120, 100%, 50%, 0.5)", 19 | }); 20 | }); 21 | 22 | test("converts rgb color to hex and hsl", () => { 23 | const result = convertColor("rgb(0, 255, 0)"); 24 | expect(result).toEqual({ 25 | hex: "#00ff00", 26 | rgb: "rgb(0, 255, 0)", 27 | hsl: "hsl(120, 100%, 50%)", 28 | }); 29 | }); 30 | 31 | test("converts rgba color with alpha channel", () => { 32 | const result = convertColor("rgba(0, 255, 0, 0.5)"); 33 | expect(result).toEqual({ 34 | hex: "#00ff0080", 35 | rgb: "rgba(0, 255, 0, 0.5)", 36 | hsl: "hsla(120, 100%, 50%, 0.5)", 37 | }); 38 | }); 39 | 40 | test("converts hsl color to hex and rgb", () => { 41 | const result = convertColor("hsl(240, 100%, 50%)"); 42 | expect(result).toEqual({ 43 | hex: "#0000ff", 44 | rgb: "rgb(0, 0, 255)", 45 | hsl: "hsl(240, 100%, 50%)", 46 | }); 47 | }); 48 | 49 | test("converts hsla color with alpha channel", () => { 50 | const result = convertColor("hsla(240, 100%, 50%, 0.5)"); 51 | expect(result).toEqual({ 52 | hex: "#0000ff80", 53 | rgb: "rgba(0, 0, 255, 0.5)", 54 | hsl: "hsla(240, 100%, 50%, 0.5)", 55 | }); 56 | }); 57 | 58 | test("returns null for invalid color format", () => { 59 | const result = convertColor("invalid-color"); 60 | expect(result).toBeUndefined(); 61 | }); 62 | 63 | test("returns null for invalid hex color", () => { 64 | const result = convertColor("#ggg"); 65 | expect(result).toBeUndefined(); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /__tests__/date.test.ts: -------------------------------------------------------------------------------- 1 | import { genDate } from "@/lib/date/index"; 2 | 3 | describe("genDate", () => { 4 | test("parses valid ISO date string", () => { 5 | const date = genDate("2023-10-05"); 6 | expect(date.getFullYear()).toBe(2023); 7 | expect(date.getMonth()).toBe(9); 8 | expect(date.getDate()).toBe(5); 9 | }); 10 | 11 | test("parses ISO date with time", () => { 12 | const date = genDate("2023-10-05T14:30:00"); 13 | expect(date.getHours()).toBe(14); 14 | expect(date.getMinutes()).toBe(30); 15 | expect(date.getSeconds()).toBe(0); 16 | }); 17 | 18 | test("parses date with different separators", () => { 19 | const date = genDate("2023/10/05"); 20 | expect(date.getFullYear()).toBe(2023); 21 | expect(date.getMonth()).toBe(9); 22 | expect(date.getDate()).toBe(5); 23 | }); 24 | 25 | test("handles invalid date string", () => { 26 | const date = genDate("invalid-date"); 27 | expect(date.getTime()).toBeNaN(); 28 | }); 29 | 30 | test("handles empty string input", () => { 31 | const date = genDate(""); 32 | expect(date.getTime()).toBeNaN(); 33 | }); 34 | 35 | test("parses date with timezone offset", () => { 36 | const date = genDate("2023-10-05T14:30:00+02:30"); 37 | expect(date.getUTCHours()).toBe(12); 38 | expect(date.getMinutes()).toBe(0); 39 | }); 40 | 41 | test("parses a 10-digit timestamp (seconds)", () => { 42 | const date = genDate("1672531200"); // 2023-01-01 00:00:00 UTC 43 | expect(date.getUTCFullYear()).toBe(2023); 44 | expect(date.getUTCMonth()).toBe(0); 45 | expect(date.getUTCDate()).toBe(1); 46 | expect(date.getUTCHours()).toBe(0); 47 | expect(date.getUTCMinutes()).toBe(0); 48 | expect(date.getUTCSeconds()).toBe(0); 49 | }); 50 | 51 | test("parses a 13-digit timestamp (milliseconds)", () => { 52 | const date = genDate("1672531200000"); // 2023-01-01 00:00:00 UTC 53 | expect(date.getUTCFullYear()).toBe(2023); 54 | expect(date.getUTCMonth()).toBe(0); 55 | expect(date.getUTCDate()).toBe(1); 56 | expect(date.getUTCHours()).toBe(0); 57 | expect(date.getUTCMinutes()).toBe(0); 58 | expect(date.getUTCSeconds()).toBe(0); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /__tests__/fixtures/compare_data1.txt: -------------------------------------------------------------------------------- 1 | { 2 | "Aidan Gillen": { 3 | "array": [ 4 | "Game of Thron\"es", 5 | "The Wire" 6 | ], 7 | "string": "some string", 8 | "int": 2, 9 | "aboolean": true, 10 | "boolean": true, 11 | "null": null, 12 | "a_null": null, 13 | "another_null": "null check", 14 | "object": { 15 | "foo": "bar", 16 | "object1": { 17 | "new prop1": "new prop value" 18 | }, 19 | "object2": { 20 | "new prop1": "new prop value" 21 | }, 22 | "object3": { 23 | "new prop1": "new prop value" 24 | }, 25 | "object4": { 26 | "new prop1": "new prop value" 27 | } 28 | } 29 | }, 30 | "Amy Ryan": { 31 | "one": "In Treatment", 32 | "two": "The Wire" 33 | }, 34 | "Annie Fitzgerald": [ 35 | "Big Love", 36 | "True Blood" 37 | ], 38 | "Anwan Glover": [ 39 | "Treme", 40 | "The Wire" 41 | ], 42 | "Alexander Skarsgard": [ 43 | "Generation Kill", 44 | "True Blood" 45 | ], 46 | "Clarke Peters": null 47 | } -------------------------------------------------------------------------------- /__tests__/fixtures/compare_data2.txt: -------------------------------------------------------------------------------- 1 | { 2 | "Aidan Gillen": { 3 | "array": [ 4 | "Game of Thrones", 5 | "The Wire" 6 | ], 7 | "string": "some string", 8 | "int": "2", 9 | "otherint": 4, 10 | "aboolean": "true", 11 | "boolean": false, 12 | "null": null, 13 | "a_null": 88, 14 | "another_null": null, 15 | "object": { 16 | "foo": "bar" 17 | } 18 | }, 19 | "Amy Ryan": [ 20 | "In Treatment", 21 | "The Wire" 22 | ], 23 | "Annie Fitzgerald": [ 24 | "True Blood", 25 | "Big Love", 26 | "The Sopranos", 27 | "Oz" 28 | ], 29 | "Anwan Glover": [ 30 | "Treme", 31 | "The Wire" 32 | ], 33 | "Alexander Skarsg?rd": [ 34 | "Generation Kill", 35 | "True Blood" 36 | ], 37 | "Alice Farmer": [ 38 | "The Corner", 39 | "Oz", 40 | "The Wire" 41 | ] 42 | } -------------------------------------------------------------------------------- /__tests__/fixtures/json_path.txt: -------------------------------------------------------------------------------- 1 | { 2 | "store": { 3 | "book": [ 4 | { 5 | "category": "reference", 6 | "author": "Nigel Rees", 7 | "title": "Sayings of the Century", 8 | "price": 8.95 9 | }, 10 | { 11 | "category": "fiction", 12 | "author": "Evelyn Waugh", 13 | "title": "Sword of Honour", 14 | "price": 12.99 15 | }, 16 | { 17 | "category": "fiction", 18 | "author": "Herman Melville", 19 | "title": "Moby Dick", 20 | "isbn": "0-553-21311-3", 21 | "price": 8.99 22 | }, 23 | { 24 | "category": "fiction", 25 | "author": "J. R. R. Tolkien", 26 | "title": "The Lord of the Rings", 27 | "isbn": "0-395-19395-8", 28 | "price": 22.99 29 | } 30 | ], 31 | "bicycle": { 32 | "color": "red", 33 | "price": 19.95 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /__tests__/fixtures/nest.txt: -------------------------------------------------------------------------------- 1 | { 2 | "a": "{\"bb\":\"{\\\"ccc\\\":321}\"}", 3 | "b": "{\"bb\":\"{\\\"ccc\\\":123}\"}", 4 | "c": "end" 5 | } -------------------------------------------------------------------------------- /__tests__/format.bench.ts: -------------------------------------------------------------------------------- 1 | import { textFormat } from "@/lib/format/text"; 2 | import { parseJSON } from "@/lib/parser"; 3 | import { readFile } from "fs/promises"; 4 | import { bench, describe } from "vitest"; 5 | 6 | const jsonString = await readFile(`${__dirname}/fixtures/complex.txt`, "utf8"); 7 | const tree = parseJSON(jsonString); 8 | 9 | describe("format", () => { 10 | assert(!tree.hasError(), "parse tree failed"); 11 | 12 | bench("pretty", () => { 13 | tree.stringify(); 14 | }); 15 | 16 | bench("simple", () => { 17 | textFormat(jsonString); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /__tests__/layout.test.ts: -------------------------------------------------------------------------------- 1 | import { genFlowNodes, Layouter } from "@/lib/graph/layout"; 2 | import { parseJSON } from "@/lib/parser/parse"; 3 | 4 | function checkNodes(jsonStr: string, nodeNum: number, edgeNum: number) { 5 | const tree = parseJSON(jsonStr); 6 | const { nodes, edges } = genFlowNodes(tree); 7 | expect(nodes.length).equals(nodeNum); 8 | expect(edges.length).equals(edgeNum); 9 | 10 | nodes.forEach((node) => { 11 | node.measured = { width: 200, height: 100 }; 12 | }); 13 | 14 | const { ordered, levelMeta } = new Layouter(tree, nodes, edges).layout(); 15 | expect(ordered.length).equals(nodeNum); 16 | expect(levelMeta.length).greaterThan(0); 17 | } 18 | 19 | describe("genFlowNodes", () => { 20 | test("value", () => { 21 | checkNodes("6", 1, 0); 22 | }); 23 | 24 | test("empty object", () => { 25 | checkNodes("{}", 1, 0); 26 | }); 27 | 28 | test("empty array", () => { 29 | checkNodes("[]", 1, 0); 30 | }); 31 | 32 | test("simple object", () => { 33 | checkNodes( 34 | `{ 35 | "int64": 12345678987654321, 36 | "key": "value", 37 | }`, 38 | 1, 39 | 0, 40 | ); 41 | }); 42 | 43 | test("object inside object", () => { 44 | checkNodes( 45 | `{ 46 | "int64": 12345678987654321, 47 | "key": "value", 48 | "array": {"a": 1, "b": 2} 49 | }`, 50 | 2, 51 | 1, 52 | ); 53 | }); 54 | 55 | test("array inside object", () => { 56 | checkNodes( 57 | `{ 58 | "int64": 12345678987654321, 59 | "key": "value", 60 | "array": [12345678987654321, 0.1234567891111111111] 61 | }`, 62 | 2, 63 | 1, 64 | ); 65 | }); 66 | 67 | test("simple array", () => { 68 | checkNodes("[12345678987654321, 0.1234567891111111111]", 1, 0); 69 | }); 70 | 71 | test("object inside array", () => { 72 | checkNodes('[{"a": 1}, {"b": 2}]', 3, 2); 73 | }); 74 | 75 | test("array inside array", () => { 76 | checkNodes("[[1, 1], [2, 2]]", 3, 2); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /__tests__/tree.test.ts: -------------------------------------------------------------------------------- 1 | import { parseJSON } from "@/lib/parser/parse"; 2 | 3 | describe("findNodeAtOffset", () => { 4 | const tree = parseJSON('{ "array": [12345678987654321, 0.1234567891111111111] }'); 5 | 6 | const expectOffset = (offset: number, id: string | undefined) => { 7 | const r = tree.findNodeAtOffset(offset); 8 | expect(r?.node?.id).toEqual(id); 9 | }; 10 | 11 | test("simple", () => { 12 | expectOffset(6, "$/array"); 13 | expectOffset(20, "$/array/0"); 14 | expectOffset(43, "$/array/1"); 15 | }); 16 | 17 | test("corner", () => { 18 | expectOffset(0, undefined); 19 | expectOffset(2, "$"); 20 | expectOffset(9, "$/array"); 21 | expectOffset(10, "$/array"); 22 | expectOffset(12, "$/array"); 23 | expectOffset(13, "$/array/0"); 24 | expectOffset(29, "$/array/0"); 25 | expectOffset(31, "$/array"); 26 | expectOffset(32, "$/array/1"); 27 | expectOffset(52, "$/array/1"); 28 | expectOffset(53, "$/array"); 29 | expectOffset(54, "$"); 30 | expectOffset(55, "$"); 31 | expectOffset(56, undefined); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /__tests__/urltojson.test.ts: -------------------------------------------------------------------------------- 1 | import { urlToJSON } from "@/lib/worker/command/urlToJSON"; 2 | 3 | describe("urlToJSON", () => { 4 | test("complex", async () => { 5 | const m = await urlToJSON("https://json4u.com/editor?a=1&a=2&b=&c=https%3A%2F%2Fjson4u.com%2Feditor%3Fc%3D1"); 6 | expect(JSON.parse(m.text)).toEqual( 7 | JSON.parse( 8 | `{ 9 | "Protocol": "https", 10 | "Host": "json4u.com", 11 | "Path": "/editor", 12 | "Query": { 13 | "a": [ 14 | "1", 15 | "2" 16 | ], 17 | "b": "", 18 | "c": { 19 | "Protocol": "https", 20 | "Host": "json4u.com", 21 | "Path": "/editor", 22 | "Query": { 23 | "c": "1" 24 | } 25 | } 26 | } 27 | }`, 28 | ), 29 | ); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /__tests__/utils.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | 4 | export function readFileIfNeed(fileNameOrText: string) { 5 | if (/(\w+\/)*\w+\.(json|txt)/.test(fileNameOrText)) { 6 | const baseDir = path.join(process.cwd(), "__tests__/fixtures"); 7 | const fullPath = path.join(baseDir, fileNameOrText); 8 | return fs.readFileSync(fullPath, "utf8"); 9 | } else { 10 | return fileNameOrText; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /auto-imports.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // @ts-nocheck 4 | // noinspection JSUnusedGlobalSymbols 5 | // Generated by unplugin-auto-import 6 | export {} 7 | declare global { 8 | const afterAll: typeof import('vitest')['afterAll'] 9 | const afterEach: typeof import('vitest')['afterEach'] 10 | const assert: typeof import('vitest')['assert'] 11 | const beforeAll: typeof import('vitest')['beforeAll'] 12 | const beforeEach: typeof import('vitest')['beforeEach'] 13 | const chai: typeof import('vitest')['chai'] 14 | const describe: typeof import('vitest')['describe'] 15 | const expect: typeof import('vitest')['expect'] 16 | const it: typeof import('vitest')['it'] 17 | const suite: typeof import('vitest')['suite'] 18 | const test: typeof import('vitest')['test'] 19 | const vi: typeof import('vitest')['vi'] 20 | const vitest: typeof import('vitest')['vitest'] 21 | } 22 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /e2e/tests/editor.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | import { clearEditor, getEditor, selectAllInEditor, writeToClipboard } from "../helpers/utils"; 3 | 4 | test.describe("edit in the editor", () => { 5 | test.beforeEach(async ({ page }) => { 6 | await getEditor(page, { goto: true, needTutorial: true }); 7 | }); 8 | 9 | test("typing will not reset the cursor", async ({ page }) => { 10 | await page.locator(".view-lines > div:nth-child(5)").getByText("Wire").first().dblclick(); 11 | await page.keyboard.press("Backspace"); 12 | await page.keyboard.type("world"); 13 | await expect(page.getByRole("treeitem").getByText("The world")).toBeVisible(); 14 | }); 15 | 16 | test("deleting all text and typing will not reset the cursor", async ({ page }) => { 17 | await clearEditor(page); 18 | await page.keyboard.type("123"); 19 | await expect(page.getByRole("treeitem").getByText("123")).toBeVisible(); 20 | await page.keyboard.type("456"); 21 | await expect(page.getByRole("treeitem").getByText("123456")).toBeVisible(); 22 | }); 23 | 24 | test("partially pasting will not reset the cursor", async ({ page }) => { 25 | await page.getByText('"string"', { exact: true }).dblclick(); 26 | await page.keyboard.press("ControlOrMeta+C"); 27 | await page.locator(".view-lines > div:nth-child(5)").getByText("Wire").first().dblclick(); 28 | await page.keyboard.press("ControlOrMeta+V"); 29 | await expect(page.getByRole("treeitem").getByText("The string")).toBeVisible(); 30 | }); 31 | 32 | test("pasting and replacing the whole text will reset the cursor", async ({ page }) => { 33 | await writeToClipboard(page, '"hello": "world"}'); 34 | await selectAllInEditor(page); 35 | await page.keyboard.press("ControlOrMeta+V"); 36 | await page.waitForTimeout(100); 37 | await page.keyboard.type("{"); 38 | await expect(page.getByRole("treeitem").getByText("hello")).toBeVisible(); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /e2e/tests/home.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | import { getEditor } from "../helpers/utils"; 3 | 4 | test.describe("Home page", () => { 5 | test.beforeEach(async ({ page }) => { 6 | await page.goto("/"); 7 | }); 8 | 9 | test("go to editor", async ({ page }) => { 10 | await page.getByRole("link", { name: "Try it now" }).click(); 11 | await getEditor(page); 12 | }); 13 | 14 | test("go to login", async ({ page }) => { 15 | await page.getByRole("link", { name: /Log in/ }).click(); 16 | await expect(page.locator("text=Login with Google")).toBeVisible(); 17 | }); 18 | 19 | test("go to terms", async ({ page }) => { 20 | await page.getByRole("link", { name: /Terms/ }).click(); 21 | await expect(page.getByRole("heading", { name: "AGREEMENT TO OUR LEGAL TERMS" })).toBeVisible(); 22 | }); 23 | 24 | test("go to privacy", async ({ page }) => { 25 | await page.getByRole("link", { name: /Privacy/ }).click(); 26 | await expect(page.getByRole("heading", { name: "WHAT INFORMATION DO WE COLLECT?" })).toBeVisible(); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /e2e/tests/table.spec.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /e2e/tests/tutorial.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | import { getEditor, getGraphNode } from "../helpers/utils"; 3 | 4 | test("tutorial data", async ({ page }) => { 5 | { 6 | const editor = await getEditor(page, { goto: true, needTutorial: true }); 7 | await expect(editor).toContainText("Aidan Gillen"); 8 | } 9 | 10 | // assert graph view has nodes. 11 | await expect(getGraphNode(page, "$")).toBeVisible(); 12 | await expect(getGraphNode(page, "$/Alexander%20Skarsgard")).toBeVisible(); 13 | await expect(getGraphNode(page, "$/Aidan%20Gillen/array")).toBeVisible(); 14 | 15 | // no tutorial data when reentry. 16 | { 17 | const editor = await getEditor(page, { goto: true }); 18 | await expect(editor).toHaveText(""); 19 | await expect(getGraphNode(page, "$")).toBeHidden(); 20 | } 21 | }); 22 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, devices } from "@playwright/test"; 2 | import dotenv from "dotenv"; 3 | import { fileURLToPath } from "node:url"; 4 | import path from "path"; 5 | 6 | const baseURL = "http://localhost:3000"; 7 | const CI = !!process.env.CI; 8 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 9 | 10 | dotenv.config({ path: path.resolve(__dirname, ".env") }); 11 | 12 | /** 13 | * See https://playwright.dev/docs/test-configuration. 14 | */ 15 | export default defineConfig({ 16 | timeout: 60 * 1000, 17 | expect: { 18 | /** 19 | * Maximum time expect() should wait for the condition to be met. 20 | * For example in `await expect(locator).toHaveText();` 21 | */ 22 | timeout: 5 * 1000, 23 | }, 24 | testDir: "./e2e/tests", 25 | /* Run tests in files in parallel */ 26 | fullyParallel: true, 27 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 28 | forbidOnly: CI, 29 | retries: 1, 30 | workers: CI ? 2 : undefined, 31 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 32 | reporter: "html", 33 | use: { 34 | baseURL, 35 | trace: "retain-on-failure", 36 | }, 37 | 38 | /* Configure projects for major browsers */ 39 | projects: [ 40 | { 41 | name: "chromium", 42 | use: { ...devices["Desktop Chrome"] }, 43 | }, 44 | { 45 | name: "firefox", 46 | use: { ...devices["Desktop Firefox"] }, 47 | }, 48 | { 49 | name: "webkit", 50 | use: { ...devices["Desktop Safari"] }, 51 | }, 52 | ], 53 | /* Run your local server before starting the tests */ 54 | webServer: { 55 | command: "pnpm preview", 56 | timeout: 10 * 60 * 1000, 57 | url: baseURL, 58 | reuseExistingServer: true, 59 | }, 60 | }); 61 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /public/ads.txt: -------------------------------------------------------------------------------- 1 | google.com, pub-6579013241267492, DIRECT, f08c47fec0942fa0 -------------------------------------------------------------------------------- /public/example/compare.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loggerhead/json4u/HEAD/public/example/compare.webp -------------------------------------------------------------------------------- /public/example/drag-drop.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loggerhead/json4u/HEAD/public/example/drag-drop.webp -------------------------------------------------------------------------------- /public/example/graph.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loggerhead/json4u/HEAD/public/example/graph.webp -------------------------------------------------------------------------------- /public/example/import-csv.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loggerhead/json4u/HEAD/public/example/import-csv.webp -------------------------------------------------------------------------------- /public/example/jq.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loggerhead/json4u/HEAD/public/example/jq.gif -------------------------------------------------------------------------------- /public/example/json-path.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loggerhead/json4u/HEAD/public/example/json-path.webp -------------------------------------------------------------------------------- /public/example/json4u.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loggerhead/json4u/HEAD/public/example/json4u.webp -------------------------------------------------------------------------------- /public/example/nest-parse.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loggerhead/json4u/HEAD/public/example/nest-parse.webp -------------------------------------------------------------------------------- /public/example/table.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loggerhead/json4u/HEAD/public/example/table.webp -------------------------------------------------------------------------------- /public/example/url-to-json.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loggerhead/json4u/HEAD/public/example/url-to-json.webp -------------------------------------------------------------------------------- /public/example/validate-in-tutorial.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loggerhead/json4u/HEAD/public/example/validate-in-tutorial.webp -------------------------------------------------------------------------------- /public/example/validate.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loggerhead/json4u/HEAD/public/example/validate.webp -------------------------------------------------------------------------------- /public/jq/1.7/jq.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loggerhead/json4u/HEAD/public/jq/1.7/jq.wasm -------------------------------------------------------------------------------- /sentry.client.config.ts: -------------------------------------------------------------------------------- 1 | // This file configures the initialization of Sentry on the client. 2 | // The config you add here will be used whenever a users loads a page in their browser. 3 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/ 4 | import { version } from "@/lib/env"; 5 | import * as Sentry from "@sentry/nextjs"; 6 | 7 | Sentry.init({ 8 | dsn: "https://d60bd8847a6d8afc72e3de0d9288fa4c@o4506325094236160.ingest.us.sentry.io/4506325157085184", 9 | ignoreErrors: [ 10 | "Invalid regular expression", 11 | "l.intersection is not a function", 12 | ], 13 | 14 | // Adjust this value in production, or use tracesSampler for greater control 15 | tracesSampleRate: 1, 16 | 17 | // Setting this option to true will print useful information to the console while you're setting up Sentry. 18 | debug: false, 19 | release: version, 20 | replaysOnErrorSampleRate: 1.0, 21 | 22 | // This sets the sample rate to be 10%. You may want this to be 100% while 23 | // in development and sample at a lower rate in production 24 | replaysSessionSampleRate: 0, 25 | 26 | // You can remove this option if you're not planning to use the Sentry Session Replay feature: 27 | integrations: [ 28 | Sentry.replayIntegration({ 29 | // Additional Replay configuration goes in here, for example: 30 | maskAllText: true, 31 | blockAllMedia: true, 32 | }), 33 | ], 34 | }); 35 | -------------------------------------------------------------------------------- /sentry.edge.config.ts: -------------------------------------------------------------------------------- 1 | // This file configures the initialization of Sentry for edge features (middleware, edge routes, and so on). 2 | // The config you add here will be used whenever one of the edge features is loaded. 3 | // Note that this config is unrelated to the Vercel Edge Runtime and is also required when running locally. 4 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/ 5 | import { version } from "@/lib/env"; 6 | import * as Sentry from "@sentry/nextjs"; 7 | 8 | Sentry.init({ 9 | dsn: "https://d60bd8847a6d8afc72e3de0d9288fa4c@o4506325094236160.ingest.us.sentry.io/4506325157085184", 10 | 11 | // Adjust this value in production, or use tracesSampler for greater control 12 | tracesSampleRate: 1, 13 | 14 | // Setting this option to true will print useful information to the console while you're setting up Sentry. 15 | debug: false, 16 | release: version, 17 | }); 18 | -------------------------------------------------------------------------------- /sentry.server.config.ts: -------------------------------------------------------------------------------- 1 | // This file configures the initialization of Sentry on the server. 2 | // The config you add here will be used whenever the server handles a request. 3 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/ 4 | import { version } from "@/lib/env"; 5 | import * as Sentry from "@sentry/nextjs"; 6 | 7 | Sentry.init({ 8 | dsn: "https://d60bd8847a6d8afc72e3de0d9288fa4c@o4506325094236160.ingest.us.sentry.io/4506325157085184", 9 | 10 | // Adjust this value in production, or use tracesSampler for greater control 11 | tracesSampleRate: 1, 12 | 13 | // Setting this option to true will print useful information to the console while you're setting up Sentry. 14 | debug: false, 15 | release: version, 16 | 17 | // Uncomment the line below to enable Spotlight (https://spotlightjs.com) 18 | // spotlight: process.env.NODE_ENV === 'development', 19 | }); 20 | -------------------------------------------------------------------------------- /src/app/(home)/(mdx)/MdxPage.tsx: -------------------------------------------------------------------------------- 1 | import { getLocale } from "next-intl/server"; 2 | 3 | function getRelativePath(dir: string) { 4 | const p = dir.split("(mdx)")[1]; 5 | return "." + p; 6 | } 7 | 8 | export async function mdxGenMetadata(dir: string) { 9 | const locale = await getLocale(); 10 | const mdx = await import(`${getRelativePath(dir)}/${locale}.mdx`); 11 | return { ...mdx.metadata }; 12 | } 13 | 14 | export default async function MdxPage({ dir }: { dir: string }) { 15 | const locale = await getLocale(); 16 | const Content = (await import(`${getRelativePath(dir)}/${locale}.mdx`)).default; 17 | return ; 18 | } 19 | -------------------------------------------------------------------------------- /src/app/(home)/(mdx)/changelog/page.tsx: -------------------------------------------------------------------------------- 1 | import MdxPage, { mdxGenMetadata } from "../MdxPage"; 2 | 3 | export async function generateMetadata() { 4 | return mdxGenMetadata(__dirname); 5 | } 6 | 7 | export default async function Page() { 8 | return ; 9 | } 10 | -------------------------------------------------------------------------------- /src/app/(home)/(mdx)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { notFound } from "next/navigation"; 2 | import "github-markdown-css"; 3 | 4 | export default function MdxLayout({ children }: { children: React.ReactNode }) { 5 | try { 6 | return ( 7 |
8 |
{children}
9 |
10 | ); 11 | } catch (error) { 12 | notFound(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/app/(home)/(mdx)/privacy/page.tsx: -------------------------------------------------------------------------------- 1 | import MdxPage, { mdxGenMetadata } from "../MdxPage"; 2 | 3 | export async function generateMetadata() { 4 | return mdxGenMetadata(__dirname); 5 | } 6 | 7 | export default async function Page() { 8 | return ; 9 | } 10 | -------------------------------------------------------------------------------- /src/app/(home)/(mdx)/privacy/zh.mdx: -------------------------------------------------------------------------------- 1 | export const metadata = { 2 | title: "隐私政策", 3 | }; 4 | 5 | # 隐私政策 6 | -------------------------------------------------------------------------------- /src/app/(home)/(mdx)/terms/page.tsx: -------------------------------------------------------------------------------- 1 | import MdxPage, { mdxGenMetadata } from "../MdxPage"; 2 | 3 | export async function generateMetadata() { 4 | return mdxGenMetadata(__dirname); 5 | } 6 | 7 | export default async function Page() { 8 | return ; 9 | } 10 | -------------------------------------------------------------------------------- /src/app/(home)/(mdx)/terms/zh.mdx: -------------------------------------------------------------------------------- 1 | export const metadata = { 2 | title: "服务条款", 3 | }; 4 | 5 | # 服务条款 6 | -------------------------------------------------------------------------------- /src/app/(home)/(mdx)/tutorial/compare/en.mdx: -------------------------------------------------------------------------------- 1 | import Script from "next/script"; 2 | import Refer from "../Refer"; 3 | 4 | export const metadata = { 5 | title: "How to Compare JSON Structurally/Semantically?", 6 | keywords: 7 | "JSON compare, JSON diff, compare JSON, JSON comparison, JSON Diff Tool, Online JSON diff, JSON diff checker, JSON diff viewer, Free JSON comparison tool, JSON file comparison, Visual JSON diff", 8 | description: 9 | "An advanced online tool to compare two JSON documents. Instantly spot differences with a side-by-side comparison view.", 10 | }; 11 | 12 | # How to Compare JSON Structurally/Semantically? 13 | 14 | JSON For You provides an easy-to-use JSON comparison feature that helps you quickly identify the differences between two JSON data, supporting features like highlighting inline differences, synchronized scrolling, and automatic text comparison. 15 | 16 | JSON Compare Feature 17 | 18 | ## Steps 19 | 20 | To use the structural comparison feature in JSON For You, follow these steps: 21 | 22 | 1. Switch the right-side view to "Text" view at the top; 23 | 2. Paste the JSON strings to be compared into the left and right editors respectively; 24 | 3. Click the "Compare" button at the top to perform a structural comparison; 25 | 26 | If there are no differences, a "No differences" prompt will appear in the bottom right corner. 27 | 28 | If there are differences, a "Differences found" prompt will appear, and the differing parts will be highlighted. The left and right editors will scroll synchronously when you view the differences. You can click the "Sync Scroll" button on the sidebar to enable or disable this feature. 29 | 30 | If you want to perform a text comparison instead of a structural one, follow these steps: 31 | 32 | 1. Click the switch to the left of the "Compare" button to change to "Text Compare" mode; 33 | 2. Click the "Text Compare" button; 34 | 35 | ## Use Cases for JSON Comparison 36 | 37 | - **API Debugging**: Compare differences between frontend and backend JSON responses 38 | - **Version Control**: View the change history of JSON configuration files 39 | - **Data Validation**: Verify if the API return data structure meets expectations 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/app/(home)/(mdx)/tutorial/compare/page.tsx: -------------------------------------------------------------------------------- 1 | import MdxPage, { mdxGenMetadata } from "../../MdxPage"; 2 | 3 | export async function generateMetadata() { 4 | return mdxGenMetadata(__dirname); 5 | } 6 | 7 | export default async function Page() { 8 | return ; 9 | } 10 | -------------------------------------------------------------------------------- /src/app/(home)/(mdx)/tutorial/compare/zh.mdx: -------------------------------------------------------------------------------- 1 | import Script from "next/script"; 2 | import Refer from "../Refer"; 3 | 4 | export const metadata = { 5 | title: "如何结构化/语义化比较 JSON?", 6 | keywords: "JSON比较工具,JSON差异对比,JSON文件对比,在线JSON比较,JSON数据比对", 7 | description: "JSON For You比较功能教程:学习如何快速对比两个JSON数据的差异,高亮显示不同之处,提升开发效率。", 8 | }; 9 | 10 | # 如何比较 JSON 数据差异? 11 | 12 | JSON For You 提供易用的 JSON 比较功能,帮助您快速识别两个 JSON 数据之间的差异,支持高亮展示行内差异、同步滚动、自动文本比较等功能。 13 | 14 | JSON 比较功能 15 | 16 | ## 操作步骤 17 | 18 | 使用 JSON For You 结构化比较功能,可以按以下步骤操作: 19 | 20 | 1. 在顶部将右侧视图切换到「文本」视图; 21 | 2. 将待比较的 JSON 字符串分别粘贴到左侧编辑器和右侧编辑器中; 22 | 3. 点击顶部的「比较」按钮进行结构化比较; 23 | 24 | 如果没有差异,右下角会弹出提示“不存在差异”。 25 | 26 | 如果存在差异,右下角会弹出提示“存在差异”,并且会高亮显示差异部分。滚动查看差异时左右两侧编辑器会同步滚动,点击侧边栏的「同步滚动」可以打开或关闭同步滚动功能。 27 | 28 | 如果不想进行结构化比较,而是想进行文本比较,可以按以下步骤操作: 29 | 30 | 1. 点击「比较」按钮左侧的开关,切换到「文本比较」模式; 31 | 2. 点击「文本比较」按钮; 32 | 33 | ## JSON 比较的应用场景 34 | 35 | - **接口调试**:对比前后端 JSON 响应差异 36 | - **版本控制**:查看 JSON 配置文件的变更历史 37 | - **数据验证**:验证 API 返回数据是否符合预期结构 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/app/(home)/(mdx)/tutorial/csv/page.tsx: -------------------------------------------------------------------------------- 1 | import MdxPage, { mdxGenMetadata } from "../../MdxPage"; 2 | 3 | export async function generateMetadata() { 4 | return mdxGenMetadata(__dirname); 5 | } 6 | 7 | export default async function Page() { 8 | return ; 9 | } 10 | -------------------------------------------------------------------------------- /src/app/(home)/(mdx)/tutorial/csv/zh.mdx: -------------------------------------------------------------------------------- 1 | import Refer from "../Refer"; 2 | 3 | export const metadata = { 4 | title: "如何进行 CSV 与 JSON 的相互转换?", 5 | keywords: "CSV 转 JSON, JSON 转 CSV, CSV to JSON, JSON to CSV, 在线转换工具", 6 | description: 7 | "了解如何使用 JSON For You 在线工具高效地将 CSV 文件转换为 JSON,或将 JSON 导出为 CSV。本教程包含详细的操作步骤、实用示例和常见问题解答。", 8 | }; 9 | 10 | # 如何进行 CSV 与 JSON 的相互转换? 11 | 12 | CSV(逗号分隔值)是一种广泛用于存储表格数据的纯文本格式,而 JSON(JavaScript 对象表示法)则是在 Web 开发中极为流行的数据交换格式。在不同的应用场景下,您可能需要在 CSV 和 JSON 之间进行数据转换。 13 | 14 | 导入 CSV 15 | 16 | JSON For You 支持将 CSV 转换为 JSON 格式,同时也支持将 JSON 导出为 CSV 格式。 17 | 18 | 1. 点击侧边栏中的「**上传**」按钮,文件类型选择 CSV; 19 | 2. 点击红色虚线的框,在弹出的对话框中选择您的 CSV 文件; 20 | 3. JSON For You 会自动将 CSV 数据转换为 JSON 格式并显示在左侧编辑器中,而原始的 CSV 数据则会显示在右侧编辑器中。 21 | 22 | ### CSV 转 JSON 示例 23 | 24 | **输入的 CSV 文件:** 25 | 26 | ```csv 27 | name,age,city 28 | John,30,New York 29 | Jane,25,London 30 | ``` 31 | 32 | **转换为 JSON (包含表头):** 33 | 34 | ```json 35 | [ 36 | { 37 | "name": "John", 38 | "age": 30, 39 | "city": "New York" 40 | }, 41 | { 42 | "name": "Jane", 43 | "age": 25, 44 | "city": "London" 45 | } 46 | ] 47 | ``` 48 | 49 | ## JSON 转 CSV 50 | 51 | 将 JSON 导出为 CSV 可以方便地将数据导入到电子表格软件(如 Excel、Google Sheets)中进行进一步的分析和可视化。 52 | 53 | JSON For You 支持将 JSON 转换为 CSV 格式并下载到本地。 54 | 55 | 1. 将 JSON 数据黏贴到左侧编辑器中; 56 | 2. 点击侧边栏中的「**导出**」按钮,文件类型选择 CSV; 57 | 3. 点击「**预览**」按钮,转换后的 CSV 数据会展示在右侧编辑器中; 58 | 4. 点击「**下载**」按钮,即可将 JSON 数据保存为 CSV 文件。 59 | 60 | ### JSON 转 CSV 示例 61 | 62 | **输入的 JSON 数据:** 63 | 64 | ```json 65 | [ 66 | { 67 | "product": "Laptop", 68 | "price": 1200, 69 | "in_stock": true 70 | }, 71 | { 72 | "product": "Mouse", 73 | "price": 25, 74 | "in_stock": false 75 | } 76 | ] 77 | ``` 78 | 79 | **导出的 CSV 文件:** 80 | 81 | ```csv 82 | product,price,in_stock 83 | Laptop,1200,true 84 | Mouse,25,false 85 | ``` 86 | 87 | 88 | -------------------------------------------------------------------------------- /src/app/(home)/(mdx)/tutorial/en.mdx: -------------------------------------------------------------------------------- 1 | import Refer from "./Refer"; 2 | 3 | export const metadata = { 4 | title: "Powerful Online JSON Tool Tutorial", 5 | keywords: 6 | "online JSON tools, JSON formatter tutorial, JSON validation method, JSON visualization tool, JSON comparison tool, JSONPath tutorial, jq command tutorial, JSON conversion tool", 7 | description: 8 | "JSON For You Online JSON Tool Tutorial: Detailed explanation of how to use functions such as JSON formatting, validation, visualization, and comparison to help developers quickly master JSON data processing skills and improve work efficiency.", 9 | }; 10 | 11 | # JSON For You Tutorial 12 | 13 | JSON For You is a powerful online JSON editor and visualization toolbox designed to maximize your efficiency in processing and browsing JSON data. It not only supports basic functions such as JSON validation, formatting, and compression, but also provides two feature-rich visualization capabilities: graph and table. This tutorial will introduce each function in detail to help you get started quickly and improve your work efficiency. 14 | 15 | 16 | 17 | ## Other Features 18 | 19 | JSON For You also provides the following features to fully meet user needs: 20 | 21 | - Drag and drop upload: Both the left and right editors support direct drag and drop file uploads. 22 | 23 | Drag and drop upload 24 | 25 | - Show JSON path: When the cursor is on the left editor, the JSON path of the current cursor position will be displayed in the bottom status bar. 26 | 27 | Show JSON path 28 | 29 | - JSON parsing error prompt: When the JSON format is incorrect, an error prompt will be displayed in the bottom status bar. If you click the error message, the cursor will be located at the error position in the editor. 30 | 31 | Validate JSON 32 | -------------------------------------------------------------------------------- /src/app/(home)/(mdx)/tutorial/escape/page.tsx: -------------------------------------------------------------------------------- 1 | import MdxPage, { mdxGenMetadata } from "../../MdxPage"; 2 | 3 | export async function generateMetadata() { 4 | return mdxGenMetadata(__dirname); 5 | } 6 | 7 | export default async function Page() { 8 | return ; 9 | } 10 | -------------------------------------------------------------------------------- /src/app/(home)/(mdx)/tutorial/format/en.mdx: -------------------------------------------------------------------------------- 1 | import Refer from "../Refer"; 2 | 3 | import Script from "next/script"; 4 | 5 | export const metadata = { 6 | title: "How to Format JSON?", 7 | keywords: "JSON formatter, format JSON, JSON beautifier, JSON prettier, online JSON formatter, JSON validator", 8 | description: 9 | "Format, view, edit, and validate your JSON data with our easy-to-use online JSON formatter. Clean up your messy JSON and make it readable.", 10 | }; 11 | 12 | # How to Format JSON? 13 | 14 | JSON For You provides a powerful JSON formatting (also known as JSON beautification) function, which supports both automatic and manual methods to help developers quickly beautify JSON data and improve readability. By default, JSON For You will **automatically format the JSON data when you type or paste it**. This means you can get well-structured and neatly indented JSON code without any extra operations. 15 | 16 | If you do not need automatic formatting and want to format manually, you can also follow these steps: 17 | 18 | 1. Paste the original JSON string into the left editor; 19 | 2. Click the "Auto Format" button on the sidebar to turn it off; 20 | 3. Click "Search Command" at the top, find "**Format**", and click to perform manual formatting. 21 | 22 | ## Use Cases for JSON Formatting 23 | 24 | Standard JSON format is crucial for the following scenarios: 25 | 26 | - **Team Collaboration**: Improve the readability of JSON for multi-person development; 27 | - **Troubleshooting**: Quickly locate syntax errors and structural problems; 28 | - **API Development**: Ensure the correct format of request/response data; 29 | - **Documentation**: Create easy-to-understand JSON examples. 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/app/(home)/(mdx)/tutorial/format/page.tsx: -------------------------------------------------------------------------------- 1 | import MdxPage, { mdxGenMetadata } from "../../MdxPage"; 2 | 3 | export async function generateMetadata() { 4 | return mdxGenMetadata(__dirname); 5 | } 6 | 7 | export default async function Page() { 8 | return ; 9 | } 10 | -------------------------------------------------------------------------------- /src/app/(home)/(mdx)/tutorial/format/zh.mdx: -------------------------------------------------------------------------------- 1 | import Refer from "../Refer"; 2 | 3 | import Script from "next/script"; 4 | 5 | export const metadata = { 6 | title: "如何进行 JSON 格式化?", 7 | keywords: "JSON格式化,JSON美化,在线JSON格式化", 8 | description: 9 | "详细介绍JSON For You的格式化功能,包括启用方法、手动格式化步骤及常见问题解决,帮助开发者快速美化JSON数据。", 10 | }; 11 | 12 | # 如何进行 JSON 格式化? 13 | 14 | JSON For You 提供强大的 JSON 格式化(又叫 JSON 美化)功能,支持自动和手动两种方式,帮助开发者快速美化 JSON 数据,提升可读性。默认情况下,JSON For You 会在您**输入或粘贴 JSON 数据时自动进行格式化**。这意味着您无需额外操作,即可获得结构清晰、缩进整齐的 JSON 代码。 15 | 16 | 如果不需要自动格式化,想手动格式化,也可以按以下步骤操作: 17 | 18 | 1. 将原始 JSON 字符串粘贴到左侧编辑器中; 19 | 2. 点击侧边栏的「自动格式化」按钮将其关闭; 20 | 3. 点击顶部的「搜索命令」,找到「**格式化**」,点击执行手动格式化。 21 | 22 | ## JSON 格式化的应用场景 23 | 24 | 规范的 JSON 格式对于以下场景至关重要: 25 | 26 | - **团队协作**:提高多人开发时的 JSON 可读性; 27 | - **错误排查**:快速定位语法错误和结构问题; 28 | - **API 开发**:确保请求/响应数据格式正确; 29 | - **文档编写**:创建易于理解的 JSON 示例。 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/app/(home)/(mdx)/tutorial/jq/page.tsx: -------------------------------------------------------------------------------- 1 | import MdxPage, { mdxGenMetadata } from "../../MdxPage"; 2 | 3 | export async function generateMetadata() { 4 | return mdxGenMetadata(__dirname); 5 | } 6 | 7 | export default async function Page() { 8 | return ; 9 | } 10 | -------------------------------------------------------------------------------- /src/app/(home)/(mdx)/tutorial/jq/zh.mdx: -------------------------------------------------------------------------------- 1 | import Refer from "../Refer"; 2 | 3 | export const metadata = { 4 | title: "如何使用 jq?", 5 | keywords: "jq教程, jq命令示例, JSON数据处理, jq过滤器, JSON提取工具", 6 | description: "全面的 jq 教程,包含常用 jq 命令示例、JSON 数据提取与转换方法,帮助开发者高效处理 JSON 数据。", 7 | }; 8 | 9 | # 如何使用 jq? 10 | 11 | jq 是一个功能强大的命令行 JSON 处理器,让开发者能够像使用 `sed`、`awk` 和 `grep` 处理文本一样轻松地过滤、映射和转换 JSON 数据。jq 提供了众多处理 JSON 数据的优势: 12 | 13 | - **轻量级高效**:无需加载整个数据集即可处理 JSON 14 | - **强大的查询语言**:支持复杂的数据转换和过滤 15 | - **高性能**:即使处理大型 JSON 文件也能保持快速响应 16 | - **丰富的社区支持**:完善的文档和大量示例 17 | 18 | 典型的应用场景包括: 19 | 20 | - **API 响应处理**:从复杂的 API 响应中提取相关数据 21 | - **日志分析**:解析和过滤 JSON 格式的日志文件 22 | - **数据清洗**:预处理 JSON 数据以用于数据分析 23 | - **配置转换**:批量修改 JSON 配置文件 24 | 25 | ## 在 JSON For You 中使用 jq 26 | 27 | JSON For You 支持您进行在线 jq 处理,只需按如下步骤操作: 28 | 29 | 1. 将 JSON 字符串粘贴到左侧编辑器中; 30 | 2. 点击顶部的「搜索命令」,找到「**展示 jq 输入框**」,点击后在底部会出现一个 jq 输入框; 31 | 3. 输入 jq 表达式(例如:`.[] | select(.status == "active")`)会自动执行 jq 命令; 32 | 4. 在右侧编辑器可以查看结果; 33 | 5. 当您需要多次编辑时,可以通过 `Ctrl+Enter` 快捷键快速交换左右两侧编辑器的文本; 34 | 35 | 在 JSON For You 中使用 jq 36 | 37 | ## 常用 jq 命令示例 38 | 39 | ### 基本过滤 40 | 41 | 从 JSON 对象中提取特定字段: 42 | 43 | ```bash 44 | # 获取数组中的所有用户名 45 | .[] | .name 46 | 47 | # 提取嵌套字段 48 | .users[] | {id: .user_id, name: .profile.name} 49 | ``` 50 | 51 | ### 数据转换 52 | 53 | 修改 JSON 结构和值: 54 | 55 | ```bash 56 | # 添加新字段 57 | .[] | . + {full_name: (.first_name + " " + .last_name)} 58 | 59 | # 转换值 60 | .items[] | {product: .name, price: (.price * 1.1)} # 添加 10% 税费 61 | ``` 62 | 63 | ### 条件选择 64 | 65 | 基于条件过滤数据: 66 | 67 | ```bash 68 | # 选择活跃用户 69 | .users[] | select(.status == "active" and .login_count > 5) 70 | 71 | # 排除空值 72 | .data[] | select(.value != null and .value != "") 73 | ``` 74 | 75 | ## jq 高级技巧 76 | 77 | ### 数组操作 78 | 79 | ```bash 80 | # 获取唯一值 81 | .[] | .category | unique 82 | 83 | # 计算元素数量 84 | .[] | .tags | length as $len | {item: ., tag_count:$len} 85 | ``` 86 | 87 | ### 复杂转换 88 | 89 | ```bash 90 | # 按类别分组项目 91 | .group_by(.category)[] | {categoryKey: .[].category, items:.} 92 | 93 | # 展平嵌套数组 94 | .recursive_field[] | .[] | {id: .id, value: .data.value} 95 | ``` 96 | 97 | 98 | -------------------------------------------------------------------------------- /src/app/(home)/(mdx)/tutorial/json-path/page.tsx: -------------------------------------------------------------------------------- 1 | import MdxPage, { mdxGenMetadata } from "../../MdxPage"; 2 | 3 | export async function generateMetadata() { 4 | return mdxGenMetadata(__dirname); 5 | } 6 | 7 | export default async function Page() { 8 | return ; 9 | } 10 | -------------------------------------------------------------------------------- /src/app/(home)/(mdx)/tutorial/minify/page.tsx: -------------------------------------------------------------------------------- 1 | import MdxPage, { mdxGenMetadata } from "../../MdxPage"; 2 | 3 | export async function generateMetadata() { 4 | return mdxGenMetadata(__dirname); 5 | } 6 | 7 | export default async function Page() { 8 | return ; 9 | } 10 | -------------------------------------------------------------------------------- /src/app/(home)/(mdx)/tutorial/minify/zh.mdx: -------------------------------------------------------------------------------- 1 | import Refer from "../Refer"; 2 | 3 | export const metadata = { 4 | title: "如何进行 JSON 压缩/最小化?", 5 | keywords: ["JSON压缩", "JSON精简", "JSON最小化", "在线JSON压缩工具", "JSON minify", "JSON压缩方法"], 6 | description: "学习如何高效压缩JSON字符串以减小文件大小、提升加载速度并优化数据传输,包含实用示例和最佳实践。", 7 | }; 8 | 9 | # 如何进行 JSON 压缩/最小化? 10 | 11 | JSON 最小化(又称 JSON 精简、JSON 压缩)是指在保持数据结构和含义不变的前提下,移除 JSON 中的所有不必要字符(空格、换行符、注释等)的过程,它可以极大的减小 JSON 字符串的大小,从而提高数据传输效率。 12 | 13 | 在 JSON For You 中压缩 JSON 字符串非常简单,只需按照以下步骤操作: 14 | 15 | 1. 将原始 JSON 字符串粘贴到左侧编辑器中; 16 | 2. 点击顶部的「搜索命令」,找到「**最小化**」,点击执行。 17 | 18 | ## JSON 最小化的应用场景 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 |
使用场景主要优势
API 请求/响应数据加快客户端收发数据的速度
配置文件减小部署包体积
移动应用节省带宽并提高电池续航
Web 存储(localStorage/sessionStorage)最大化存储效率
IoT设备在带宽受限环境中优化数据传输
50 | 51 | ## 示例 52 | 53 | **原始JSON(86 个字符):** 54 | 55 | ```json 56 | { 57 | "name": "张三", 58 | "age": 30, 59 | "isStudent": false, 60 | "hobbies": ["阅读", "编程", "徒步"] 61 | } 62 | ``` 63 | 64 | **压缩后(67 个字符):** 65 | 66 | ```json 67 | { "name": "张三", "age": 30, "isStudent": false, "hobbies": ["阅读", "编程", "徒步"] } 68 | ``` 69 | 70 | ## JSON 最小化的最佳实践 71 | 72 | - 部署前务必验证压缩后的 JSON 有效性 73 | - 为开发和调试保留未压缩版本 74 | - 考虑在构建过程中实现自动化压缩 75 | - 将压缩与 gzip 压缩结合使用以获得最大效率 76 | - 对于超大 JSON 文件,采用流式压缩方式 77 | 78 | 79 | -------------------------------------------------------------------------------- /src/app/(home)/(mdx)/tutorial/page.tsx: -------------------------------------------------------------------------------- 1 | import MdxPage, { mdxGenMetadata } from "../MdxPage"; 2 | 3 | export async function generateMetadata() { 4 | return mdxGenMetadata(__dirname); 5 | } 6 | 7 | export default async function Page() { 8 | return ; 9 | } 10 | -------------------------------------------------------------------------------- /src/app/(home)/(mdx)/tutorial/python-dict-to-json/page.tsx: -------------------------------------------------------------------------------- 1 | import MdxPage, { mdxGenMetadata } from "../../MdxPage"; 2 | 3 | export async function generateMetadata() { 4 | return mdxGenMetadata(__dirname); 5 | } 6 | 7 | export default async function Page() { 8 | return ; 9 | } 10 | -------------------------------------------------------------------------------- /src/app/(home)/(mdx)/tutorial/python-dict-to-json/zh.mdx: -------------------------------------------------------------------------------- 1 | import Refer from "../Refer"; 2 | 3 | export const metadata = { 4 | title: "如何将 Python dict 转换为 JSON?", 5 | keywords: ["Python 字典转 JSON", "Python dict 转 JSON", "Python 到 JSON 转换器", "Python dict 转 JSON 在线工具"], 6 | description: "学习如何将 Python dict 转换为 JSON 格式,包含实用示例、数据类型处理和解决常见转换问题的完整教程。", 7 | }; 8 | 9 | # 如何将 Python dict 转换为 JSON? 10 | 11 | Python dict 与 JSON 的键值对结构很相似,但在语法和数据类型上存在差异。如果您需要对 python dict 做可视化或处理,可以先将它转换为 JSON,然后使用 JSON For You 上强大的 JSON 编辑和可视化功能。操作步骤如下: 12 | 13 | 1. 将原始 JSON 字符串粘贴到左侧编辑器中; 14 | 2. 点击顶部的「搜索命令」,找到「**Python dict 转 JSON**」,点击执行。 15 | 16 | 由于 python dict 与 JSON 存在显著区别,JSON For You 目前只支持简单的文本替换,因此对复杂的 dict 做转换可能会失败。 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 |
特性Python 字典JSON
字符串引号单引号或双引号仅双引号
尾随逗号允许不允许
注释代码中允许(不在字典字面量中)不支持
key 类型可以是任何可哈希类型字符串
value 类型支持元组、集合、datetime 和自定义对象仅支持字符串、数字、布尔值、null、数组和对象
54 | 55 | ## 示例 56 | 57 | **Python dict:** 58 | 59 | ```python 60 | { 61 | '姓名': '张三', 62 | '年龄': 30, 63 | '是否学生': False, 64 | '爱好': ['阅读', '编程', '徒步'], 65 | '地址': { 66 | '街道': ' Main St 123号', 67 | '城市': '任意市' 68 | }, 69 | } 70 | ``` 71 | 72 | **转换后的 JSON:** 73 | 74 | ```json 75 | { 76 | "姓名": "张三", 77 | "年龄": 30, 78 | "是否学生": false, 79 | "爱好": ["阅读", "编程", "徒步"], 80 | "地址": { 81 | "街道": "MainSt123号", 82 | "城市": "任意市" 83 | } 84 | } 85 | ``` 86 | 87 | 88 | -------------------------------------------------------------------------------- /src/app/(home)/(mdx)/tutorial/sort/en.mdx: -------------------------------------------------------------------------------- 1 | import Refer from "../Refer"; 2 | 3 | export const metadata = { 4 | title: "How to Sort JSON by Key?", 5 | keywords: ["JSON sort", "sort JSON", "JSON key sort", "online JSON sorter", "JSON formatter", "JSON tools"], 6 | description: 7 | "A free online tool to sort JSON data alphabetically. Sorts keys in JSON objects, including nested objects and arrays. Simple, fast, and efficient.", 8 | }; 9 | 10 | # How to Sort JSON by Key? 11 | 12 | JSON sorting is the process of organizing key-value pairs in a specific order, which can enhance readability, ensure consistent data presentation, and facilitate modification or comparison of JSON objects. Properly sorted JSON is particularly valuable in debugging and documentation. 13 | 14 | JSON For You supports automatic sorting (ascending by default, not enabled by default) and manual sorting (both ascending and descending are supported), and the sorting is recursive, which means that nested JSON objects will also be sorted. To enable automatic sorting: 15 | 16 | 1. Click "Auto Sort" on the sidebar to enable automatic sorting; 17 | 2. Paste the original JSON string into the left editor, and it will be sorted automatically upon pasting. 18 | 19 | If you need to manually sort in descending order, you can do this: 20 | 21 | 1. Paste the original JSON string into the left editor; 22 | 2. Click "Search Command" at the top, find "**JSON Sort (Descending)**", and click to execute. 23 | 24 | ## Example 25 | 26 | **Unsorted JSON:** 27 | 28 | ```json 29 | { 30 | "email": "user@example.com", 31 | "name": "John Doe", 32 | "age": 30, 33 | "is_active": true 34 | } 35 | ``` 36 | 37 | **Sorted (Ascending):** 38 | 39 | ```json 40 | { 41 | "age": 30, 42 | "email": "user@example.com", 43 | "is_active": true, 44 | "name": "John Doe" 45 | } 46 | ``` 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/app/(home)/(mdx)/tutorial/sort/page.tsx: -------------------------------------------------------------------------------- 1 | import MdxPage, { mdxGenMetadata } from "../../MdxPage"; 2 | 3 | export async function generateMetadata() { 4 | return mdxGenMetadata(__dirname); 5 | } 6 | 7 | export default async function Page() { 8 | return ; 9 | } 10 | -------------------------------------------------------------------------------- /src/app/(home)/(mdx)/tutorial/sort/zh.mdx: -------------------------------------------------------------------------------- 1 | import Refer from "../Refer"; 2 | 3 | export const metadata = { 4 | title: "如何按 key 的字典序对 JSON 进行排序?", 5 | keywords: ["JSON 排序", "JSON 升序排序", "JSON 降序排序", "在线 JSON 排序"], 6 | description: "学习如何按字母顺序对 JSON 对象进行升序/降序排序,掌握 JSON 排序技巧和最佳实践。", 7 | }; 8 | 9 | # 如何按 key 的字典序对 JSON 进行排序? 10 | 11 | JSON 排序是按特定顺序组织键值对的过程,能增强可读性、确保数据展示一致性,并便于对 JSON 对象做修改或比较。正确排序的 JSON 在调试和文档编写中特别有价值。 12 | 13 | JSON For You 支持自动排序(按升序排序,默认不启用)和手动排序(按升序和降序排序都支持),并且排序是递归的,会对嵌套的 JSON 对象也进行排序。开启自动排序的方式如下: 14 | 15 | 1. 点击侧边栏的「自动排序」启用自动排序; 16 | 2. 将原始 JSON 字符串粘贴到左侧编辑器中,黏贴时会自动排序。 17 | 18 | 如果您需要手动进行降序排序,可以这样操作: 19 | 20 | 1. 将原始 JSON 字符串粘贴到左侧编辑器中; 21 | 2. 点击顶部的「搜索命令」,找到「**JSON 排序(降序)**」,点击执行。 22 | 23 | ## 示例 24 | 25 | **未排序 JSON:** 26 | 27 | ```json 28 | { 29 | "邮箱": "user@example.com", 30 | "姓名": "张三", 31 | "年龄": 30, 32 | "是否活跃": true 33 | } 34 | ``` 35 | 36 | **排序后(升序):** 37 | 38 | ```json 39 | { 40 | "年龄": 30, 41 | "邮箱": "user@example.com", 42 | "是否活跃": true, 43 | "姓名": "张三" 44 | } 45 | ``` 46 | 47 | 48 | -------------------------------------------------------------------------------- /src/app/(home)/(mdx)/tutorial/url-to-json/en.mdx: -------------------------------------------------------------------------------- 1 | import Refer from "../Refer"; 2 | 3 | export const metadata = { 4 | title: "How to Convert URL to JSON?", 5 | keywords: [ 6 | "url to json", 7 | "online converter", 8 | "web to json", 9 | "api to json", 10 | "convert url to json", 11 | "json from url", 12 | "url data extractor", 13 | "free url to json tool", 14 | ], 15 | description: 16 | "Convert data from a URL into structured JSON format instantly. Free, fast, and easy-to-use online tool for developers and data analysts.", 17 | }; 18 | 19 | # How to Convert URL to JSON? 20 | 21 | In web development and API testing, parameters are often passed through the URL query string. Converting a URL to JSON can help you quickly understand the information in the URL and improve your development and testing efficiency. 22 | 23 | URL to JSON 24 | 25 | Converting a URL to JSON with JSON For You is very simple, just follow these steps: 26 | 27 | 1. Paste the URL with query parameters into the left editor; 28 | 2. Click "Search Command" at the top, find "**URL to JSON**", and click to execute. 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/app/(home)/(mdx)/tutorial/url-to-json/page.tsx: -------------------------------------------------------------------------------- 1 | import MdxPage, { mdxGenMetadata } from "../../MdxPage"; 2 | 3 | export async function generateMetadata() { 4 | return mdxGenMetadata(__dirname); 5 | } 6 | 7 | export default async function Page() { 8 | return ; 9 | } 10 | -------------------------------------------------------------------------------- /src/app/(home)/(mdx)/tutorial/url-to-json/zh.mdx: -------------------------------------------------------------------------------- 1 | import Refer from "../Refer"; 2 | 3 | export const metadata = { 4 | title: "如何将 URL 转换为 JSON?", 5 | keywords: "URL 转 JSON, URL 参数转 JSON, URL 解析, JSON 在线转换", 6 | description: "详细介绍如何使用 JSON For You 将 URL 查询参数高效转换为 JSON 格式,包含实用示例和常见问题解决方案。", 7 | }; 8 | 9 | # 如何将 URL 转换为 JSON? 10 | 11 | 在 Web 开发和 API 测试中,经常会通过 URL query string 传递参数。将 URL 转换为 JSON 有助于快速理解 URL 中的信息,提升您的开发、测试效率。 12 | 13 | URL 转成 JSON 14 | 15 | 通过 JSON For You 将 URL 转成 JSON 非常简单,只需按以下步骤操作: 16 | 17 | 1. 将带有查询参数的 URL 粘贴到左侧编辑器中; 18 | 2. 点击顶部的「搜索命令」,找到「**URL 转 JSON**」,点击执行。 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/app/(home)/(mdx)/tutorial/zh.mdx: -------------------------------------------------------------------------------- 1 | import Refer from "./Refer"; 2 | 3 | export const metadata = { 4 | title: "功能强大的在线 JSON 工具使用教程", 5 | keywords: "在线JSON工具,JSON格式化教程,JSON验证方法,JSON可视化工具,JSON比较工具,JSONPath教程,jq命令教程,JSON转换工具", 6 | description: 7 | "JSON For You在线JSON工具教程:详细讲解JSON格式化、验证、可视化、比较等功能的使用方法,帮助开发者快速掌握JSON数据处理技巧,提升工作效率。", 8 | }; 9 | 10 | # JSON For You 使用教程 11 | 12 | JSON For You 是一个强大的在线 JSON 编辑器和可视化工具箱,旨在最大限度地提升您处理和浏览 JSON 数据的效率。它不仅支持基础的 JSON 校验、格式化、压缩等功能,还提供 graph 和 table 两种功能丰富的可视化能力。本教程将详细介绍各项功能,帮助您快速上手,提升工作效率。 13 | 14 | 15 | 16 | ## 其他功能 17 | 18 | JSON For You 充分考虑用户的需求,还提供了以下功能: 19 | 20 | - 拖拽上传:左右两侧编辑器都支持直接拖拽文件上传。 21 | 22 | 拖拽上传 23 | 24 | - 展示 JSON path:光标停留在左侧编辑器上时,会在底部状态栏展示当前光标所在的 JSON path。 25 | 26 | 展示 JSON path 27 | 28 | - JSON 解析错误提示:当 JSON 格式错误时,会在底部状态栏展示错误提示,点击错误信息的话,光标会定位到编辑器中的错误位置。 29 | 30 | 校验 JSON 31 | -------------------------------------------------------------------------------- /src/app/(home)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Inter } from "next/font/google"; 2 | import Footer from "@/containers/landing/Footer"; 3 | import Header from "@/containers/landing/Header"; 4 | 5 | const inter = Inter({ subsets: ["latin"] }); 6 | 7 | export default function HomeLayout({ 8 | children, 9 | }: Readonly<{ 10 | children: React.ReactNode; 11 | }>) { 12 | return ( 13 |
14 |
15 |
16 |
{children}
17 |
18 |
19 |
20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/app/(home)/login-error/page.tsx: -------------------------------------------------------------------------------- 1 | import LinkButton from "@/components/LinkButton"; 2 | import Typography from "@/components/ui/typography"; 3 | import { useTranslations } from "next-intl"; 4 | 5 | export default function LoginError() { 6 | const t = useTranslations("LoginError"); 7 | 8 | return ( 9 |
10 |
11 | {t("login_error")} 12 | 13 | {t("go_back_home")} 14 | 15 |
16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/app/(home)/login/layout.tsx: -------------------------------------------------------------------------------- 1 | import { getLocale, getTranslations } from "next-intl/server"; 2 | 3 | export async function generateMetadata() { 4 | const locale = await getLocale(); 5 | const t = await getTranslations({ locale, namespace: "Home" }); 6 | 7 | return { title: t("login") }; 8 | } 9 | 10 | export default function Layout({ children }: { children: React.ReactNode }) { 11 | return <>{children}; 12 | } 13 | -------------------------------------------------------------------------------- /src/app/(home)/login/page.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense } from "react"; 2 | import Link from "next/link"; 3 | import Background from "@/components/Background"; 4 | import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; 5 | import { Separator } from "@/components/ui/separator"; 6 | import Typography from "@/components/ui/typography"; 7 | import OAuthButton from "@/containers/login/OAuthButton"; 8 | import { useTranslations } from "next-intl"; 9 | 10 | export default function LoginPage() { 11 | const t = useTranslations("Home"); 12 | 13 | return ( 14 | 15 | 16 | 17 | 18 |
19 | {t("login")} 20 |
21 |
22 |
23 | 24 |
25 | 26 | 27 |
28 | 29 | {t("or")} 30 | 31 |
32 | {/* TODO: temp disable */} 33 | {/* */} 34 |
35 |
36 | 37 | 38 | {t.rich("before_login_statement", { 39 | terms: (chunks) => ( 40 | 41 | {chunks} 42 | 43 | ), 44 | privacy: (chunks) => ( 45 | 46 | {chunks} 47 | 48 | ), 49 | })} 50 | 51 | 52 |
53 | 54 |
55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /src/app/(home)/page.tsx: -------------------------------------------------------------------------------- 1 | import FAQ from "@/containers/landing/FAQ"; 2 | import Features from "@/containers/landing/Features"; 3 | import HeroTitle from "@/containers/landing/HeroTitle"; 4 | import { Pricing } from "@/containers/pricing"; 5 | 6 | export default function Index() { 7 | return ( 8 |
9 | 10 | 11 | 12 | 13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/app/api/auth/callback/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import { env } from "@/lib/env"; 3 | import { createClient } from "@/lib/supabase/server"; 4 | 5 | export async function GET(request: Request) { 6 | const { searchParams } = new URL(request.url); 7 | const code = searchParams.get("code"); 8 | const next = searchParams.get("next") ?? env.NEXT_PUBLIC_APP_URL; 9 | 10 | if (code) { 11 | const supabase = createClient(); 12 | const { error } = await supabase.auth.exchangeCodeForSession(code); 13 | if (!error) { 14 | console.log("User logged in successfully", next); 15 | return NextResponse.redirect(next); 16 | } 17 | } 18 | 19 | // return the user to an error page with instructions 20 | return NextResponse.redirect(`${origin}/login-error`); 21 | } 22 | -------------------------------------------------------------------------------- /src/app/apple-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loggerhead/json4u/HEAD/src/app/apple-icon.png -------------------------------------------------------------------------------- /src/app/editor/layout.tsx: -------------------------------------------------------------------------------- 1 | import { CookiesProvider } from "next-client-cookies/server"; 2 | import { getLocale, getTranslations } from "next-intl/server"; 3 | 4 | export async function generateMetadata() { 5 | const locale = await getLocale(); 6 | const t = await getTranslations({ locale, namespace: "Home" }); 7 | return { title: t("Editor") }; 8 | } 9 | 10 | export default function Layout({ children }: { children: React.ReactNode }) { 11 | return ( 12 |
13 | {children} 14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /src/app/editor/page.tsx: -------------------------------------------------------------------------------- 1 | import { Separator } from "@/components/ui/separator"; 2 | import { TooltipProvider } from "@/components/ui/tooltip"; 3 | import MainPanel from "@/containers/editor/panels/MainPanel"; 4 | import SideNav from "@/containers/editor/sidenav"; 5 | import { PricingOverlay } from "@/containers/pricing"; 6 | 7 | export default async function Page() { 8 | return ( 9 | 10 |
11 | 12 | 13 | 14 | 15 |
16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/app/global-error.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect } from "react"; 4 | import NextError from "next/error"; 5 | import * as Sentry from "@sentry/nextjs"; 6 | 7 | export default function GlobalError({ error }: { error: Error & { digest?: string } }) { 8 | useEffect(() => { 9 | Sentry.captureException(error); 10 | }, [error]); 11 | 12 | return ( 13 | 14 | 15 | {/* `NextError` is the default Next.js error page component. Its type 16 | definition requires a `statusCode` prop. However, since the App Router 17 | does not expose status codes for errors, we simply pass 0 to render a 18 | generic error message. */} 19 | 20 | 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/app/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/app/not-found.tsx: -------------------------------------------------------------------------------- 1 | import LinkButton from "@/components/LinkButton"; 2 | import Typography from "@/components/ui/typography"; 3 | import { useTranslations } from "next-intl"; 4 | 5 | export default function NotFound() { 6 | const t = useTranslations("NotFound"); 7 | 8 | return ( 9 |
10 |
11 | {t("not_found")} 12 | 13 | {t("go_back_home")} 14 | 15 |
16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/app/robots.ts: -------------------------------------------------------------------------------- 1 | import { MetadataRoute } from "next"; 2 | import { env } from "@/lib/env"; 3 | 4 | // https://nextjs.org/docs/app/api-reference/file-conventions/metadata/robots 5 | export default function robots(): MetadataRoute.Robots { 6 | return { 7 | rules: { 8 | userAgent: "*", 9 | allow: "/", 10 | }, 11 | sitemap: `${env.NEXT_PUBLIC_APP_URL}/sitemap.xml`, 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /src/app/sitemap.ts: -------------------------------------------------------------------------------- 1 | import { MetadataRoute } from "next"; 2 | import { env } from "@/lib/env"; 3 | 4 | // https://next-intl-docs.vercel.app/docs/environments/metadata-route-handlers 5 | export default function sitemap(): MetadataRoute.Sitemap { 6 | return [ 7 | getEntry("/"), 8 | getEntry("/editor"), 9 | getEntry("/changelog"), 10 | getEntry("/terms"), 11 | getEntry("/privacy"), 12 | getEntry("/tutorial"), 13 | ]; 14 | } 15 | 16 | function getEntry(pathname: string) { 17 | return { 18 | url: getUrl(pathname), 19 | lastModified: new Date(), 20 | alternates: { 21 | languages: { 22 | en: getUrl(pathname, "https://json4u.com"), 23 | zh: getUrl(pathname, "https://json4u.cn"), 24 | }, 25 | }, 26 | }; 27 | } 28 | 29 | function getUrl(pathname: string, appURL: string = env.NEXT_PUBLIC_APP_URL) { 30 | return `${appURL}/${pathname.startsWith("/") ? pathname.slice(1) : pathname}`; 31 | } 32 | -------------------------------------------------------------------------------- /src/components/Background.tsx: -------------------------------------------------------------------------------- 1 | interface BackgroundProps { 2 | className?: string; 3 | size?: number; 4 | variant?: "lines" | "dots"; 5 | } 6 | 7 | // stolen from xyflow 8 | export default function Background({ className, size = 15, variant = "lines" }: BackgroundProps) { 9 | return ( 10 | 21 | 30 | {variant === "lines" ? ( 31 | 32 | ) : ( 33 | 34 | )} 35 | 36 | 37 | 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /src/components/Container.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { type ReactNode, type HTMLAttributes } from "react"; 4 | import { Separator } from "@/components/ui/separator"; 5 | import { type CommandMode } from "@/stores/statusStore"; 6 | 7 | export function Container({ children, ...props }: HTMLAttributes) { 8 | return ( 9 |
10 | {children} 11 |
12 | ); 13 | } 14 | 15 | interface ContainerHeaderProps extends HTMLAttributes { 16 | mode?: CommandMode; 17 | modeHeaders?: Record; 18 | } 19 | 20 | export function ContainerHeader({ children, mode, modeHeaders, ...props }: ContainerHeaderProps) { 21 | return ( 22 | <> 23 |
24 | {children} 25 |
26 | 27 | 28 | ); 29 | } 30 | 31 | export function ContainerContent({ children, ...props }: HTMLAttributes) { 32 | return ( 33 |
34 | {children} 35 |
36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /src/components/LinkButton.tsx: -------------------------------------------------------------------------------- 1 | import { forwardRef } from "react"; 2 | import Link, { LinkProps } from "next/link"; 3 | import { Route } from "next/types"; 4 | import { Button, ButtonProps } from "./ui/button"; 5 | 6 | export type Href = LinkProps>["href"]; 7 | 8 | export interface LinkButtonProps extends ButtonProps { 9 | href: Href; 10 | newWindow?: boolean; 11 | } 12 | 13 | const LinkButton = forwardRef(({ href, newWindow, children, ...props }, ref) => { 14 | return ( 15 | 16 | 17 | 18 | ); 19 | }); 20 | LinkButton.displayName = "LinkButton"; 21 | 22 | export default LinkButton; 23 | -------------------------------------------------------------------------------- /src/components/Loading.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | import { LoaderCircle } from "lucide-react"; 5 | import { useTranslations } from "next-intl"; 6 | 7 | interface LoadingProps { 8 | className?: string; 9 | } 10 | export default function Loading({ className }: LoadingProps) { 11 | const t = useTranslations(); 12 | return ( 13 |
14 | 15 | {`${t("Loading")}...`} 16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/components/LoadingButton.tsx: -------------------------------------------------------------------------------- 1 | import { forwardRef } from "react"; 2 | import LoadingIcon from "./ui/LoadingIcon"; 3 | import { Button, ButtonProps } from "./ui/button"; 4 | 5 | export interface LoadingButtonProps extends ButtonProps { 6 | loading: boolean; 7 | } 8 | 9 | const LoadingButton = forwardRef(({ loading, children, ...props }, ref) => { 10 | return ( 11 | 15 | ); 16 | }); 17 | LoadingButton.displayName = "LoadingButton"; 18 | 19 | export default LoadingButton; 20 | -------------------------------------------------------------------------------- /src/components/LogoutButton.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { forwardRef, useState } from "react"; 4 | import { useRouter } from "next/navigation"; 5 | import LoadingButton from "@/components/LoadingButton"; 6 | import { supabase } from "@/lib/supabase/client"; 7 | import { cn, toastErr, toastSucc } from "@/lib/utils"; 8 | import { sendGAEvent } from "@next/third-parties/google"; 9 | import { useTranslations } from "next-intl"; 10 | import { ButtonProps } from "./ui/button"; 11 | 12 | const LogoutButton = forwardRef(({ className, ...props }, ref) => { 13 | const t = useTranslations("Home"); 14 | const router = useRouter(); 15 | const [loading, setLoading] = useState(false); 16 | 17 | return ( 18 | { 23 | setLoading(true); 24 | const { error } = await supabase.auth.signOut(); 25 | sendGAEvent("event", "logout", { error: error?.message ?? "succ" }); 26 | setLoading(false); 27 | router.refresh(); 28 | 29 | if (error) { 30 | toastErr(t("logout_failed")); 31 | } else { 32 | toastSucc(t("logout_succ")); 33 | } 34 | }} 35 | > 36 | {t("logout")} 37 | 38 | ); 39 | }); 40 | LogoutButton.displayName = "LogoutButton"; 41 | 42 | export default LogoutButton; 43 | -------------------------------------------------------------------------------- /src/components/Section.tsx: -------------------------------------------------------------------------------- 1 | import React, { forwardRef } from "react"; 2 | import { cn } from "@/lib/utils"; 3 | 4 | export interface SectionProps extends React.HTMLAttributes {} 5 | 6 | const Section = forwardRef(({ className, children, ...props }, ref) => { 7 | return ( 8 |
13 | {children} 14 |
15 | ); 16 | }); 17 | Section.displayName = "Section"; 18 | 19 | export default Section; 20 | -------------------------------------------------------------------------------- /src/components/UserAvatar.tsx: -------------------------------------------------------------------------------- 1 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; 2 | 3 | interface UserAvatarProps { 4 | name: string; 5 | url?: string; 6 | className?: string; 7 | } 8 | 9 | export default function UserAvatar({ name, url, className }: UserAvatarProps) { 10 | return ( 11 | 12 | 13 | 14 | 15 | 16 | {name?.charAt(0) ?? "U"} 17 | 18 | 19 | 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/components/icons/CircleCheck.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | 3 | export default function CircleCheck({ className }: { className?: string }) { 4 | return ( 5 | 11 | 20 | 21 | 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /src/components/icons/CircleX.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | 3 | export default function CircleX({ className }: { className?: string }) { 4 | return ( 5 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/components/icons/GitHub.tsx: -------------------------------------------------------------------------------- 1 | export default function GitHub({ className }: { className?: string }) { 2 | return ( 3 | 4 | {"GitHub"} 5 | 6 | 7 | ); 8 | } 9 | -------------------------------------------------------------------------------- /src/components/icons/Google.tsx: -------------------------------------------------------------------------------- 1 | export default function Google({ className }: { className?: string }) { 2 | return ( 3 | 17 | 23 | 28 | 33 | 38 | 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /src/components/icons/Logo.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | 3 | interface LogoProps { 4 | size?: number; 5 | className?: string; 6 | } 7 | 8 | export default function Logo({ size, className }: LogoProps) { 9 | return ( 10 | JSON For You logo 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/components/icons/Twitter.tsx: -------------------------------------------------------------------------------- 1 | export default function Twitter({ className }: { className?: string }) { 2 | return ( 3 | 4 | {"X"} 5 | 6 | 7 | ); 8 | } 9 | -------------------------------------------------------------------------------- /src/components/icons/Weibo.tsx: -------------------------------------------------------------------------------- 1 | export default function Weibo({ className }: { className?: string }) { 2 | return ( 3 | 10 | {"GitHub"} 11 | 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/components/ui/LoadingIcon.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | import { LoaderCircle } from "lucide-react"; 3 | 4 | export interface LoadingIconProps { 5 | loading?: boolean; 6 | className?: string; 7 | } 8 | 9 | const LoadingIcon = ({ loading = true, className }: LoadingIconProps) => ( 10 | 11 | ); 12 | export default LoadingIcon; 13 | -------------------------------------------------------------------------------- /src/components/ui/accordion.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AccordionPrimitive from "@radix-ui/react-accordion" 5 | import { ChevronDownIcon } from "@radix-ui/react-icons" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const Accordion = AccordionPrimitive.Root 10 | 11 | const AccordionItem = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef 14 | >(({ className, ...props }, ref) => ( 15 | 20 | )) 21 | AccordionItem.displayName = "AccordionItem" 22 | 23 | const AccordionTrigger = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, children, ...props }, ref) => ( 27 | 28 | svg]:rotate-180", 32 | className 33 | )} 34 | {...props} 35 | > 36 | {children} 37 | 38 | 39 | 40 | )) 41 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName 42 | 43 | const AccordionContent = React.forwardRef< 44 | React.ElementRef, 45 | React.ComponentPropsWithoutRef 46 | >(({ className, children, ...props }, ref) => ( 47 | 52 |
{children}
53 |
54 | )) 55 | AccordionContent.displayName = AccordionPrimitive.Content.displayName 56 | 57 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } 58 | -------------------------------------------------------------------------------- /src/components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AvatarPrimitive from "@radix-ui/react-avatar" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Avatar = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | )) 21 | Avatar.displayName = AvatarPrimitive.Root.displayName 22 | 23 | const AvatarImage = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, ...props }, ref) => ( 27 | 32 | )) 33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName 34 | 35 | const AvatarFallback = React.forwardRef< 36 | React.ElementRef, 37 | React.ComponentPropsWithoutRef 38 | >(({ className, ...props }, ref) => ( 39 | 47 | )) 48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName 49 | 50 | export { Avatar, AvatarImage, AvatarFallback } 51 | -------------------------------------------------------------------------------- /src/components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const badgeVariants = cva( 7 | "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", 8 | { 9 | variants: { 10 | variant: { 11 | default: "border-transparent bg-primary text-primary-foreground", 12 | secondary: "border-transparent bg-secondary text-secondary-foreground", 13 | destructive: "border-transparent bg-destructive text-destructive-foreground", 14 | outline: "text-foreground", 15 | }, 16 | }, 17 | defaultVariants: { 18 | variant: "default", 19 | }, 20 | } 21 | ) 22 | 23 | export interface BadgeProps 24 | extends React.HTMLAttributes, 25 | VariantProps {} 26 | 27 | function Badge({ className, variant, ...props }: BadgeProps) { 28 | return ( 29 |
30 | ) 31 | } 32 | 33 | export { Badge, badgeVariants } 34 | -------------------------------------------------------------------------------- /src/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )) 18 | Card.displayName = "Card" 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )) 30 | CardHeader.displayName = "CardHeader" 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLParagraphElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |

41 | )) 42 | CardTitle.displayName = "CardTitle" 43 | 44 | const CardDescription = React.forwardRef< 45 | HTMLParagraphElement, 46 | React.HTMLAttributes 47 | >(({ className, ...props }, ref) => ( 48 |

53 | )) 54 | CardDescription.displayName = "CardDescription" 55 | 56 | const CardContent = React.forwardRef< 57 | HTMLDivElement, 58 | React.HTMLAttributes 59 | >(({ className, ...props }, ref) => ( 60 |

61 | )) 62 | CardContent.displayName = "CardContent" 63 | 64 | const CardFooter = React.forwardRef< 65 | HTMLDivElement, 66 | React.HTMLAttributes 67 | >(({ className, ...props }, ref) => ( 68 |
73 | )) 74 | CardFooter.displayName = "CardFooter" 75 | 76 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 77 | -------------------------------------------------------------------------------- /src/components/ui/checkbox.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox" 5 | import { CheckIcon } from "@radix-ui/react-icons" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const Checkbox = React.forwardRef< 10 | React.ElementRef, 11 | React.ComponentPropsWithoutRef 12 | >(({ className, ...props }, ref) => ( 13 | 21 | 24 | 25 | 26 | 27 | )) 28 | Checkbox.displayName = CheckboxPrimitive.Root.displayName 29 | 30 | export { Checkbox } 31 | -------------------------------------------------------------------------------- /src/components/ui/collapsible.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as CollapsiblePrimitive from "@radix-ui/react-collapsible" 4 | 5 | const Collapsible = CollapsiblePrimitive.Root 6 | 7 | const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger 8 | 9 | const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent 10 | 11 | export { Collapsible, CollapsibleTrigger, CollapsibleContent } 12 | -------------------------------------------------------------------------------- /src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ) 21 | } 22 | ) 23 | Input.displayName = "Input" 24 | 25 | export { Input } 26 | -------------------------------------------------------------------------------- /src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | import { cva, type VariantProps } from "class-variance-authority" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const labelVariants = cva( 10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 11 | ) 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef & 16 | VariantProps 17 | >(({ className, ...props }, ref) => ( 18 | 23 | )) 24 | Label.displayName = LabelPrimitive.Root.displayName 25 | 26 | export { Label } 27 | -------------------------------------------------------------------------------- /src/components/ui/popover.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as PopoverPrimitive from "@radix-ui/react-popover" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Popover = PopoverPrimitive.Root 9 | 10 | const PopoverTrigger = PopoverPrimitive.Trigger 11 | 12 | const PopoverAnchor = PopoverPrimitive.Anchor 13 | 14 | const PopoverContent = React.forwardRef< 15 | React.ElementRef, 16 | React.ComponentPropsWithoutRef 17 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( 18 | 28 | )) 29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName 30 | 31 | export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor } 32 | -------------------------------------------------------------------------------- /src/components/ui/resizable.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { DragHandleDots2Icon } from "@radix-ui/react-icons" 4 | import * as ResizablePrimitive from "react-resizable-panels" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const ResizablePanelGroup = ({ 9 | className, 10 | ...props 11 | }: React.ComponentProps) => ( 12 | 19 | ) 20 | 21 | const ResizablePanel = ResizablePrimitive.Panel 22 | 23 | const ResizableHandle = ({ 24 | withHandle, 25 | className, 26 | ...props 27 | }: React.ComponentProps & { 28 | withHandle?: boolean 29 | }) => ( 30 | div]:rotate-90", 33 | className 34 | )} 35 | {...props} 36 | > 37 | {withHandle && ( 38 |
39 | 40 |
41 | )} 42 |
43 | ) 44 | 45 | export { ResizablePanelGroup, ResizablePanel, ResizableHandle } 46 | -------------------------------------------------------------------------------- /src/components/ui/search/CommandSearchInput.tsx: -------------------------------------------------------------------------------- 1 | import SearchInput from "@/components/ui/search/SearchInput"; 2 | import { type MessageKey } from "@/global"; 3 | import { type Command, useEditorStore } from "@/stores/editorStore"; 4 | import fuzzysort from "fuzzysort"; 5 | import { useTranslations } from "next-intl"; 6 | import { useShallow } from "zustand/shallow"; 7 | 8 | export default function CommandSearch() { 9 | const t = useTranslations(); 10 | const { commands, runCommand } = useEditorStore( 11 | useShallow((state) => ({ 12 | commands: state.commands, 13 | runCommand: state.runCommand, 14 | })), 15 | ); 16 | const displayCommands = commands.filter((c) => !c.hidden); 17 | 18 | const search = (input: string) => 19 | input.trim() 20 | ? fuzzysort 21 | .go(input, displayCommands, { 22 | keys: [(cmd) => cmd.id, (cmd) => t(cmd.id)], 23 | }) 24 | .map((r) => r.obj) 25 | : displayCommands; 26 | 27 | return ( 28 | runCommand(cmd.id)} 36 | itemHeight={32} 37 | Item={Item} 38 | /> 39 | ); 40 | } 41 | 42 | function Item({ id, Icon }: Command) { 43 | const t = useTranslations(); 44 | return ( 45 |
46 | {Icon && } 47 | {t(id as MessageKey)} 48 |
49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /src/components/ui/search/ViewSearchInput.tsx: -------------------------------------------------------------------------------- 1 | import { LeftTruncate } from "@/components/ui/truncate"; 2 | import { genValueAttrs } from "@/lib/graph/layout"; 3 | import { toPath } from "@/lib/idgen"; 4 | import { hasChildren } from "@/lib/parser"; 5 | import { cn } from "@/lib/utils"; 6 | import { type SearchResult } from "@/lib/worker/stores/types"; 7 | import { useStatusStore } from "@/stores/statusStore"; 8 | import { getTree } from "@/stores/treeStore"; 9 | import SearchInput from "./SearchInput"; 10 | 11 | export default function ViewSearchInput() { 12 | const setRevealPosition = useStatusStore((state) => state.setRevealPosition); 13 | 14 | return ( 15 | window.worker?.searchInView(input)} 19 | onSelect={(item) => setRevealPosition({ treeNodeId: item.id, target: item.revealTarget, from: "search" })} 20 | Item={Item} 21 | itemHeight={48} 22 | placeholder={"search_json"} 23 | bindShortcut="F" 24 | /> 25 | ); 26 | } 27 | 28 | function Item(props: SearchResult) { 29 | const { revealTarget, id, label } = props; 30 | const node = getTree().node(id); 31 | 32 | if (!node) { 33 | return null; 34 | } 35 | 36 | const pathStr = ["$", ...toPath(id)].join(" > "); 37 | let className = ""; 38 | 39 | if (revealTarget === "value") { 40 | const { className: cls } = genValueAttrs(node); 41 | className = cls; 42 | } else if (!hasChildren(node)) { 43 | className = "text-hl-key"; 44 | } 45 | 46 | return ( 47 |
48 |
{label}
49 | 50 |
51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /src/components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SeparatorPrimitive from "@radix-ui/react-separator" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Separator = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >( 12 | ( 13 | { className, orientation = "horizontal", decorative = true, ...props }, 14 | ref 15 | ) => ( 16 | 27 | ) 28 | ) 29 | Separator.displayName = SeparatorPrimitive.Root.displayName 30 | 31 | export { Separator } 32 | -------------------------------------------------------------------------------- /src/components/ui/sonner.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useTheme } from "next-themes" 4 | import { Toaster as Sonner } from "sonner" 5 | 6 | type ToasterProps = React.ComponentProps 7 | 8 | const Toaster = ({ ...props }: ToasterProps) => { 9 | const { theme = "system" } = useTheme() 10 | 11 | return ( 12 | 28 | ) 29 | } 30 | 31 | export { Toaster } 32 | -------------------------------------------------------------------------------- /src/components/ui/switch.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SwitchPrimitives from "@radix-ui/react-switch" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Switch = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | 25 | 26 | )) 27 | Switch.displayName = SwitchPrimitives.Root.displayName 28 | 29 | export { Switch } 30 | -------------------------------------------------------------------------------- /src/components/ui/tabs.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as TabsPrimitive from "@radix-ui/react-tabs" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Tabs = TabsPrimitive.Root 9 | 10 | const TabsList = React.forwardRef< 11 | React.ElementRef, 12 | React.ComponentPropsWithoutRef 13 | >(({ className, ...props }, ref) => ( 14 | 22 | )) 23 | TabsList.displayName = TabsPrimitive.List.displayName 24 | 25 | const TabsTrigger = React.forwardRef< 26 | React.ElementRef, 27 | React.ComponentPropsWithoutRef 28 | >(({ className, ...props }, ref) => ( 29 | 37 | )) 38 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName 39 | 40 | const TabsContent = React.forwardRef< 41 | React.ElementRef, 42 | React.ComponentPropsWithoutRef 43 | >(({ className, ...props }, ref) => ( 44 | 52 | )) 53 | TabsContent.displayName = TabsPrimitive.Content.displayName 54 | 55 | export { Tabs, TabsList, TabsTrigger, TabsContent } 56 | -------------------------------------------------------------------------------- /src/components/ui/toggle.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as TogglePrimitive from "@radix-ui/react-toggle" 5 | import { cva, type VariantProps } from "class-variance-authority" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const toggleVariants = cva( 10 | "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors transform scale-100 active:scale-95 transition-transform duration-100 hover:bg-muted hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-input data-[state=on]:text-accent-foreground", 11 | { 12 | variants: { 13 | variant: { 14 | default: "bg-transparent", 15 | outline: 16 | "border border-input bg-transparent shadow-sm hover:bg-accent hover:text-accent-foreground", 17 | }, 18 | size: { 19 | default: "h-9 px-3", 20 | sm: "h-8 px-2", 21 | lg: "h-10 px-3", 22 | xs: "h-6 px-2", 23 | }, 24 | }, 25 | defaultVariants: { 26 | variant: "default", 27 | size: "sm", 28 | }, 29 | } 30 | ) 31 | 32 | const Toggle = React.forwardRef< 33 | React.ElementRef, 34 | React.ComponentPropsWithoutRef & 35 | VariantProps 36 | >(({ className, variant, size, ...props }, ref) => ( 37 | 42 | )) 43 | 44 | Toggle.displayName = TogglePrimitive.Root.displayName 45 | 46 | export { Toggle, toggleVariants } 47 | -------------------------------------------------------------------------------- /src/components/ui/tooltip.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const TooltipProvider = TooltipPrimitive.Provider 9 | 10 | const Tooltip = TooltipPrimitive.Root 11 | 12 | const TooltipTrigger = TooltipPrimitive.Trigger 13 | 14 | const TooltipContent = React.forwardRef< 15 | React.ElementRef, 16 | React.ComponentPropsWithoutRef 17 | >(({ className, sideOffset = 4, ...props }, ref) => ( 18 | 27 | )) 28 | TooltipContent.displayName = TooltipPrimitive.Content.displayName 29 | 30 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } 31 | -------------------------------------------------------------------------------- /src/components/ui/truncate.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { cn } from "@/lib/utils"; 3 | 4 | type TruncateProps = React.ComponentPropsWithoutRef<"div"> & { 5 | text: string; 6 | }; 7 | 8 | // https://stackoverflow.com/a/71864029/2934618 9 | const LeftTruncate = React.forwardRef(({ className, text, ...props }, ref) => { 10 | const blankNum = text.length - text.trimEnd().length; 11 | 12 | return ( 13 |
14 |

15 | {text.trim()} 16 |

17 | {blankNum > 0 &&
 
} 18 |
19 | ); 20 | }); 21 | LeftTruncate.displayName = "LeftTruncate"; 22 | 23 | const RightTruncate = React.forwardRef(({ className, text, ...props }, ref) => { 24 | const blankNum = text.length - text.trimStart().length; 25 | 26 | return ( 27 |
28 | {blankNum > 0 &&
 
} 29 | {text} 30 |
31 | ); 32 | }); 33 | RightTruncate.displayName = "RightTruncate"; 34 | 35 | export { LeftTruncate, RightTruncate }; 36 | -------------------------------------------------------------------------------- /src/components/ui/typography.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | import { 3 | cva, 4 | type VariantProps 5 | } from "class-variance-authority" 6 | import React from "react" 7 | 8 | export const typographyVariants = cva("text-xl", { 9 | variants: { 10 | variant: { 11 | h1: "md:text-6xl scroll-m-20 text-4xl font-extrabold tracking-tight", 12 | h2: "scroll-m-20 pb-2 md:text-5xl text-3xl font-semibold tracking-tight first:mt-0", 13 | h3: "scroll-m-20 text-2xl font-semibold tracking-tight", 14 | h4: "scroll-m-20 text-xl font-semibold tracking-tight", 15 | h5: "scroll-m-18 text-lg tracking-tight text-minor", 16 | p: "md:text-sm text-sm leading-7 text-minor" 17 | }, 18 | affects: { 19 | default: "", 20 | lead: "md:text-xl text-xl font-normal", 21 | large: "md:text-lg text-lg font-semibold", 22 | small: "md:text-sm text-sm font-medium leading-none", 23 | muted: "md:text-sm text-sm text-muted-foreground", 24 | xs: "md:text-xs text-xs text-muted-foreground", 25 | removePMargin: "[&:not(:first-child)]:mt-0", 26 | bold: "text-sm font-semibold", 27 | } 28 | }, 29 | defaultVariants: { 30 | variant: "p", 31 | affects: "default" 32 | } 33 | }) 34 | 35 | export interface TypographyProps 36 | extends React.HTMLAttributes, 37 | VariantProps {} 38 | 39 | const Typography = React.forwardRef< 40 | HTMLHeadingElement, 41 | TypographyProps 42 | >(({ className, variant, affects, ...props }, ref) => { 43 | const Comp = variant ?? "p" 44 | return ( 45 | 53 | ) 54 | }) 55 | Typography.displayName = "Typography" 56 | 57 | export default Typography 58 | -------------------------------------------------------------------------------- /src/containers/editor/components/CollapseHint.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | import { ArrowLeftToLine, ArrowRightToLine } from "lucide-react"; 3 | import { useTranslations } from "next-intl"; 4 | 5 | /** 6 | * @description A hint component to indicate that the panel can be collapsed. 7 | * @param {object} props - The component props. 8 | * @param {'left' | 'right'} props.side - The side to display the hint on. 9 | * @returns {JSX.Element} The hint component. 10 | */ 11 | export function CollapseHint({ side }: { side: "left" | "right" }) { 12 | const t = useTranslations(); 13 | 14 | return ( 15 |
22 | {side === "left" ? : } 23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/containers/editor/components/FullScreenButton.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { useState } from "react"; 3 | import { Button } from "@/components/ui/button"; 4 | import { Expand, Shrink } from "lucide-react"; 5 | import { useTranslations } from "next-intl"; 6 | 7 | export default function FullScreenButton() { 8 | const [show, setShow] = React.useState(false); 9 | 10 | React.useEffect(() => { 11 | setShow(true); 12 | }, []); 13 | 14 | return show ? : null; 15 | } 16 | 17 | function ClientFullScreenButton() { 18 | const t = useTranslations(); 19 | const [fullscreen, setFullscreen] = useState(false); 20 | const Icon = fullscreen ? Shrink : Expand; 21 | 22 | const el = document.documentElement; 23 | const requestFullscreenFn = 24 | el?.requestFullscreen || 25 | (el as any).webkitRequestFullscreen || 26 | (el as any).mozRequestFullScreen || 27 | (el as any).msRequestFullscreen; 28 | const exitFullscreenFn = 29 | document.exitFullscreen || 30 | (document as any).webkitExitFullscreen || 31 | (document as any).mozCancelFullScreen || 32 | (document as any).msExitFullscreen; 33 | 34 | if (!requestFullscreenFn || !exitFullscreenFn) { 35 | return null; 36 | } 37 | 38 | return ( 39 | 55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /src/containers/editor/components/LeftPanelButtons.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { Button } from "@/components/ui/button"; 3 | import { toastSucc } from "@/lib/utils"; 4 | import { useEditor } from "@/stores/editorStore"; 5 | import { Copy, Check } from "lucide-react"; 6 | import { useTranslations } from "next-intl"; 7 | 8 | export default function LeftPanelButtons() { 9 | const t = useTranslations(); 10 | const editor = useEditor()!; 11 | const [copied, setCopied] = useState(false); 12 | const Icon = copied ? Check : Copy; 13 | 14 | return ( 15 |
16 | 31 |
32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /src/containers/editor/components/RightPanelButtons.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | import ViewSearchInput from "@/components/ui/search/ViewSearchInput"; 3 | import { Switch } from "@/components/ui/switch"; 4 | import SwapButton from "@/containers/editor/mode/SwapButton"; 5 | import { ViewMode } from "@/lib/db/config"; 6 | import { useEditorStore } from "@/stores/editorStore"; 7 | import { useConfigFromCookies } from "@/stores/hook"; 8 | import { useStatusStore } from "@/stores/statusStore"; 9 | import { includes } from "lodash-es"; 10 | import { useTranslations } from "next-intl"; 11 | import { useShallow } from "zustand/shallow"; 12 | import FullScreenButton from "./FullScreenButton"; 13 | 14 | export default function RightPanelButtons({ viewMode }: { viewMode: ViewMode }) { 15 | const cc = useConfigFromCookies(); 16 | const t = useTranslations(); 17 | const runCommand = useEditorStore((state) => state.runCommand); 18 | const { enableTextCompare, setEnableTextCompare } = useStatusStore( 19 | useShallow((state) => ({ 20 | enableTextCompare: state._hasHydrated ? state.enableTextCompare : cc.enableTextCompare, 21 | setEnableTextCompare: state.setEnableTextCompare, 22 | })), 23 | ); 24 | 25 | return ( 26 |
27 | {viewMode === ViewMode.Text && ( 28 | <> 29 |
30 | 31 | 34 |
35 | 36 | 37 | )} 38 | {includes([ViewMode.Graph, ViewMode.Table], viewMode) && } 39 | 40 |
41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /src/containers/editor/components/ViewTabs.tsx: -------------------------------------------------------------------------------- 1 | import { TabsContent, TabsTrigger } from "@/components/ui/tabs"; 2 | import { ViewMode } from "@/lib/db/config"; 3 | import { Table2, Text, Waypoints } from "lucide-react"; 4 | import { useTranslations } from "next-intl"; 5 | 6 | const viewMode2Icon = { 7 | [ViewMode.Text]: Text, 8 | [ViewMode.Graph]: Waypoints, 9 | [ViewMode.Table]: Table2, 10 | }; 11 | 12 | export function TabIcon({ viewMode, className }: { viewMode: ViewMode; className: string }) { 13 | const t = useTranslations(); 14 | const Icon = viewMode2Icon[viewMode]; 15 | 16 | return ( 17 | 18 | 19 | 20 | ); 21 | } 22 | 23 | export function TabView({ viewMode, children }: { viewMode: ViewMode; children: React.ReactNode }) { 24 | // `data-[state=inactive]` used for fix https://github.com/radix-ui/primitives/issues/1155#issuecomment-2041571341 25 | return ( 26 | 27 | {children} 28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /src/containers/editor/components/index.ts: -------------------------------------------------------------------------------- 1 | export { CollapseHint } from "./CollapseHint"; 2 | export { default as StatusBar } from "./StatusBar"; 3 | export { default as LeftPanelButtons } from "./LeftPanelButtons"; 4 | export { default as FullScreenButton } from "./FullScreenButton"; 5 | export * from "./ViewTabs"; 6 | export { default as InitialSetup } from "./InitialSetup"; 7 | -------------------------------------------------------------------------------- /src/containers/editor/editor/data.ts: -------------------------------------------------------------------------------- 1 | const example = `{ 2 | "Aidan Gillen": { 3 | "array": [ 4 | "Game of Thron\\"es", 5 | "The Wire" 6 | ], 7 | "string": "some string", 8 | "int": 2, 9 | "aboolean": true, 10 | "boolean": true, 11 | "null": null, 12 | "a_null": null, 13 | "another_null": "null check", 14 | "object": { 15 | "foo": "bar", 16 | "object1": { 17 | "new prop1": "new prop value" 18 | }, 19 | "object2": { 20 | "new prop1": "new prop value" 21 | }, 22 | "object3": { 23 | "new prop1": "new prop value" 24 | }, 25 | "object4": { 26 | "new prop1": "new prop value" 27 | } 28 | } 29 | }, 30 | "Amy Ryan": { 31 | "one": "In Treatment", 32 | "two": "The Wire" 33 | }, 34 | "Annie Fitzgerald": [ 35 | "Big Love", 36 | "True Blood" 37 | ], 38 | "Anwan Glover": [ 39 | "Treme", 40 | "The Wire" 41 | ], 42 | "Alexander Skarsgard": [ 43 | "Generation Kill", 44 | "True Blood" 45 | ], 46 | "Clarke Peters": null 47 | }`; 48 | 49 | export { example }; 50 | -------------------------------------------------------------------------------- /src/containers/editor/editor/index.tsx: -------------------------------------------------------------------------------- 1 | import Editor from "./Editor"; 2 | 3 | export default Editor; 4 | -------------------------------------------------------------------------------- /src/containers/editor/graph/Handle.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from "react"; 2 | import { computeSourceHandleOffset, computeTargetHandleOffset } from "@/lib/graph/layout"; 3 | import type { GraphNodeId } from "@/lib/idgen"; 4 | import { Handle, Position, useReactFlow } from "@xyflow/react"; 5 | import { useTranslations } from "next-intl"; 6 | 7 | interface TargetHandleProps { 8 | childrenNum: number; 9 | } 10 | 11 | export const TargetHandle = memo(({ childrenNum }: TargetHandleProps) => { 12 | const top = computeTargetHandleOffset(childrenNum); 13 | return ; 14 | }); 15 | TargetHandle.displayName = "TargetHandle"; 16 | 17 | interface SourceHandleProps { 18 | id: string; 19 | nodeId: GraphNodeId; 20 | indexInParent: number; 21 | isChildrenHidden?: boolean; 22 | } 23 | 24 | export const SourceHandle = memo(({ id, nodeId, indexInParent, isChildrenHidden }: SourceHandleProps) => { 25 | const top = indexInParent !== undefined ? computeSourceHandleOffset(indexInParent) : undefined; 26 | const backgroundColor = isChildrenHidden ? "rgb(156 163 175)" : undefined; 27 | const { setNodes, setEdges } = useReactFlow(); 28 | const t = useTranslations(); 29 | 30 | return ( 31 | { 39 | e.stopPropagation(); 40 | const { nodes, edges } = await window.worker.toggleGraphNodeHidden(nodeId, id); 41 | setNodes(nodes); 42 | setEdges(edges); 43 | }} 44 | /> 45 | ); 46 | }); 47 | SourceHandle.displayName = "SourceHandle"; 48 | -------------------------------------------------------------------------------- /src/containers/editor/graph/MouseButton.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { detectOS } from "@/lib/utils"; 3 | import { useStatusStore } from "@/stores/statusStore"; 4 | import { ControlButton } from "@xyflow/react"; 5 | import { Mouse, Touchpad } from "lucide-react"; 6 | import { useShallow } from "zustand/shallow"; 7 | 8 | export default function MouseButton() { 9 | const { isTouchpad, setIsTouchpad } = useStatusStore( 10 | useShallow((state) => ({ 11 | isTouchpad: state.isTouchpad, 12 | setIsTouchpad: state.setIsTouchpad, 13 | })), 14 | ); 15 | 16 | useEffect(() => { 17 | if (isTouchpad === undefined) { 18 | setIsTouchpad(detectOS() === "Mac"); 19 | } 20 | }, []); 21 | 22 | return ( 23 | setIsTouchpad(!isTouchpad)}> 24 | {isTouchpad ? : } 25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /src/containers/editor/graph/Popover.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from "react"; 2 | import { globalStyle } from "@/lib/graph/style"; 3 | import { cn } from "@/lib/utils"; 4 | import * as Tooltip from "@radix-ui/react-tooltip"; 5 | 6 | interface PopoverProps extends React.PropsWithChildren { 7 | text: string; 8 | hlClassNames: string[]; 9 | width?: number; 10 | } 11 | 12 | const Popover = memo(({ text, hlClassNames, width, children }: PopoverProps) => { 13 | const maxWidth = width ?? globalStyle.maxValueWidth; 14 | return ( 15 | 16 | 17 | {children} 18 | 19 | 20 |
21 |
22 | {text} 23 |
24 |
25 |
26 |
27 |
28 |
29 | ); 30 | }); 31 | 32 | Popover.displayName = "Popover"; 33 | 34 | export default Popover; 35 | -------------------------------------------------------------------------------- /src/containers/editor/graph/useClickNode.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, type MouseEvent } from "react"; 2 | import type { RevealFrom, RevealTarget } from "@/lib/graph/types"; 3 | import { useStatusStore } from "@/stores/statusStore"; 4 | import { debounce } from "lodash-es"; 5 | 6 | export default function useClickNode() { 7 | const setRevealPosition = useStatusStore((state) => state.setRevealPosition); 8 | const delaySetRevealPosition = debounce(setRevealPosition, 200, { trailing: true }); 9 | 10 | return { 11 | onClick: useCallback( 12 | async (e: MouseEvent, treeNodeId: string, target: RevealTarget, from: RevealFrom) => { 13 | e.stopPropagation(); 14 | const pos = { treeNodeId, target, from }; 15 | console.l("set reveal position:", pos); 16 | delaySetRevealPosition(pos); 17 | }, 18 | [setRevealPosition, delaySetRevealPosition], 19 | ), 20 | cancelClickNode: delaySetRevealPosition.cancel, 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /src/containers/editor/hooks/useObserveResize.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { useStatusStore } from "@/stores/statusStore"; 3 | import { useShallow } from "zustand/shallow"; 4 | 5 | export function useObserveResize(leftPanelId: string, rightPanelId: string) { 6 | const { setLeftPanelWidth, setRightPanelWidth } = useStatusStore( 7 | useShallow((state) => ({ 8 | setLeftPanelWidth: state.setLeftPanelWidth, 9 | setRightPanelWidth: state.setRightPanelWidth, 10 | })), 11 | ); 12 | 13 | useEffect(() => { 14 | const leftPanel = document.getElementById(leftPanelId)!; 15 | const rightPanel = document.getElementById(rightPanelId)!; 16 | setLeftPanelWidth(leftPanel.offsetWidth); 17 | setRightPanelWidth(rightPanel.offsetWidth); 18 | 19 | const resizeObserver = new ResizeObserver((entries) => { 20 | for (const entry of entries) { 21 | if (entry.target.id === leftPanelId) { 22 | setLeftPanelWidth(entry.contentRect.width); 23 | } else if (entry.target.id === rightPanelId) { 24 | setRightPanelWidth(entry.contentRect.width); 25 | } 26 | } 27 | }); 28 | 29 | resizeObserver.observe(leftPanel); 30 | resizeObserver.observe(rightPanel); 31 | }, []); 32 | } 33 | -------------------------------------------------------------------------------- /src/containers/editor/mode/JsonPathInput.tsx: -------------------------------------------------------------------------------- 1 | import { type ComponentPropsWithoutRef, type ElementRef, type FC, forwardRef } from "react"; 2 | import { Input } from "@/components/ui/input"; 3 | import { ViewMode } from "@/lib/db/config"; 4 | import { toastErr, toastSucc } from "@/lib/utils"; 5 | import { useEditorStore } from "@/stores/editorStore"; 6 | import { useStatusStore } from "@/stores/statusStore"; 7 | import { useTranslations } from "next-intl"; 8 | import { useShallow } from "zustand/shallow"; 9 | import InputBox from "./InputBox"; 10 | 11 | function useFilter() { 12 | const t = useTranslations(); 13 | const secondary = useEditorStore((state) => state.secondary); 14 | const { viewMode, setViewMode } = useStatusStore( 15 | useShallow((state) => ({ 16 | viewMode: state.viewMode, 17 | setViewMode: state.setViewMode, 18 | setCommandMode: state.setCommandMode, 19 | })), 20 | ); 21 | 22 | return async (filter: string) => { 23 | filter = filter.trim(); 24 | 25 | if (!filter) { 26 | toastSucc(t("cmd_exec_succ", { name: t("json_path_filter") })); 27 | return; 28 | } else if (!window.worker) { 29 | toastErr(t("cmd_exec_fail", { name: t("json_path_filter") })); 30 | return; 31 | } 32 | 33 | if (viewMode != ViewMode.Text) { 34 | setViewMode(ViewMode.Text); 35 | } 36 | 37 | const { output, error } = await window.worker.jsonPath(filter); 38 | 39 | if (error) { 40 | toastErr(t("cmd_exec_fail", { name: t("json_path_filter") }) + ": " + filter); 41 | } else { 42 | await secondary!.parseAndSet(output ?? "", {}, false); 43 | toastSucc(t("cmd_exec_succ", { name: t("json_path_filter") })); 44 | } 45 | }; 46 | } 47 | 48 | const JsonPathInput: FC = forwardRef, ComponentPropsWithoutRef>( 49 | ({ className, ...props }, ref) => { 50 | const t = useTranslations(); 51 | const filter = useFilter(); 52 | 53 | return ; 54 | }, 55 | ); 56 | 57 | JsonPathInput.displayName = "JsonPathInput"; 58 | export default JsonPathInput; 59 | -------------------------------------------------------------------------------- /src/containers/editor/mode/SwapButton.tsx: -------------------------------------------------------------------------------- 1 | import { forwardRef, useEffect } from "react"; 2 | import { Button, ButtonProps } from "@/components/ui/button"; 3 | import { useEditorStore } from "@/stores/editorStore"; 4 | import { ArrowRightLeft } from "lucide-react"; 5 | import { useTranslations } from "next-intl"; 6 | 7 | const SwapButton = forwardRef( 8 | ({ className, variant, size, asChild = false, ...props }, ref) => { 9 | const t = useTranslations(); 10 | const runCommand = useEditorStore((state) => state.runCommand); 11 | 12 | useEffect(() => { 13 | if (!runCommand) return; 14 | 15 | const onKeyDown = (e: KeyboardEvent) => { 16 | if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) { 17 | e.preventDefault(); 18 | e.stopPropagation(); 19 | runCommand("swapLeftRight"); 20 | } 21 | }; 22 | 23 | document.addEventListener("keydown", onKeyDown); 24 | return () => document.removeEventListener("keydown", onKeyDown); 25 | }, [runCommand]); 26 | 27 | return ( 28 | 37 | ); 38 | }, 39 | ); 40 | 41 | SwapButton.displayName = "SwapButton"; 42 | export default SwapButton; 43 | -------------------------------------------------------------------------------- /src/containers/editor/panels/LeftPanel.tsx: -------------------------------------------------------------------------------- 1 | import { Container, ContainerContent, ContainerHeader } from "@/components/Container"; 2 | import CommandSearchInput from "@/components/ui/search/CommandSearchInput"; 3 | import { LeftPanelButtons } from "@/containers/editor/components"; 4 | import Editor from "@/containers/editor/editor/Editor"; 5 | 6 | export default function LeftPanel() { 7 | return ( 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/containers/editor/panels/RightPanel.tsx: -------------------------------------------------------------------------------- 1 | import { Container, ContainerContent, ContainerHeader } from "@/components/Container"; 2 | import { Tabs, TabsList } from "@/components/ui/tabs"; 3 | import { TabIcon, TabView } from "@/containers/editor/components"; 4 | import RightPanelButtons from "@/containers/editor/components/RightPanelButtons"; 5 | import Editor from "@/containers/editor/editor/Editor"; 6 | import Graph from "@/containers/editor/graph/Graph"; 7 | import { Table } from "@/containers/editor/table/Table"; 8 | import { ViewMode, ViewModeValue } from "@/lib/db/config"; 9 | import { useConfigFromCookies } from "@/stores/hook"; 10 | import { useStatusStore } from "@/stores/statusStore"; 11 | import { useShallow } from "zustand/shallow"; 12 | 13 | export default function RightPanel() { 14 | const cc = useConfigFromCookies(); 15 | const { viewMode, setViewMode } = useStatusStore( 16 | useShallow((state) => ({ 17 | viewMode: state._hasHydrated ? state.viewMode : cc.viewMode, 18 | setViewMode: state.setViewMode, 19 | })), 20 | ); 21 | 22 | return ( 23 | setViewMode(mode as ViewModeValue)}> 24 | 25 | 26 | 27 | {[ViewMode.Text, ViewMode.Graph, ViewMode.Table].map((mode) => ( 28 | 29 | ))} 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /src/containers/editor/sidenav/BasePopover.tsx: -------------------------------------------------------------------------------- 1 | import { type MessageKey } from "@/global"; 2 | import { cn } from "@/lib/utils"; 3 | import { useTranslations } from "next-intl"; 4 | 5 | export interface BasePopoverProps { 6 | title: MessageKey; 7 | children: React.ReactNode; 8 | optionsNode?: React.ReactNode; 9 | extraNode?: React.ReactNode; 10 | className?: string; 11 | } 12 | 13 | export default function BasePopover({ title, className, children, optionsNode, extraNode }: BasePopoverProps) { 14 | const t = useTranslations(); 15 | 16 | return ( 17 |
18 |
19 |

{t(title)}

20 |
{children}
21 |
22 | {optionsNode &&
{optionsNode}
} 23 | {extraNode} 24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/containers/editor/sidenav/Button.tsx: -------------------------------------------------------------------------------- 1 | import { ElementRef, forwardRef } from "react"; 2 | import { Button as RButton } from "@/components/ui/button"; 3 | import { cn } from "@/lib/utils"; 4 | import { cva, type VariantProps } from "class-variance-authority"; 5 | import IconLabel from "./IconLabel"; 6 | 7 | export const btnVariants = cva( 8 | "w-6 h-6 relative group-data-[expanded=true]:w-full transition-all duration-200 flex items-center rounded-sm group-data-[expanded=false]:justify-center group-data-[expanded=true]:-space-x-2 hover:bg-surface-200", 9 | ); 10 | 11 | interface ButtonProps extends VariantProps { 12 | title: string; 13 | onClick?: () => void; 14 | icon: React.ReactNode; 15 | className?: string; 16 | } 17 | 18 | const Button = forwardRef, ButtonProps>(({ icon, title, onClick, className }, ref) => ( 19 | 20 | 21 | 22 | )); 23 | Button.displayName = "Button"; 24 | 25 | export default Button; 26 | -------------------------------------------------------------------------------- /src/containers/editor/sidenav/FileTypeSelect.tsx: -------------------------------------------------------------------------------- 1 | import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; 2 | 3 | export type FileType = "JSON" | "CSV"; 4 | 5 | export interface FileTypeSelectProps { 6 | fileType: FileType; 7 | setFileType: (v: FileType) => void; 8 | } 9 | 10 | export function FileTypeSelect({ fileType, setFileType }: FileTypeSelectProps) { 11 | return ( 12 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /src/containers/editor/sidenav/IconLabel.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | 3 | interface IconLabelProps extends LabelProps { 4 | icon: React.ReactNode; 5 | } 6 | 7 | export default function IconLabel({ icon, ...props }: IconLabelProps) { 8 | return ( 9 | <> 10 | {icon} 11 |