Skip to content

Commit

Permalink
feat(components): add layout
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexGarrixen committed Jan 12, 2024
1 parent c588c9e commit 112817e
Show file tree
Hide file tree
Showing 13 changed files with 304 additions and 39 deletions.
17 changes: 13 additions & 4 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { css } from "@root/styled-system/css";
import { container } from "@root/styled-system/patterns";

import { dmSans } from "@/styles/fonts";
import { Layout } from "@/components/layout";
import AsideSuggestions from "@/components/aside-suggestions";
import { Navbar } from "@/components/navbar";
import { Footer } from "@/components/footer";
import { GoogleAnalytics } from "@/components/google-analytics";
Expand All @@ -19,13 +21,20 @@ export default function RootLayout({ children }: { children: React.ReactNode })
<body
className={`${dmSans.className} ${css({
color: "text-primary",
bg: "bg-tertiary",
bg: "bg-primary",
textStyle: "body-base",
})}`}
>
<Navbar />
<main className={container()}>{children}</main>
<Footer />
<Layout
aside={<AsideSuggestions />}
content={
<>
<div className={container()}>{children}</div>
<Footer />
</>
}
header={<Navbar />}
/>
{IS_PROD ? <GoogleAnalytics /> : null}
</body>
</html>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { describe, it } from "@jest/globals";
import { render } from "@testing-library/react";
import * as useHooks from "@uidotdev/usehooks";

import AsideSuggestions, { Panel } from "../aside-suggestions";

jest.mock("@uidotdev/usehooks", () => ({
__esModule: true,
useMediaQuery: jest.fn(() => true),
}));

describe("Aside Suggestions", () => {
it("Correct rendering and unmount", () => {
const screen = render(<AsideSuggestions />);

expect(() => screen.unmount()).not.toThrow();
});

it("Correct rendering and unmount of Panel", () => {
const screen = render(<Panel>content</Panel>);

expect(screen.getByText("content")).toBeInTheDocument();
expect(() => screen.unmount()).not.toThrow();
});

it("Should render only is small device", () => {
jest.spyOn(useHooks, "useMediaQuery").mockReturnValue(false);

const screen = render(<AsideSuggestions />);

expect(screen.container.firstChild).toBeNull();
});
});
22 changes: 22 additions & 0 deletions src/components/aside-suggestions/aside-suggestions.styled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { css } from "@root/styled-system/css";

export default {
header: css({
h: "16",
bgColor: "bg-primary",
textStyle: "display-xs",
color: "text-primary",
fontWeight: "500",
borderBottom: "1px solid",
borderColor: "border-secondary",
display: "flex",
alignItems: "center",
px: { base: "4", lg: "8" },
position: "sticky",
top: 0,
}),

headerIcon: css({ fontSize: "icon-24", mr: "2" }),

body: css({ py: "6", overflowY: "auto", px: { base: "4", lg: "8" } }),
};
32 changes: 32 additions & 0 deletions src/components/aside-suggestions/aside-suggestions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"use client";

import type { ReactNode } from "react";

import { useMediaQuery } from "@uidotdev/usehooks";

import { Suggestions } from "@/components/suggestions";
import { LightFill } from "@/components/icons";

import classes from "./aside-suggestions.styled";

export function Panel({ children }: { children?: ReactNode }) {
return (
<>
<header className={classes.header}>
<LightFill className={classes.headerIcon} />
Suggestions
</header>
<div className={classes.body}>{children}</div>
</>
);
}

export default function AsideSuggestions() {
const isLargeDevice = useMediaQuery("only screen and (min-width : 1024px)");

return isLargeDevice ? (
<Panel>
<Suggestions />
</Panel>
) : null;
}
5 changes: 5 additions & 0 deletions src/components/aside-suggestions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import dynamic from "next/dynamic";

import { AsideSkeleton } from "./skeleton";

export default dynamic(() => import("./aside-suggestions"), { ssr: false, loading: AsideSkeleton });
55 changes: 55 additions & 0 deletions src/components/aside-suggestions/skeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { css } from "@root/styled-system/css";
import { stack } from "@root/styled-system/patterns";

import { Panel } from "./aside-suggestions";

const classes = {
suggestion: css({
border: "1px solid",
borderColor: "border-secondary",
rounded: "lg",
overflow: "hidden",
}),

suggestionPreviews: css({ display: "flex" }),

suggestionPreview: css({
aspectRatio: "3/2",
bgColor: "bg-secondary",
flex: 1,
"&:first-child": {
borderRight: "1px solid",
borderColor: "border-secondary",
},
}),

suggestionContent: css({
borderTop: "1px solid",
borderColor: "border-secondary",
h: "10",
}),
};

export function AsideSkeleton() {
return (
<Panel>
<div className={stack({ gap: "5" })}>
<SuggestionSkeleton />
<SuggestionSkeleton />
<SuggestionSkeleton />
</div>
</Panel>
);
}

function SuggestionSkeleton() {
return (
<article className={classes.suggestion}>
<div className={classes.suggestionPreviews}>
<div className={classes.suggestionPreview} />
<div className={classes.suggestionPreview} />
</div>
<div className={classes.suggestionContent} />
</article>
);
}
22 changes: 22 additions & 0 deletions src/components/layout/__tests__/layout.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { describe, it } from "@jest/globals";
import { render } from "@testing-library/react";

