diff options
Diffstat (limited to 'src/components/organisms/layout')
| -rw-r--r-- | src/components/organisms/layout/header.module.scss | 50 | ||||
| -rw-r--r-- | src/components/organisms/layout/header.stories.tsx | 131 | ||||
| -rw-r--r-- | src/components/organisms/layout/header.test.tsx | 27 | ||||
| -rw-r--r-- | src/components/organisms/layout/header.tsx | 35 |
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; |
