Created Layout & Responsive Header Component
This commit is contained in:
42
components/MobileNav.tsx
Normal file
42
components/MobileNav.tsx
Normal 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
30
components/button.tsx
Normal 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
6
components/footer.tsx
Normal 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
39
components/header.tsx
Normal 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>
|
||||
)
|
||||
19
components/modal/modal-portal.tsx
Normal file
19
components/modal/modal-portal.tsx
Normal 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
|
||||
}
|
||||
41
components/modal/modal.tsx
Normal file
41
components/modal/modal.tsx
Normal 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
|
||||
}
|
||||
50
components/nav-dropdown.tsx
Normal file
50
components/nav-dropdown.tsx
Normal 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
45
components/navbar.tsx
Normal 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
33
components/search-bar.tsx
Normal 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>
|
||||
)
|
||||
Reference in New Issue
Block a user