mirror of
https://github.com/ershisan99/DevToysWeb.git
synced 2025-12-16 20:49:23 +00:00
feat: add layout component
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
116
src/components/layout/Drawer.tsx
Normal file
116
src/components/layout/Drawer.tsx
Normal 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;
|
||||
56
src/components/layout/DrawerCollapseItem.tsx
Normal file
56
src/components/layout/DrawerCollapseItem.tsx
Normal 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;
|
||||
47
src/components/layout/DrawerItem.tsx
Normal file
47
src/components/layout/DrawerItem.tsx
Normal 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;
|
||||
17
src/components/layout/DrawerItemIcon.tsx
Normal file
17
src/components/layout/DrawerItemIcon.tsx
Normal 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;
|
||||
85
src/components/layout/Header.tsx
Normal file
85
src/components/layout/Header.tsx
Normal 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;
|
||||
41
src/components/layout/Layout.tsx
Normal file
41
src/components/layout/Layout.tsx
Normal 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;
|
||||
67
src/components/layout/SearchBar.tsx
Normal file
67
src/components/layout/SearchBar.tsx
Normal 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;
|
||||
3
src/components/layout/index.ts
Normal file
3
src/components/layout/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import Layout from "./Layout";
|
||||
|
||||
export { Layout };
|
||||
1
src/components/layout/states/index.ts
Normal file
1
src/components/layout/states/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./searchText";
|
||||
6
src/components/layout/states/searchText.ts
Normal file
6
src/components/layout/states/searchText.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { atom } from "recoil";
|
||||
|
||||
export const searchTextState = atom({
|
||||
key: "searchText",
|
||||
default: "",
|
||||
});
|
||||
@@ -1,2 +1,3 @@
|
||||
export * as emotion from "./emotion";
|
||||
export * as mui from "./mui";
|
||||
export * as site from "./site";
|
||||
|
||||
@@ -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
1
src/data/site.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const title = "DevToysWeb";
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user