diff --git a/README.md b/README.md index d37fe64..a140263 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ A web clone of [DevToys](https://github.com/veler/DevToys) ## Todo -- [ ] Add site layout +- [x] Add site layout - [ ] Add all tools page mock - [ ] Implement tools - [ ] Converters diff --git a/package.json b/package.json index d0e4db4..dd9a0a9 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "@fontsource/roboto": "^4.5.3", "@mui/icons-material": "^5.5.1", "@mui/material": "^5.5.1", + "fast-deep-equal": "^3.1.3", "next": "12.1.0", "react": "17.0.2", "react-dom": "17.0.2", diff --git a/src/components/layout/Drawer.tsx b/src/components/layout/Drawer.tsx new file mode 100644 index 0000000..f7ad7e0 --- /dev/null +++ b/src/components/layout/Drawer.tsx @@ -0,0 +1,116 @@ +import CodeIcon from "@mui/icons-material/Code"; +import DataObjectIcon from "@mui/icons-material/DataObject"; +import DragHandleIcon from "@mui/icons-material/DragHandle"; +import FilterIcon from "@mui/icons-material/Filter"; +import FingerprintIcon from "@mui/icons-material/Fingerprint"; +import HomeIcon from "@mui/icons-material/Home"; +import ImageIcon from "@mui/icons-material/Image"; +import KeyIcon from "@mui/icons-material/Key"; +import LinkIcon from "@mui/icons-material/Link"; +import NoteAddIcon from "@mui/icons-material/NoteAdd"; +import NumbersIcon from "@mui/icons-material/Numbers"; +import SortIcon from "@mui/icons-material/Sort"; +import SyncAltIcon from "@mui/icons-material/SyncAlt"; +import TextFieldsIcon from "@mui/icons-material/TextFields"; +import TextIncreaseIcon from "@mui/icons-material/TextIncrease"; +import TransformIcon from "@mui/icons-material/Transform"; +import { Box, css, Divider, Drawer, List, Stack } from "@mui/material"; +import { memo } from "react"; + +import { pagesPath } from "@/libs/$path"; + +import DrawerCollapseItem from "./DrawerCollapseItem"; +import DrawerItem from "./DrawerItem"; +import SearchBar from "./SearchBar"; + +export const drawerWidth = 300; + +const drawer = css` + width: ${drawerWidth}px; + flex-shrink: 0; + & .MuiDrawer-paper { + position: relative; + border: none; + background-color: transparent; + } +`; + +const divider = css` + border-color: rgba(0, 0, 0, 0.08); +`; + +const toolGroups = [ + { + icon: , + title: "Converters", + tools: [ + { icon: , title: "Json <> Yaml", href: pagesPath.$url(), disabled: true }, + { icon: , title: "Number Base", href: pagesPath.$url(), disabled: true }, + ], + }, + { + icon: , + title: "Encoders / Decoders", + tools: [ + { icon: , title: "HTML", href: pagesPath.$url(), disabled: true }, + { icon: , title: "URL", href: pagesPath.$url(), disabled: true }, + { icon: , title: "Base 64", href: pagesPath.$url(), disabled: true }, + { icon: , title: "JWT", href: pagesPath.$url(), disabled: true }, + ], + }, + { + icon: , + title: "Formatters", + tools: [{ icon: , title: "Json", href: pagesPath.$url(), disabled: true }], + }, + { + icon: , + title: "Generators", + tools: [ + { icon: , title: "Hash", href: pagesPath.$url(), disabled: true }, + { icon: , title: "UUID", href: pagesPath.$url(), disabled: true }, + ], + }, + { + icon: , + title: "Text", + tools: [ + { icon: , title: "Regex Tester", href: pagesPath.$url(), disabled: true }, + ], + }, + { + icon: , + title: "Graphic", + tools: [ + { + icon: , + title: "PNG / JPEG Compressor", + href: pagesPath.$url(), + disabled: true, + }, + ], + }, +]; + +const StyledComponent = () => ( + + + + + + + } title="All tools" href={pagesPath.$url()} /> + + + {toolGroups.map(({ icon, title, tools }) => ( + + ))} + + + + +); + +export const Component = memo(StyledComponent); + +export default Component; diff --git a/src/components/layout/DrawerCollapseItem.tsx b/src/components/layout/DrawerCollapseItem.tsx new file mode 100644 index 0000000..a3bc03a --- /dev/null +++ b/src/components/layout/DrawerCollapseItem.tsx @@ -0,0 +1,56 @@ +import ExpandLess from "@mui/icons-material/ExpandLess"; +import ExpandMore from "@mui/icons-material/ExpandMore"; +import { Collapse, List, ListItemButton, ListItemText } from "@mui/material"; +import equal from "fast-deep-equal"; +import { ComponentPropsWithoutRef, memo, MouseEventHandler, useCallback, useState } from "react"; + +import DrawerItem from "./DrawerItem"; +import DrawerItemIcon from "./DrawerItemIcon"; + +type ContainerProps = { + title: string; + tools: ComponentPropsWithoutRef[]; +} & ComponentPropsWithoutRef; + +type Props = { + open: boolean; + onClick: MouseEventHandler; +} & ContainerProps; + +const StyledComponent = ({ icon, title, tools, open, onClick }: Props) => ( + <> + + + + {open ? : } + + + + {tools.map(tool => ( + + ))} + + + +); + +export const Component = memo(StyledComponent, equal); + +const Container = ({ icon, title, tools }: ContainerProps) => { + const [open, setOpen] = useState(false); + + const onClick = useCallback(() => { + setOpen(!open); + }, [open]); + + return ; +}; + +export default Container; diff --git a/src/components/layout/DrawerItem.tsx b/src/components/layout/DrawerItem.tsx new file mode 100644 index 0000000..be39dc6 --- /dev/null +++ b/src/components/layout/DrawerItem.tsx @@ -0,0 +1,47 @@ +import { Box, css, ListItemButton, ListItemText, Tooltip } from "@mui/material"; +import NextLink, { LinkProps } from "next/link"; +import { ComponentPropsWithoutRef, memo } from "react"; + +import DrawerItemIcon from "./DrawerItemIcon"; + +type Props = { + title: string; + disabled?: boolean; + subItem?: true; +} & Pick & + ComponentPropsWithoutRef; + +const indent = css` + text-indent: 40px; +`; + +const StyledComponent = ({ href, icon, title, disabled, subItem }: Props) => { + const wrappedIcon = subItem ? ( + + + + ) : ( + + ); + + const link = ( + + + {wrappedIcon} + + + + ); + + return disabled ? ( + + {link} + + ) : ( + link + ); +}; + +export const Component = memo(StyledComponent); + +export default Component; diff --git a/src/components/layout/DrawerItemIcon.tsx b/src/components/layout/DrawerItemIcon.tsx new file mode 100644 index 0000000..f9a151c --- /dev/null +++ b/src/components/layout/DrawerItemIcon.tsx @@ -0,0 +1,17 @@ +import { css, ListItemIcon } from "@mui/material"; +import { memo, ReactNode } from "react"; + +type Props = { + icon: ReactNode; +}; + +const itemIcon = css` + min-width: 40px; + vertical-align: middle; +`; + +const StyledComponent = ({ icon }: Props) => {icon}; + +export const Component = memo(StyledComponent); + +export default Component; diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx new file mode 100644 index 0000000..19c1038 --- /dev/null +++ b/src/components/layout/Header.tsx @@ -0,0 +1,85 @@ +import GitHubIcon from "@mui/icons-material/GitHub"; +import { + AppBar, + css, + IconButton, + Link as MuiLink, + Stack, + Theme, + Toolbar, + Typography, +} from "@mui/material"; +import NextLink from "next/link"; +import { memo } from "react"; + +import { site } from "@/data"; +import { pagesPath } from "@/libs/$path"; + +export const headerHeight = 48; + +const appBar = css` + background-color: transparent; + box-shadow: none; +`; + +const toolbar = (theme: Theme) => css` + padding: 0 ${theme.spacing(2)} !important; +`; + +const topLink = css` + color: black; + text-decoration: none; + cursor: pointer; + font-weight: 400; +`; + +const description = (theme: Theme) => css` + color: black; + font-size: ${theme.typography.fontSize * 1.4}px; +`; + +const gitHubLink = (theme: Theme) => css` + color: black; + margin-left: auto; + width: ${theme.typography.fontSize * 3}px; + height: ${theme.typography.fontSize * 3}px; +`; + +const gitHubIcon = (theme: Theme) => css` + width: ${theme.typography.fontSize * 3}; + height: ${theme.typography.fontSize * 3}; +`; + +const StyledComponent = () => ( + + + + + + {site.title} + + + + web clone of{" "} + + DevToys + + + + + + + + + + +); + +export const Component = memo(StyledComponent); + +export default Component; diff --git a/src/components/layout/Layout.tsx b/src/components/layout/Layout.tsx new file mode 100644 index 0000000..4dc8782 --- /dev/null +++ b/src/components/layout/Layout.tsx @@ -0,0 +1,41 @@ +import { Box, css, Stack } from "@mui/material"; +import Head from "next/head"; +import { memo, PropsWithChildren } from "react"; + +import { site } from "@/data"; +import { staticPath } from "@/libs/$path"; + +import Drawer, { drawerWidth } from "./Drawer"; +import Header, { headerHeight } from "./Header"; + +type Props = PropsWithChildren; + +const content = css` + width: calc(100vw - ${drawerWidth}px); + background-color: #f9f9f9; + overflow: auto; + border: 1px solid rgba(0, 0, 0, 0.08); + border-top-left-radius: 8px; +`; + +const StyledComponent = ({ children }: Props) => ( + <> + + {site.title} + + + + + + +
+ + + {children} + + +); + +export const Component = memo(StyledComponent); + +export default Component; diff --git a/src/components/layout/SearchBar.tsx b/src/components/layout/SearchBar.tsx new file mode 100644 index 0000000..27345ee --- /dev/null +++ b/src/components/layout/SearchBar.tsx @@ -0,0 +1,67 @@ +import SearchIcon from "@mui/icons-material/Search"; +import { Box, css, InputBase, Paper, Theme } from "@mui/material"; +import { useRouter } from "next/router"; +import { ChangeEventHandler, memo, useCallback } from "react"; +import { useRecoilState } from "recoil"; + +import { pagesPath } from "@/libs/$path"; + +import { searchTextState } from "./states"; + +type Props = { + text: string; + onChange: ChangeEventHandler; +}; + +const container = css` + display: flex; + align-items: center; + background-color: #f9f9f9; +`; + +const input = (theme: Theme) => css` + flex: 1; + margin-left: ${theme.spacing(1)}; +`; + +const iconWrapper = (theme: Theme) => css` + padding: ${theme.spacing(1)}; + > * { + color: rgba(0, 0, 0, 0.54); + vertical-align: middle; + } +`; + +const StyledComponent = ({ text, onChange }: Props) => ( + + + + + + +); + +export const Component = memo(StyledComponent); + +const Container = () => { + const [text, setText] = useRecoilState(searchTextState); + const router = useRouter(); + + const onChange: Props["onChange"] = useCallback( + ({ currentTarget: { value } }) => { + setText(value); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + router.push(pagesPath.$url()); + }, + [setText, router] + ); + + return ; +}; + +export default Container; diff --git a/src/components/layout/index.ts b/src/components/layout/index.ts new file mode 100644 index 0000000..4ea2400 --- /dev/null +++ b/src/components/layout/index.ts @@ -0,0 +1,3 @@ +import Layout from "./Layout"; + +export { Layout }; diff --git a/src/components/layout/states/index.ts b/src/components/layout/states/index.ts new file mode 100644 index 0000000..31b80d7 --- /dev/null +++ b/src/components/layout/states/index.ts @@ -0,0 +1 @@ +export * from "./searchText"; diff --git a/src/components/layout/states/searchText.ts b/src/components/layout/states/searchText.ts new file mode 100644 index 0000000..95eeba4 --- /dev/null +++ b/src/components/layout/states/searchText.ts @@ -0,0 +1,6 @@ +import { atom } from "recoil"; + +export const searchTextState = atom({ + key: "searchText", + default: "", +}); diff --git a/src/data/index.ts b/src/data/index.ts index c1cc2b4..e169727 100644 --- a/src/data/index.ts +++ b/src/data/index.ts @@ -1,2 +1,3 @@ export * as emotion from "./emotion"; export * as mui from "./mui"; +export * as site from "./site"; diff --git a/src/data/mui.ts b/src/data/mui.ts index 8814d96..964e07d 100644 --- a/src/data/mui.ts +++ b/src/data/mui.ts @@ -1,3 +1,20 @@ import { createTheme } from "@mui/material"; -export const theme = createTheme(); +export const theme = createTheme({ + palette: { + background: { + default: "#f0f3f8", + }, + }, + typography: { + htmlFontSize: 10, + fontSize: 8, + }, + components: { + MuiButtonBase: { + defaultProps: { + disableRipple: true, + }, + }, + }, +}); diff --git a/src/data/site.ts b/src/data/site.ts new file mode 100644 index 0000000..926230d --- /dev/null +++ b/src/data/site.ts @@ -0,0 +1 @@ +export const title = "DevToysWeb"; diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index a5899c8..01bf3cb 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -4,6 +4,7 @@ import type { AppProps } from "next/app"; import Head from "next/head"; import { RecoilRoot } from "recoil"; +import { Layout } from "@/components/layout"; import { emotion, mui } from "@/data"; import "@fontsource/roboto/300.css"; @@ -23,7 +24,9 @@ const MyApp = ({ Component, emotionCache = emotion.cache, pageProps }: MyAppProp - + + + diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 6bd1b9a..510d058 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,16 +1,5 @@ import type { NextPage } from "next"; -import Head from "next/head"; -import { staticPath } from "@/libs/$path"; - -const Page: NextPage = () => ( - <> - - DevToysWeb - - -

hello

- -); +const Page: NextPage = () =>

hello

; export default Page;