diff --git a/package-lock.json b/package-lock.json index f198409..48278ec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,7 @@ "name": "jsonpath-online-evaluator", "license": "MIT", "dependencies": { + "@formkit/tempo": "^0.1.2", "@icons-pack/react-simple-icons": "^10.2.0", "@monaco-editor/react": "^4.6.0", "@radix-ui/react-alert-dialog": "^1.1.6", @@ -17,6 +18,7 @@ "@radix-ui/react-select": "^2.1.4", "@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-switch": "^1.1.3", + "@radix-ui/react-tooltip": "^1.1.8", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.0.0", @@ -35,6 +37,7 @@ }, "devDependencies": { "@eslint/js": "^9.17.0", + "@types/file-saver": "^2.0.7", "@types/node": "^22.13.14", "@types/react": "^18.3.18", "@types/react-dom": "^18.3.5", @@ -43,6 +46,7 @@ "eslint": "^9.23.0", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.19", + "file-saver": "^2.0.5", "globals": "^15.14.0", "peggy": "^4.2.0", "postcss": "^8.4.49", @@ -975,6 +979,12 @@ "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==", "license": "MIT" }, + "node_modules/@formkit/tempo": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@formkit/tempo/-/tempo-0.1.2.tgz", + "integrity": "sha512-jNPPbjL8oj7hK3eHX++CwbR6X4GKQt+x00/q4yeXkwynXHGKL27dylYhpEgwrmediPP4y7s0XtN1if/M/JYujg==", + "license": "MIT" + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -1771,6 +1781,40 @@ } } }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.8.tgz", + "integrity": "sha512-YAA2cu48EkJZdAMHC0dqo9kialOcRStbtiY4nJPaht7Ptrhcvpo+eDChaM6BIs8kL6a8Z5l5poiqLnXcNduOkA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.5", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-popper": "1.2.2", + "@radix-ui/react-portal": "1.1.4", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-slot": "1.1.2", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-visually-hidden": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-callback-ref": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", @@ -2249,6 +2293,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/file-saver": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.7.tgz", + "integrity": "sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -3539,6 +3590,13 @@ "node": ">=16.0.0" } }, + "node_modules/file-saver": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", + "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==", + "dev": true, + "license": "MIT" + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", diff --git a/package.json b/package.json index 0aed3d2..5108e31 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "preview": "vite preview" }, "dependencies": { + "@formkit/tempo": "^0.1.2", "@icons-pack/react-simple-icons": "^10.2.0", "@monaco-editor/react": "^4.6.0", "@radix-ui/react-alert-dialog": "^1.1.6", @@ -23,6 +24,7 @@ "@radix-ui/react-select": "^2.1.4", "@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-switch": "^1.1.3", + "@radix-ui/react-tooltip": "^1.1.8", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.0.0", @@ -41,6 +43,7 @@ }, "devDependencies": { "@eslint/js": "^9.17.0", + "@types/file-saver": "^2.0.7", "@types/node": "^22.13.14", "@types/react": "^18.3.18", "@types/react-dom": "^18.3.5", @@ -49,6 +52,7 @@ "eslint": "^9.23.0", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.19", + "file-saver": "^2.0.5", "globals": "^15.14.0", "peggy": "^4.2.0", "postcss": "^8.4.49", @@ -58,4 +62,4 @@ "vite": "^6.2.5", "vitest": "^3.0.9" } -} \ No newline at end of file +} diff --git a/src/components/download-button.tsx b/src/components/download-button.tsx new file mode 100644 index 0000000..828a194 --- /dev/null +++ b/src/components/download-button.tsx @@ -0,0 +1,47 @@ +import { Button, ButtonProps } from "@/components/ui/button"; +import { saveAs } from "file-saver"; +import { Download } from "lucide-react"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { cn } from "@/lib/utils"; +import { useJSONPath } from "@/hooks/use-jsonpath"; +import { format } from "@formkit/tempo"; + +export const DownloadButton = ({ className }: ButtonProps) => { + const { result } = useJSONPath(); + + const handleDownload = () => { + const text = result.isValid ? JSON.stringify(result.values, null, 2) : "[]"; + const blob = new Blob([text], { + type: "application/json", + }); + saveAs( + blob, + `evaluation_results_${format(new Date(), "YYYYMMDD_HHmmss")}.json` + ); + }; + + return ( + + + + + + +

Download file

+
+
+
+ ); +}; diff --git a/src/components/drop-zone.tsx b/src/components/drop-zone.tsx new file mode 100644 index 0000000..9bb1b24 --- /dev/null +++ b/src/components/drop-zone.tsx @@ -0,0 +1,57 @@ +import { Upload } from "lucide-react"; +import { ReactNode, useState } from "react"; + +interface DropZoneProps { + onDrop?: (file: File) => void; + children: ReactNode; +} + +export const DropZone = ({ onDrop, children }: DropZoneProps) => { + const [isDragging, setIsDragging] = useState(false); + + const handleOnDrop = (e: React.DragEvent) => { + e.preventDefault(); + setIsDragging(false); + const file = e.dataTransfer.files[0]; + if (file) { + onDrop?.(file); + } + }; + + return ( +
{ + setIsDragging(true); + e.preventDefault(); + }} + onDragEnter={() => setIsDragging(true)} + onDragLeave={() => setIsDragging(false)} + data-drag={isDragging ? "true" : "false"} + className="relative" + > + {children} +
+
+
Drop JSON file here
+
+ +
+
+
+
+ ); +}; diff --git a/src/components/editor/json-editor.tsx b/src/components/editor/json-editor.tsx index 4c15505..a086855 100644 --- a/src/components/editor/json-editor.tsx +++ b/src/components/editor/json-editor.tsx @@ -9,6 +9,7 @@ import { findSmallestNode, generateNormalizedPathNode, } from "@/lib/normalized-path"; +import { DropZone } from "../drop-zone"; export const JSONEditor = () => { const { document, setDocument, jsonDocument } = useJSONPath(); @@ -63,25 +64,36 @@ export const JSONEditor = () => { setDocument(value || ""); }; + const handleOnDrop = (file: File) => { + const reader = new FileReader(); + reader.onload = (event) => { + const content = event.target?.result as string; + setDocument(content); + }; + reader.readAsText(file); + }; + return ( - + + + ); }; diff --git a/src/components/import-file.tsx b/src/components/import-file.tsx new file mode 100644 index 0000000..1e5eaf8 --- /dev/null +++ b/src/components/import-file.tsx @@ -0,0 +1,68 @@ +import { useRef } from "react"; +import { Button } from "./ui/button"; +import { useJSONPath } from "@/hooks/use-jsonpath"; + +export const ImportFile = () => { + const { setDocument } = useJSONPath(); + const inputRef = useRef(null); + + const handleOnClick = () => { + inputRef.current?.click(); + }; + + const handleOnInput = (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (file) { + const reader = new FileReader(); + reader.onload = (event) => { + const content = event.target?.result as string; + setDocument(content); + }; + reader.readAsText(file); + } + }; + + return ( +
+ + +
+ ); +}; diff --git a/src/components/online-evaluator.tsx b/src/components/online-evaluator.tsx index 98b4d67..139d4e8 100644 --- a/src/components/online-evaluator.tsx +++ b/src/components/online-evaluator.tsx @@ -3,6 +3,8 @@ import { JSONEditor } from "./editor/json-editor"; import { Result } from "./editor/result"; import { OutputPathSwitch } from "./output-path-switch"; import { useJSONPath } from "@/hooks/use-jsonpath"; +import { DownloadButton } from "./download-button"; +import { ImportFile } from "./import-file"; export const JSONPathOnlineEvaluator = () => { const { outputPaths, setOutputPaths } = useJSONPath(); @@ -13,17 +15,28 @@ export const JSONPathOnlineEvaluator = () => {
-

Document

+
+

Document

+ +
-
+

Evaluation Results

- +
+ +
+
+
+ +
-
diff --git a/src/components/ui/tooltip.tsx b/src/components/ui/tooltip.tsx new file mode 100644 index 0000000..99ad630 --- /dev/null +++ b/src/components/ui/tooltip.tsx @@ -0,0 +1,30 @@ +import * as React from "react" +import * as TooltipPrimitive from "@radix-ui/react-tooltip" + +import { cn } from "@/lib/utils" + +const TooltipProvider = TooltipPrimitive.Provider + +const Tooltip = TooltipPrimitive.Root + +const TooltipTrigger = TooltipPrimitive.Trigger + +const TooltipContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)) +TooltipContent.displayName = TooltipPrimitive.Content.displayName + +export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } diff --git a/src/index.css b/src/index.css index 59b9f13..4d668ff 100644 --- a/src/index.css +++ b/src/index.css @@ -34,7 +34,7 @@ h3 { --card-foreground: 240 10% 3.9%; --popover: 0 0% 100%; --popover-foreground: 240 10% 3.9%; - --primary: 142.1 76.2% 36.3%; + --primary: 175, 100%, 33%; --primary-foreground: 355.7 100% 97.3%; --secondary: 240 4.8% 95.9%; --secondary-foreground: 240 5.9% 10%; @@ -46,7 +46,7 @@ h3 { --destructive-foreground: 0 0% 98%; --border: 240 5.9% 90%; --input: 240 5.9% 90%; - --ring: 142.1 76.2% 36.3%; + --ring: 175, 100%, 33%; --radius: 0.3rem; --chart-1: 12 76% 61%; --chart-2: 173 58% 39%;