import { Layout } from "../layout";

describe("Layout", () => {
it("Correct rendering and unmount", () => {
const screen = render(<Layout />);

expect(() => screen.unmount()).not.toThrow();
});

it("Should render slot content", () => {
const screen = render(
<Layout aside={<>aside</>} content={<>content</>} header={<>header</>} />,
);

expect(screen.getByText("header")).toBeInTheDocument();
expect(screen.getByText("aside")).toBeInTheDocument();
expect(screen.getByText("content")).toBeInTheDocument();
});
});
1 change: 1 addition & 0 deletions src/components/layout/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Layout } from "./layout";
26 changes: 26 additions & 0 deletions src/components/layout/layout.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { StoryObj, Meta } from "@storybook/react";

import { css } from "@root/styled-system/css";

import { Layout } from "./layout";

export default {
title: "Components / Layout",
tags: ["autodocs"],
} satisfies Meta<typeof Layout>;

type Story = StoryObj<typeof Layout>;

export const Preview: Story = {
name: "Default Preview",
args: {
header: <>header</>,
aside: <>aside</>,
content: <>content</>,
},
render: (args) => (
<div className={css({ "& aside": { borderColor: "border-primary" } })}>
<Layout {...args} />
</div>
),
};
41 changes: 41 additions & 0 deletions src/components/layout/layout.styled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { css } from "@root/styled-system/css";

export default {
header: css({
height: "16",
display: "flex",
flexDirection: "column",
justifyContent: "center",
borderBottom: "1px solid",
borderColor: "border-secondary",
position: "sticky",
zIndex: "10",
top: 0,
bg: "bg-primary",
}),

main: css({
display: "grid",
alignItems: "start",
gridTemplateColumns: { lg: "12" },
}),

content: css({
gridColumn: { lg: "9" },
}),

aside: css({
gridColumn: "3",
borderLeft: "1px solid",
borderColor: "border-secondary",
overflow: "auto",
display: "none",

lg: {
top: "16",
display: "block",
position: "sticky",
height: "calc(100vh - 64px)",
},
}),
};
25 changes: 25 additions & 0 deletions src/components/layout/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { ReactNode } from "react";

import { container } from "@root/styled-system/patterns";

import classes from "./layout.styled";

interface LayoutProps {
header?: ReactNode;
aside?: ReactNode;
content?: ReactNode;
}

export function Layout({ header, aside, content }: LayoutProps) {
return (
<>
<header className={classes.header}>
<div className={container({ maxW: "100%", mx: "0" })}>{header}</div>
</header>
<main className={classes.main}>
<section className={classes.content}>{content}</section>
<aside className={classes.aside}>{aside}</aside>
</main>
</>
);
}
6 changes: 1 addition & 5 deletions src/components/navbar/navbar.styled.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
import { css } from "@root/styled-system/css";
import { stack, container } from "@root/styled-system/patterns";
import { stack } from "@root/styled-system/patterns";

export default {
header: container(),

nav: css({
display: "flex",
alignItems: "center",
justifyContent: "space-between",
flexWrap: "wrap",
pb: "3",
color: "text-primary",
mt: "8",
gap: "5",
}),

Expand Down
58 changes: 28 additions & 30 deletions src/components/navbar/navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,35 +4,33 @@ import classes from "./navbar.styled";

export function Navbar() {
return (
<header className={classes.header}>
<nav className={classes.nav}>
<div className={classes.logo}>
<Brand className={`${classes.icon} ${classes.logoIcon}`} />
Cool Contrast
</div>
<div className={classes.rightContent}>
<a
aria-label="buy me a coffee"
className={classes.link}
href="https://www.buymeacoffee.com/alexgarrixen"
rel="noopener"
target="_blank"
>
<Coffee className={`${classes.leftIcon} ${classes.icon}`} />
<span className={classes.btnLabel}>Buy me a coffee</span>
</a>
<a
aria-label="star on github"
className={classes.link}
href="https://github.com/AlexGarrixen/Cool-Contrast"
rel="noopener"
target="_blank"
>
<GithubFill className={`${classes.leftIcon} ${classes.icon}`} />
<span className={classes.btnLabel}>Github</span>
</a>
</div>
</nav>
</header>
<nav className={classes.nav}>
<div className={classes.logo}>
<Brand className={`${classes.icon} ${classes.logoIcon}`} />
Cool Contrast
</div>
<div className={classes.rightContent}>
<a
aria-label="buy me a coffee"
className={classes.link}
href="https://www.buymeacoffee.com/alexgarrixen"
rel="noopener"
target="_blank"
>
<Coffee className={`${classes.leftIcon} ${classes.icon}`} />
<span className={classes.btnLabel}>Buy me a coffee</span>
</a>
<a
aria-label="star on github"
className={classes.link}
href="https://github.com/AlexGarrixen/Cool-Contrast"
rel="noopener"
target="_blank"
>
<GithubFill className={`${classes.leftIcon} ${classes.icon}`} />
<span className={classes.btnLabel}>Github</span>
</a>
</div>
</nav>
);
}

0 comments on commit 112817e

Please sign in to comment.