Created Layout & Responsive Header Component

This commit is contained in:
aftabrehan
2024-02-01 20:28:42 +05:00
parent 9ac97c7f9a
commit bc7cad7e3b
21 changed files with 413 additions and 30 deletions

42
components/MobileNav.tsx Normal file
View File

@@ -0,0 +1,42 @@
'use client'
import { useState } from 'react'
import Image from 'next/image'
import { Modal } from '@/components//modal/modal'
import { NavDropdown } from '@/components/nav-dropdown'
import { ALL_NAV_ITEMS } from '@/constants/nav-items'
export const MobileNav = () => {
const [isOpen, setIsOpen] = useState(false)
return (
<>
<button
className="w-12 h-12 flex items-center justify-center rounded-md"
onClick={() => setIsOpen(!isOpen)}
style={{ background: isOpen ? '#000' : '#f4f4f5' }}
>
<Image
alt=""
src={isOpen ? '/cross.svg' : '/hamburger.svg'}
width={24}
height={24}
/>
</button>
<Modal isOpen={isOpen} close={() => setIsOpen(false)}>
<div className="w-full h-[calc(100vh-144px)] mt-36 flex sm:hidden bg-white pointer-events-auto border-t border-zinc-100 border animation-slide-from-right">
<nav className="w-full">
<ul className="w-full flex flex-col items-end justify-center gap-2 p-4">
{ALL_NAV_ITEMS.map((navItem, i) => (
<NavDropdown key={i} {...navItem} />
))}
</ul>
</nav>
</div>
</Modal>
</>
)
}

30
components/button.tsx Normal file
View File

@@ -0,0 +1,30 @@
import clsx from 'clsx'
interface buttonProps {
label: string
onClick?: () => void
variant?: 'primary' | 'secondary'
}
export const Button = ({
label,
onClick,
variant = 'primary',
}: buttonProps) => {
const cls = {
primary: 'bg-primary text-white',
secondary: 'text-black hover:bg-zinc-100',
}[variant]
return (
<button
onClick={onClick}
className={clsx(
'h-12 px-5 flex items-center justify-center text-base font-medium text-nowrap rounded-full hover:opacity-75 transition-all duration-200',
cls
)}
>
{label}
</button>
)
}

6
components/footer.tsx Normal file
View File

@@ -0,0 +1,6 @@
export const Footer = () => (
// TODO: remove dummy height min-h-[600px]
<footer className="w-full min-h-[600px] bg-gray-100">
<div className="w-full max-w-7xl mx-auto">Footer</div>
</footer>
)

39
components/header.tsx Normal file
View File

@@ -0,0 +1,39 @@
import Link from 'next/link'
import Image from 'next/image'
import { Searchbar } from '@/components/search-bar'
import { Button } from '@/components/button'
import { Navbar } from '@/components/navbar'
import { MobileNav } from '@/components/MobileNav'
export const Header = () => (
<header className="w-full h-36 fixed top-0 shadow-header px-4 pt-6 bg-white">
<div className="w-full max-w-7xl mx-auto flex flex-col items-center justify-center gap-3 sm:gap-4 md:gap-6">
<div className="w-full flex items-center justify-between">
<Link href="/" className="overflow-hidden">
<Image
src="/logo.svg"
alt="ZinTools Logo"
width={138}
height={38}
className="w-[120px] sm:w-[130px] md:w-[138px] h-auto"
/>
</Link>
<Searchbar className="hidden sm:flex" />
<div className="flex items-center justify-center gap-2">
<Button label="Login" variant="secondary" />
<Button label="Sign up" />
</div>
</div>
<Navbar className="hidden sm:block" />
<div className="w-full flex sm:hidden gap-x-2">
<Searchbar />
<MobileNav />
</div>
</div>
</header>
)

View File

@@ -0,0 +1,19 @@
import { useRef, useEffect, useState } from 'react'
import { createPortal } from 'react-dom'
interface modalPortal {
children: React.ReactNode
selector?: string
}
export const ModalPortal = ({ children, selector = 'modal' }: modalPortal) => {
const ref = useRef<HTMLDivElement | null>(null)
const [mounted, setMounted] = useState(false)
useEffect(() => {
ref.current = document.querySelector(selector)
setMounted(true)
}, [selector])
return mounted && ref.current ? createPortal(children, ref.current) : null
}

View File

