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;