Compare commits

..
4 Commits
Author SHA1 Message Date
lumither 57a0b66b84 feat(string coder) add some algorithms 2024-10-15 21:12:36 -07:00
lumither efdea9872f feat(string coder) implementation 2024-10-15 01:48:25 -07:00
lumither b84f81611a feat(json parser) export, formatter 2024-10-13 21:11:57 -07:00
lumither f4ab75fd30 fix(layout provider) fix theme 2024-10-13 00:58:48 -07:00
13 changed files with 466 additions and 60 deletions

No files matched your search

+10 -32
View File
@@ -1,36 +1,14 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
# LMTLab
## Getting Started
Source code of LMTLab
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
# dev
npm i
npm run dev
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
# release
npm i
npm build
# static export avliable at /out
```
+23 -5
View File
@@ -2,23 +2,41 @@
import React from 'react';
import ThemeSwitcher from '@/Components/ThemeSwitcher';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { urlList } from '@/app/urlList';
const searchPageName = (url: string) => {
const page = urlList.find((it) => it.url === url);
return page && page.name;
};
const Header = () => {
const path = usePathname();
const currModule = searchPageName(path);
return (
<div>
<div className={ 'z-50' }>
<div
className={ 'h-16 fixed backdrop-blur w-full z-50 transition-colors border-b-[1px] border-neutral-200 dark:border-neutral-700' }
>
<div className={ 'p-3 px-5' }>
<div className={ 'flex flex-row items-center justify-between align-baseline' }>
<div className={ 'font-bold text-2xl' }>
<div className={ 'grid grid-cols-2 md:grid-cols-3 items-center justify-between' }>
<Link
className={ 'font-bold text-2xl' }
href={ '/' }
>
LMTLab
</div>
</Link>
<p className={ 'hidden md:block text-2xl text-weak justify-self-center' }>
{ currModule }
</p>
<div className={ 'justify-self-end' }>
<ThemeSwitcher />
</div>
</div>
</div>
</div>
</div>
+4
View File
@@ -2,6 +2,10 @@
@tailwind components;
@tailwind utilities;
.text-weak {
@apply text-zinc-500 dark:text-zinc-400;
}
html.light {
--background: #ffffff;
--foreground: #171717;
+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 MonacoEditor from '@/app/public/MonacoEditor';
import JsonEditor from '@/app/jsonparser/JsonEditor';
const Page = () => {
return (
@@ -7,7 +7,7 @@ const Page = () => {
<div
className={ 'p-2 lg:p-20 h-full w-full' }
>
<MonacoEditor defaultValue={ '{}' } language={ 'json' } />
<JsonEditor />
</div>
</>
);
+1 -7
View File
@@ -1,13 +1,7 @@
import { Card, CardBody } from '@nextui-org/card';
import Link from 'next/link';
import { urlList } from '@/app/urlList';
const urlList = [
{
name: 'Json Parser',
desc: 'Parse a Json file, with formatter and syntax highlighting',
url: '/jsonparser'
}
];
export default function Home() {
+49 -13
View File
@@ -1,41 +1,77 @@
'use client';
import React, { useState } from 'react';
import React, { useRef, useState } from 'react';
import { Editor as Monaco } from '@monaco-editor/react';
import { useTheme } from 'next-themes';
import { Button } from '@nextui-org/react';
import { HiOutlineClipboardDocumentList } from 'react-icons/hi2';
import { Button, Spinner } from '@nextui-org/react';
import { toast } from 'react-toastify';
import { HiOutlineDuplicate } from 'react-icons/hi';
import { MdFormatShapes } from 'react-icons/md';
type Props = {
language: 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 [ editorValue, setEditorValue ] = useState(props.defaultValue);
console.log(editorValue);
const [ editorValue, setEditorValue ] = useState(defaultValue);
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 (
<div className={ 'rounded-2xl overflow-hidden shadow-2xl relative' }>
<div className={ 'relative' }>
<Monaco
height="75vh"
defaultLanguage={ props.language }
height={ height }
defaultLanguage={ language }
theme={ theme === 'light' ? 'light' : 'vs-dark' }
defaultValue={ props.defaultValue }
onChange={ (val) => setEditorValue(val as string) }
defaultValue={ defaultValue }
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
variant={ 'light' }
isIconOnly
aria-label={ 'copy to clipboard' }
onClick={ () => {
navigator.clipboard.writeText(editorValue);
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>
</div>
</div>
+71
View File
@@ -0,0 +1,71 @@
import { enc, MD5, SHA1, SHA256, SHA512 } from 'crypto-js';
import basex from 'base-x';
const BASE58 = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
export const hashAlgorithms = {
'SHA-1': (input: string) => {
return SHA1(input);
},
'SHA-256': (input: string) => {
return SHA256(input);
},
'SHA-512': (input: string) => {
return SHA512(input);
},
'MD5': (input: string) => {
return MD5(input);
}
};
export const encodeAlgorithms = {
'Base58': (input: string) => {
return basex(BASE58).encode(Buffer.from(input));
},
'Base64': (input: string) => {
const wordArray = enc.Utf8.parse(input);
return enc.Base64.stringify(wordArray);
},
'URI': (input: string) => {
return encodeURI(input).toString();
},
'URI Component': (input: string) => {
return encodeURIComponent(input).toString();
},
'Hex': (input: string) => {
return enc.Hex.stringify(enc.Utf8.parse(input));
}
};
export const decodeAlgorithms = {
'Base58': (input: string) => {
return Buffer.from(basex(BASE58).decode(input)).toString();
},
'Base64': (input: string) => {
return enc.Base64.parse(input).toString(enc.Utf8);
},
'URI': (input: string) => {
return decodeURI(input).toString();
},
'URI Component': (input: string) => {
return decodeURIComponent(input).toString();
},
'Hex': (input: string) => {
return enc.Hex.parse(input).toString(enc.Utf8);
}
};
export const algorithms = {
'hash': {
list: hashAlgorithms,
color: 'primary'
},
'encode': {
list: encodeAlgorithms,
color: 'secondary'
},
'decode': {
list: decodeAlgorithms,
color: 'success'
}
};
+127
View File
@@ -0,0 +1,127 @@
'use client';
import React, { useState } from 'react';
import { Button, Textarea } from '@nextui-org/react';
import { toast } from 'react-toastify';
import { HiOutlineBackspace, HiOutlineDuplicate } from 'react-icons/hi';
import { algorithms } from '@/app/strcoder/algorithms';
import { useTheme } from 'next-themes';
type ButtonColor = 'primary' | 'secondary' | 'success' | 'default' | 'warning' | 'danger' | undefined;
const Page = () => {
const [ input, setInput ] = useState('');
const [ output, setOutput ] = useState('');
const { theme } = useTheme();
return (
<>
<div
className={ 'p-5 px-10 lg:p-20 h-full w-full' }
>
<div className={ 'flex flex-col gap-4 w-full' }>
<div
className={ 'h-full w-full relative' }
>
<Textarea
variant={ 'faded' }
label={ 'Input' }
value={ input }
onValueChange={ setInput }
/>
<div className={ 'flex flex-row-reverse absolute bottom-4 right-4 z-10' }>
<Button
variant={ 'light' }
isIconOnly
aria-label={ 'copy to clipboard' }
onClick={ () => {
navigator.clipboard.writeText(input);
toast.success('Copied to Clipboard');
} }
>
<HiOutlineDuplicate size={ 20 } />
</Button>
<Button
variant={ 'light' }
isIconOnly
aria-label={ 'clear input' }
onClick={ () => {
setInput('');
setOutput('');
toast.success('Input Cleared');
} }
>
<HiOutlineBackspace size={ 20 } />
</Button>
</div>
</div>
<div>
<p className={ 'text-center text-xl font-bold' }>Algorithms</p>
</div>
<ul className={ 'flex flex-col gap-6 w-full mb-4' }>
{
Object.entries(algorithms).map(([ category, list ]) => (
<li key={ category } className={ 'space-y-2' }>
<p className={ 'font-bold' }>{ category.toUpperCase() }</p>
<ul className={ 'flex flex-row flex-wrap gap-4 w-full' }>
{
Object.entries(list.list).map(([ key, value ]) => (
<li key={ key }>
<Button
variant={ theme === 'light' ? 'flat' : 'solid' }
aria-label={ `${ category } algorithm ${ key }` }
color={ list.color as ButtonColor }
onClick={ () => {
try {
setOutput((value as (input: string) => string)(input));
toast.success('Operation Succeed');
} catch (error) {
toast.error(`Operation Failed: ${ error }`, {
autoClose: 5000
});
}
} }
>
{ key }
</Button>
</li>
))
}
</ul>
</li>
))
}
</ul>
<div
className={ 'h-full w-full relative' }
>
<Textarea
readOnly
variant={ 'faded' }
label={ 'Output' }
value={ output }
/>
<div className={ 'flex flex-row-reverse absolute bottom-4 right-4 z-10' }>
<Button
variant={ 'light' }
isIconOnly
aria-label={ 'copy to clipboard' }
onClick={ () => {
navigator.clipboard.writeText(output);
toast.success('Copied to Clipboard');
} }
>
<HiOutlineDuplicate size={ 20 } />
</Button>
</div>
</div>
</div>
</div>
</>
);
};
export default Page;
+3 -1
View File
@@ -4,12 +4,14 @@ import 'react-toastify/dist/ReactToastify.css';
// import '../../app/globals.css';
import { ToastContainer } from 'react-toastify';
import React from 'react';
import { useTheme } from 'next-themes';
interface ToastProviderProps {
children: React.ReactNode;
}
export default function ToastProvider({ children }: ToastProviderProps) {
const { theme } = useTheme();
return (
<>
@@ -24,7 +26,7 @@ export default function ToastProvider({ children }: ToastProviderProps) {
pauseOnFocusLoss
draggable
pauseOnHover
theme="dark"
theme={ theme }
/>
</>
);
+12
View File
@@ -0,0 +1,12 @@
export const urlList = [
{
name: 'Json Parser',
desc: 'Parse a Json file, with formatter and syntax highlighting',
url: '/jsonparser'
},
{
name: 'String Coder',
desc: 'String encoding & decoding, hashing',
url: '/strcoder'
}
];
+3
View File
@@ -11,6 +11,8 @@
"dependencies": {
"@monaco-editor/react": "^4.6.0",
"@nextui-org/react": "^2.4.8",
"base-x": "^5.0.0",
"crypto-js": "^4.2.0",
"framer-motion": "^11.11.0",
"next": "14.2.14",
"next-themes": "^0.3.0",
@@ -20,6 +22,7 @@
"react-toastify": "^10.0.6"
},
"devDependencies": {
"@types/crypto-js": "^4.2.2",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",