feat(json parser) export, formatter

This commit is contained in:
lumither committed 2024-10-13 21:11:57 -07:00
1 parent f4ab75fd30
commit b84f81611a
5 files changed
+218 -19

No files matched your search

+6 -4
View File
@@ -1,7 +1,6 @@
'use client';
import React from 'react'; import React from 'react';
import ThemeSwitcher from '@/Components/ThemeSwitcher'; import ThemeSwitcher from '@/Components/ThemeSwitcher';
import Link from 'next/link';
const Header = () => { const Header = () => {
@@ -13,9 +12,12 @@ const Header = () => {
<div className={ 'p-3 px-5' }> <div className={ 'p-3 px-5' }>
<div className={ 'flex flex-row items-center justify-between align-baseline' }> <div className={ 'flex flex-row items-center justify-between align-baseline' }>
<div className={ 'font-bold text-2xl' }> <Link
className={ 'font-bold text-2xl' }
href={ '/' }
>
LMTLab LMTLab
</div> </Link>
<ThemeSwitcher /> <ThemeSwitcher />
</div> </div>
</div> </div>
+127
View File
@@ -0,0 +1,127 @@
'use client';
import React, { useEffect, useState } from 'react';
import {
Button,
Checkbox,
CheckboxGroup,
Modal,
ModalBody,
ModalContent,
ModalFooter,
ModalHeader,
Textarea
} from '@nextui-org/react';
import { toast } from 'react-toastify';
import { HiOutlineDownload, HiOutlineDuplicate } from 'react-icons/hi';
const workflows = new Map<string, (json: string) => string>([
[ 'minify', (json) => {
try {
const value = JSON.parse(json);
return JSON.stringify(value);
} catch (e) {
toast.error(`Operation Failed: ${ e }`, { autoClose: 5000 });
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
return e.toString();
}
} ]
]);
type Props = { isOpen: boolean, onOpenChange: () => void, value: string };
const ExportModal = ({ isOpen, onOpenChange, value }: Props) => {
const [ workflow, setWorkflow ] = useState<string[]>([]);
const [ editorValue, setEditorValue ] = useState(value);
useEffect(() => {
setEditorValue(value);
}, [ value ]);
const [ exportValue, setExportValue ] = useState(editorValue);
useEffect(() => {
let value = editorValue;
for (const fn of workflow) {
const func = workflows.get(fn);
value = func ? func(value) : value;
}
setExportValue(value);
}, [ workflow, editorValue ]);
return (
<Modal
isOpen={ isOpen }
onOpenChange={ onOpenChange }
size={ '4xl' }
hideCloseButton
>
<ModalContent>
{ (onClose) => (
<>
<ModalHeader className="gap-1">Export</ModalHeader>
<ModalBody>
<CheckboxGroup
label={ 'Config' }
color={ 'primary' }
value={ workflow }
onChange={ setWorkflow }
>
<Checkbox value={ 'minify' }>Minify</Checkbox>
</CheckboxGroup>
<Textarea
label={ 'Value' }
variant={ 'flat' }
value={ exportValue }
readOnly
/>
<div
className={ 'mt-3 flex flex-col md:flex-row gap-1 justify-center' }
>
<Button
color={ 'primary' }
aria-label={ 'Download' }
startContent={ <HiOutlineDownload size={ 20 } /> }
onClick={ () => {
const blob = new Blob([ exportValue ], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'value.json';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
toast.success('File Downloaded');
} }
>
Download
</Button>
<Button
color={ 'success' }
aria-label={ 'Copy' }
startContent={ <HiOutlineDuplicate size={ 20 } /> }
onClick={ () => {
navigator.clipboard.writeText(exportValue);
toast.success('Copied to Clipboard');
} }
>
Copy
</Button>
</div>
</ModalBody>
<ModalFooter>
<Button color="danger" variant="light" onPress={ onClose }>
Close
</Button>
</ModalFooter>
</>
) }
</ModalContent>
</Modal>
);
};
export default ExportModal;
+34
View File
@@ -0,0 +1,34 @@
'use client';
import React, { useState } from 'react';
import MonacoEditor from '@/app/public/MonacoEditor';
import { Button, useDisclosure } from '@nextui-org/react';
import ExportModal from '@/app/jsonparser/ExportModal';
import { HiOutlineExternalLink } from 'react-icons/hi';
const DEFAULT_EDITOR_VALUE = '{}';
const JsonEditor = () => {
const [ editorValue, setEditorValue ] = useState<string>(DEFAULT_EDITOR_VALUE);
const { isOpen, onOpen, onOpenChange } = useDisclosure();
return (
<>
<div className={ 'rounded-2xl shadow-2xl overflow-hidden relative' }>
<MonacoEditor language={ 'json' } defaultValue={ DEFAULT_EDITOR_VALUE } setValue={ setEditorValue } />
<div className={ 'absolute top-4 right-4 z-10' }>
<Button
aria-label={ 'export' }
variant={ 'light' }
isIconOnly
onPress={ onOpen }
startContent={ <HiOutlineExternalLink size={ 20 } /> } />
<ExportModal isOpen={ isOpen } onOpenChange={ onOpenChange } value={ editorValue } />
</div>
</div>
</>
);
};
export default JsonEditor;
+2 -2
View File
@@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import MonacoEditor from '@/app/public/MonacoEditor'; import JsonEditor from '@/app/jsonparser/JsonEditor';
const Page = () => { const Page = () => {
return ( return (
@@ -7,7 +7,7 @@ const Page = () => {
<div <div
className={ 'p-2 lg:p-20 h-full w-full' } className={ 'p-2 lg:p-20 h-full w-full' }
> >
<MonacoEditor defaultValue={ '{}' } language={ 'json' } /> <JsonEditor />
</div> </div>
</> </>
); );
+49 -13
View File
@@ -1,41 +1,77 @@
'use client'; 'use client';
import React, { useState } from 'react'; import React, { useRef, useState } from 'react';
import { Editor as Monaco } from '@monaco-editor/react'; import { Editor as Monaco } from '@monaco-editor/react';
import { useTheme } from 'next-themes'; import { useTheme } from 'next-themes';
import { Button } from '@nextui-org/react'; import { Button, Spinner } from '@nextui-org/react';
import { HiOutlineClipboardDocumentList } from 'react-icons/hi2';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { HiOutlineDuplicate } from 'react-icons/hi';
import { MdFormatShapes } from 'react-icons/md';
type Props = { type Props = {
language: string, language: string,
defaultValue: string defaultValue: string
height?: string
setValue?: (value: string) => void
} }
const MonacoEditor = (props: Props) => { const MonacoEditor = ({ language, defaultValue, height = '75vh', setValue }: Props) => {
const { theme } = useTheme(); const { theme } = useTheme();
const [ editorValue, setEditorValue ] = useState(props.defaultValue); const [ editorValue, setEditorValue ] = useState(defaultValue);
console.log(editorValue);
const editorRef = useRef(null);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
const handleEditorDidMount = (editor) => {
editorRef.current = editor;
};
const formatCode = () => {
if (editorRef.current) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
editorRef.current.getAction('editor.action.formatDocument').run();
}
};
return ( return (
<div className={ 'rounded-2xl overflow-hidden shadow-2xl relative' }> <div className={ 'relative' }>
<Monaco <Monaco
height="75vh" height={ height }
defaultLanguage={ props.language } defaultLanguage={ language }
theme={ theme === 'light' ? 'light' : 'vs-dark' } theme={ theme === 'light' ? 'light' : 'vs-dark' }
defaultValue={ props.defaultValue } defaultValue={ defaultValue }
onChange={ (val) => setEditorValue(val as string) } loading={ <Spinner size={ 'lg' } /> }
onMount={ handleEditorDidMount }
onChange={ (val) => {
setEditorValue(val as string);
if (setValue) {
setValue(val as string);
}
} }
/> />
<div className={ 'absolute bottom-4 right-4 z-10' }> <div className={ 'flex flex-row-reverse absolute bottom-4 right-4 z-10' }>
<Button <Button
variant={ 'light' } variant={ 'light' }
isIconOnly isIconOnly
aria-label={ 'copy to clipboard' }
onClick={ () => { onClick={ () => {
navigator.clipboard.writeText(editorValue); navigator.clipboard.writeText(editorValue);
toast.success('Copied to Clipboard'); toast.success('Copied to Clipboard');
} } } }
> >
<HiOutlineClipboardDocumentList size={ 20 } /> <HiOutlineDuplicate size={ 20 } />
</Button>
<Button
variant={ 'light' }
isIconOnly
aria-label={ 'copy to clipboard' }
onClick={ () => {
formatCode();
toast.success('Formatted');
} }
>
<MdFormatShapes size={ 20 } />
</Button> </Button>
</div> </div>
</div> </div>