├── .env.example ├── .eslintrc.json ├── .gitignore ├── LICENSE ├── README.md ├── components.json ├── generate-env.sh ├── next.config.mjs ├── package.json ├── pnpm-lock.yaml ├── postcss.config.mjs ├── prisma ├── migrations │ ├── 20241031154736_init_json_documents │ │ └── migration.sql │ └── migration_lock.toml └── schema.prisma ├── public └── screenshot.png ├── src ├── app │ ├── (application) │ │ ├── admin │ │ │ └── page.tsx │ │ ├── explore │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ └── stats │ │ │ └── page.tsx │ ├── about │ │ ├── page.client.tsx │ │ └── page.tsx │ ├── api │ │ ├── cleanup │ │ │ └── route.ts │ │ ├── openai │ │ │ └── route.ts │ │ ├── share │ │ │ └── route.ts │ │ └── stats │ │ │ └── route.ts │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ ├── page.tsx │ └── s │ │ └── [id] │ │ └── page.tsx ├── components │ ├── admin-grid.tsx │ ├── api-key-dialog.tsx │ ├── app-sidebar.tsx │ ├── explore-grid.tsx │ ├── json-explanation.tsx │ ├── json-grid.tsx │ ├── json-header.tsx │ ├── json-input.tsx │ ├── json-tree.tsx │ ├── json-visualizer.tsx │ ├── layout │ │ └── header.tsx │ ├── learn-more-popup.tsx │ ├── loader.tsx │ ├── mode-toggle.tsx │ ├── nav-main.tsx │ ├── share-dialog.tsx │ ├── shared-json-viewer.tsx │ ├── site-header.tsx │ ├── stats-card.tsx │ ├── type-generator-modal.tsx │ └── ui │ │ ├── alert.tsx │ │ ├── breadcrumb.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── dialog.tsx │ │ ├── dropdown-menu.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── pagination.tsx │ │ ├── scroll-area.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── sheet.tsx │ │ ├── sidebar.tsx │ │ ├── skeleton.tsx │ │ ├── tabs.tsx │ │ ├── textarea.tsx │ │ └── tooltip.tsx ├── env.ts ├── hooks │ └── use-mobile.tsx ├── lib │ ├── data │ │ └── index.ts │ ├── providers │ │ ├── index.tsx │ │ ├── query-provider.tsx │ │ ├── theme-provider.tsx │ │ └── toast-provider.tsx │ ├── services │ │ ├── admin │ │ │ └── index.ts │ │ ├── openai │ │ │ └── index.ts │ │ └── share │ │ │ └── index.ts │ ├── stores │ │ ├── create-selectors.ts │ │ ├── json-visualizer-store.ts │ │ └── key-store.ts │ └── utils.ts ├── server │ └── db.ts └── types │ ├── api.ts │ └── react-json-grid.d.ts ├── tailwind.config.ts └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres" 2 | ADMIN_KEY="your-admin-key" -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "next/typescript"], 3 | "rules": { 4 | "@typescript-eslint/no-explicit-any": "off" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env 30 | .env*.local 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | 39 | # cursor 40 | .cursorrules -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 JSON Visualiser - Milind Mishra 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JSON Visualiser 2 | 3 | An application for visualising, sharing, and analyzing JSON data with multiple viewing modes and AI-powered explanations. 4 | 5 | > Light Mode 6 | image 7 | 8 | > Dark Mode 9 | image 10 | 11 | ## Features 12 | 13 | - 🎯 **Multiple Visualisation Modes** 14 | 15 | - Raw Input: Edit and validate JSON with syntax highlighting 16 | 17 | image 18 | 19 | - Tree View: Hierarchical representation of JSON data 20 | 21 | Screenshot 2024-11-06 at 9 19 19 AM 22 | 23 | - Grid View: Tabular view for array-based JSON 24 | 25 | Screenshot 2024-11-06 at 9 19 28 AM 26 | 27 | - AI Analysis: Get AI-powered explanations of your JSON structure 28 | 29 | - 🔄 **Real-time Validation** 30 | 31 | - Instant JSON syntax validation 32 | - Clear error messages for debugging 33 | 34 | - 🌓 **Dark/Light Mode** 35 | 36 | - Automatic theme detection 37 | - Manual theme toggle 38 | 39 | - 📤 **Sharing Capabilities** 40 | 41 | - Generate shareable links for JSON snippets 42 | - View shared JSON with metadata 43 | 44 | Screenshot 2024-11-06 at 9 19 48 AM 45 | 46 | Screenshot 2024-11-06 at 9 20 11 AM 47 | 48 | ## Tech Stack 49 | 50 | - **Framework**: Next.js 15 51 | - **Language**: TypeScript 52 | - **Styling**: Tailwind CSS 53 | - **UI Components**: Shadcn UI 54 | - **Database**: PostgreSQL 55 | 56 | ## Getting Started 57 | 58 | ### Prerequisites 59 | 60 | - Node.js 20+ 61 | - pnpm (recommended) or npm 62 | - PostgreSQL database 63 | 64 | ### Installation 65 | 66 | 1. Clone the repository: 67 | 68 | ```bash 69 | git clone https://github.com/thatbeautifuldream/json-visualizer.git --depth 1 70 | cd json-visualizer 71 | ``` 72 | 73 | 2. Install dependencies: 74 | 75 | ```bash 76 | pnpm install 77 | # or 78 | npm install 79 | ``` 80 | 81 | 3. Set up environment variables: 82 | 83 | Create a `.env` file in the root directory with the following variables: 84 | 85 | ```env 86 | # Database 87 | DATABASE_URL="postgresql://postgres:postgres@localhost:5432/json_visualizer" 88 | ADMIN_KEY="your-super-secret-admin-key" 89 | ``` 90 | 91 | 4. Start the development server: 92 | 93 | ```bash 94 | pnpm dev 95 | # or 96 | npm run dev 97 | ``` 98 | 99 | The application will be available at `http://localhost:3000`. 100 | 101 | ## Database Setup 102 | 103 | 1. Ensure PostgreSQL is installed and running 104 | 2. Create a new database: 105 | 106 | ```sql 107 | CREATE DATABASE json_visualizer; 108 | ``` 109 | 110 | 3. To create the tables, run the following command: 111 | 112 | ```bash 113 | pnpm db:push 114 | ``` 115 | 116 | ## Production Deployment 117 | 118 | 1. Build the application: 119 | 120 | ```bash 121 | pnpm build 122 | # or 123 | npm run build 124 | ``` 125 | 126 | 2. Start the production server: 127 | 128 | ```bash 129 | pnpm start 130 | # or 131 | npm start 132 | ``` 133 | 134 | ## Contributing 135 | 136 | 1. Fork the repository 137 | 2. Create your feature branch (`git checkout -b feature/amazing-feature`) 138 | 3. Commit your changes (`git commit -m 'Add some amazing feature'`) 139 | 4. Push to the branch (`git push origin feature/amazing-feature`) 140 | 5. Open a Pull Request 141 | 142 | ## License 143 | 144 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 145 | -------------------------------------------------------------------------------- /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": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | } 20 | } -------------------------------------------------------------------------------- /generate-env.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Check if .env file already exists 4 | if [ -f .env ]; then 5 | echo ".env file already exists. Please remove it first if you want to generate a new one." 6 | exit 1 7 | fi 8 | 9 | # Generate .env file 10 | cat > .env << EOL 11 | DATABASE_URL="" 12 | ADMIN_KEY="" 13 | EOL 14 | 15 | echo ".env file has been generated successfully!" 16 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | export default nextConfig; 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "json-visualizer", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev --turbo", 7 | "build": "pnpm db:generate && next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "db:generate": "prisma generate", 11 | "db:migrate": "prisma migrate dev", 12 | "db:push": "prisma db push", 13 | "db:studio": "prisma studio" 14 | }, 15 | "dependencies": { 16 | "@prisma/client": "^6.4.1", 17 | "@radix-ui/react-dialog": "^1.1.1", 18 | "@radix-ui/react-dropdown-menu": "^2.1.2", 19 | "@radix-ui/react-icons": "^1.3.0", 20 | "@radix-ui/react-label": "^2.1.0", 21 | "@radix-ui/react-scroll-area": "^1.2.0", 22 | "@radix-ui/react-select": "^2.1.2", 23 | "@radix-ui/react-separator": "^1.1.0", 24 | "@radix-ui/react-slot": "^1.1.0", 25 | "@radix-ui/react-tabs": "^1.1.0", 26 | "@radix-ui/react-toast": "^1.2.2", 27 | "@radix-ui/react-tooltip": "^1.1.3", 28 | "@redheadphone/react-json-grid": "^0.7.0", 29 | "@t3-oss/env-nextjs": "^0.11.1", 30 | "@tanstack/react-query": "^5.59.0", 31 | "@types/prismjs": "^1.26.5", 32 | "class-variance-authority": "^0.7.0", 33 | "clsx": "^2.1.1", 34 | "date-fns": "^4.1.0", 35 | "json-to-ts": "^2.1.0", 36 | "lucide-react": "^0.446.0", 37 | "motion": "^12.5.0", 38 | "next": "15.0.2", 39 | "next-themes": "^0.3.0", 40 | "nuqs": "^1.19.3", 41 | "openai": "^4.67.1", 42 | "prismjs": "^1.29.0", 43 | "react": "^18", 44 | "react-dom": "^18", 45 | "react-json-view": "^1.21.3", 46 | "react-syntax-highlighter": "^15.5.0", 47 | "sonner": "^1.5.0", 48 | "tailwind-merge": "^2.5.2", 49 | "tailwindcss-animate": "^1.0.7", 50 | "zod": "^3.23.8", 51 | "zustand": "5.0.0-rc.2" 52 | }, 53 | "devDependencies": { 54 | "@types/node": "^20", 55 | "@types/react": "^18", 56 | "@types/react-dom": "^18", 57 | "@types/react-syntax-highlighter": "^15.5.13", 58 | "eslint": "^8", 59 | "eslint-config-next": "14.2.13", 60 | "postcss": "^8", 61 | "prisma": "^6.4.1", 62 | "tailwindcss": "^3.4.1", 63 | "typescript": "^5" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /prisma/migrations/20241031154736_init_json_documents/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "JsonDocument" ( 3 | "id" TEXT NOT NULL, 4 | "title" VARCHAR(255), 5 | "content" TEXT NOT NULL, 6 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 7 | "expiresAt" TIMESTAMP(3), 8 | "viewCount" INTEGER NOT NULL DEFAULT 0, 9 | "size" INTEGER NOT NULL, 10 | "isValid" BOOLEAN NOT NULL DEFAULT true, 11 | 12 | CONSTRAINT "JsonDocument_pkey" PRIMARY KEY ("id") 13 | ); 14 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | generator client { 2 | provider = "prisma-client-js" 3 | } 4 | 5 | datasource db { 6 | provider = "postgresql" 7 | url = env("DATABASE_URL") 8 | } 9 | 10 | model JsonDocument { 11 | id String @id @default(cuid()) 12 | title String? @db.VarChar(255) 13 | content String @db.Text 14 | 15 | // Metadata 16 | createdAt DateTime @default(now()) 17 | expiresAt DateTime? 18 | 19 | // Analytics 20 | viewCount Int @default(0) 21 | 22 | // JSON metadata 23 | size Int // Size in bytes 24 | isValid Boolean @default(true) 25 | } -------------------------------------------------------------------------------- /public/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thatbeautifuldream/jsonvisualiser/aedc17b53580cf287d51bd7acd6a16f14edadaee/public/screenshot.png -------------------------------------------------------------------------------- /src/app/(application)/admin/page.tsx: -------------------------------------------------------------------------------- 1 | import { AdminGrid } from "@/components/admin-grid"; 2 | import { env } from "@/env"; 3 | import { cookies } from "next/headers"; 4 | import { redirect } from "next/navigation"; 5 | 6 | export default async function AdminPage({ 7 | params, 8 | }: { 9 | params: Promise<{ 10 | page: number; 11 | }>; 12 | }) { 13 | const cookieStore = await cookies(); 14 | const adminKey = cookieStore.get("ADMIN_KEY")?.value; 15 | 16 | if (!adminKey || adminKey !== env.ADMIN_KEY) { 17 | redirect("/"); 18 | } 19 | 20 | const page = (await params).page ?? 1; 21 | return ( 22 |
23 | 24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/app/(application)/explore/page.tsx: -------------------------------------------------------------------------------- 1 | import { ExploreGrid } from "@/components/explore-grid"; 2 | import { env } from "@/env"; 3 | import { cookies } from "next/headers"; 4 | import { redirect } from "next/navigation"; 5 | 6 | export default async function ExplorePage({ 7 | params, 8 | }: { 9 | params: Promise<{ 10 | page: number; 11 | }>; 12 | }) { 13 | const cookieStore = await cookies(); 14 | const adminKey = cookieStore.get("ADMIN_KEY")?.value; 15 | 16 | if (!adminKey || adminKey !== env.ADMIN_KEY) { 17 | redirect("/"); 18 | } 19 | 20 | const page = (await params).page ?? 1; 21 | return ( 22 |
23 | 24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/app/(application)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Header } from "@/components/layout/header"; 2 | 3 | export default function Layout({ children }: { children: React.ReactNode }) { 4 | return ( 5 | <> 6 |
7 | {children} 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /src/app/(application)/stats/page.tsx: -------------------------------------------------------------------------------- 1 | import { StatsCard } from "@/components/stats-card"; 2 | import { env } from "@/env"; 3 | import { cookies } from "next/headers"; 4 | import { redirect } from "next/navigation"; 5 | 6 | export default async function StatsPage() { 7 | const cookieStore = await cookies(); 8 | const adminKey = cookieStore.get("ADMIN_KEY")?.value; 9 | 10 | if (!adminKey || adminKey !== env.ADMIN_KEY) { 11 | redirect("/"); 12 | } 13 | 14 | return ( 15 |
16 | 17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/app/about/page.client.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { motion } from "motion/react"; 4 | import { Button } from "@/components/ui/button"; 5 | import Link from "next/link"; 6 | import { useTheme } from "next-themes"; 7 | 8 | const fadeInUp = { 9 | initial: { opacity: 0, y: 20 }, 10 | animate: { opacity: 1, y: 0 }, 11 | transition: { duration: 0.5 }, 12 | }; 13 | 14 | const staggerContainer = { 15 | animate: { 16 | transition: { 17 | staggerChildren: 0.2, 18 | }, 19 | }, 20 | }; 21 | 22 | export function AboutContent() { 23 | const { theme } = useTheme(); 24 | 25 | return ( 26 | <> 27 | 33 | 37 | A Developer's Journey with JSON 38 | 39 | 44 | From Frustration to Innovation 45 | 46 | 47 | 53 | 54 |

55 | Hi, I'm Milind, and this project emerged from my daily 56 | struggles as a developer. One late night, while debugging a 57 | particularly complex API response, I found myself lost in a maze 58 | of nested JSON objects. The standard tools weren't cutting it 59 | - they either oversimplified the data or made it even more 60 | confusing to navigate. 61 |

62 |

63 | I remember thinking, "There has to be a better way." 64 | After trying countless JSON viewers and feeling frustrated with 65 | their limitations, I decided to build something that would 66 | actually make sense to developers like me who work with complex 67 | data structures daily. 68 |

69 |
70 | 71 |

72 | What started as a personal tool quickly evolved into something 73 | more. I focused on creating an interface that I would want to use 74 | - one that combines the simplicity of a basic JSON viewer with 75 | powerful features like collapsible sections, intuitive navigation, 76 | and most importantly, a clean visual hierarchy that helps you 77 | understand the data structure at a glance. 78 |

79 |

80 | Today, this tool represents my vision of what JSON visualization 81 | should be - straightforward, powerful, and actually helpful. 82 | Whether you're debugging an API, exploring data structures, 83 | or just trying to make sense of a complex JSON file, I hope this 84 | tool makes your development journey a little bit easier. 85 |

86 |
87 |
88 | 89 | 95 | 98 | 99 |
100 | 101 | 107 |
108 | 119 | 123 | 124 | 125 | ); 126 | } 127 | -------------------------------------------------------------------------------- /src/app/about/page.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from "next"; 2 | import { AboutContent } from "./page.client"; 3 | 4 | export const metadata: Metadata = { 5 | title: "About | JSON Visualizer", 6 | description: "About the JSON Visualizer", 7 | }; 8 | 9 | export default function AboutPage() { 10 | return ( 11 |
12 |
13 | 14 |
15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/app/api/cleanup/route.ts: -------------------------------------------------------------------------------- 1 | import { db } from "@/server/db"; 2 | import { NextResponse } from "next/server"; 3 | 4 | export async function DELETE() { 5 | try { 6 | const deleted = await db.jsonDocument.deleteMany({ 7 | where: { 8 | expiresAt: { 9 | lt: new Date(), 10 | }, 11 | }, 12 | }); 13 | 14 | return NextResponse.json({ 15 | message: "Cleanup completed", 16 | deletedCount: deleted.count, 17 | }); 18 | } catch (error) { 19 | console.error("Cleanup error:", error); 20 | return NextResponse.json( 21 | { 22 | error: "Failed to cleanup expired records", 23 | details: error instanceof Error ? error.message : "Unknown error", 24 | }, 25 | { status: 500 } 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/app/api/openai/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | import OpenAI from "openai"; 3 | import { zodResponseFormat } from "openai/helpers/zod"; 4 | import { z } from "zod"; 5 | 6 | export const maxDuration = 60; 7 | 8 | const InputSchema = z.object({ 9 | apiKey: z.string().optional(), 10 | json: z.record(z.unknown()), 11 | }); 12 | 13 | const ExplanationStep = z.object({ 14 | explanation: z.string(), 15 | output: z.string(), 16 | }); 17 | 18 | const JsonExplanationResponse = z.object({ 19 | steps: z.array(ExplanationStep), 20 | summary: z.string(), 21 | }); 22 | 23 | export const POST = async (req: NextRequest) => { 24 | try { 25 | const body = await req.json(); 26 | const { json, apiKey } = InputSchema.parse(body); 27 | 28 | const client = new OpenAI({ 29 | apiKey: apiKey || process.env.OPENAI_API_KEY, 30 | }); 31 | 32 | const completion = await client.beta.chat.completions.parse({ 33 | model: "gpt-4o-2024-08-06", 34 | messages: [ 35 | { 36 | role: "system", 37 | content: 38 | "You are an AI assistant that explains JSON structures. Provide a detailed, step-by-step explanation of the given JSON.", 39 | }, 40 | { 41 | role: "user", 42 | content: `Explain this JSON structure: ${JSON.stringify(json)}`, 43 | }, 44 | ], 45 | temperature: 0.7, 46 | max_tokens: 4096, 47 | response_format: zodResponseFormat( 48 | JsonExplanationResponse, 49 | "jsonExplanation" 50 | ), 51 | }); 52 | 53 | const message = completion.choices[0]?.message; 54 | if (message?.parsed) { 55 | return NextResponse.json( 56 | { status: true, data: message.parsed }, 57 | { status: 200 } 58 | ); 59 | } else { 60 | return NextResponse.json( 61 | { 62 | status: false, 63 | message: message?.refusal || "Failed to parse the response.", 64 | }, 65 | { status: 500 } 66 | ); 67 | } 68 | } catch (e) { 69 | console.error("The sample encountered an error:", e); 70 | if (e instanceof z.ZodError) { 71 | return NextResponse.json( 72 | { 73 | status: false, 74 | message: "Invalid input or output format.", 75 | errors: e.errors, 76 | }, 77 | { status: 400 } 78 | ); 79 | } 80 | return NextResponse.json( 81 | { status: false, message: "Something went wrong." }, 82 | { status: 500 } 83 | ); 84 | } 85 | }; 86 | -------------------------------------------------------------------------------- /src/app/api/share/route.ts: -------------------------------------------------------------------------------- 1 | import { env } from "@/env"; 2 | import { db } from "@/server/db"; 3 | import { NextResponse } from "next/server"; 4 | import { cookies } from "next/headers"; 5 | 6 | const THIRTY_DAYS = 1000 * 60 * 60 * 24 * 30; 7 | 8 | export async function POST(req: Request) { 9 | try { 10 | const { json, title } = await req.json(); 11 | 12 | // Calculate the size of the JSON string 13 | const jsonString = JSON.stringify(json); 14 | const size = new Blob([jsonString]).size; 15 | 16 | // Validate JSON 17 | let isValid = true; 18 | try { 19 | JSON.parse(jsonString); 20 | } catch { 21 | isValid = false; 22 | } 23 | 24 | const jsonDocument = await db.jsonDocument.create({ 25 | data: { 26 | title: title ?? "Untitled", 27 | content: jsonString, 28 | size, 29 | isValid, 30 | expiresAt: new Date(Date.now() + THIRTY_DAYS), 31 | }, 32 | }); 33 | 34 | return NextResponse.json({ id: jsonDocument.id }); 35 | } catch (error) { 36 | console.error("Database error:", error); 37 | return NextResponse.json( 38 | { 39 | error: "Failed to share JSON", 40 | details: error instanceof Error ? error.message : "Unknown error", 41 | }, 42 | { status: 500 } 43 | ); 44 | } 45 | } 46 | 47 | export async function GET(req: Request) { 48 | try { 49 | const url = new URL(req.url); 50 | const id = url.searchParams.get("id"); 51 | const all = url.searchParams.get("all"); 52 | 53 | if (all === "true") { 54 | const page = parseInt(url.searchParams.get("page") ?? "1"); 55 | const pageSize = 10; 56 | 57 | const allDocuments = await db.jsonDocument.findMany({ 58 | where: { 59 | expiresAt: { 60 | gt: new Date(), 61 | }, 62 | }, 63 | orderBy: { createdAt: "desc" }, 64 | take: pageSize, 65 | skip: (page - 1) * pageSize, 66 | select: { 67 | id: true, 68 | title: true, 69 | size: true, 70 | viewCount: true, 71 | createdAt: true, 72 | expiresAt: true, 73 | isValid: true, 74 | }, 75 | }); 76 | 77 | const total = await db.jsonDocument.count({ 78 | where: { 79 | expiresAt: { 80 | gt: new Date(), 81 | }, 82 | }, 83 | }); 84 | 85 | return NextResponse.json({ 86 | documents: allDocuments, 87 | pagination: { 88 | total, 89 | pageSize, 90 | currentPage: page, 91 | totalPages: Math.ceil(total / pageSize), 92 | }, 93 | }); 94 | } 95 | 96 | if (!id) { 97 | return NextResponse.json({ error: "ID is required" }, { status: 400 }); 98 | } 99 | 100 | const document = await db.jsonDocument.update({ 101 | where: { id }, 102 | data: { 103 | viewCount: { 104 | increment: 1, 105 | }, 106 | }, 107 | select: { 108 | content: true, 109 | title: true, 110 | viewCount: true, 111 | expiresAt: true, 112 | isValid: true, 113 | size: true, 114 | }, 115 | }); 116 | 117 | if (!document) { 118 | return NextResponse.json({ error: "JSON not found" }, { status: 404 }); 119 | } 120 | 121 | if (document.expiresAt && document.expiresAt < new Date()) { 122 | return NextResponse.json({ error: "JSON has expired" }, { status: 410 }); 123 | } 124 | 125 | return NextResponse.json({ 126 | json: JSON.parse(document.content), 127 | metadata: { 128 | title: document.title, 129 | viewCount: document.viewCount, 130 | size: document.size, 131 | isValid: document.isValid, 132 | }, 133 | }); 134 | } catch (error) { 135 | console.error("Database error:", error); 136 | return NextResponse.json( 137 | { 138 | error: "Failed to fetch JSON", 139 | details: error instanceof Error ? error.message : "Unknown error", 140 | }, 141 | { status: 500 } 142 | ); 143 | } 144 | } 145 | 146 | export async function DELETE(req: Request) { 147 | try { 148 | const url = new URL(req.url); 149 | const id = url.searchParams.get("id"); 150 | const cookieStore = await cookies(); 151 | const adminKey = cookieStore.get("ADMIN_KEY")?.value; 152 | 153 | if (!id) { 154 | return NextResponse.json({ error: "ID is required" }, { status: 400 }); 155 | } 156 | 157 | if (!adminKey || adminKey !== env.ADMIN_KEY) { 158 | return NextResponse.json( 159 | { error: "Unauthorized: Invalid admin key" }, 160 | { status: 401 } 161 | ); 162 | } 163 | 164 | const deleted = await db.jsonDocument.delete({ 165 | where: { id }, 166 | }); 167 | 168 | return NextResponse.json({ 169 | message: "Document deleted successfully", 170 | id: deleted.id, 171 | }); 172 | } catch (error) { 173 | console.error("Delete error:", error); 174 | return NextResponse.json( 175 | { 176 | error: "Failed to delete document", 177 | details: error instanceof Error ? error.message : "Unknown error", 178 | }, 179 | { status: 500 } 180 | ); 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /src/app/api/stats/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import { db } from "@/server/db"; 3 | 4 | export async function GET() { 5 | try { 6 | const stats = await db.$transaction(async (tx) => { 7 | // Get total documents count 8 | const totalDocs = await tx.jsonDocument.count(); 9 | 10 | // Get documents created in last 24 hours 11 | const last24Hours = await tx.jsonDocument.count({ 12 | where: { 13 | createdAt: { 14 | gte: new Date(Date.now() - 24 * 60 * 60 * 1000), 15 | }, 16 | }, 17 | }); 18 | 19 | // Get total views 20 | const viewsResult = await tx.jsonDocument.aggregate({ 21 | _sum: { 22 | viewCount: true, 23 | }, 24 | }); 25 | 26 | // Get average document size 27 | const sizeResult = await tx.jsonDocument.aggregate({ 28 | _avg: { 29 | size: true, 30 | }, 31 | }); 32 | 33 | // Get expired documents count 34 | const expiredDocs = await tx.jsonDocument.count({ 35 | where: { 36 | expiresAt: { 37 | lt: new Date(), 38 | }, 39 | }, 40 | }); 41 | 42 | return { 43 | totalDocuments: totalDocs, 44 | documentsLast24h: last24Hours, 45 | totalViews: viewsResult._sum.viewCount ?? 0, 46 | averageSize: Math.round(sizeResult._avg.size ?? 0), 47 | expiredDocuments: expiredDocs, 48 | }; 49 | }); 50 | 51 | return NextResponse.json(stats, { status: 200 }); 52 | } catch (error) { 53 | console.error("Stats API Error:", error); 54 | return NextResponse.json( 55 | { error: "Failed to fetch statistics" }, 56 | { status: 500 } 57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thatbeautifuldream/jsonvisualiser/aedc17b53580cf287d51bd7acd6a16f14edadaee/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | font-family: Arial, Helvetica, sans-serif; 7 | } 8 | 9 | @layer utilities { 10 | .text-balance { 11 | text-wrap: balance; 12 | } 13 | } 14 | 15 | @layer base { 16 | :root { 17 | --background: 0 0% 100%; 18 | --foreground: 0 0% 3.9%; 19 | --card: 0 0% 100%; 20 | --card-foreground: 0 0% 3.9%; 21 | --popover: 0 0% 100%; 22 | --popover-foreground: 0 0% 3.9%; 23 | --primary: 0 0% 9%; 24 | --primary-foreground: 0 0% 98%; 25 | --secondary: 0 0% 96.1%; 26 | --secondary-foreground: 0 0% 9%; 27 | --muted: 0 0% 96.1%; 28 | --muted-foreground: 0 0% 45.1%; 29 | --accent: 0 0% 96.1%; 30 | --accent-foreground: 0 0% 9%; 31 | --destructive: 0 84.2% 60.2%; 32 | --destructive-foreground: 0 0% 98%; 33 | --border: 0 0% 89.8%; 34 | --input: 0 0% 89.8%; 35 | --ring: 0 0% 3.9%; 36 | --chart-1: 12 76% 61%; 37 | --chart-2: 173 58% 39%; 38 | --chart-3: 197 37% 24%; 39 | --chart-4: 43 74% 66%; 40 | --chart-5: 27 87% 67%; 41 | --radius: 0.5rem; 42 | } 43 | .dark { 44 | --background: 0 0% 3.9%; 45 | --foreground: 0 0% 98%; 46 | --card: 0 0% 3.9%; 47 | --card-foreground: 0 0% 98%; 48 | --popover: 0 0% 3.9%; 49 | --popover-foreground: 0 0% 98%; 50 | --primary: 0 0% 98%; 51 | --primary-foreground: 0 0% 9%; 52 | --secondary: 0 0% 14.9%; 53 | --secondary-foreground: 0 0% 98%; 54 | --muted: 0 0% 14.9%; 55 | --muted-foreground: 0 0% 63.9%; 56 | --accent: 0 0% 14.9%; 57 | --accent-foreground: 0 0% 98%; 58 | --destructive: 0 62.8% 30.6%; 59 | --destructive-foreground: 0 0% 98%; 60 | --border: 0 0% 14.9%; 61 | --input: 0 0% 14.9%; 62 | --ring: 0 0% 83.1%; 63 | --chart-1: 220 70% 50%; 64 | --chart-2: 160 60% 45%; 65 | --chart-3: 30 80% 55%; 66 | --chart-4: 280 65% 60%; 67 | --chart-5: 340 75% 55%; 68 | } 69 | } 70 | 71 | @layer base { 72 | * { 73 | @apply border-border; 74 | } 75 | body { 76 | @apply bg-background text-foreground; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import Providers from "@/lib/providers"; 2 | import type { Metadata } from "next"; 3 | import { Inter } from "next/font/google"; 4 | import "./globals.css"; 5 | 6 | const inter = Inter({ subsets: ["latin"] }); 7 | 8 | export const metadata: Metadata = { 9 | title: "JSON Visualiser", 10 | description: "A tool to visualise and format JSON data", 11 | }; 12 | 13 | export default function RootLayout({ 14 | children, 15 | }: { 16 | children: React.ReactNode; 17 | }) { 18 | return ( 19 | 20 | 21 | {children} 22 | 23 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { JsonVisualizer } from "@/components/json-visualizer"; 2 | 3 | export default function Page() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/s/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import { JsonVisualizer } from "@/components/json-visualizer"; 2 | 3 | interface SharedJsonPageProps { 4 | params: Promise<{ 5 | id: string; 6 | }>; 7 | } 8 | 9 | export default async function SharedJsonPage({ params }: SharedJsonPageProps) { 10 | const jsonId = (await params).id; 11 | return
{jsonId && }
; 12 | } 13 | -------------------------------------------------------------------------------- /src/components/admin-grid.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | Pagination, 5 | PaginationContent, 6 | PaginationNext, 7 | PaginationPrevious, 8 | } from "@/components/ui/pagination"; 9 | import { deleteDocument, fetchDocuments } from "@/lib/services/admin"; 10 | import { useQuery, useQueryClient } from "@tanstack/react-query"; 11 | import { formatDistanceToNow } from "date-fns"; 12 | import { Eye, FileJson, Trash2 } from "lucide-react"; 13 | import Link from "next/link"; 14 | import { useRouter } from "next/navigation"; 15 | import { toast } from "sonner"; 16 | import { Button } from "./ui/button"; 17 | import { Card } from "./ui/card"; 18 | import { Skeleton } from "./ui/skeleton"; 19 | import { 20 | Tooltip, 21 | TooltipContent, 22 | TooltipProvider, 23 | TooltipTrigger, 24 | } from "./ui/tooltip"; 25 | 26 | export function AdminGrid({ page }: { page: number }) { 27 | const router = useRouter(); 28 | const queryClient = useQueryClient(); 29 | const { data, isLoading } = useQuery({ 30 | queryKey: ["admin-explore", page], 31 | queryFn: () => fetchDocuments(page), 32 | }); 33 | 34 | const documents = data?.documents; 35 | const pagination = data?.pagination; 36 | 37 | function handlePageChange(page: number) { 38 | router.push(`/admin?page=${page}`); 39 | } 40 | 41 | async function handleDelete(e: React.MouseEvent, id: string, title: string) { 42 | e.preventDefault(); // Prevent navigation 43 | try { 44 | await deleteDocument(id); 45 | await queryClient.invalidateQueries({ queryKey: ["admin-explore"] }); 46 | toast.success(`Deleted "${title || "Untitled"}" successfully`); 47 | } catch { 48 | toast.error("Failed to delete document"); 49 | } 50 | } 51 | 52 | if (isLoading) { 53 | return ; 54 | } 55 | 56 | return ( 57 |
58 |
59 | {documents && 60 | documents?.map((doc) => ( 61 | 62 | 63 |
64 |
65 | 66 | 67 | 68 | 76 | 77 | Delete document 78 | 79 | 80 |
81 |
82 | 83 | 84 | {doc.title || "Untitled"} 85 | 86 |
87 |
88 | 89 | {doc.viewCount} 90 |
91 |
92 | {formatDistanceToNow(new Date(doc.createdAt))} ago 93 |
94 |
95 |
96 | 97 | ))} 98 |
99 | 100 | {pagination && ( 101 | 102 | 103 | { 105 | if (page >= 2) { 106 | handlePageChange(page - 1); 107 | } 108 | }} 109 | isActive={page >= 2} 110 | /> 111 |
112 | Page {page} of {pagination?.totalPages} 113 |
114 | { 116 | if (page < (pagination?.totalPages ?? 0)) { 117 | handlePageChange(page + 1); 118 | } 119 | }} 120 | isActive={page < (pagination?.totalPages ?? 0)} 121 | /> 122 |
123 |
124 | )} 125 |
126 | ); 127 | } 128 | 129 | function LoadingSkeleton() { 130 | return ( 131 |
132 | {[...Array(6)].map((_, i) => ( 133 | 134 |
135 | 136 | 137 |
138 | 139 |
140 | ))} 141 |
142 | ); 143 | } 144 | -------------------------------------------------------------------------------- /src/components/api-key-dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState, useEffect } from "react"; 4 | import { Button } from "@/components/ui/button"; 5 | import { 6 | Dialog, 7 | DialogContent, 8 | DialogDescription, 9 | DialogFooter, 10 | DialogHeader, 11 | DialogTitle, 12 | DialogTrigger, 13 | } from "@/components/ui/dialog"; 14 | import { Input } from "@/components/ui/input"; 15 | import { Label } from "@/components/ui/label"; 16 | import { useKeyStore } from "@/lib/stores/key-store"; 17 | 18 | export function ApiKeyDialog() { 19 | const [open, setOpen] = useState(false); 20 | const [inputKey, setInputKey] = useState(""); 21 | const { setOpenAIKey, openAIKey } = useKeyStore(); 22 | 23 | useEffect(() => { 24 | if (open && openAIKey) { 25 | setInputKey("•".repeat(openAIKey.length)); 26 | } else { 27 | setInputKey(""); 28 | } 29 | }, [open, openAIKey]); 30 | 31 | const handleSave = () => { 32 | if (inputKey !== "•".repeat(openAIKey?.length)) { 33 | setOpenAIKey(inputKey); 34 | } 35 | setOpen(false); 36 | }; 37 | 38 | const handleInputChange = (e: React.ChangeEvent) => { 39 | const value = e.target.value; 40 | if (value !== "•".repeat(value.length)) { 41 | setInputKey(value); 42 | } 43 | }; 44 | 45 | return ( 46 | 47 | 48 | 51 | 52 | 53 | 54 | OpenAI API Key Configuration 55 | 56 | Enhance your JSON understanding with AI-powered explanations. Your 57 | API key enables secure, on-demand insights without being stored on 58 | our servers. 59 | 60 | 61 |
62 |
63 | 64 | 71 |
72 |

73 | Your API key is used solely for generating explanations and is never 74 | stored or logged. Ensure you keep your key confidential and do not 75 | share it with others. 76 |

77 |
78 | 79 | 80 | 81 |
82 |
83 | ); 84 | } 85 | -------------------------------------------------------------------------------- /src/components/app-sidebar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Button } from "@/components/ui/button"; 4 | import { ScrollArea } from "@/components/ui/scroll-area"; 5 | import { Sidebar, SidebarFooter } from "@/components/ui/sidebar"; 6 | import { cn } from "@/lib/utils"; 7 | import { 8 | Code2Icon, 9 | HomeIcon, 10 | LayoutGridIcon, 11 | LineChartIcon, 12 | ShareIcon, 13 | CalendarIcon, 14 | } from "lucide-react"; 15 | import Link from "next/link"; 16 | import { usePathname } from "next/navigation"; 17 | 18 | interface SidebarItem { 19 | title: string; 20 | href: string; 21 | icon: React.ComponentType<{ className?: string }>; 22 | } 23 | 24 | const sidebarItems: SidebarItem[] = [ 25 | { 26 | title: "Home", 27 | href: "/", 28 | icon: HomeIcon, 29 | }, 30 | { 31 | title: "JSON Visualiser", 32 | href: "/visualiser", 33 | icon: Code2Icon, 34 | }, 35 | { 36 | title: "Explore Examples", 37 | href: "/explore", 38 | icon: LayoutGridIcon, 39 | }, 40 | { 41 | title: "Usage Stats", 42 | href: "/stats", 43 | icon: LineChartIcon, 44 | }, 45 | ]; 46 | 47 | export function AppSidebar() { 48 | const pathname = usePathname(); 49 | 50 | return ( 51 | 52 | 53 |
54 |
55 |

JSON Visualiser

56 |
57 | {sidebarItems.map((item) => ( 58 | 69 | ))} 70 |
71 |
72 |
73 |
74 | 75 |
76 | 81 | 90 |
91 |
92 |
93 | ); 94 | } 95 | -------------------------------------------------------------------------------- /src/components/explore-grid.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | Pagination, 5 | PaginationContent, 6 | PaginationNext, 7 | PaginationPrevious, 8 | } from "@/components/ui/pagination"; 9 | import { useQuery } from "@tanstack/react-query"; 10 | import { formatDistanceToNow } from "date-fns"; 11 | import { Eye, FileJson } from "lucide-react"; 12 | import Link from "next/link"; 13 | import { useRouter } from "next/navigation"; 14 | import { Card } from "./ui/card"; 15 | import { Skeleton } from "./ui/skeleton"; 16 | 17 | interface Document { 18 | id: string; 19 | title: string; 20 | size: number; 21 | viewCount: number; 22 | createdAt: string; 23 | expiresAt: string; 24 | isValid: boolean; 25 | } 26 | 27 | interface PaginationData { 28 | total: number; 29 | pageSize: number; 30 | currentPage: number; 31 | totalPages: number; 32 | } 33 | 34 | async function fetchDocuments(page: number) { 35 | const res = await fetch(`/api/share?all=true&page=${page}`); 36 | if (!res.ok) throw new Error("Failed to fetch documents"); 37 | return res.json(); 38 | } 39 | 40 | export function ExploreGrid({ page }: { page: number }) { 41 | const router = useRouter(); 42 | const { data, isLoading } = useQuery({ 43 | queryKey: ["explore", page], 44 | queryFn: () => fetchDocuments(page), 45 | }); 46 | 47 | const documents: Document[] = data?.documents ?? []; 48 | const pagination: PaginationData = data?.pagination; 49 | 50 | function handlePageChange(page: number) { 51 | router.push(`/explore?page=${page}`); 52 | } 53 | 54 | if (isLoading) { 55 | return ; 56 | } 57 | 58 | return ( 59 |
60 |
61 | {documents.map((doc) => ( 62 | 63 | 64 |
65 |
66 |
67 | 68 | 69 | {doc.title || "Untitled"} 70 | 71 |
72 |
73 | 74 | {doc.viewCount} 75 |
76 |
77 |
78 | {formatDistanceToNow(new Date(doc.createdAt))} ago 79 |
80 |
81 |
82 | 83 | ))} 84 |
85 | 86 | {pagination && ( 87 | 88 | 89 | handlePageChange(page - 1)} 91 | isActive={page > 1} 92 | /> 93 |
94 | Page {page} of {pagination.totalPages} 95 |
96 | handlePageChange(page + 1)} 98 | isActive={page < pagination.totalPages} 99 | /> 100 |
101 |
102 | )} 103 |
104 | ); 105 | } 106 | 107 | function LoadingSkeleton() { 108 | return ( 109 |
110 | {[...Array(6)].map((_, i) => ( 111 | 112 |
113 | 114 | 115 |
116 | 117 |
118 | ))} 119 |
120 | ); 121 | } 122 | -------------------------------------------------------------------------------- /src/components/json-explanation.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; 4 | import { ScrollArea } from "@/components/ui/scroll-area"; 5 | import { explainJson } from "@/lib/services/openai"; 6 | import { useMutation } from "@tanstack/react-query"; 7 | import { useTheme } from "next-themes"; 8 | import { useEffect } from "react"; 9 | import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; 10 | import { 11 | a11yDark, 12 | // @ts-expect-error : types are not correct 13 | a11yLight, 14 | } from "react-syntax-highlighter/dist/cjs/styles/prism"; 15 | import { useKeyStore } from "@/lib/stores/key-store"; 16 | import { ApiKeyDialog } from "./api-key-dialog"; 17 | import Loader from "./loader"; 18 | import { useJsonVisualizerStore } from "@/lib/stores/json-visualizer-store"; 19 | 20 | interface JsonExplanationProps { 21 | jsonData: any; 22 | } 23 | 24 | export function JsonExplanation({ jsonData }: JsonExplanationProps) { 25 | const { theme } = useTheme(); 26 | const { openAIKey } = useKeyStore(); 27 | const aiExplanation = useJsonVisualizerStore.use.aiExplanation(); 28 | const setAIExplanation = useJsonVisualizerStore.use.setAIExplanation(); 29 | 30 | const explainJsonMutation = useMutation({ 31 | mutationKey: ["explainJson"], 32 | mutationFn: explainJson, 33 | }); 34 | 35 | useEffect(() => { 36 | if (openAIKey && !aiExplanation) { 37 | explainJsonMutation.mutate({ 38 | apiKey: openAIKey, 39 | jsonData: jsonData, 40 | }); 41 | } 42 | }, [openAIKey, jsonData, aiExplanation]); 43 | 44 | useEffect(() => { 45 | if (explainJsonMutation.isSuccess) { 46 | setAIExplanation(explainJsonMutation?.data?.data); 47 | } 48 | }, [explainJsonMutation.isSuccess, setAIExplanation]); 49 | 50 | if (!openAIKey) { 51 | return ( 52 |
53 |

Please set your OpenAI API key to get JSON explanations.

54 | 55 |
56 | ); 57 | } 58 | 59 | if (explainJsonMutation.isPending && !aiExplanation) { 60 | return ( 61 |
62 | 63 |
64 | ); 65 | } 66 | 67 | if (explainJsonMutation.isError && !aiExplanation) { 68 | return ( 69 |
70 |

Error: {explainJsonMutation.error.message}

71 | 72 |
73 | ); 74 | } 75 | 76 | if (aiExplanation) { 77 | return ( 78 |
79 | 80 | 81 |
82 | 83 |
84 | Summary 85 |
86 | {aiExplanation.summary} 87 |
88 | 89 | 90 | Detailed Explanation 91 | 92 | 93 | 94 | {aiExplanation.steps.map((step, index) => ( 95 |
96 |

{step.explanation}

97 | 107 | {formatJSON(step.output)} 108 | 109 |
110 | ))} 111 |
112 |
113 |
114 |
115 | ); 116 | } 117 | } 118 | 119 | // Helper function to format JSON (unchanged) 120 | function formatJSON(jsonString: string): string { 121 | try { 122 | return JSON.stringify(JSON.parse(jsonString), null, 2); 123 | } catch (error) { 124 | console.warn("Failed to parse JSON:", error); 125 | return jsonString; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/components/json-grid.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import JSONGrid from "@redheadphone/react-json-grid"; 3 | import { Alert, AlertDescription, AlertTitle } from "./ui/alert"; 4 | import { AlertCircle } from "lucide-react"; 5 | import { useTheme } from "next-themes"; 6 | 7 | interface JsonGridProps { 8 | data: any; 9 | error: string | null; 10 | } 11 | 12 | export function JsonGrid({ data, error }: JsonGridProps) { 13 | const { theme, resolvedTheme } = useTheme(); 14 | 15 | const jsonViewTheme = 16 | (resolvedTheme || theme) === "dark" ? "moonLight" : "defaultLight"; 17 | if (error) { 18 | return ( 19 | 20 | 21 | Error 22 | {error} 23 | 24 | ); 25 | } 26 | return ( 27 |
28 | 29 |
30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /src/components/json-header.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ShareDialog } from "@/components/share-dialog"; 4 | import { Button } from "@/components/ui/button"; 5 | import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; 6 | import { TabsList, TabsTrigger } from "@/components/ui/tabs"; 7 | import { TabValue } from "@/lib/stores/json-visualizer-store"; 8 | import { 9 | Code2Icon, 10 | Github, 11 | LayoutGridIcon, 12 | ListTree, 13 | SparklesIcon, 14 | Braces, 15 | } from "lucide-react"; 16 | import Link from "next/link"; 17 | import { ModeToggle } from "./mode-toggle"; 18 | 19 | type TJsonHeaderProps = { 20 | setActiveTab: (value: TabValue) => void; 21 | initialShareId?: string; 22 | jsonInput: string; 23 | parsedJson: any; 24 | error: string | null; 25 | }; 26 | 27 | export function JsonHeader({ 28 | setActiveTab, 29 | initialShareId, 30 | jsonInput, 31 | parsedJson, 32 | error, 33 | }: TJsonHeaderProps) { 34 | return ( 35 |
36 |
37 | 41 |
42 | 43 |
44 | JSON Visualiser 45 | 46 | 47 | 48 | setActiveTab("input")} 52 | > 53 | 60 | setActiveTab("tree")} 64 | disabled={!parsedJson} 65 | > 66 | 73 | setActiveTab("grid")} 77 | disabled={!parsedJson} 78 | > 79 | 86 | setActiveTab("ai")} 90 | disabled={!parsedJson} 91 | > 92 | 99 | 100 | 101 | 102 |
103 |
104 | {!initialShareId && parsedJson && !error && ( 105 | 106 | )} 107 | 121 | 122 |
123 |
124 | ); 125 | } 126 | -------------------------------------------------------------------------------- /src/components/json-input.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | import { Textarea } from "@/components/ui/textarea"; 3 | import { 4 | Check, 5 | Clipboard, 6 | Copy, 7 | Cross, 8 | Eye, 9 | FileJson, 10 | FileText, 11 | HardDrive, 12 | Trash, 13 | X, 14 | } from "lucide-react"; 15 | import { toast } from "sonner"; 16 | import { Skeleton } from "./ui/skeleton"; 17 | import { TypeGeneratorModal } from "@/components/type-generator-modal"; 18 | 19 | interface SharedJsonMetadata { 20 | title: string; 21 | viewCount: number; 22 | size: number; 23 | isValid: boolean; 24 | } 25 | 26 | interface JsonInputProps { 27 | jsonInput: string; 28 | setJsonInput: (value: string) => void; 29 | isSharedJson: boolean; 30 | sharedJsonMetadata?: SharedJsonMetadata; 31 | isSharedJsonLoading?: boolean; 32 | } 33 | 34 | export function JsonInput({ 35 | jsonInput, 36 | setJsonInput, 37 | isSharedJson, 38 | sharedJsonMetadata, 39 | isSharedJsonLoading, 40 | }: JsonInputProps) { 41 | const handlePaste = async () => { 42 | try { 43 | const text = await navigator.clipboard.readText(); 44 | setJsonInput(text); 45 | toast.success("JSON pasted successfully"); 46 | } catch { 47 | toast.error("Failed to paste from clipboard"); 48 | } 49 | }; 50 | 51 | const handleCopy = () => { 52 | try { 53 | navigator.clipboard.writeText(jsonInput); 54 | toast.success("JSON copied to clipboard"); 55 | } catch { 56 | toast.error("Failed to copy to clipboard"); 57 | } 58 | }; 59 | 60 | const handleFormat = () => { 61 | try { 62 | const parsed = JSON.parse(jsonInput); 63 | const formatted = JSON.stringify(parsed, null, 2); 64 | setJsonInput(formatted); 65 | toast.success("JSON formatted successfully"); 66 | } catch (error) { 67 | toast.error(`Invalid JSON: ${(error as Error).message}`); 68 | } 69 | }; 70 | 71 | const handleRemoveWhitespace = () => { 72 | try { 73 | const parsed = JSON.parse(jsonInput); 74 | const compact = JSON.stringify(parsed); 75 | setJsonInput(compact); 76 | toast.success("Whitespace removed successfully"); 77 | } catch { 78 | toast.error("Invalid JSON: Unable to remove whitespace"); 79 | } 80 | }; 81 | 82 | const handleClear = () => { 83 | setJsonInput(""); 84 | toast.info("Input cleared"); 85 | }; 86 | 87 | const handleInputChange = (event: React.ChangeEvent) => { 88 | setJsonInput(event.target.value); 89 | }; 90 | 91 | function formatFileSize(bytes: number): string { 92 | const kb = bytes / 1024; 93 | if (kb < 1) { 94 | return `${bytes} bytes`; 95 | } 96 | return `${kb.toFixed(1)} KB`; 97 | } 98 | 99 | return ( 100 |
101 |
102 |
103 | {isSharedJson ? ( 104 | <> 105 | 114 | 115 | 116 | ) : ( 117 | <> 118 | 122 | 131 | 140 | 149 | 158 | 159 | 160 | )} 161 |
162 |
163 | {isSharedJsonLoading ? ( 164 |
165 | 166 |
167 |
168 | 169 | 170 |
171 |
172 | 173 | 174 |
175 |
176 | 177 | 178 |
179 |
180 | 181 |
182 | ) : ( 183 | isSharedJson && ( 184 | <> 185 |
186 | 187 | {sharedJsonMetadata?.title} 188 |
189 |
190 | 191 | {sharedJsonMetadata?.viewCount} views 192 |
193 |
194 | 195 | {formatFileSize(sharedJsonMetadata?.size ?? 0)} 196 |
197 |
198 | {sharedJsonMetadata?.isValid ? ( 199 | 200 | ) : ( 201 | 202 | )} 203 | {sharedJsonMetadata?.isValid ? "OK" : "Error"} 204 |
205 | 206 | ) 207 | )} 208 |
209 |
210 |