diff --git a/package-lock.json b/package-lock.json index 04eecab..c5761e1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,8 @@ "@emotion/styled": "^11.11.5", "@mui/icons-material": "^5.15.20", "@mui/material": "^5.15.20", + "@types/lodash": "^4.17.5", + "lodash": "^4.17.21", "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^6.23.1" @@ -1976,6 +1978,11 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, + "node_modules/@types/lodash": { + "version": "4.17.5", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.5.tgz", + "integrity": "sha512-MBIOHVZqVqgfro1euRDWX7OO0fBVUUMrN6Pwm8LQsz8cWhEpihlvR70ENj3f40j58TNxZaWv2ndSkInykNBBJw==" + }, "node_modules/@types/node": { "version": "20.14.6", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.6.tgz", @@ -5149,8 +5156,7 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "node_modules/lodash.merge": { "version": "4.6.2", diff --git a/package.json b/package.json index 766c09b..d5bbb0e 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,8 @@ "@emotion/styled": "^11.11.5", "@mui/icons-material": "^5.15.20", "@mui/material": "^5.15.20", + "@types/lodash": "^4.17.5", + "lodash": "^4.17.21", "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^6.23.1" diff --git a/src/assets/text.png b/src/assets/text.png new file mode 100644 index 0000000..c2044a1 Binary files /dev/null and b/src/assets/text.png differ diff --git a/src/components/App.tsx b/src/components/App.tsx index 9097790..e8b24c4 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -1,6 +1,10 @@ import {BrowserRouter, useRoutes} from "react-router-dom"; import routesConfig from "../config/routesConfig"; import Navbar from "./Navbar"; +import {Suspense} from "react"; +import Loading from "./Loading"; +import {ThemeProvider} from "@mui/material"; +import theme from "../config/muiConfig"; const AppRoutes = () => { return useRoutes(routesConfig); @@ -9,10 +13,14 @@ const AppRoutes = () => { function App() { return ( - - - - + + + + }> + + + + ) } diff --git a/src/components/Loading.tsx b/src/components/Loading.tsx new file mode 100644 index 0000000..b4e7c97 --- /dev/null +++ b/src/components/Loading.tsx @@ -0,0 +1,53 @@ +import Typography from "@mui/material/Typography"; +import {useState} from "react"; +import clsx from "clsx"; +import Box from "@mui/material/Box"; +import {useTimeout} from "../hooks"; + +export type FuseLoadingProps = { + delay?: number; + className?: string; +}; + +/** + * FuseLoading displays a loading state with an optional delay + */ +function FuseLoading(props: FuseLoadingProps) { + const {delay = 0, className} = props; + const [showLoading, setShowLoading] = useState(!delay); + + useTimeout(() => { + setShowLoading(true); + }, delay); + + return ( +
+ + Chargement + + div": { + backgroundColor: "palette.secondary.main", + }, + }} + > +
+
+
+ +
+ ); +} + +export default FuseLoading; diff --git a/src/components/ToolHeader.tsx b/src/components/ToolHeader.tsx new file mode 100644 index 0000000..09cbf94 --- /dev/null +++ b/src/components/ToolHeader.tsx @@ -0,0 +1,18 @@ +import {Box, Stack} from "@mui/material"; +import Typography from "@mui/material/Typography"; +import textImage from '../assets/text.png' + +interface ToolHeaderProps { + title: string; + description: string; +} + +export default function ToolHeader({title, description}: ToolHeaderProps) { + return ( + + {title} + {description} + + + ) +} diff --git a/src/components/ToolLayout.tsx b/src/components/ToolLayout.tsx new file mode 100644 index 0000000..4612fcb --- /dev/null +++ b/src/components/ToolLayout.tsx @@ -0,0 +1,7 @@ +import {Box} from "@mui/material"; +import {ReactNode} from "react"; + +export default function ToolLayout({children}: { children: ReactNode }) { + return ({children}) +} diff --git a/src/config/muiConfig.ts b/src/config/muiConfig.ts new file mode 100644 index 0000000..53308fc --- /dev/null +++ b/src/config/muiConfig.ts @@ -0,0 +1,11 @@ +import {createTheme} from "@mui/material"; + +const theme = createTheme({ + typography: { + button: { + textTransform: 'none' + } + } +}); + +export default theme; diff --git a/src/config/routesConfig.tsx b/src/config/routesConfig.tsx index f68f5a1..f62024a 100644 --- a/src/config/routesConfig.tsx +++ b/src/config/routesConfig.tsx @@ -2,6 +2,7 @@ import {RouteObject} from "react-router-dom"; import {Navigate} from "react-router-dom"; import {ImagesConfig} from "../pages/images/ImagesConfig"; import {lazy} from "react"; +import {StringConfig} from "../pages/string/StringConfig"; const Home = lazy(() => import("../pages/home")); @@ -14,6 +15,10 @@ const routes: RouteObject[] = [ path: "images", children: ImagesConfig }, + { + path: "string", + children: StringConfig + }, { path: "*", element: , diff --git a/src/hooks/index.ts b/src/hooks/index.ts new file mode 100644 index 0000000..cc9ab3f --- /dev/null +++ b/src/hooks/index.ts @@ -0,0 +1,4 @@ +export {default as useDebounce} from "./useDebounce"; +export {default as useTimeout} from "./useTimeout"; +export {default as usePrevious} from "./usePrevious"; +export {default as useUpdateEffect} from "./useUpdateEffect"; diff --git a/src/hooks/useDebounce.ts b/src/hooks/useDebounce.ts new file mode 100644 index 0000000..4bb06ad --- /dev/null +++ b/src/hooks/useDebounce.ts @@ -0,0 +1,38 @@ +import {useCallback, useEffect, useRef} from "react"; +import _ from "lodash"; + +/** + * Debounce hook. + * @param {T} callback + * @param {number} delay + * @returns {T} + */ +function useDebounce void>( + callback: T, + delay: number, +): T { + const callbackRef = useRef(callback); + + // Update the current callback each time it changes. + useEffect(() => { + callbackRef.current = callback; + }, [callback]); + + const debouncedFn = useCallback( + _.debounce((...args: never[]) => { + callbackRef.current(...args); + }, delay), + [delay], + ); + + useEffect(() => { + // Cleanup function to cancel any pending debounced calls + return () => { + debouncedFn.cancel(); + }; + }, [debouncedFn]); + + return debouncedFn as unknown as T; +} + +export default useDebounce; diff --git a/src/hooks/usePrevious.ts b/src/hooks/usePrevious.ts new file mode 100644 index 0000000..1fec16f --- /dev/null +++ b/src/hooks/usePrevious.ts @@ -0,0 +1,19 @@ +import { useEffect, useRef } from "react"; + +/** + * The usePrevious function is a custom hook that returns the previous value of a variable. + * It takes in a value as a parameter and returns the previous value. + */ +function usePrevious(value: T): T | undefined { + const ref = useRef(); + + // Store current value in ref + useEffect(() => { + ref.current = value; + }, [value]); + + // Return previous value (happens before update in useEffect above) + return ref.current; +} + +export default usePrevious; diff --git a/src/hooks/useTimeout.ts b/src/hooks/useTimeout.ts new file mode 100644 index 0000000..bb2ffd2 --- /dev/null +++ b/src/hooks/useTimeout.ts @@ -0,0 +1,30 @@ +import { useEffect, useRef } from "react"; + +/** + * The useTimeout function is a custom hook that sets a timeout for a given callback function. + * It takes in a callback function and a delay time in milliseconds as parameters. + * It returns nothing. + */ +function useTimeout(callback: () => void, delay: number) { + const callbackRef = useRef(callback); + + useEffect(() => { + callbackRef.current = callback; + }, [callback]); + + useEffect(() => { + let timer: NodeJS.Timeout | undefined; + + if (delay !== null && callback && typeof callback === "function") { + timer = setTimeout(callbackRef.current, delay); + } + + return () => { + if (timer) { + clearTimeout(timer); + } + }; + }, [callback, delay]); +} + +export default useTimeout; diff --git a/src/hooks/useUpdateEffect.ts b/src/hooks/useUpdateEffect.ts new file mode 100644 index 0000000..d172f8a --- /dev/null +++ b/src/hooks/useUpdateEffect.ts @@ -0,0 +1,19 @@ +import { DependencyList, EffectCallback, useEffect, useRef } from "react"; + +/** + * The useUpdateEffect function is a custom hook that behaves like useEffect, but only runs on updates and not on initial mount. + * It takes in an effect function and an optional dependency list as parameters. + * It returns nothing. + */ +const useUpdateEffect = (effect: EffectCallback, deps?: DependencyList) => { + const isInitialMount = useRef(true); + + useEffect(() => { + if (isInitialMount.current) { + isInitialMount.current = false; + } + return effect(); + }, deps); +}; + +export default useUpdateEffect; diff --git a/src/pages/home/index.tsx b/src/pages/home/index.tsx index b3fdbfb..07f74c4 100644 --- a/src/pages/home/index.tsx +++ b/src/pages/home/index.tsx @@ -1,21 +1,25 @@ -import {Box, Grid, Icon, Input, Stack, TextField} from "@mui/material"; +import {Box, Icon, Input, Stack, TextField} from "@mui/material"; +import Grid from "@mui/material/Grid"; import Typography from "@mui/material/Typography"; import SearchIcon from '@mui/icons-material/Search'; +import {useNavigate} from "react-router-dom"; +const exampleTools: { label: string; url: string }[] = [{ + label: 'Create a transparent image', + url: '' +}, + {label: 'Convert text to morse code', url: ''}, + {label: 'Change GIF speed', url: ''}, + {label: 'Pick a random item', url: ''}, + {label: 'Find and replace text', url: ''}, + {label: 'Convert emoji to image', url: ''}, + {label: 'Split a string', url: '/string/split'}, + {label: 'Calculate number sum', url: ''}, + {label: 'Pixelate an image', url: ''}, +] export default function Home() { - const exampleTools: { label: string; url: string }[] = [{ - label: 'Create a transparent image', - url: '' - }, - {label: 'Convert text to morse code', url: ''}, - {label: 'Change GIF speed', url: ''}, - {label: 'Pick a random item', url: ''}, - {label: 'Find and replace text', url: ''}, - {label: 'Convert emoji to image', url: ''}, - {label: 'Split a string', url: ''}, - {label: 'Calculate number sum', url: ''}, - {label: 'Pixelate an image', url: ''}, - ] + const navigate = useNavigate() + return ( @@ -38,6 +42,7 @@ export default function Home() { {exampleTools.map((tool) => ( navigate(tool.url)} item xs={4} key={tool.label} diff --git a/src/pages/string/StringConfig.tsx b/src/pages/string/StringConfig.tsx new file mode 100644 index 0000000..dc70628 --- /dev/null +++ b/src/pages/string/StringConfig.tsx @@ -0,0 +1,10 @@ +import {RouteObject} from "react-router-dom"; +import {lazy} from "react"; + +const StringHome = lazy(() => import("./index")); +const StringSplit = lazy(() => import("./split")); + +export const StringConfig: RouteObject[] = [ + {path: '', element: }, + {path: 'split', element: }, +] diff --git a/src/pages/string/index.tsx b/src/pages/string/index.tsx new file mode 100644 index 0000000..bf9ff71 --- /dev/null +++ b/src/pages/string/index.tsx @@ -0,0 +1,5 @@ +import {Box} from "@mui/material"; + +export default function StringHome() { + return () +} diff --git a/src/pages/string/split/index.tsx b/src/pages/string/split/index.tsx new file mode 100644 index 0000000..4b590a2 --- /dev/null +++ b/src/pages/string/split/index.tsx @@ -0,0 +1,45 @@ +import ToolHeader from "../../../components/ToolHeader"; +import ToolLayout from "../../../components/ToolLayout"; +import {Box, Stack, TextField} from "@mui/material"; +import Grid from "@mui/material/Grid"; +import Typography from "@mui/material/Typography"; +import Button from "@mui/material/Button"; +import PublishIcon from '@mui/icons-material/Publish'; +import ContentPasteIcon from '@mui/icons-material/ContentPaste'; +import DownloadIcon from '@mui/icons-material/Download'; +import React, {useEffect, useState} from "react"; + +export default function SplitText() { + const [input, setInput] = useState(''); + const [result, setResult] = useState('') + useEffect(() => { + setResult(input.split(' ').join('\n')) + }, [input]); + return ( + + + + + + Input text + setInput(event.target.value)} fullWidth multiline rows={10}/> + + + + + + + Text pieces + + + + + + + + + + + ) +}