feat: add layout component

This commit is contained in:
rusconn
2022-03-24 01:37:16 +00:00
parent 673bb1aadd
commit 29247936cc
17 changed files with 466 additions and 15 deletions

View File

@@ -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

View File

@@ -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",

View File

@@ -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: <TransformIcon />,
title: "Converters",
tools: [
{ icon: <TransformIcon />, title: "Json <> Yaml", href: pagesPath.$url(), disabled: true },
{ icon: <NumbersIcon />, title: "Number Base", href: pagesPath.$url(), disabled: true },
],
},
{
icon: <SyncAltIcon />,
title: "Encoders / Decoders",
tools: [
{ icon: <CodeIcon />, title: "HTML", href: pagesPath.$url(), disabled: true },
{ icon: <LinkIcon />, title: "URL", href: pagesPath.$url(), disabled: true },
{ icon: <DragHandleIcon />, title: "Base 64", href: pagesPath.$url(), disabled: true },
{ icon: <KeyIcon />, title: "JWT", href: pagesPath.$url(), disabled: true },
],
},
{
icon: <SortIcon />,
title: "Formatters",
tools: [{ icon: <DataObjectIcon />, title: "Json", href: pagesPath.$url(), disabled: true }],
},
{
icon: <NoteAddIcon />,
title: "Generators",
tools: [
{ icon: <FingerprintIcon />, title: "Hash", href: pagesPath.$url(), disabled: true },
{ icon: <NumbersIcon />, title: "UUID", href: pagesPath.$url(), disabled: true },
],
},
{
icon: <TextFieldsIcon />,
title: "Text",
tools: [
{ icon: <TextIncreaseIcon />, title: "Regex Tester", href: pagesPath.$url(), disabled: true },
],
},
{
icon: <ImageIcon />,
title: "Graphic",
tools: [
{
icon: <FilterIcon />,
title: "PNG / JPEG Compressor",
href: pagesPath.$url(),
disabled: true,
},
],
},
];
const StyledComponent = () => (
<Drawer variant="permanent" css={drawer}>
<Box paddingLeft={2} paddingRight={2} marginTop="1px">
<SearchBar />
</Box>
<List component="nav">
<Stack spacing={1}>
<DrawerItem icon={<HomeIcon />} title="All tools" href={pagesPath.$url()} />
<Divider css={divider} />
<Box>
{toolGroups.map(({ icon, title, tools }) => (
<DrawerCollapseItem key={title} {...{ icon, title, tools }} />
))}
</Box>
</Stack>
</List>
</Drawer>
);
export const Component = memo(StyledComponent);
export default Component;

View File

@@ -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<typeof DrawerItem>[];
} & ComponentPropsWithoutRef<typeof DrawerItemIcon>;
type Props = {
open: boolean;
onClick: MouseEventHandler<HTMLDivElement>;
} & ContainerProps;
const StyledComponent = ({ icon, title, tools, open, onClick }: Props) => (
<>
<ListItemButton {...{ onClick }}>
<DrawerItemIcon {...{ icon }} />
<ListItemText primary={title} />
{open ? <ExpandLess /> : <ExpandMore />}
</ListItemButton>
<Collapse in={open} timeout="auto" unmountOnExit>
<List component="div" disablePadding>
{tools.map(tool => (
<DrawerItem
key={tool.title}
icon={tool.icon}
title={tool.title}
href={tool.href}
disabled={tool.disabled}
subItem
/>
))}
</List>
</Collapse>
</>
);
export const Component = memo(StyledComponent, equal);
const Container = ({ icon, title, tools }: ContainerProps) => {
const [open, setOpen] = useState(false);
const onClick = useCallback(() => {
setOpen(!open);
}, [open]);
return <Component {...{ icon, title, tools, open, onClick }} />;
};
export default Container;

View File

