Compare commits
4
Commits
3556eaed5e
...
cf-deploy
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
57a0b66b84 | ||
|
|
efdea9872f | ||
|
|
b84f81611a | ||
|
|
f4ab75fd30 |
No files matched your search
@@ -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
@@ -2,21 +2,39 @@
|
||||
|
||||
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
|
||||
</Link>
|
||||
<p className={ 'hidden md:block text-2xl text-weak justify-self-center' }>
|
||||
{ currModule }
|
||||
</p>
|
||||
<div className={ 'justify-self-end' }>
|
||||
<ThemeSwitcher />
|
||||
</div>
|
||||
<ThemeSwitcher />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
.text-weak {
|
||||
@apply text-zinc-500 dark:text-zinc-400;
|
||||
}
|
||||
|
||||
html.light {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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 }
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
];
|
||||
@@ -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",
|
||||
|
||||
Reference in new issue
Block a user