├── .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 |
7 |
8 | > Dark Mode
9 |
10 |
11 | ## Features
12 |
13 | - 🎯 **Multiple Visualisation Modes**
14 |
15 | - Raw Input: Edit and validate JSON with syntax highlighting
16 |
17 |
18 |
19 | - Tree View: Hierarchical representation of JSON data
20 |
21 |
22 |
23 | - Grid View: Tabular view for array-based JSON
24 |
25 |
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 |
45 |
46 |
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 |
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 |
96 | Start Visualizing
97 |
98 |
99 |
100 |
101 |
107 |
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 |
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 | handleDelete(e, doc.id, doc.title)}
73 | >
74 |
75 |
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 |
49 | {openAIKey ? "Update OpenAI API Key" : "Set OpenAI API Key"}
50 |
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 | OpenAI API Key
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 | Save API Key
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 |
64 |
65 |
66 | {item.title}
67 |
68 |
69 | ))}
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
87 |
88 |
89 |
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 |
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 |
58 | Input
59 |
60 | setActiveTab("tree")}
64 | disabled={!parsedJson}
65 | >
66 |
71 | Tree
72 |
73 | setActiveTab("grid")}
77 | disabled={!parsedJson}
78 | >
79 |
84 | Grid
85 |
86 | setActiveTab("ai")}
90 | disabled={!parsedJson}
91 | >
92 |
97 | AI
98 |
99 |
100 |
101 |
102 |
103 |
104 | {!initialShareId && parsedJson && !error && (
105 |
106 | )}
107 |
112 | window.open(
113 | "https://github.com/thatbeautifuldream/jsonvisualiser",
114 | "_blank"
115 | )
116 | }
117 | >
118 |
119 | jsonvisualiser
120 |
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 |
111 |
112 | Copy
113 |
114 |
115 | >
116 | ) : (
117 | <>
118 |
119 |
120 | Paste
121 |
122 |
128 |
129 | Copy
130 |
131 |
137 |
138 | Format
139 |
140 |
146 |
147 | Remove whitespace
148 |
149 |
155 |
156 | Clear
157 |
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 |
217 |
218 | );
219 | }
220 |
--------------------------------------------------------------------------------
/src/components/json-tree.tsx:
--------------------------------------------------------------------------------
1 | import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
2 | import { AlertCircle } from "lucide-react";
3 | import { useTheme } from "next-themes";
4 | import dynamic from "next/dynamic";
5 |
6 | const ReactJson = dynamic(() => import("react-json-view"), { ssr: false });
7 |
8 | interface JsonViewProps {
9 | parsedJson: any;
10 | error: string | null;
11 | }
12 |
13 | export function JsonTree({ parsedJson, error }: JsonViewProps) {
14 | const { theme, resolvedTheme } = useTheme();
15 |
16 | const jsonViewTheme =
17 | (resolvedTheme || theme) === "dark" ? "bright" : "bright:inverted";
18 |
19 | return (
20 |
21 | {error && (
22 |
23 |
24 | Error
25 | {error}
26 |
27 | )}
28 | {parsedJson && (
29 |
37 | )}
38 |
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/src/components/json-visualizer.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { JsonExplanation } from "@/components/json-explanation";
4 | import { JsonGrid } from "@/components/json-grid";
5 | import { JsonHeader } from "@/components/json-header";
6 | import { JsonInput } from "@/components/json-input";
7 | import { Tabs, TabsContent } from "@/components/ui/tabs";
8 | import { fetchSharedJson } from "@/lib/services/share";
9 | import {
10 | TabValue,
11 | useJsonVisualizerStore,
12 | } from "@/lib/stores/json-visualizer-store";
13 | import { useQuery } from "@tanstack/react-query";
14 | import { useEffect } from "react";
15 | import { JsonTree } from "./json-tree";
16 |
17 | interface JsonVisualizerProps {
18 | initialShareId?: string;
19 | }
20 |
21 | export function JsonVisualizer({ initialShareId }: JsonVisualizerProps) {
22 | const {
23 | activeTab,
24 | jsonInput,
25 | parsedJson,
26 | error,
27 | setActiveTab,
28 | setJsonInput,
29 | setParsedJson,
30 | setError,
31 | } = useJsonVisualizerStore();
32 |
33 | const { data, isLoading, isSuccess, isError } = useQuery({
34 | enabled: !!initialShareId,
35 | queryKey: ["sharedJson", initialShareId],
36 | queryFn: () => fetchSharedJson(initialShareId || ""),
37 | });
38 |
39 | useEffect(() => {
40 | if (isSuccess && data?.json) {
41 | setJsonInput(data.json);
42 | try {
43 | const parsed = JSON.parse(data.json);
44 | setParsedJson(parsed);
45 | setError(null);
46 | } catch {
47 | setError("Invalid JSON format");
48 | }
49 | }
50 | if (isError) {
51 | setError("Error fetching shared JSON");
52 | }
53 | }, [isSuccess, isError, data, setJsonInput, setParsedJson, setError]);
54 |
55 | const handleJsonInputChange = (value: string) => {
56 | setJsonInput(value);
57 | try {
58 | const parsed = JSON.parse(value);
59 | setParsedJson(parsed);
60 | setError(null);
61 | } catch {
62 | setError("Invalid JSON format");
63 | }
64 | };
65 |
66 | return (
67 |
68 |
setActiveTab(value as TabValue)}
71 | className="flex-grow flex flex-col"
72 | >
73 |
80 |
81 |
82 | {isSuccess ? (
83 |
90 | ) : (
91 |
96 | )}
97 |
98 |
99 | {parsedJson && }
100 |
101 |
102 | {parsedJson && }
103 |
104 |
105 | {parsedJson && }
106 |
107 |
108 |
109 |
110 | );
111 | }
112 |
--------------------------------------------------------------------------------
/src/components/layout/header.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { ModeToggle } from "@/components/mode-toggle";
4 | import { Button } from "@/components/ui/button";
5 | import { cn } from "@/lib/utils";
6 | import { Braces, Github } from "lucide-react";
7 | import Link from "next/link";
8 |
9 | interface HeaderProps {
10 | children?: React.ReactNode;
11 | className?: string;
12 | }
13 |
14 | export function Header({ children, className }: HeaderProps) {
15 | return (
16 |
22 |
23 |
27 |
28 |
29 |
30 |
JSON Visualiser
31 |
32 | {children}
33 |
34 |
35 |
40 | window.open(
41 | "https://github.com/thatbeautifuldream/json-visualizer",
42 | "_blank"
43 | )
44 | }
45 | >
46 |
47 |
48 | thatbeautifuldream/json-visualizer
49 |
50 |
51 |
52 |
53 |
54 | );
55 | }
56 |
--------------------------------------------------------------------------------
/src/components/learn-more-popup.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { cn } from "@/lib/utils";
4 | import { useEffect, useState } from "react";
5 | import { Button } from "./ui/button";
6 |
7 | export function LearnMorePopup({
8 | onAcceptCallback = () => {},
9 | onDeclineCallback = () => {},
10 | }) {
11 | const [isOpen, setIsOpen] = useState(false);
12 | const [hide, setHide] = useState(false);
13 |
14 | const accept = () => {
15 | setIsOpen(false);
16 | document.cookie =
17 | "learnMorePopupSeen=true; expires=Fri, 31 Dec 9999 23:59:59 GMT";
18 | setTimeout(() => {
19 | setHide(true);
20 | }, 700);
21 | onAcceptCallback();
22 | };
23 |
24 | const decline = () => {
25 | setIsOpen(false);
26 | setTimeout(() => {
27 | setHide(true);
28 | }, 700);
29 | onDeclineCallback();
30 | };
31 |
32 | useEffect(() => {
33 | try {
34 | setIsOpen(true);
35 | if (document.cookie.includes("learnMorePopupSeen=true")) {
36 | setIsOpen(false);
37 | setTimeout(() => {
38 | setHide(true);
39 | }, 700);
40 | }
41 | } catch (e) {
42 | // Handle error
43 | console.error(e);
44 | }
45 | }, []);
46 |
47 | return (
48 |
57 |
58 |
59 |
60 |
Welcome to JSON Visualiser!
61 |
62 |
63 |
64 | Transform your JSON data into clear visualizations.
65 |
66 | Learn how to supercharge your JSON workflow.
67 |
68 |
69 | Built by{" "}
70 |
75 | Milind Mishra
76 |
77 |
78 |
79 |
80 |
81 | Learn More
82 |
83 |
84 | Close
85 |
86 |
87 |
88 |
89 |
90 | );
91 | }
92 |
--------------------------------------------------------------------------------
/src/components/loader.tsx:
--------------------------------------------------------------------------------
1 | import { Loader2 } from "lucide-react";
2 | import React from "react";
3 |
4 | export default function Loader() {
5 | return (
6 |
7 |
8 |
9 | );
10 | }
11 |
--------------------------------------------------------------------------------
/src/components/mode-toggle.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useTheme } from "next-themes";
4 | import { Button } from "@/components/ui/button";
5 | import { Sun, Moon, Monitor } from "lucide-react";
6 | import { useEffect, useState } from "react";
7 |
8 | export function ModeToggle() {
9 | const { theme, setTheme } = useTheme();
10 | const [mounted, setMounted] = useState(false);
11 |
12 | // Ensure component is mounted to avoid hydration mismatch
13 | useEffect(() => {
14 | setMounted(true);
15 | }, []);
16 |
17 | if (!mounted) {
18 | return null;
19 | }
20 |
21 | const cycleTheme = () => {
22 | if (theme === "system") {
23 | setTheme("light");
24 | } else if (theme === "light") {
25 | setTheme("dark");
26 | } else {
27 | setTheme("system");
28 | }
29 | };
30 |
31 | return (
32 |
38 | {theme === "light" && }
39 | {theme === "dark" && }
40 | {theme === "system" && }
41 | Toggle theme
42 |
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/src/components/nav-main.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { MoreHorizontal, type LucideIcon } from "lucide-react";
4 |
5 | import {
6 | DropdownMenu,
7 | DropdownMenuContent,
8 | DropdownMenuItem,
9 | DropdownMenuTrigger,
10 | } from "@/components/ui/dropdown-menu";
11 | import {
12 | SidebarGroup,
13 | SidebarMenu,
14 | SidebarMenuButton,
15 | SidebarMenuItem,
16 | useSidebar,
17 | } from "@/components/ui/sidebar";
18 |
19 | export function NavMain({
20 | items,
21 | }: {
22 | items: {
23 | title: string;
24 | url: string;
25 | icon?: LucideIcon;
26 | isActive?: boolean;
27 | items?: {
28 | title: string;
29 | url: string;
30 | }[];
31 | }[];
32 | }) {
33 | const { isMobile } = useSidebar();
34 |
35 | return (
36 |
37 |
38 | {items.map((item) => (
39 |
40 |
41 |
42 |
43 | {item.title}
44 |
45 |
46 | {item.items?.length ? (
47 |
52 | {item.items.map((item) => (
53 |
54 | {item.title}
55 |
56 | ))}
57 |
58 | ) : null}
59 |
60 |
61 | ))}
62 |
63 |
64 | );
65 | }
66 |
--------------------------------------------------------------------------------
/src/components/share-dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Button } from "@/components/ui/button";
4 | import {
5 | Dialog,
6 | DialogContent,
7 | DialogDescription,
8 | DialogFooter,
9 | DialogHeader,
10 | DialogTitle,
11 | DialogTrigger,
12 | } from "@/components/ui/dialog";
13 | import { Input } from "@/components/ui/input";
14 | import { useState } from "react";
15 | import { toast } from "sonner";
16 | import { useRouter } from "next/navigation";
17 | import { Share2 } from "lucide-react";
18 |
19 | interface ShareDialogProps {
20 | jsonInput: string;
21 | }
22 |
23 | export function ShareDialog({ jsonInput }: ShareDialogProps) {
24 | const router = useRouter();
25 | const [title, setTitle] = useState("");
26 | const [isOpen, setIsOpen] = useState(false);
27 | const [isSharing, setIsSharing] = useState(false);
28 |
29 | const validateJson = (jsonString: string) => {
30 | try {
31 | JSON.parse(jsonString);
32 | return true;
33 | } catch {
34 | return false;
35 | }
36 | };
37 |
38 | const handleShare = async () => {
39 | if (!title.trim()) {
40 | toast.error("Please enter a title for your JSON");
41 | return;
42 | }
43 |
44 | if (!validateJson(jsonInput)) {
45 | toast.error("Invalid JSON format");
46 | return;
47 | }
48 |
49 | setIsSharing(true);
50 | try {
51 | const response = await fetch("/api/share", {
52 | method: "POST",
53 | headers: {
54 | "Content-Type": "application/json",
55 | },
56 | body: JSON.stringify({
57 | json: jsonInput,
58 | title: title.trim(),
59 | }),
60 | });
61 |
62 | if (!response.ok) {
63 | const error = await response.json();
64 | throw new Error(error.error || "Failed to share JSON");
65 | }
66 |
67 | const data = await response.json();
68 | if (data.id) {
69 | const shareUrl = `${window.location.origin}/s/${data.id}`;
70 | try {
71 | await navigator.clipboard.writeText(shareUrl);
72 | toast.success("JSON shared successfully!", {
73 | description: "Share URL copied to clipboard",
74 | });
75 | } catch {
76 | toast.success("JSON shared successfully!", {
77 | description: "Redirecting to shared URL...",
78 | });
79 | router.push(shareUrl);
80 | }
81 | setIsOpen(false);
82 | setTitle("");
83 | }
84 | } catch (error) {
85 | toast.error(
86 | error instanceof Error ? error.message : "Failed to share JSON"
87 | );
88 | } finally {
89 | setIsSharing(false);
90 | }
91 | };
92 |
93 | return (
94 |
95 |
96 |
101 |
102 | Share JSON
103 |
104 |
105 |
106 |
107 | Share JSON
108 |
109 | Give your JSON a title before sharing it.
110 |
111 |
112 |
113 | setTitle(e.target.value)}
117 | onKeyDown={(e) => {
118 | if (e.key === "Enter") {
119 | handleShare();
120 | }
121 | }}
122 | />
123 |
124 |
125 |
130 | {isSharing ? "Sharing..." : "Share"}
131 |
132 |
133 |
134 |
135 | );
136 | }
137 |
--------------------------------------------------------------------------------
/src/components/shared-json-viewer.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useEffect, useState } from "react";
4 | import { toast } from "sonner";
5 | import { Card } from "@/components/ui/card";
6 | import { Button } from "@/components/ui/button";
7 | import { Skeleton } from "@/components/ui/skeleton";
8 | import { ScrollArea } from "@/components/ui/scroll-area";
9 | import Prism from "prismjs";
10 | import { Copy, Check } from "lucide-react";
11 | import "prismjs/themes/prism-tomorrow.css";
12 | import "prismjs/components/prism-json";
13 |
14 | interface SharedJson {
15 | json: any;
16 | metadata: {
17 | title: string;
18 | viewCount: number;
19 | size: number;
20 | isValid: boolean;
21 | };
22 | }
23 |
24 | interface SharedJsonViewerProps {
25 | id: string;
26 | }
27 |
28 | function formatJson(json: any): string {
29 | try {
30 | const parsed = typeof json === "string" ? JSON.parse(json) : json;
31 | return JSON.stringify(parsed, null, 2)
32 | .replace(/&/g, "&")
33 | .replace(//g, ">");
35 | } catch {
36 | return String(json);
37 | }
38 | }
39 |
40 | export function SharedJsonViewer({ id }: SharedJsonViewerProps) {
41 | const [data, setData] = useState(null);
42 | const [isLoading, setIsLoading] = useState(true);
43 | const [hasCopied, setHasCopied] = useState(false);
44 | const [viewerHeight, setViewerHeight] = useState("calc(100vh - 12rem)");
45 |
46 | useEffect(() => {
47 | async function fetchSharedJson() {
48 | try {
49 | const response = await fetch(`/api/share?id=${id}`);
50 |
51 | if (!response.ok) {
52 | const error = await response.json();
53 | throw new Error(error.error || "Failed to fetch JSON");
54 | }
55 |
56 | const jsonData = await response.json();
57 | setData(jsonData);
58 | } catch (error) {
59 | toast.error(
60 | error instanceof Error ? error.message : "Failed to load shared JSON"
61 | );
62 | } finally {
63 | setIsLoading(false);
64 | }
65 | }
66 |
67 | fetchSharedJson();
68 | }, [id]);
69 |
70 | useEffect(() => {
71 | if (data) {
72 | Prism.highlightAll();
73 | }
74 | }, [data]);
75 |
76 | useEffect(() => {
77 | function updateHeight() {
78 | const viewer = document.getElementById("json-viewer");
79 | if (viewer) {
80 | const viewerTop = viewer.getBoundingClientRect().top;
81 | const remainingHeight = `calc(100vh - ${viewerTop + 32}px)`;
82 | setViewerHeight(remainingHeight);
83 | }
84 | }
85 |
86 | updateHeight();
87 | window.addEventListener("resize", updateHeight);
88 |
89 | return () => window.removeEventListener("resize", updateHeight);
90 | }, [data]);
91 |
92 | const onCopy = async () => {
93 | await navigator.clipboard.writeText(JSON.stringify(data?.json, null, 2));
94 | setHasCopied(true);
95 | toast.success("JSON copied to clipboard");
96 |
97 | setTimeout(() => {
98 | setHasCopied(false);
99 | }, 2000);
100 | };
101 |
102 | if (isLoading) {
103 | return ;
104 | }
105 |
106 | if (!data) {
107 | return Failed to load shared JSON
;
108 | }
109 |
110 | return (
111 |
112 |
113 |
{data.metadata.title}
114 |
115 | Views: {data.metadata.viewCount} | Size:{" "}
116 | {(data.metadata.size / 1024).toFixed(2)}KB
117 |
118 |
119 |
120 |
126 | {hasCopied ? (
127 |
128 | ) : (
129 |
130 | )}
131 | Copy JSON
132 |
133 |
134 |
135 |
136 | {formatJson(data.json)}
137 |
138 |
139 |
140 |
141 |
142 | );
143 | }
144 |
--------------------------------------------------------------------------------
/src/components/site-header.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import { ModeToggle } from "./mode-toggle";
3 | import { Button } from "./ui/button";
4 | import { FileJson } from "lucide-react";
5 |
6 | export function SiteHeader() {
7 | return (
8 |
29 | );
30 | }
31 |
--------------------------------------------------------------------------------
/src/components/stats-card.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
4 | import { Skeleton } from "@/components/ui/skeleton";
5 | import { useQuery } from "@tanstack/react-query";
6 |
7 | interface Stats {
8 | totalDocuments: number;
9 | documentsLast24h: number;
10 | totalViews: number;
11 | averageSize: number;
12 | expiredDocuments: number;
13 | }
14 |
15 | async function fetchStats(): Promise {
16 | const response = await fetch("/api/stats");
17 | if (!response.ok) {
18 | throw new Error("Failed to fetch stats");
19 | }
20 | return response.json();
21 | }
22 |
23 | export function StatsCard() {
24 | const { data: stats, isLoading } = useQuery({
25 | queryKey: ["stats"],
26 | queryFn: fetchStats,
27 | refetchInterval: 30000, // Refetch every 30 seconds
28 | });
29 |
30 | if (isLoading) {
31 | return ;
32 | }
33 |
34 | return (
35 |
36 |
37 |
38 | Total Documents
39 |
40 |
41 | {stats?.totalDocuments}
42 |
43 |
44 |
45 |
46 |
47 | Last 24 Hours
48 |
49 |
50 | {stats?.documentsLast24h}
51 |
52 |
53 |
54 |
55 |
56 | Total Views
57 |
58 |
59 | {stats?.totalViews}
60 |
61 |
62 |
63 |
64 |
65 |
66 | Average Size (bytes)
67 |
68 |
69 |
70 | {stats?.averageSize}
71 |
72 |
73 |
74 |
75 |
76 |
77 | Expired Documents
78 |
79 |
80 |
81 | {stats?.expiredDocuments}
82 |
83 |
84 |
85 | );
86 | }
87 |
88 | function StatsCardSkeleton() {
89 | return (
90 |
91 | {[...Array(5)].map((_, i) => (
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 | ))}
101 |
102 | );
103 | }
104 |
--------------------------------------------------------------------------------
/src/components/type-generator-modal.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/ui/button";
2 | import {
3 | Dialog,
4 | DialogContent,
5 | DialogHeader,
6 | DialogTitle,
7 | DialogTrigger,
8 | } from "@/components/ui/dialog";
9 | import { Check, Code2, Copy } from "lucide-react";
10 | import { useEffect, useState } from "react";
11 | import { toast } from "sonner";
12 | import JsonToTS from "json-to-ts";
13 |
14 | interface TypeGeneratorModalProps {
15 | jsonInput: string;
16 | }
17 |
18 | export function TypeGeneratorModal({ jsonInput }: TypeGeneratorModalProps) {
19 | const [isOpen, setIsOpen] = useState(false);
20 | const [isCopied, setIsCopied] = useState(false);
21 | const [generatedInterfaces, setGeneratedInterfaces] = useState("");
22 |
23 | useEffect(() => {
24 | try {
25 | const parsedJson = JSON.parse(jsonInput);
26 | const interfaces = JsonToTS(parsedJson).join("\n\n");
27 | setGeneratedInterfaces(interfaces);
28 | } catch {
29 | // toast.error("Invalid JSON: Unable to generate type");
30 | }
31 | }, [jsonInput]);
32 |
33 | const handleCopy = () => {
34 | navigator.clipboard
35 | .writeText(generatedInterfaces)
36 | .then(() => {
37 | setIsCopied(true);
38 | toast.success("Type definition copied to clipboard");
39 | setTimeout(() => setIsCopied(false), 1500);
40 | })
41 | .catch(() => toast.error("Failed to copy to clipboard"));
42 | };
43 |
44 | return (
45 |
46 |
47 |
48 |
49 | Generate Type
50 |
51 |
52 |
53 |
54 | Generated TypeScript Type
55 |
56 |
57 |
58 | {generatedInterfaces}
59 |
60 |
66 | {isCopied ? (
67 |
68 | ) : (
69 |
70 | )}
71 |
72 |
73 |
74 |
75 | );
76 | }
77 |
--------------------------------------------------------------------------------
/src/components/ui/alert.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 alertVariants = cva(
7 | "relative w-full border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
8 | {
9 | variants: {
10 | variant: {
11 | default: "bg-background text-foreground",
12 | destructive:
13 | "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
14 | },
15 | },
16 | defaultVariants: {
17 | variant: "default",
18 | },
19 | }
20 | );
21 |
22 | const Alert = React.forwardRef<
23 | HTMLDivElement,
24 | React.HTMLAttributes & VariantProps
25 | >(({ className, variant, ...props }, ref) => (
26 |
32 | ));
33 | Alert.displayName = "Alert";
34 |
35 | const AlertTitle = React.forwardRef<
36 | HTMLParagraphElement,
37 | React.HTMLAttributes
38 | >(({ className, ...props }, ref) => (
39 |
44 | ));
45 | AlertTitle.displayName = "AlertTitle";
46 |
47 | const AlertDescription = React.forwardRef<
48 | HTMLParagraphElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ));
57 | AlertDescription.displayName = "AlertDescription";
58 |
59 | export { Alert, AlertTitle, AlertDescription };
60 |
--------------------------------------------------------------------------------
/src/components/ui/breadcrumb.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { ChevronRightIcon, DotsHorizontalIcon } from "@radix-ui/react-icons"
3 | import { Slot } from "@radix-ui/react-slot"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const Breadcrumb = React.forwardRef<
8 | HTMLElement,
9 | React.ComponentPropsWithoutRef<"nav"> & {
10 | separator?: React.ReactNode
11 | }
12 | >(({ ...props }, ref) => )
13 | Breadcrumb.displayName = "Breadcrumb"
14 |
15 | const BreadcrumbList = React.forwardRef<
16 | HTMLOListElement,
17 | React.ComponentPropsWithoutRef<"ol">
18 | >(({ className, ...props }, ref) => (
19 |
27 | ))
28 | BreadcrumbList.displayName = "BreadcrumbList"
29 |
30 | const BreadcrumbItem = React.forwardRef<
31 | HTMLLIElement,
32 | React.ComponentPropsWithoutRef<"li">
33 | >(({ className, ...props }, ref) => (
34 |
39 | ))
40 | BreadcrumbItem.displayName = "BreadcrumbItem"
41 |
42 | const BreadcrumbLink = React.forwardRef<
43 | HTMLAnchorElement,
44 | React.ComponentPropsWithoutRef<"a"> & {
45 | asChild?: boolean
46 | }
47 | >(({ asChild, className, ...props }, ref) => {
48 | const Comp = asChild ? Slot : "a"
49 |
50 | return (
51 |
56 | )
57 | })
58 | BreadcrumbLink.displayName = "BreadcrumbLink"
59 |
60 | const BreadcrumbPage = React.forwardRef<
61 | HTMLSpanElement,
62 | React.ComponentPropsWithoutRef<"span">
63 | >(({ className, ...props }, ref) => (
64 |
72 | ))
73 | BreadcrumbPage.displayName = "BreadcrumbPage"
74 |
75 | const BreadcrumbSeparator = ({
76 | children,
77 | className,
78 | ...props
79 | }: React.ComponentProps<"li">) => (
80 | svg]:w-3.5 [&>svg]:h-3.5", className)}
84 | {...props}
85 | >
86 | {children ?? }
87 |
88 | )
89 | BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
90 |
91 | const BreadcrumbEllipsis = ({
92 | className,
93 | ...props
94 | }: React.ComponentProps<"span">) => (
95 |
101 |
102 | More
103 |
104 | )
105 | BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
106 |
107 | export {
108 | Breadcrumb,
109 | BreadcrumbList,
110 | BreadcrumbItem,
111 | BreadcrumbLink,
112 | BreadcrumbPage,
113 | BreadcrumbSeparator,
114 | BreadcrumbEllipsis,
115 | }
116 |
--------------------------------------------------------------------------------
/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { Slot, Slottable } from "@radix-ui/react-slot";
3 | import { cva, type VariantProps } from "class-variance-authority";
4 | import { cn } from "@/lib/utils";
5 |
6 | const buttonVariants = cva(
7 | "inline-flex items-center justify-center whitespace-nowrap text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
8 | {
9 | variants: {
10 | variant: {
11 | default: "bg-primary text-primary-foreground hover:bg-primary/90",
12 | destructive:
13 | "bg-destructive text-destructive-foreground hover:bg-destructive/90",
14 | outline:
15 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
16 | secondary:
17 | "bg-secondary text-secondary-foreground hover:bg-secondary/80",
18 | ghost: "hover:bg-accent hover:text-accent-foreground",
19 | link: "text-primary underline-offset-4 hover:underline",
20 | expandIcon:
21 | "group relative text-primary-foreground bg-primary hover:bg-primary/90",
22 | ringHover:
23 | "bg-primary text-primary-foreground transition-all duration-300 hover:bg-primary/90 hover:ring-2 hover:ring-primary/90 hover:ring-offset-2",
24 | shine:
25 | "text-primary-foreground animate-shine bg-gradient-to-r from-primary via-primary/75 to-primary bg-[length:400%_100%] ",
26 | gooeyRight:
27 | "text-primary-foreground relative bg-primary z-0 overflow-hidden transition-all duration-500 before:absolute before:inset-0 before:-z-10 before:translate-x-[150%] before:translate-y-[150%] before:scale-[2.5] before:bg-gradient-to-r from-zinc-400 before:transition-transform before:duration-1000 hover:before:translate-x-[0%] hover:before:translate-y-[0%] ",
28 | gooeyLeft:
29 | "text-primary-foreground relative bg-primary z-0 overflow-hidden transition-all duration-500 after:absolute after:inset-0 after:-z-10 after:translate-x-[-150%] after:translate-y-[150%] after:scale-[2.5] after:bg-gradient-to-l from-zinc-400 after:transition-transform after:duration-1000 hover:after:translate-x-[0%] hover:after:translate-y-[0%] ",
30 | linkHover1:
31 | "relative after:absolute after:bg-primary after:bottom-2 after:h-[1px] after:w-2/3 after:origin-bottom-left after:scale-x-100 hover:after:origin-bottom-right hover:after:scale-x-0 after:transition-transform after:ease-in-out after:duration-300",
32 | linkHover2:
33 | "relative after:absolute after:bg-primary after:bottom-2 after:h-[1px] after:w-2/3 after:origin-bottom-right after:scale-x-0 hover:after:origin-bottom-left hover:after:scale-x-100 after:transition-transform after:ease-in-out after:duration-300",
34 | },
35 | size: {
36 | default: "h-10 px-4 py-2",
37 | xs: "h-7 px-2",
38 | sm: "h-9 px-3",
39 | lg: "h-11 px-8",
40 | icon: "h-10 w-10",
41 | },
42 | },
43 | defaultVariants: {
44 | variant: "default",
45 | size: "default",
46 | },
47 | }
48 | );
49 |
50 | interface IconProps {
51 | Icon: React.ElementType;
52 | iconPlacement: "left" | "right";
53 | }
54 |
55 | interface IconRefProps {
56 | Icon?: never;
57 | iconPlacement?: undefined;
58 | }
59 |
60 | export interface ButtonProps
61 | extends React.ButtonHTMLAttributes,
62 | VariantProps {
63 | asChild?: boolean;
64 | }
65 |
66 | export type ButtonIconProps = IconProps | IconRefProps;
67 |
68 | const Button = React.forwardRef<
69 | HTMLButtonElement,
70 | ButtonProps & ButtonIconProps
71 | >(
72 | (
73 | {
74 | className,
75 | variant,
76 | size,
77 | asChild = false,
78 | Icon,
79 | iconPlacement,
80 | ...props
81 | },
82 | ref
83 | ) => {
84 | const Comp = asChild ? Slot : "button";
85 | return (
86 |
91 | {Icon && iconPlacement === "left" && (
92 |
93 |
94 |
95 | )}
96 | {props.children}
97 | {Icon && iconPlacement === "right" && (
98 |
99 |
100 |
101 | )}
102 |
103 | );
104 | }
105 | );
106 | Button.displayName = "Button";
107 |
108 | export { Button, buttonVariants };
109 |
--------------------------------------------------------------------------------
/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 |
14 | ));
15 | Card.displayName = "Card";
16 |
17 | const CardHeader = React.forwardRef<
18 | HTMLDivElement,
19 | React.HTMLAttributes
20 | >(({ className, ...props }, ref) => (
21 |
26 | ));
27 | CardHeader.displayName = "CardHeader";
28 |
29 | const CardTitle = React.forwardRef<
30 | HTMLParagraphElement,
31 | React.HTMLAttributes
32 | >(({ className, ...props }, ref) => (
33 |
38 | ));
39 | CardTitle.displayName = "CardTitle";
40 |
41 | const CardDescription = React.forwardRef<
42 | HTMLParagraphElement,
43 | React.HTMLAttributes
44 | >(({ className, ...props }, ref) => (
45 |
50 | ));
51 | CardDescription.displayName = "CardDescription";
52 |
53 | const CardContent = React.forwardRef<
54 | HTMLDivElement,
55 | React.HTMLAttributes
56 | >(({ className, ...props }, ref) => (
57 |
58 | ));
59 | CardContent.displayName = "CardContent";
60 |
61 | const CardFooter = React.forwardRef<
62 | HTMLDivElement,
63 | React.HTMLAttributes
64 | >(({ className, ...props }, ref) => (
65 |
70 | ));
71 | CardFooter.displayName = "CardFooter";
72 |
73 | export {
74 | Card,
75 | CardHeader,
76 | CardFooter,
77 | CardTitle,
78 | CardDescription,
79 | CardContent,
80 | };
81 |
--------------------------------------------------------------------------------
/src/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as DialogPrimitive from "@radix-ui/react-dialog";
5 | import { Cross2Icon } from "@radix-ui/react-icons";
6 |
7 | import { cn } from "@/lib/utils";
8 |
9 | const Dialog = DialogPrimitive.Root;
10 |
11 | const DialogTrigger = DialogPrimitive.Trigger;
12 |
13 | const DialogPortal = DialogPrimitive.Portal;
14 |
15 | const DialogClose = DialogPrimitive.Close;
16 |
17 | const DialogOverlay = React.forwardRef<
18 | React.ElementRef,
19 | React.ComponentPropsWithoutRef
20 | >(({ className, ...props }, ref) => (
21 |
29 | ));
30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
31 |
32 | const DialogContent = React.forwardRef<
33 | React.ElementRef,
34 | React.ComponentPropsWithoutRef
35 | >(({ className, children, ...props }, ref) => (
36 |
37 |
38 |
46 | {children}
47 |
48 |
49 | Close
50 |
51 |
52 |
53 | ));
54 | DialogContent.displayName = DialogPrimitive.Content.displayName;
55 |
56 | const DialogHeader = ({
57 | className,
58 | ...props
59 | }: React.HTMLAttributes) => (
60 |
67 | );
68 | DialogHeader.displayName = "DialogHeader";
69 |
70 | const DialogFooter = ({
71 | className,
72 | ...props
73 | }: React.HTMLAttributes) => (
74 |
81 | );
82 | DialogFooter.displayName = "DialogFooter";
83 |
84 | const DialogTitle = React.forwardRef<
85 | React.ElementRef,
86 | React.ComponentPropsWithoutRef
87 | >(({ className, ...props }, ref) => (
88 |
96 | ));
97 | DialogTitle.displayName = DialogPrimitive.Title.displayName;
98 |
99 | const DialogDescription = React.forwardRef<
100 | React.ElementRef,
101 | React.ComponentPropsWithoutRef
102 | >(({ className, ...props }, ref) => (
103 |
108 | ));
109 | DialogDescription.displayName = DialogPrimitive.Description.displayName;
110 |
111 | export {
112 | Dialog,
113 | DialogPortal,
114 | DialogOverlay,
115 | DialogTrigger,
116 | DialogClose,
117 | DialogContent,
118 | DialogHeader,
119 | DialogFooter,
120 | DialogTitle,
121 | DialogDescription,
122 | };
123 |
--------------------------------------------------------------------------------
/src/components/ui/dropdown-menu.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
5 | import {
6 | CheckIcon,
7 | ChevronRightIcon,
8 | DotFilledIcon,
9 | } from "@radix-ui/react-icons";
10 |
11 | import { cn } from "@/lib/utils";
12 |
13 | const DropdownMenu = DropdownMenuPrimitive.Root;
14 |
15 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
16 |
17 | const DropdownMenuGroup = DropdownMenuPrimitive.Group;
18 |
19 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
20 |
21 | const DropdownMenuSub = DropdownMenuPrimitive.Sub;
22 |
23 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
24 |
25 | const DropdownMenuSubTrigger = React.forwardRef<
26 | React.ElementRef,
27 | React.ComponentPropsWithoutRef & {
28 | inset?: boolean;
29 | }
30 | >(({ className, inset, children, ...props }, ref) => (
31 |
40 | {children}
41 |
42 |
43 | ));
44 | DropdownMenuSubTrigger.displayName =
45 | DropdownMenuPrimitive.SubTrigger.displayName;
46 |
47 | const DropdownMenuSubContent = React.forwardRef<
48 | React.ElementRef,
49 | React.ComponentPropsWithoutRef
50 | >(({ className, ...props }, ref) => (
51 |
59 | ));
60 | DropdownMenuSubContent.displayName =
61 | DropdownMenuPrimitive.SubContent.displayName;
62 |
63 | const DropdownMenuContent = React.forwardRef<
64 | React.ElementRef,
65 | React.ComponentPropsWithoutRef
66 | >(({ className, sideOffset = 4, ...props }, ref) => (
67 |
68 |
78 |
79 | ));
80 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
81 |
82 | const DropdownMenuItem = React.forwardRef<
83 | React.ElementRef,
84 | React.ComponentPropsWithoutRef & {
85 | inset?: boolean;
86 | }
87 | >(({ className, inset, ...props }, ref) => (
88 |
97 | ));
98 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
99 |
100 | const DropdownMenuCheckboxItem = React.forwardRef<
101 | React.ElementRef,
102 | React.ComponentPropsWithoutRef
103 | >(({ className, children, checked, ...props }, ref) => (
104 |
113 |
114 |
115 |
116 |
117 |
118 | {children}
119 |
120 | ));
121 | DropdownMenuCheckboxItem.displayName =
122 | DropdownMenuPrimitive.CheckboxItem.displayName;
123 |
124 | const DropdownMenuRadioItem = React.forwardRef<
125 | React.ElementRef,
126 | React.ComponentPropsWithoutRef
127 | >(({ className, children, ...props }, ref) => (
128 |
136 |
137 |
138 |
139 |
140 |
141 | {children}
142 |
143 | ));
144 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
145 |
146 | const DropdownMenuLabel = React.forwardRef<
147 | React.ElementRef,
148 | React.ComponentPropsWithoutRef & {
149 | inset?: boolean;
150 | }
151 | >(({ className, inset, ...props }, ref) => (
152 |
161 | ));
162 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
163 |
164 | const DropdownMenuSeparator = React.forwardRef<
165 | React.ElementRef,
166 | React.ComponentPropsWithoutRef
167 | >(({ className, ...props }, ref) => (
168 |
173 | ));
174 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
175 |
176 | const DropdownMenuShortcut = ({
177 | className,
178 | ...props
179 | }: React.HTMLAttributes) => {
180 | return (
181 |
185 | );
186 | };
187 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
188 |
189 | export {
190 | DropdownMenu,
191 | DropdownMenuTrigger,
192 | DropdownMenuContent,
193 | DropdownMenuItem,
194 | DropdownMenuCheckboxItem,
195 | DropdownMenuRadioItem,
196 | DropdownMenuLabel,
197 | DropdownMenuSeparator,
198 | DropdownMenuShortcut,
199 | DropdownMenuGroup,
200 | DropdownMenuPortal,
201 | DropdownMenuSub,
202 | DropdownMenuSubContent,
203 | DropdownMenuSubTrigger,
204 | DropdownMenuRadioGroup,
205 | };
206 |
--------------------------------------------------------------------------------
/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "@/lib/utils";
4 |
5 | const Input = React.forwardRef<
6 | HTMLInputElement,
7 | React.InputHTMLAttributes
8 | >(({ className, type, ...props }, ref) => {
9 | return (
10 |
19 | );
20 | });
21 | Input.displayName = "Input";
22 |
23 | export { Input };
24 |
--------------------------------------------------------------------------------
/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/pagination.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import {
3 | ChevronLeftIcon,
4 | ChevronRightIcon,
5 | DotsHorizontalIcon,
6 | } from "@radix-ui/react-icons"
7 |
8 | import { cn } from "@/lib/utils"
9 | import { ButtonProps, buttonVariants } from "@/components/ui/button"
10 |
11 | const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
12 |
18 | )
19 | Pagination.displayName = "Pagination"
20 |
21 | const PaginationContent = React.forwardRef<
22 | HTMLUListElement,
23 | React.ComponentProps<"ul">
24 | >(({ className, ...props }, ref) => (
25 |
30 | ))
31 | PaginationContent.displayName = "PaginationContent"
32 |
33 | const PaginationItem = React.forwardRef<
34 | HTMLLIElement,
35 | React.ComponentProps<"li">
36 | >(({ className, ...props }, ref) => (
37 |
38 | ))
39 | PaginationItem.displayName = "PaginationItem"
40 |
41 | type PaginationLinkProps = {
42 | isActive?: boolean
43 | } & Pick &
44 | React.ComponentProps<"a">
45 |
46 | const PaginationLink = ({
47 | className,
48 | isActive,
49 | size = "icon",
50 | ...props
51 | }: PaginationLinkProps) => (
52 |
63 | )
64 | PaginationLink.displayName = "PaginationLink"
65 |
66 | const PaginationPrevious = ({
67 | className,
68 | ...props
69 | }: React.ComponentProps) => (
70 |
76 |
77 | Previous
78 |
79 | )
80 | PaginationPrevious.displayName = "PaginationPrevious"
81 |
82 | const PaginationNext = ({
83 | className,
84 | ...props
85 | }: React.ComponentProps) => (
86 |
92 | Next
93 |
94 |
95 | )
96 | PaginationNext.displayName = "PaginationNext"
97 |
98 | const PaginationEllipsis = ({
99 | className,
100 | ...props
101 | }: React.ComponentProps<"span">) => (
102 |
107 |
108 | More pages
109 |
110 | )
111 | PaginationEllipsis.displayName = "PaginationEllipsis"
112 |
113 | export {
114 | Pagination,
115 | PaginationContent,
116 | PaginationLink,
117 | PaginationItem,
118 | PaginationPrevious,
119 | PaginationNext,
120 | PaginationEllipsis,
121 | }
122 |
--------------------------------------------------------------------------------
/src/components/ui/scroll-area.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | const ScrollArea = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, children, ...props }, ref) => (
12 |
17 |
18 | {children}
19 |
20 |
21 |
22 |
23 | ));
24 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
25 |
26 | const ScrollBar = React.forwardRef<
27 | React.ElementRef,
28 | React.ComponentPropsWithoutRef
29 | >(({ className, orientation = "vertical", ...props }, ref) => (
30 |
43 |
44 |
45 | ));
46 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
47 |
48 | export { ScrollArea, ScrollBar };
49 |
--------------------------------------------------------------------------------
/src/components/ui/select.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SelectPrimitive from "@radix-ui/react-select"
5 | import { cn } from "@/lib/utils"
6 | import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "@radix-ui/react-icons"
7 |
8 | const Select = SelectPrimitive.Root
9 |
10 | const SelectGroup = SelectPrimitive.Group
11 |
12 | const SelectValue = SelectPrimitive.Value
13 |
14 | const SelectTrigger = React.forwardRef<
15 | React.ElementRef,
16 | React.ComponentPropsWithoutRef
17 | >(({ className, children, ...props }, ref) => (
18 | span]:line-clamp-1",
22 | className
23 | )}
24 | {...props}
25 | >
26 | {children}
27 |
28 |
29 |
30 |
31 | ))
32 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
33 |
34 | const SelectScrollUpButton = React.forwardRef<
35 | React.ElementRef,
36 | React.ComponentPropsWithoutRef
37 | >(({ className, ...props }, ref) => (
38 |
46 |
47 |
48 | ))
49 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
50 |
51 | const SelectScrollDownButton = React.forwardRef<
52 | React.ElementRef,
53 | React.ComponentPropsWithoutRef
54 | >(({ className, ...props }, ref) => (
55 |
63 |
64 |
65 | ))
66 | SelectScrollDownButton.displayName =
67 | SelectPrimitive.ScrollDownButton.displayName
68 |
69 | const SelectContent = React.forwardRef<
70 | React.ElementRef,
71 | React.ComponentPropsWithoutRef
72 | >(({ className, children, position = "popper", ...props }, ref) => (
73 |
74 |
85 |
86 |
93 | {children}
94 |
95 |
96 |
97 |
98 | ))
99 | SelectContent.displayName = SelectPrimitive.Content.displayName
100 |
101 | const SelectLabel = React.forwardRef<
102 | React.ElementRef,
103 | React.ComponentPropsWithoutRef
104 | >(({ className, ...props }, ref) => (
105 |
110 | ))
111 | SelectLabel.displayName = SelectPrimitive.Label.displayName
112 |
113 | const SelectItem = React.forwardRef<
114 | React.ElementRef,
115 | React.ComponentPropsWithoutRef
116 | >(({ className, children, ...props }, ref) => (
117 |
125 |
126 |
127 |
128 |
129 |
130 | {children}
131 |
132 | ))
133 | SelectItem.displayName = SelectPrimitive.Item.displayName
134 |
135 | const SelectSeparator = React.forwardRef<
136 | React.ElementRef,
137 | React.ComponentPropsWithoutRef
138 | >(({ className, ...props }, ref) => (
139 |
144 | ))
145 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName
146 |
147 | export {
148 | Select,
149 | SelectGroup,
150 | SelectValue,
151 | SelectTrigger,
152 | SelectContent,
153 | SelectLabel,
154 | SelectItem,
155 | SelectSeparator,
156 | SelectScrollUpButton,
157 | SelectScrollDownButton,
158 | }
159 |
--------------------------------------------------------------------------------
/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/sheet.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as SheetPrimitive from "@radix-ui/react-dialog";
5 | import { Cross2Icon } from "@radix-ui/react-icons";
6 | import { cva, type VariantProps } from "class-variance-authority";
7 |
8 | import { cn } from "@/lib/utils";
9 |
10 | const Sheet = SheetPrimitive.Root;
11 |
12 | const SheetTrigger = SheetPrimitive.Trigger;
13 |
14 | const SheetClose = SheetPrimitive.Close;
15 |
16 | const SheetPortal = SheetPrimitive.Portal;
17 |
18 | const SheetOverlay = React.forwardRef<
19 | React.ElementRef,
20 | React.ComponentPropsWithoutRef
21 | >(({ className, ...props }, ref) => (
22 |
30 | ));
31 | SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
32 |
33 | const sheetVariants = cva(
34 | "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out",
35 | {
36 | variants: {
37 | side: {
38 | top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
39 | bottom:
40 | "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
41 | left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
42 | right:
43 | "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
44 | },
45 | },
46 | defaultVariants: {
47 | side: "right",
48 | },
49 | }
50 | );
51 |
52 | interface SheetContentProps
53 | extends React.ComponentPropsWithoutRef,
54 | VariantProps {}
55 |
56 | const SheetContent = React.forwardRef<
57 | React.ElementRef,
58 | SheetContentProps
59 | >(({ side = "right", className, children, ...props }, ref) => (
60 |
61 |
62 |
67 |
68 |
69 | Close
70 |
71 | {children}
72 |
73 |
74 | ));
75 | SheetContent.displayName = SheetPrimitive.Content.displayName;
76 |
77 | const SheetHeader = ({
78 | className,
79 | ...props
80 | }: React.HTMLAttributes) => (
81 |
88 | );
89 | SheetHeader.displayName = "SheetHeader";
90 |
91 | const SheetFooter = ({
92 | className,
93 | ...props
94 | }: React.HTMLAttributes) => (
95 |
102 | );
103 | SheetFooter.displayName = "SheetFooter";
104 |
105 | const SheetTitle = React.forwardRef<
106 | React.ElementRef,
107 | React.ComponentPropsWithoutRef
108 | >(({ className, ...props }, ref) => (
109 |
114 | ));
115 | SheetTitle.displayName = SheetPrimitive.Title.displayName;
116 |
117 | const SheetDescription = React.forwardRef<
118 | React.ElementRef,
119 | React.ComponentPropsWithoutRef
120 | >(({ className, ...props }, ref) => (
121 |
126 | ));
127 | SheetDescription.displayName = SheetPrimitive.Description.displayName;
128 |
129 | export {
130 | Sheet,
131 | SheetPortal,
132 | SheetOverlay,
133 | SheetTrigger,
134 | SheetClose,
135 | SheetContent,
136 | SheetHeader,
137 | SheetFooter,
138 | SheetTitle,
139 | SheetDescription,
140 | };
141 |
--------------------------------------------------------------------------------
/src/components/ui/sidebar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import { Slot } from "@radix-ui/react-slot";
5 | import { VariantProps, cva } from "class-variance-authority";
6 | import { PanelLeft } from "lucide-react";
7 |
8 | import { useIsMobile } from "@/hooks/use-mobile";
9 | import { cn } from "@/lib/utils";
10 | import { Button } from "@/components/ui/button";
11 | import { Input } from "@/components/ui/input";
12 | import { Separator } from "@/components/ui/separator";
13 | import { Sheet, SheetContent } from "@/components/ui/sheet";
14 | import { Skeleton } from "@/components/ui/skeleton";
15 | import {
16 | Tooltip,
17 | TooltipContent,
18 | TooltipProvider,
19 | TooltipTrigger,
20 | } from "@/components/ui/tooltip";
21 |
22 | const SIDEBAR_COOKIE_NAME = "sidebar:state";
23 | const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
24 | const SIDEBAR_WIDTH = "16rem";
25 | const SIDEBAR_WIDTH_MOBILE = "18rem";
26 | const SIDEBAR_WIDTH_ICON = "3rem";
27 | const SIDEBAR_KEYBOARD_SHORTCUT = "b";
28 |
29 | type SidebarContext = {
30 | state: "expanded" | "collapsed";
31 | open: boolean;
32 | setOpen: (open: boolean) => void;
33 | openMobile: boolean;
34 | setOpenMobile: (open: boolean) => void;
35 | isMobile: boolean;
36 | toggleSidebar: () => void;
37 | };
38 |
39 | const SidebarContext = React.createContext(null);
40 |
41 | function useSidebar() {
42 | const context = React.useContext(SidebarContext);
43 | if (!context) {
44 | throw new Error("useSidebar must be used within a SidebarProvider.");
45 | }
46 |
47 | return context;
48 | }
49 |
50 | const SidebarProvider = React.forwardRef<
51 | HTMLDivElement,
52 | React.ComponentProps<"div"> & {
53 | defaultOpen?: boolean;
54 | open?: boolean;
55 | onOpenChange?: (open: boolean) => void;
56 | }
57 | >(
58 | (
59 | {
60 | defaultOpen = true,
61 | open: openProp,
62 | onOpenChange: setOpenProp,
63 | className,
64 | style,
65 | children,
66 | ...props
67 | },
68 | ref
69 | ) => {
70 | const isMobile = useIsMobile();
71 | const [openMobile, setOpenMobile] = React.useState(false);
72 |
73 | // This is the internal state of the sidebar.
74 | // We use openProp and setOpenProp for control from outside the component.
75 | const [_open, _setOpen] = React.useState(defaultOpen);
76 | const open = openProp ?? _open;
77 | const setOpen = React.useCallback(
78 | (value: boolean | ((value: boolean) => boolean)) => {
79 | const openState = typeof value === "function" ? value(open) : value;
80 | if (setOpenProp) {
81 | setOpenProp(openState);
82 | } else {
83 | _setOpen(openState);
84 | }
85 |
86 | // This sets the cookie to keep the sidebar state.
87 | document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
88 | },
89 | [setOpenProp, open]
90 | );
91 |
92 | // Helper to toggle the sidebar.
93 | const toggleSidebar = React.useCallback(() => {
94 | return isMobile
95 | ? setOpenMobile((open) => !open)
96 | : setOpen((open) => !open);
97 | }, [isMobile, setOpen, setOpenMobile]);
98 |
99 | // Adds a keyboard shortcut to toggle the sidebar.
100 | React.useEffect(() => {
101 | const handleKeyDown = (event: KeyboardEvent) => {
102 | if (
103 | event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
104 | (event.metaKey || event.ctrlKey)
105 | ) {
106 | event.preventDefault();
107 | toggleSidebar();
108 | }
109 | };
110 |
111 | window.addEventListener("keydown", handleKeyDown);
112 | return () => window.removeEventListener("keydown", handleKeyDown);
113 | }, [toggleSidebar]);
114 |
115 | // We add a state so that we can do data-state="expanded" or "collapsed".
116 | // This makes it easier to style the sidebar with Tailwind classes.
117 | const state = open ? "expanded" : "collapsed";
118 |
119 | const contextValue = React.useMemo(
120 | () => ({
121 | state,
122 | open,
123 | setOpen,
124 | isMobile,
125 | openMobile,
126 | setOpenMobile,
127 | toggleSidebar,
128 | }),
129 | [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
130 | );
131 |
132 | return (
133 |
134 |
135 |
150 | {children}
151 |
152 |
153 |
154 | );
155 | }
156 | );
157 | SidebarProvider.displayName = "SidebarProvider";
158 |
159 | const Sidebar = React.forwardRef<
160 | HTMLDivElement,
161 | React.ComponentProps<"div"> & {
162 | side?: "left" | "right";
163 | variant?: "sidebar" | "floating" | "inset";
164 | collapsible?: "offcanvas" | "icon" | "none";
165 | }
166 | >(
167 | (
168 | {
169 | side = "left",
170 | variant = "sidebar",
171 | collapsible = "offcanvas",
172 | className,
173 | children,
174 | ...props
175 | },
176 | ref
177 | ) => {
178 | const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
179 |
180 | if (collapsible === "none") {
181 | return (
182 |
190 | {children}
191 |
192 | );
193 | }
194 |
195 | if (isMobile) {
196 | return (
197 |
198 |
209 | {children}
210 |
211 |
212 | );
213 | }
214 |
215 | return (
216 |
224 | {/* This is what handles the sidebar gap on desktop */}
225 |
235 |
249 |
253 | {children}
254 |
255 |
256 |
257 | );
258 | }
259 | );
260 | Sidebar.displayName = "Sidebar";
261 |
262 | const SidebarTrigger = React.forwardRef<
263 | React.ElementRef,
264 | React.ComponentProps
265 | >(({ className, onClick, ...props }, ref) => {
266 | const { toggleSidebar } = useSidebar();
267 |
268 | return (
269 | {
276 | onClick?.(event);
277 | toggleSidebar();
278 | }}
279 | {...props}
280 | >
281 |
282 | Toggle Sidebar
283 |
284 | );
285 | });
286 | SidebarTrigger.displayName = "SidebarTrigger";
287 |
288 | const SidebarRail = React.forwardRef<
289 | HTMLButtonElement,
290 | React.ComponentProps<"button">
291 | >(({ className, ...props }, ref) => {
292 | const { toggleSidebar } = useSidebar();
293 |
294 | return (
295 |
313 | );
314 | });
315 | SidebarRail.displayName = "SidebarRail";
316 |
317 | const SidebarInset = React.forwardRef<
318 | HTMLDivElement,
319 | React.ComponentProps<"main">
320 | >(({ className, ...props }, ref) => {
321 | return (
322 |
331 | );
332 | });
333 | SidebarInset.displayName = "SidebarInset";
334 |
335 | const SidebarInput = React.forwardRef<
336 | React.ElementRef,
337 | React.ComponentProps
338 | >(({ className, ...props }, ref) => {
339 | return (
340 |
349 | );
350 | });
351 | SidebarInput.displayName = "SidebarInput";
352 |
353 | const SidebarHeader = React.forwardRef<
354 | HTMLDivElement,
355 | React.ComponentProps<"div">
356 | >(({ className, ...props }, ref) => {
357 | return (
358 |
364 | );
365 | });
366 | SidebarHeader.displayName = "SidebarHeader";
367 |
368 | const SidebarFooter = React.forwardRef<
369 | HTMLDivElement,
370 | React.ComponentProps<"div">
371 | >(({ className, ...props }, ref) => {
372 | return (
373 |
379 | );
380 | });
381 | SidebarFooter.displayName = "SidebarFooter";
382 |
383 | const SidebarSeparator = React.forwardRef<
384 | React.ElementRef,
385 | React.ComponentProps
386 | >(({ className, ...props }, ref) => {
387 | return (
388 |
394 | );
395 | });
396 | SidebarSeparator.displayName = "SidebarSeparator";
397 |
398 | const SidebarContent = React.forwardRef<
399 | HTMLDivElement,
400 | React.ComponentProps<"div">
401 | >(({ className, ...props }, ref) => {
402 | return (
403 |
412 | );
413 | });
414 | SidebarContent.displayName = "SidebarContent";
415 |
416 | const SidebarGroup = React.forwardRef<
417 | HTMLDivElement,
418 | React.ComponentProps<"div">
419 | >(({ className, ...props }, ref) => {
420 | return (
421 |
427 | );
428 | });
429 | SidebarGroup.displayName = "SidebarGroup";
430 |
431 | const SidebarGroupLabel = React.forwardRef<
432 | HTMLDivElement,
433 | React.ComponentProps<"div"> & { asChild?: boolean }
434 | >(({ className, asChild = false, ...props }, ref) => {
435 | const Comp = asChild ? Slot : "div";
436 |
437 | return (
438 | svg]:size-4 [&>svg]:shrink-0",
443 | "group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
444 | className
445 | )}
446 | {...props}
447 | />
448 | );
449 | });
450 | SidebarGroupLabel.displayName = "SidebarGroupLabel";
451 |
452 | const SidebarGroupAction = React.forwardRef<
453 | HTMLButtonElement,
454 | React.ComponentProps<"button"> & { asChild?: boolean }
455 | >(({ className, asChild = false, ...props }, ref) => {
456 | const Comp = asChild ? Slot : "button";
457 |
458 | return (
459 | svg]:size-4 [&>svg]:shrink-0",
464 | // Increases the hit area of the button on mobile.
465 | "after:absolute after:-inset-2 after:md:hidden",
466 | "group-data-[collapsible=icon]:hidden",
467 | className
468 | )}
469 | {...props}
470 | />
471 | );
472 | });
473 | SidebarGroupAction.displayName = "SidebarGroupAction";
474 |
475 | const SidebarGroupContent = React.forwardRef<
476 | HTMLDivElement,
477 | React.ComponentProps<"div">
478 | >(({ className, ...props }, ref) => (
479 |
485 | ));
486 | SidebarGroupContent.displayName = "SidebarGroupContent";
487 |
488 | const SidebarMenu = React.forwardRef<
489 | HTMLUListElement,
490 | React.ComponentProps<"ul">
491 | >(({ className, ...props }, ref) => (
492 |
498 | ));
499 | SidebarMenu.displayName = "SidebarMenu";
500 |
501 | const SidebarMenuItem = React.forwardRef<
502 | HTMLLIElement,
503 | React.ComponentProps<"li">
504 | >(({ className, ...props }, ref) => (
505 |
511 | ));
512 | SidebarMenuItem.displayName = "SidebarMenuItem";
513 |
514 | const sidebarMenuButtonVariants = cva(
515 | "peer/menu-button flex w-full items-center gap-2 overflow-hidden p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
516 | {
517 | variants: {
518 | variant: {
519 | default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
520 | outline:
521 | "bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
522 | },
523 | size: {
524 | default: "h-8 text-sm",
525 | sm: "h-7 text-xs",
526 | lg: "h-12 text-sm group-data-[collapsible=icon]:!p-0",
527 | },
528 | },
529 | defaultVariants: {
530 | variant: "default",
531 | size: "default",
532 | },
533 | }
534 | );
535 |
536 | const SidebarMenuButton = React.forwardRef<
537 | HTMLButtonElement,
538 | React.ComponentProps<"button"> & {
539 | asChild?: boolean;
540 | isActive?: boolean;
541 | tooltip?: string | React.ComponentProps;
542 | } & VariantProps
543 | >(
544 | (
545 | {
546 | asChild = false,
547 | isActive = false,
548 | variant = "default",
549 | size = "default",
550 | tooltip,
551 | className,
552 | ...props
553 | },
554 | ref
555 | ) => {
556 | const Comp = asChild ? Slot : "button";
557 | const { isMobile, state } = useSidebar();
558 |
559 | const button = (
560 |
568 | );
569 |
570 | if (!tooltip) {
571 | return button;
572 | }
573 |
574 | if (typeof tooltip === "string") {
575 | tooltip = {
576 | children: tooltip,
577 | };
578 | }
579 |
580 | return (
581 |
582 | {button}
583 |
589 |
590 | );
591 | }
592 | );
593 | SidebarMenuButton.displayName = "SidebarMenuButton";
594 |
595 | const SidebarMenuAction = React.forwardRef<
596 | HTMLButtonElement,
597 | React.ComponentProps<"button"> & {
598 | asChild?: boolean;
599 | showOnHover?: boolean;
600 | }
601 | >(({ className, asChild = false, showOnHover = false, ...props }, ref) => {
602 | const Comp = asChild ? Slot : "button";
603 |
604 | return (
605 | svg]:size-4 [&>svg]:shrink-0",
610 | // Increases the hit area of the button on mobile.
611 | "after:absolute after:-inset-2 after:md:hidden",
612 | "peer-data-[size=sm]/menu-button:top-1",
613 | "peer-data-[size=default]/menu-button:top-1.5",
614 | "peer-data-[size=lg]/menu-button:top-2.5",
615 | "group-data-[collapsible=icon]:hidden",
616 | showOnHover &&
617 | "group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0",
618 | className
619 | )}
620 | {...props}
621 | />
622 | );
623 | });
624 | SidebarMenuAction.displayName = "SidebarMenuAction";
625 |
626 | const SidebarMenuBadge = React.forwardRef<
627 | HTMLDivElement,
628 | React.ComponentProps<"div">
629 | >(({ className, ...props }, ref) => (
630 |
644 | ));
645 | SidebarMenuBadge.displayName = "SidebarMenuBadge";
646 |
647 | const SidebarMenuSkeleton = React.forwardRef<
648 | HTMLDivElement,
649 | React.ComponentProps<"div"> & {
650 | showIcon?: boolean;
651 | }
652 | >(({ className, showIcon = false, ...props }, ref) => {
653 | // Random width between 50 to 90%.
654 | const width = React.useMemo(() => {
655 | return `${Math.floor(Math.random() * 40) + 50}%`;
656 | }, []);
657 |
658 | return (
659 |
665 | {showIcon && (
666 |
667 | )}
668 |
677 |
678 | );
679 | });
680 | SidebarMenuSkeleton.displayName = "SidebarMenuSkeleton";
681 |
682 | const SidebarMenuSub = React.forwardRef<
683 | HTMLUListElement,
684 | React.ComponentProps<"ul">
685 | >(({ className, ...props }, ref) => (
686 |
696 | ));
697 | SidebarMenuSub.displayName = "SidebarMenuSub";
698 |
699 | const SidebarMenuSubItem = React.forwardRef<
700 | HTMLLIElement,
701 | React.ComponentProps<"li">
702 | >(({ ...props }, ref) => );
703 | SidebarMenuSubItem.displayName = "SidebarMenuSubItem";
704 |
705 | const SidebarMenuSubButton = React.forwardRef<
706 | HTMLAnchorElement,
707 | React.ComponentProps<"a"> & {
708 | asChild?: boolean;
709 | size?: "sm" | "md";
710 | isActive?: boolean;
711 | }
712 | >(({ asChild = false, size = "md", isActive, className, ...props }, ref) => {
713 | const Comp = asChild ? Slot : "a";
714 |
715 | return (
716 | span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
723 | "data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
724 | size === "sm" && "text-xs",
725 | size === "md" && "text-sm",
726 | "group-data-[collapsible=icon]:hidden",
727 | className
728 | )}
729 | {...props}
730 | />
731 | );
732 | });
733 | SidebarMenuSubButton.displayName = "SidebarMenuSubButton";
734 |
735 | export {
736 | Sidebar,
737 | SidebarContent,
738 | SidebarFooter,
739 | SidebarGroup,
740 | SidebarGroupAction,
741 | SidebarGroupContent,
742 | SidebarGroupLabel,
743 | SidebarHeader,
744 | SidebarInput,
745 | SidebarInset,
746 | SidebarMenu,
747 | SidebarMenuAction,
748 | SidebarMenuBadge,
749 | SidebarMenuButton,
750 | SidebarMenuItem,
751 | SidebarMenuSkeleton,
752 | SidebarMenuSub,
753 | SidebarMenuSubButton,
754 | SidebarMenuSubItem,
755 | SidebarProvider,
756 | SidebarRail,
757 | SidebarSeparator,
758 | SidebarTrigger,
759 | useSidebar,
760 | };
761 |
--------------------------------------------------------------------------------
/src/components/ui/skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 |
3 | function Skeleton({
4 | className,
5 | ...props
6 | }: React.HTMLAttributes) {
7 | return (
8 |
9 | );
10 | }
11 |
12 | export { Skeleton };
13 |
--------------------------------------------------------------------------------
/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/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "@/lib/utils";
4 |
5 | const Textarea = React.forwardRef<
6 | HTMLTextAreaElement,
7 | React.TextareaHTMLAttributes
8 | >(({ className, ...props }, ref) => {
9 | return (
10 |
18 | );
19 | });
20 | Textarea.displayName = "Textarea";
21 |
22 | export { Textarea };
23 |
--------------------------------------------------------------------------------
/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 |
19 |
28 |
29 | ));
30 | TooltipContent.displayName = TooltipPrimitive.Content.displayName;
31 |
32 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
33 |
--------------------------------------------------------------------------------
/src/env.ts:
--------------------------------------------------------------------------------
1 | import { createEnv } from "@t3-oss/env-nextjs";
2 | import { z } from "zod";
3 |
4 | export const env = createEnv({
5 | server: {
6 | NODE_ENV: z.enum(["development", "test", "production"]),
7 | DATABASE_URL: z.string().url(),
8 | ADMIN_KEY: z.string(),
9 | },
10 | client: {},
11 | runtimeEnv: {
12 | NODE_ENV: process.env.NODE_ENV,
13 | DATABASE_URL: process.env.DATABASE_URL,
14 | ADMIN_KEY: process.env.ADMIN_KEY,
15 | },
16 | });
17 |
--------------------------------------------------------------------------------
/src/hooks/use-mobile.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | const MOBILE_BREAKPOINT = 768
4 |
5 | export function useIsMobile() {
6 | const [isMobile, setIsMobile] = React.useState(undefined)
7 |
8 | React.useEffect(() => {
9 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
10 | const onChange = () => {
11 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
12 | }
13 | mql.addEventListener("change", onChange)
14 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
15 | return () => mql.removeEventListener("change", onChange)
16 | }, [])
17 |
18 | return !!isMobile
19 | }
20 |
--------------------------------------------------------------------------------
/src/lib/data/index.ts:
--------------------------------------------------------------------------------
1 | export const json_explanation = {
2 | status: true,
3 | data: {
4 | steps: [
5 | {
6 | explanation:
7 | "The JSON structure is a single object that contains three main keys at the top level: 'user', 'preferences', and 'recentOrders'.",
8 | output:
9 | '{"user":{"id":1,"name":"John Doe","email":"john@example.com","age":30,"isActive":true},"preferences":{"theme":"dark","notifications":{"email":true,"push":false}},"recentOrders":[{"orderId":"ORD-001","total":99.99,"items":["item1","item2"]},{"orderId":"ORD-002","total":149.99,"items":["item3","item4","item5"]}]}.',
10 | },
11 | {
12 | explanation:
13 | "The 'user' key maps to an object that contains information about a user.",
14 | output:
15 | '{"id":1,"name":"John Doe","email":"john@example.com","age":30,"isActive":true}',
16 | },
17 | {
18 | explanation:
19 | "Within the 'user' object, there are five keys: 'id', 'name', 'email', 'age', and 'isActive'.",
20 | output:
21 | '{"id":1,"name":"John Doe","email":"john@example.com","age":30,"isActive":true}',
22 | },
23 | {
24 | explanation:
25 | "The 'id' key has a value of 1, indicating the user's unique identifier.",
26 | output: '"id":1',
27 | },
28 | {
29 | explanation:
30 | "The 'name' key has a value of 'John Doe', representing the user's full name.",
31 | output: '"name":"John Doe"',
32 | },
33 | {
34 | explanation:
35 | "The 'email' key has a value of 'john@example.com', which is the user's email address.",
36 | output: '"email":"john@example.com"',
37 | },
38 | {
39 | explanation:
40 | "The 'age' key has a value of 30, indicating the user's age.",
41 | output: '"age":30',
42 | },
43 | {
44 | explanation:
45 | "The 'isActive' key has a value of true, suggesting that the user's account is currently active.",
46 | output: '"isActive":true',
47 | },
48 | {
49 | explanation:
50 | "The 'preferences' key maps to an object that contains user preferences.",
51 | output: '{"theme":"dark","notifications":{"email":true,"push":false}}',
52 | },
53 | {
54 | explanation:
55 | "Within the 'preferences' object, there are two keys: 'theme' and 'notifications'.",
56 | output: '{"theme":"dark","notifications":{"email":true,"push":false}}',
57 | },
58 | {
59 | explanation:
60 | "The 'theme' key has a value of 'dark', indicating the user's preferred theme setting.",
61 | output: '"theme":"dark"',
62 | },
63 | {
64 | explanation:
65 | "The 'notifications' key maps to another object that specifies notification preferences.",
66 | output: '{"email":true,"push":false}',
67 | },
68 | {
69 | explanation:
70 | "Within the 'notifications' object, there are two keys: 'email' and 'push'.",
71 | output: '{"email":true,"push":false}',
72 | },
73 | {
74 | explanation:
75 | "The 'email' key has a value of true, meaning the user wants to receive email notifications.",
76 | output: '"email":true',
77 | },
78 | {
79 | explanation:
80 | "The 'push' key has a value of false, indicating the user does not want to receive push notifications.",
81 | output: '"push":false',
82 | },
83 | {
84 | explanation:
85 | "The 'recentOrders' key maps to an array containing objects, each representing a recent order.",
86 | output:
87 | '[{"orderId":"ORD-001","total":99.99,"items":["item1","item2"]},{"orderId":"ORD-002","total":149.99,"items":["item3","item4","item5"]}]',
88 | },
89 | {
90 | explanation:
91 | "Each object in the 'recentOrders' array has three keys: 'orderId', 'total', and 'items'.",
92 | output: '{"orderId":"ORD-001","total":99.99,"items":["item1","item2"]}',
93 | },
94 | {
95 | explanation:
96 | "The 'orderId' key has a string value that uniquely identifies the order, such as 'ORD-001'.",
97 | output: '"orderId":"ORD-001"',
98 | },
99 | {
100 | explanation:
101 | "The 'total' key has a numeric value representing the total cost of the order, such as 99.99.",
102 | output: '"total":99.99',
103 | },
104 | {
105 | explanation:
106 | "The 'items' key maps to an array of strings, each representing an item included in the order.",
107 | output: '"items":["item1","item2"]',
108 | },
109 | ],
110 | summary:
111 | "This JSON structure represents a user's data with details about their identity, preferences, and recent orders. It includes nested objects and arrays to organize related information effectively.",
112 | },
113 | };
114 |
--------------------------------------------------------------------------------
/src/lib/providers/index.tsx:
--------------------------------------------------------------------------------
1 | import QueryProvider from "./query-provider";
2 | import ToastProvider from "./toast-provider";
3 | import { ThemeProvider } from "./theme-provider";
4 |
5 | export default function Providers({ children }: { children: React.ReactNode }) {
6 | return (
7 |
8 |
13 | {children}
14 |
15 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/src/lib/providers/query-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
4 | import { useState } from "react";
5 |
6 | const QueryProvider = ({ children }: { children: React.ReactNode }) => {
7 | const [queryClient] = useState(() => new QueryClient());
8 |
9 | return (
10 | {children}
11 | );
12 | };
13 |
14 | export default QueryProvider;
15 |
--------------------------------------------------------------------------------
/src/lib/providers/theme-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import { ThemeProvider as NextThemesProvider } from "next-themes";
5 | import { type ThemeProviderProps } from "next-themes/dist/types";
6 |
7 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
8 | return (
9 |
15 | {children}
16 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/src/lib/providers/toast-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useTheme } from "next-themes";
4 | import { Toaster } from "sonner";
5 |
6 | export default function ToastProvider({
7 | children,
8 | }: {
9 | children: React.ReactNode;
10 | }) {
11 | const { theme } = useTheme();
12 | return (
13 | <>
14 |
19 | {children}
20 | >
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/src/lib/services/admin/index.ts:
--------------------------------------------------------------------------------
1 | type FetchDocumentsResponse = {
2 | documents: {
3 | id: string;
4 | title: string;
5 | size: number;
6 | viewCount: number;
7 | createdAt: string;
8 | expiresAt: string;
9 | isValid: boolean;
10 | }[];
11 | pagination: {
12 | total: number;
13 | pageSize: number;
14 | currentPage: number;
15 | totalPages: number;
16 | };
17 | };
18 |
19 | export async function fetchDocuments(
20 | page: number
21 | ): Promise {
22 | const res = await fetch(`/api/share?all=true&page=${page}`);
23 | if (!res.ok) throw new Error("Failed to fetch documents");
24 | return res.json();
25 | }
26 |
27 | type DeleteDocumentResponse = {
28 | message: string;
29 | id: string;
30 | };
31 |
32 | export async function deleteDocument(
33 | id: string
34 | ): Promise {
35 | const res = await fetch(`/api/share?id=${id}`, {
36 | method: "DELETE",
37 | });
38 | if (!res.ok) throw new Error("Failed to delete document");
39 | return res.json();
40 | }
41 |
--------------------------------------------------------------------------------
/src/lib/services/openai/index.ts:
--------------------------------------------------------------------------------
1 | // const myHeaders = new Headers();
2 | // myHeaders.append("Content-Type", "application/json");
3 |
4 | // const raw = JSON.stringify({
5 | // apiKey: "sk-proj-xxxxx",
6 | // json: {
7 | // user: {
8 | // id: 1,
9 | // name: "John Doe",
10 | // email: "john@example.com",
11 | // age: 30,
12 | // isActive: true,
13 | // },
14 | // preferences: {
15 | // theme: "dark",
16 | // notifications: {
17 | // email: true,
18 | // push: false,
19 | // },
20 | // },
21 | // recentOrders: [
22 | // {
23 | // orderId: "ORD-001",
24 | // total: 99.99,
25 | // items: ["item1", "item2"],
26 | // },
27 | // {
28 | // orderId: "ORD-002",
29 | // total: 149.99,
30 | // items: ["item3", "item4", "item5"],
31 | // },
32 | // ],
33 | // },
34 | // });
35 |
36 | // const requestOptions = {
37 | // method: "POST",
38 | // headers: myHeaders,
39 | // body: raw,
40 | // };
41 |
42 | // fetch("http://localhost:3000/api/openai", requestOptions)
43 | // .then((response) => response.text())
44 | // .then((result) => console.log(result))
45 | // .catch((error) => console.error(error));
46 |
47 | type ExplainJsonResponse = {
48 | status: boolean;
49 | data: {
50 | summary: string;
51 | steps: {
52 | explanation: string;
53 | output: string;
54 | }[];
55 | };
56 | };
57 |
58 | export async function explainJson({
59 | apiKey,
60 | jsonData,
61 | }: {
62 | apiKey: string;
63 | jsonData: any;
64 | }): Promise {
65 | const raw = JSON.stringify({
66 | apiKey,
67 | json: jsonData,
68 | });
69 |
70 | const response = await fetch("/api/openai", {
71 | method: "POST",
72 | headers: {
73 | "Content-Type": "application/json",
74 | },
75 | body: raw,
76 | });
77 | return response.json();
78 | }
79 |
--------------------------------------------------------------------------------
/src/lib/services/share/index.ts:
--------------------------------------------------------------------------------
1 | type SharedJsonResponse = {
2 | json: string;
3 | metadata: {
4 | title: string;
5 | viewCount: number;
6 | size: number;
7 | isValid: boolean;
8 | };
9 | };
10 |
11 | export async function fetchSharedJson(
12 | shareId: string
13 | ): Promise {
14 | const response = await fetch(`/api/share?id=${shareId}`);
15 |
16 | if (!response.ok) {
17 | throw new Error("Failed to fetch shared JSON");
18 | }
19 |
20 | return response.json();
21 | }
22 |
--------------------------------------------------------------------------------
/src/lib/stores/create-selectors.ts:
--------------------------------------------------------------------------------
1 | import { StoreApi, UseBoundStore } from "zustand";
2 |
3 | type WithSelectors = S extends { getState: () => infer T }
4 | ? S & { use: { [K in keyof T]: () => T[K] } }
5 | : never;
6 |
7 | export const createSelectors = >>(
8 | _store: S
9 | ) => {
10 | const store = _store as WithSelectors;
11 | store.use = {};
12 | for (const k of Object.keys(store.getState())) {
13 | (store.use as any)[k] = () => store((s) => s[k as keyof typeof s]);
14 | }
15 |
16 | return store;
17 | };
18 |
--------------------------------------------------------------------------------
/src/lib/stores/json-visualizer-store.ts:
--------------------------------------------------------------------------------
1 | import { create } from "zustand";
2 | import { createSelectors } from "./create-selectors";
3 | // import { persist, createJSONStorage } from "zustand/middleware";
4 |
5 | export type TabValue = "input" | "tree" | "grid" | "ai";
6 |
7 | interface ExplanationStep {
8 | explanation: string;
9 | output: string;
10 | }
11 |
12 | interface AIExplanation {
13 | summary: string;
14 | steps: ExplanationStep[];
15 | }
16 |
17 | interface JsonVisualizerState {
18 | activeTab: TabValue;
19 | jsonInput: string;
20 | parsedJson: any;
21 | error: string | null;
22 | isLoading: boolean;
23 | aiExplanation: AIExplanation | null;
24 | setActiveTab: (tab: TabValue) => void;
25 | setJsonInput: (input: string) => void;
26 | setParsedJson: (json: any) => void;
27 | setError: (error: string | null) => void;
28 | setIsLoading: (isLoading: boolean) => void;
29 | setAIExplanation: (explanation: AIExplanation | null) => void;
30 | }
31 |
32 | const useJsonVisualizerStoreBase = create()((set) => ({
33 | activeTab: "input",
34 | jsonInput: "",
35 | parsedJson: null,
36 | error: null,
37 | isLoading: false,
38 | aiExplanation: null,
39 | setActiveTab: (tab) => set({ activeTab: tab }),
40 | setJsonInput: (input) => set({ jsonInput: input }),
41 | setParsedJson: (json) => set({ parsedJson: json }),
42 | setError: (error) => set({ error }),
43 | setIsLoading: (isLoading) => set({ isLoading }),
44 | setAIExplanation: (explanation) => set({ aiExplanation: explanation }),
45 | }));
46 |
47 | export const useJsonVisualizerStore = createSelectors(
48 | useJsonVisualizerStoreBase
49 | );
50 |
--------------------------------------------------------------------------------
/src/lib/stores/key-store.ts:
--------------------------------------------------------------------------------
1 | import { create } from "zustand";
2 | import { persist, createJSONStorage } from "zustand/middleware";
3 | import { createSelectors } from "./create-selectors";
4 |
5 | interface KeyState {
6 | openAIKey: string;
7 | setOpenAIKey: (key: string) => void;
8 | clearOpenAIKey: () => void;
9 | }
10 |
11 | export const useKeyStoreBase = create()(
12 | persist(
13 | (set) => ({
14 | openAIKey: "",
15 | setOpenAIKey: (key: string) => set({ openAIKey: key }),
16 | clearOpenAIKey: () => set({ openAIKey: "" }),
17 | }),
18 | {
19 | name: "openai-key-storage",
20 | storage: createJSONStorage(() => localStorage),
21 | }
22 | )
23 | );
24 |
25 | export const useKeyStore = createSelectors(useKeyStoreBase);
26 |
--------------------------------------------------------------------------------
/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from "clsx";
2 | import { twMerge } from "tailwind-merge";
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs));
6 | }
7 |
8 | export function capitalize(str: string): string {
9 | return str.charAt(0).toUpperCase() + str.slice(1);
10 | }
11 |
--------------------------------------------------------------------------------
/src/server/db.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from "@prisma/client";
2 |
3 | import { env } from "@/env";
4 |
5 | const createPrismaClient = () =>
6 | new PrismaClient({
7 | log:
8 | env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"],
9 | });
10 |
11 | const globalForPrisma = globalThis as unknown as {
12 | prisma: ReturnType | undefined;
13 | };
14 |
15 | export const db = globalForPrisma.prisma ?? createPrismaClient();
16 |
17 | if (env.NODE_ENV !== "production") globalForPrisma.prisma = db;
18 |
--------------------------------------------------------------------------------
/src/types/api.ts:
--------------------------------------------------------------------------------
1 | export interface JsonStats {
2 | totalDocuments: number;
3 | documentsLast24h: number;
4 | totalViews: number;
5 | averageSize: number;
6 | expiredDocuments: number;
7 | }
8 |
--------------------------------------------------------------------------------
/src/types/react-json-grid.d.ts:
--------------------------------------------------------------------------------
1 | declare module "@redheadphone/react-json-grid";
2 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 |
3 | const config: Config = {
4 | darkMode: ["class"],
5 | content: [
6 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
7 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
8 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}",
9 | ],
10 | theme: {
11 | extend: {
12 | colors: {
13 | background: "hsl(var(--background))",
14 | foreground: "hsl(var(--foreground))",
15 | card: {
16 | DEFAULT: "hsl(var(--card))",
17 | foreground: "hsl(var(--card-foreground))",
18 | },
19 | popover: {
20 | DEFAULT: "hsl(var(--popover))",
21 | foreground: "hsl(var(--popover-foreground))",
22 | },
23 | primary: {
24 | DEFAULT: "hsl(var(--primary))",
25 | foreground: "hsl(var(--primary-foreground))",
26 | },
27 | secondary: {
28 | DEFAULT: "hsl(var(--secondary))",
29 | foreground: "hsl(var(--secondary-foreground))",
30 | },
31 | muted: {
32 | DEFAULT: "hsl(var(--muted))",
33 | foreground: "hsl(var(--muted-foreground))",
34 | },
35 | accent: {
36 | DEFAULT: "hsl(var(--accent))",
37 | foreground: "hsl(var(--accent-foreground))",
38 | },
39 | destructive: {
40 | DEFAULT: "hsl(var(--destructive))",
41 | foreground: "hsl(var(--destructive-foreground))",
42 | },
43 | border: "hsl(var(--border))",
44 | input: "hsl(var(--input))",
45 | ring: "hsl(var(--ring))",
46 | chart: {
47 | "1": "hsl(var(--chart-1))",
48 | "2": "hsl(var(--chart-2))",
49 | "3": "hsl(var(--chart-3))",
50 | "4": "hsl(var(--chart-4))",
51 | "5": "hsl(var(--chart-5))",
52 | },
53 | },
54 | borderRadius: {
55 | lg: "var(--radius)",
56 | md: "calc(var(--radius) - 2px)",
57 | sm: "calc(var(--radius) - 4px)",
58 | },
59 | keyframes: {
60 | shine: {
61 | from: { backgroundPosition: "200% 0" },
62 | to: { backgroundPosition: "-200% 0" },
63 | },
64 | },
65 | animation: {
66 | shine: "shine 8s ease-in-out infinite",
67 | },
68 | },
69 | },
70 | plugins: [require("tailwindcss-animate")],
71 | };
72 | export default config;
73 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": [
4 | "dom",
5 | "dom.iterable",
6 | "esnext"
7 | ],
8 | "allowJs": true,
9 | "skipLibCheck": true,
10 | "strict": true,
11 | "noEmit": true,
12 | "esModuleInterop": true,
13 | "module": "esnext",
14 | "moduleResolution": "bundler",
15 | "resolveJsonModule": true,
16 | "isolatedModules": true,
17 | "jsx": "preserve",
18 | "incremental": true,
19 | "plugins": [
20 | {
21 | "name": "next"
22 | }
23 | ],
24 | "paths": {
25 | "@/*": [
26 | "./src/*"
27 | ]
28 | },
29 | "target": "ES2017"
30 | },
31 | "include": [
32 | "next-env.d.ts",
33 | "**/*.ts",
34 | "**/*.tsx",
35 | ".next/types/**/*.ts"
36 | ],
37 | "exclude": [
38 | "node_modules"
39 | ]
40 | }
41 |
--------------------------------------------------------------------------------