@@ -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<LinkProps, "href"> &
ComponentPropsWithoutRef<typeof DrawerItemIcon>;
const indent = css`
text-indent: 40px;
`;
const StyledComponent = ({ href, icon, title, disabled, subItem }: Props) => {
const wrappedIcon = subItem ? (
<Box css={indent}>
<DrawerItemIcon {...{ icon }} />
</Box>
) : (
<DrawerItemIcon {...{ icon }} />
);
const link = (
<NextLink {...{ href }} passHref>
<ListItemButton {...{ disabled }}>
{wrappedIcon}
<ListItemText primary={title} />
</ListItemButton>
</NextLink>
);
return disabled ? (
<Tooltip title="coming soon!" placement="right" arrow>
<span>{link}</span>
</Tooltip>
) : (
link
);
};
export const Component = memo(StyledComponent);
export default Component;

View File

@@ -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) => <ListItemIcon css={itemIcon}>{icon}</ListItemIcon>;
export const Component = memo(StyledComponent);
export default Component;

View File

@@ -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 = () => (
<AppBar position="relative" css={appBar}>
<Toolbar variant="dense" css={toolbar}>
<Stack direction="row" spacing={1} alignItems="baseline" flexGrow={1}>
<NextLink href={pagesPath.$url()} passHref>
<MuiLink variant="h6" css={topLink}>
{site.title}
</MuiLink>
</NextLink>
<Typography css={description}>
web clone of{" "}
<a href="https://devtoys.app" target="_blank" rel="noreferrer">
DevToys
</a>
</Typography>
</Stack>
<IconButton>
<a
href="https://github.com/rusconn/DevToysWeb"
target="_blank"
rel="noreferrer"
css={gitHubLink}
>
<GitHubIcon css={gitHubIcon} />
</a>
</IconButton>
</Toolbar>
</AppBar>
);
export const Component = memo(StyledComponent);
export default Component;

View File

@@ -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<unknown>;
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) => (
<>
<Head>
<title>{site.title}</title>
<link rel="icon" href={staticPath.favicon_ico} />
<meta name="description" content="DevToys web clone" />
<meta name="og:title" content={site.title} />
<meta name="og:site_name" content={site.title} />
<meta name="og:type" content="website" />
</Head>
<Header />
<Stack direction="row" height={`calc(100vh - ${headerHeight}px)`}>
<Drawer />
<Box css={content}>{children}</Box>
</Stack>
</>
);
export const Component = memo(StyledComponent);
export default Component;

View File

@@ -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<HTMLInputElement>;
};
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) => (
<Paper css={container}>
<InputBase
placeholder="Type to search for tools..."
value={text}
{...{ onChange }}
css={input}
/>
<Box css={iconWrapper}>
<SearchIcon />
</Box>
</Paper>
);
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 <Component {...{ text, onChange }} />;
};
export default Container;

View File

@@ -0,0 +1,3 @@
import Layout from "./Layout";
export { Layout };

View File

@@ -0,0 +1 @@
export * from "./searchText";

View File

@@ -0,0 +1,6 @@
import { atom } from "recoil";
export const searchTextState = atom({
key: "searchText",
default: "",
});

View File

@@ -1,2 +1,3 @@
export * as emotion from "./emotion";
export * as mui from "./mui";
export * as site from "./site";

View File

@@ -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,
},
},
},
});

1
src/data/site.ts Normal file
View File

@@ -0,0 +1 @@
export const title = "DevToysWeb";

View File

@@ -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
<ThemeProvider theme={mui.theme}>
<CssBaseline />
<RecoilRoot>
<Component {...pageProps} />
<Layout>
<Component {...pageProps} />
</Layout>
</RecoilRoot>
</ThemeProvider>
</CacheProvider>

View File

@@ -1,16 +1,5 @@
import type { NextPage } from "next";
import Head from "next/head";
import { staticPath } from "@/libs/$path";
const Page: NextPage = () => (
<>
<Head>
<title>DevToysWeb</title>
<link rel="icon" href={staticPath.favicon_ico} />
</Head>
<p>hello</p>
</>
);
const Page: NextPage = () => <p>hello</p>;
export default Page;