26
loading...
This website collects cookies to deliver better user experience
npx create-next-app@latest --typescript
npm i styled-components
npm i -D @types/styled-components
npm i @styled-icons/material
//pages/_document.tsx
import Document, { DocumentContext } from "next/document";
import { ServerStyleSheet } from "styled-components";
export default class MyDocument extends Document {
static async getInitialProps(ctx: DocumentContext) {
const sheet = new ServerStyleSheet();
const originalRenderPage = ctx.renderPage;
try {
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: (App) => (props) =>
sheet.collectStyles(<App {...props} />),
});
const initialProps = await Document.getInitialProps(ctx);
return {
...initialProps,
styles: (
<>
{initialProps.styles}
{sheet.getStyleElement()}
</>
),
};
} finally {
sheet.seal();
}
}
}
//components/DashboardLayout/index.tsx
type DashboardLayoutProps = {
children: React.ReactNode,
};
export default function DashboardLayout({ children }: DashboardLayoutProps) {
return (
<Container>
<Header />
<Content>
<Sidebar />
<PageContainer>{children}</PageContainer>
</Content>
<Footer />
</Container>
);
}
//components/DashboardLayout/styles.ts
import styled from "styled-components";
export const Container = styled.div`
text-align: center;
display: flex;
flex-direction: column;
min-height: 100vh;
color: #000133;
`;
export const Content = styled.div`
display: flex;
flex: 1;
`;
export const PageContainer = styled.div`
padding: 20px;
width: 80vw;
`;
//components/Header/index.tsx
import { HeaderContainer, TitleContainer } from "./styles";
export default function Header() {
return (
<HeaderContainer>
<TitleContainer>Header</TitleContainer>
</HeaderContainer>
);
}
//components/Header/styles.ts
import styled from "styled-components";
export const HeaderContainer = styled.header`
display: flex;
background: #00022e;
height: 50px;
align-items: center;
justify-content: center;
color: #fc86aa;
`;
export const TitleContainer = styled.div`
margin: auto;
`;
//components/Footer/index.tsx
import { FooterContainer } from "./Footer.styles";
export default function Footer() {
return <FooterContainer>Footer</FooterContainer>;
}
//components/Footer/styles.ts
import styled from "styled-components";
export const FooterContainer = styled.footer`
display: flex;
background: #00022e;
height: 50px;
align-items: center;
justify-content: center;
color: #fc86aa;
`;
// components/Sidebar/index.tsx
import { SidebarContainer } from "./styles";
export default function Sidebar() {
return <SidebarContainer>Menu</SidebarContainer>;
}
// components/Sidebar/styles.ts
import styled from "styled-components";
export const SidebarContainer = styled.aside`
background: #d8dcd6;
width: 20vw;
`;
// pages/_app.tsx
import "../styles/globals.css";
import type { AppProps } from "next/app";
import DashboardLayout from "../components/DashboardLayout";
function MyApp({ Component, pageProps }: AppProps) {
return (
<DashboardLayout>
<Component {...pageProps} />
</DashboardLayout>
);
}
export default MyApp;
//pages/index.tsx
import type { NextPage } from "next";
const Home: NextPage = () => {
return <div>Content</div>;
};
export default Home;
npm run dev
//components/DashboardLayout/index.tsx
...
import { useState } from "react";
type DashboardLayoutProps = {
children: React.ReactNode;
};
export default function DashboardLayout({ children }: DashboardLayoutProps) {
const [isOpened, setOpened] = useState(false);
const toggleDrawer = () => {
setOpened((prev) => !prev);
};
return (
<Container>
<Header isOpened={isOpened} toggleDrawer={toggleDrawer} />
<Content>
<Sidebar isOpened={isOpened} />
<PageContainer>{children}</PageContainer>
</Content>
<Footer />
</Container>
);
}
//components/Header/index.tsx
import { HeaderContainer, TitleContainer, IconContainer } from "./styles";
import { Menu, ChevronLeft } from "@styled-icons/material";
type HeaderProps = {
isOpened: boolean,
toggleDrawer: () => void,
};
export default function Header({ isOpened, toggleDrawer }: HeaderProps) {
return (
<HeaderContainer>
<IconContainer onClick={toggleDrawer}>
{isOpened ? <ChevronLeft /> : <Menu />}
</IconContainer>
<TitleContainer>Header</TitleContainer>
</HeaderContainer>
);
}
// components/Header/styles.ts
...
export const IconContainer = styled.div`
padding: 10px;
cursor: pointer;
& svg {
height: 30px;
}
`;
//components/Sidebar/index.tsx
import { SidebarContainer } from "./styles";
type SidebarProps = {
isOpened: boolean;
};
export default function Sidebar({ isOpened }: SidebarProps) {
return <SidebarContainer isOpened={isOpened}>Menu</SidebarContainer>;
// components/Sidebar/styles.ts`
...
export const SidebarContainer = styled.aside<{ isOpened: boolean }>`
background: #d8dcd6;
width: ${(props) => (props.isOpened ? "20vw" : "0vw")};
transition: width 0.5s;
overflow: hidden;
`;
//constants/menu-items.ts
import {
Dashboard,
ShoppingCart,
People,
AttachMoney,
AddShoppingCart,
Done,
Business,
HomeWork,
Person,
} from "@styled-icons/material";
type MenuOption = {
name: string,
icon: React.ComponentType,
url: string,
subItems?: MenuOption[],
};
const MENU_OPTIONS: MenuOption[] = [
{
name: "Dashboard",
icon: Dashboard,
url: "/",
},
{
name: "Orders",
icon: ShoppingCart,
url: "/orders",
subItems: [
{
name: "New",
icon: AddShoppingCart,
url: "/new-orders",
},
{
name: "Completed",
icon: Done,
url: "/completed-orders",
},
],
},
{
name: "Customers",
icon: People,
url: "/customers",
subItems: [
{
name: "Corporate",
icon: Business,
url: "/corporate",
},
{
name: "SMB",
icon: HomeWork,
url: "/smb",
subItems: [
{
name: "Retail",
icon: Person,
url: "/retail",
},
],
},
],
},
{
name: "Inventory",
icon: AttachMoney,
url: "/inventory",
},
];
//constants/menu-items.ts
...
export type MenuItem = {
name: string,
icon: React.ComponentType,
url: string,
id: string,
depth: number,
subItems?: MenuItem[],
};
function makeMenuLevel(options: MenuOption[], depth = 0): MenuItem[] {
return options.map((option, idx) => ({
...option,
id: depth === 0 ? idx.toString() : `${depth}.${idx}`,
depth,
subItems:
option.subItems && option.subItems.length > 0
? makeMenuLevel(option.subItems, depth + 1)
: undefined,
}));
}
export const MENU_ITEMS: MenuItem[] = makeMenuLevel(MENU_OPTIONS);
// components/Sidebar/index.tsx
import { SidebarContainer } from "./styles";
import { MENU_ITEMS } from "../../constants/menu-items";
import MenuItemsList from "../MenuItemsList";
type SidebarProps = {
isOpened: boolean,
};
export default function Sidebar({ isOpened }: SidebarProps) {
return (
<SidebarContainer isOpened={isOpened}>
<MenuItemsList options={MENU_ITEMS} />
</SidebarContainer>
);
}
//components/MenuItemsList/index.tsx
import { MenuItem as MenuItemType } from "../../constants/menu-items";
import MenuItem from "../MenuItem";
type MenuItemsListProps = {
options: MenuItemType[],
};
export default function MenuItemsList({ options }: MenuItemsListProps) {
return (
<>
{options.map((option) => (
<MenuItem menuItem={option} key={option.id} />
))}
</>
);
}
//components/MenuItem/index.tsx
import Link from "next/link";
import { useRouter } from "next/router";
import { MenuItem as MenuItemType } from "../../constants/menu-items";
import { MenuItemContainer } from "./styles";
import MenuItemsList from "../MenuItemsList";
import ExpandIcon from "../ExpandIcon";
import { useState } from "react";
type MenuItemProps = {
menuItem: MenuItemType,
};
export default function MenuItem({
menuItem: { name, icon: Icon, url, depth, subItems },
}: MenuItemProps) {
const [isExpanded, toggleExpanded] = useState(false);
const router = useRouter();
const selected = router.asPath === url;
const isNested = subItems && subItems?.length > 0;
const onClick = () => {
toggleExpanded((prev) => !prev);
};
return (
<>
<MenuItemContainer className={selected ? "selected" : ""} depth={depth}>
<Link href={url} passHref>
<div className="menu-item">
<Icon />
<span>{name}</span>
</div>
</Link>
{isNested ? (
<ExpandIcon isExpanded={isExpanded} handleClick={onClick} />
) : null}
</MenuItemContainer>
{isExpanded && isNested ? <MenuItemsList options={subItems} /> : null}
</>
);
}
//components/MenuItem/styles.ts
import styled from "styled-components";
export const MenuItemContainer =
styled.a <
{ depth: number } >
`
display: flex;
flex-direction: row;
font-size: 20px;
padding: 10px 0px 10px 10px;
align-items: center;
justify-content: space-between;
& svg {
height: 30px;
margin-right: 10px;
}
&:hover {
background-color: #00022e;
color: #fc86aa;
opacity: 0.5;
cursor: pointer;
}
.menu-item {
display: flex;
flex-direction: row;
align-items: center;
margin-left: ${({ depth }) => `${depth}rem`};
}
&.selected {
background-color: #00022e;
color: #fff;
}
`;
//components/ExpandIcon/index.tsx
import { ExpandMore, ExpandLess } from "@styled-icons/material";
type ExpandIconPros = {
isExpanded: boolean,
handleClick: () => void,
};
export default function ExpandIcon({
isExpanded,
handleClick,
}: ExpandIconPros) {
return isExpanded ? (
<ExpandLess onClick={handleClick} />
) : (
<ExpandMore onClick={handleClick} />
);
}
// pages/orders.tsx
import type { NextPage } from "next";
const Orders: NextPage = () => {
return <div>Orders</div>;
};
export default Orders;