aboutsummaryrefslogtreecommitdiffstats
path: root/src/components/organisms/layout
diff options
context:
space:
mode:
authorArmand Philippot <git@armandphilippot.com>2022-04-22 18:07:26 +0200
committerArmand Philippot <git@armandphilippot.com>2022-04-22 18:08:15 +0200
commit52c185d0f23504fc6410cf36285968eff9e7b21f (patch)
tree3077ac11f78e9062193e5fe64a2650f246788d71 /src/components/organisms/layout
parentcb6a54e54f2f013e06049b20388ca78e26201e16 (diff)
chore: add a Header component
Diffstat (limited to 'src/components/organisms/layout')
-rw-r--r--src/components/organisms/layout/header.module.scss50
-rw-r--r--src/components/organisms/layout/header.stories.tsx131
-rw-r--r--src/components/organisms/layout/header.test.tsx27
-rw-r--r--src/components/organisms/layout/header.tsx35
4 files changed, 243 insertions, 0 deletions
diff --git a/src/components/organisms/layout/header.module.scss b/src/components/organisms/layout/header.module.scss
new file mode 100644
index 0000000..7ae683f
--- /dev/null
+++ b/src/components/organisms/layout/header.module.scss
@@ -0,0 +1,50 @@
+@use "@styles/abstracts/functions" as fun;
+@use "@styles/abstracts/mixins" as mix;
+
+.wrapper {
+ display: grid;
+ grid-template-columns:
+ minmax(0, 1fr) min(calc(100vw - calc(var(--spacing-md) * 2)), 100ch)
+ minmax(0, 1fr);
+ align-items: center;
+ padding: var(--spacing-sm) 0 var(--spacing-md);
+
+ .toolbar {
+ justify-content: space-around;
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ z-index: 5;
+ background: var(--color-bg);
+ border-top: fun.convert-px(4) solid;
+ border-image: radial-gradient(
+ ellipse at top,
+ var(--color-primary-lighter) 20%,
+ var(--color-primary) 100%
+ )
+ 1;
+ box-shadow: 0 fun.convert-px(-2) fun.convert-px(3) fun.convert-px(-1)
+ var(--color-shadow-dark);
+
+ @include mix.media("screen") {
+ @include mix.dimensions("sm") {
+ justify-content: flex-end;
+ width: auto;
+ position: relative;
+ left: unset;
+ background: inherit;
+ border: none;
+ box-shadow: none;
+ }
+ }
+ }
+}
+
+.body {
+ grid-column: 2;
+ display: flex;
+ flex-flow: row wrap;
+ align-items: center;
+ justify-content: space-between;
+ gap: var(--spacing-md);
+}
diff --git a/src/components/organisms/layout/header.stories.tsx b/src/components/organisms/layout/header.stories.tsx
new file mode 100644
index 0000000..c58c344
--- /dev/null
+++ b/src/components/organisms/layout/header.stories.tsx
@@ -0,0 +1,131 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import { IntlProvider } from 'react-intl';
+import HeaderComponent from './header';
+
+/**
+ * Header - Storybook Meta
+ */
+export default {
+ title: 'Organisms/Layout',
+ component: HeaderComponent,
+ args: {
+ isHome: false,
+ withLink: false,
+ },
+ argTypes: {
+ baseline: {
+ control: {
+ type: 'text',
+ },
+ description: 'The branding baseline.',
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the header wrapper.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ isHome: {
+ control: {
+ type: 'boolean',
+ },
+ description: 'Determine if the current page is homepage or not.',
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'boolean',
+ required: false,
+ },
+ },
+ nav: {
+ description: 'The main navigation items.',
+ type: {
+ name: 'object',
+ required: true,
+ value: {},
+ },
+ },
+ photo: {
+ control: {
+ type: 'text',
+ },
+ description: 'The branding photo.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ title: {
+ control: {
+ type: 'text',
+ },
+ description: 'The website title.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ unoptimized: { table: { disable: true } },
+ withLink: {
+ control: {
+ type: 'boolean',
+ },
+ description: 'Wrap the website title with a link to homepage.',
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'boolean',
+ required: false,
+ },
+ },
+ },
+ decorators: [
+ (Story) => (
+ <IntlProvider locale="en">
+ <Story />
+ </IntlProvider>
+ ),
+ ],
+ parameters: {
+ layout: 'fullscreen',
+ },
+} as ComponentMeta<typeof HeaderComponent>;
+
+const Template: ComponentStory<typeof HeaderComponent> = (args) => (
+ <HeaderComponent {...args} />
+);
+
+const nav = [
+ { id: 'home-link', href: '#', label: 'Home' },
+ { id: 'blog-link', href: '#', label: 'Blog' },
+ { id: 'cv-link', href: '#', label: 'CV' },
+ { id: 'contact-link', href: '#', label: 'Contact' },
+];
+
+/**
+ * Layout Stories - Header
+ */
+export const Header = Template.bind({});
+Header.args = {
+ nav,
+ photo: 'http://placeimg.com/640/480/people',
+ title: 'Website title',
+ // @ts-ignore - Needed because of the placeholder image.
+ unoptimized: true,
+};
diff --git a/src/components/organisms/layout/header.test.tsx b/src/components/organisms/layout/header.test.tsx
new file mode 100644
index 0000000..05baaec
--- /dev/null
+++ b/src/components/organisms/layout/header.test.tsx
@@ -0,0 +1,27 @@
+import { render, screen } from '@test-utils';
+import Header from './header';
+
+const nav = [
+ { id: 'home-link', href: '#', label: 'Home' },
+ { id: 'blog-link', href: '#', label: 'Blog' },
+ { id: 'cv-link', href: '#', label: 'CV' },
+ { id: 'contact-link', href: '#', label: 'Contact' },
+];
+
+const photo = 'http://placeimg.com/640/480/nightlife';
+
+const title = 'Assumenda quis quod';
+
+describe('Header', () => {
+ it('renders the website title', () => {
+ render(<Header title={title} photo={photo} nav={nav} isHome={true} />);
+ expect(
+ screen.getByRole('heading', { level: 1, name: title })
+ ).toBeInTheDocument();
+ });
+
+ it('renders the main nav', () => {
+ render(<Header title={title} photo={photo} nav={nav} />);
+ expect(screen.getByRole('navigation')).toBeInTheDocument();
+ });
+});
diff --git a/src/components/organisms/layout/header.tsx b/src/components/organisms/layout/header.tsx
new file mode 100644
index 0000000..f6ebc9c
--- /dev/null
+++ b/src/components/organisms/layout/header.tsx
@@ -0,0 +1,35 @@
+import Branding, {
+ type BrandingProps,
+} from '@components/molecules/layout/branding';
+import { FC } from 'react';
+import Toolbar, { type ToolbarProps } from '../toolbar/toolbar';
+import styles from './header.module.scss';
+
+export type HeaderProps = BrandingProps & {
+ /**
+ * Set additional classnames to the header element.
+ */
+ className?: string;
+ /**
+ * The main nav items.
+ */
+ nav: ToolbarProps['nav'];
+};
+
+/**
+ * Header component
+ *
+ * Render the website header.
+ */
+const Header: FC<HeaderProps> = ({ className, nav, ...props }) => {
+ return (
+ <header className={`${styles.wrapper} ${className}`}>
+ <div className={styles.body}>
+ <Branding {...props} />
+ <Toolbar nav={nav} className={styles.toolbar} />
+ </div>
+ </header>
+ );
+};
+
+export default Header;