@@ -0,0 +1,41 @@
import { useEffect, useRef } from 'react'
import clsx from 'clsx'
import { ModalPortal } from '@/components/modal/modal-portal'
import { useClickOutside } from '@/hooks/useClickOutside'
interface modalProps {
isOpen: boolean
close: () => void
children: React.ReactNode
closeOnClickAway?: boolean
className?: string
}
export const Modal = ({
isOpen,
close,
children,
closeOnClickAway = false,
className,
}: modalProps) => {
const contentRef = useRef(null)
useClickOutside({
onClick: () => closeOnClickAway && close(),
ref: contentRef,
})
useEffect(() => {
if (!isOpen) return
}, [isOpen])
return isOpen ? (
<ModalPortal selector="#modal">
<div ref={contentRef} className={clsx('w-full h-full', className)}>
{children}
</div>
</ModalPortal>
) : null
}

View File

@@ -0,0 +1,50 @@
import Link from 'next/link'
import clsx from 'clsx'
interface navDropdownProps {
title: string
link: string
options: Array<{ label: string; link: string }>
position?: 'left' | 'right' | 'center'
}
export const NavDropdown = ({
title,
link,
options,
position = 'center',
}: navDropdownProps) => {
const positionCls = {
left: 'left-0',
right: 'right-0',
center: '',
}[position]
return (
<li className="group relative flex items-center justify-center">
<Link
href={link}
className="relative text-base sm:text-sm font-medium whitespace-nowrap px-3 py-3.5 after:absolute after:bottom-0 after:left-0 after:content-none::after after:w-full after:h-[2px] after:bg-primary after:hidden group-hover:after:block"
>
{title}
</Link>
<div
className={clsx(
'hidden sm:group-hover:flex absolute top-12 flex-col min-w-[220px] gap-2 p-4 bg-white rounded-b-md shadow-md border border-zinc-1',
positionCls
)}
>
{options.map(({ label, link: opLink }, i) => (
<Link
key={i}
href={opLink}
className="w-full p-2 hover:bg-zinc-100 rounded-md transition-colors duration-200 text-sm"
>
{label}
</Link>
))}
</div>
</li>
)
}

45
components/navbar.tsx Normal file
View File

@@ -0,0 +1,45 @@
import clsx from 'clsx'
import { NavDropdown } from '@/components/nav-dropdown'
import { ALL_NAV_ITEMS, LESS_NAV_ITEMS } from '@/constants/nav-items'
interface navbarProps {
className?: string
}
export const Navbar = ({ className }: navbarProps) => (
<nav className={clsx('w-full max-w-6xl', className)}>
<ul className="w-full hidden lg:flex items-center justify-between gap-1">
{ALL_NAV_ITEMS.map((navItem, i) => (
<NavDropdown
key={i}
position={
i === 0
? 'left'
: i === ALL_NAV_ITEMS.length - 1
? 'right'
: 'center'
}
{...navItem}
/>
))}
</ul>
<ul className="w-full flex lg:hidden items-center justify-between gap-1">
{LESS_NAV_ITEMS.map((navItem, i) => (
<NavDropdown
key={i}
position={
i === 0
? 'left'
: i === LESS_NAV_ITEMS.length - 1
? 'right'
: 'center'
}
{...navItem}
/>
))}
</ul>
</nav>
)

33
components/search-bar.tsx Normal file
View File

@@ -0,0 +1,33 @@
import Image from 'next/image'
import clsx from 'clsx'
interface searchbarProps {
className?: string
}
export const Searchbar = ({ className }: searchbarProps) => (
<div
className={clsx(
'flex-1 sm:max-w-[280px] md:max-w-[400px] lg:max-w-[500px] flex items-center justify-between gap-x-4 md:gap-x-6 px-4 sm:px-6 py-1.5 bg-zinc-100 rounded-3xl',
className
)}
>
<input
type="search"
placeholder="Search for Movies, TV Shows, Themes & Cast"
className="flex flex-1 outline-none bg-transparent text-black text-base"
/>
<div className="flex items-center justify-end gap-x-2 md:gap-x-6">
<div className="w-[1px] h-9 bg-gray-400/50 cursor-default" />
<div className="w-8 h-8 flex items-center justify-center hover:bg-gray-200 rounded-full cursor-pointer">
<Image
alt=""
src="/search.svg"
width={16}
height={16}
className="select-none"
/>
</div>
</div>
</div>
)