feat: add tool json (#5)

Co-authored-by: typist <git@mail.typist.cc>
Reviewed-on: #5
This commit is contained in:
2025-10-28 03:48:37 +08:00
parent c0aa618dfa
commit 97b2093b86
8 changed files with 214 additions and 1 deletions

View File

@@ -1,7 +1,8 @@
import type { ReactNode } from 'react';
import { Hash } from 'lucide-react'
import { FileJson, Hash } from 'lucide-react'
import UUID from './uuid'
import JSON from './json'
export interface Tool {
path: string;
@@ -18,5 +19,12 @@ export const tools: Tool[] = [
description: "Generate a UUID",
icon: <Hash />,
component: <UUID />,
},
{
path: "json",
name: "JSON Formatter",
description: "Format and validate JSON",
icon: <FileJson />,
component: <JSON />,
}
];

View File

@@ -0,0 +1,49 @@
import { useState, type FC } from "react";
import { Textarea } from "@/components/ui/textarea";
import { Button } from "@/components/ui/button";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { toast } from "sonner";
const Tool: FC = () => {
const [json, setJson] = useState<string>("");
const validateJson = () => {
try {
JSON.parse(json);
return true;
} catch (error: unknown) {
if (error instanceof Error) {
toast.error(error.message);
} else {
toast.error("Invalid JSON");
}
return false;
}
};
const minifyJson = () => {
if (!validateJson()) return;
const formattedJson = JSON.stringify(JSON.parse(json), null, 0);
setJson(formattedJson);
toast.success("Minified successfully");
};
const prettifyJson = () => {
if (!validateJson()) return;
const formattedJson = JSON.stringify(JSON.parse(json), null, 2);
setJson(formattedJson);
toast.success("JSON prettified successfully");
};
return (
<div className="h-full flex flex-col gap-4">
<Textarea className="flex-1 w-full resize-none" placeholder="Enter your JSON here" value={json} onChange={(e) => setJson(e.target.value)} />
<div className="flex flex-row gap-2">
<Button onClick={minifyJson}>Minify</Button>
<Button onClick={prettifyJson}>Pretty</Button>
</div>
</div>
);
};
export default Tool;

View File

@@ -0,0 +1,66 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const alertVariants = cva(
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
{
variants: {
variant: {
default: "bg-card text-card-foreground",
destructive:
"text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
},
},
defaultVariants: {
variant: "default",
},
}
)
function Alert({
className,
variant,
...props
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
return (
<div
data-slot="alert"
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
)
}
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-title"
className={cn(
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
className
)}
{...props}
/>
)
}
function AlertDescription({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-description"
className={cn(
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
className
)}
{...props}
/>
)
}
export { Alert, AlertTitle, AlertDescription }

View File

@@ -0,0 +1,38 @@
import {
CircleCheckIcon,
InfoIcon,
Loader2Icon,
OctagonXIcon,
TriangleAlertIcon,
} from "lucide-react"
import { useTheme } from "next-themes"
import { Toaster as Sonner, type ToasterProps } from "sonner"
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme()
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
icons={{
success: <CircleCheckIcon className="size-4" />,
info: <InfoIcon className="size-4" />,
warning: <TriangleAlertIcon className="size-4" />,
error: <OctagonXIcon className="size-4" />,
loading: <Loader2Icon className="size-4 animate-spin" />,
}}
style={
{
"--normal-bg": "var(--popover)",
"--normal-text": "var(--popover-foreground)",
"--normal-border": "var(--border)",
"--border-radius": "var(--radius)",
} as React.CSSProperties
}
{...props}
/>
)
}
export { Toaster }

View File

@@ -0,0 +1,18 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
return (
<textarea
data-slot="textarea"
className={cn(
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
{...props}
/>
)
}
export { Textarea }

View File

@@ -1,10 +1,14 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { Toaster } from '@/components/ui/sonner'
import './index.css'
import { AppRouter } from './router'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<AppRouter />
<Toaster />
</StrictMode>
)