feat: implement news layout components and context for improved structure and functionality
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
import { type PropsWithChildren } from 'react'
|
||||
|
||||
import { PopupModal } from '~/components/popup/modal'
|
||||
import Banner from '~/components/ui/banner'
|
||||
import { FormLogin } from '~/components/ui/form-login'
|
||||
import { FormRegister } from '~/components/ui/form-register'
|
||||
import { useNewsContext } from '~/contexts/news'
|
||||
|
||||
import { FooterLinks } from './footer-links'
|
||||
import { FooterNewsletter } from './footer-newsletter'
|
||||
import { HeaderMenu } from './header-menu'
|
||||
import { HeaderTop } from './header-top'
|
||||
|
||||
export const NewsDefaultLayout = (properties: PropsWithChildren) => {
|
||||
const { children } = properties
|
||||
const { isLoginOpen, setIsLoginOpen, isRegisterOpen, setIsRegisterOpen } =
|
||||
useNewsContext()
|
||||
return (
|
||||
<main className="relative min-h-dvh bg-[#ECECEC]">
|
||||
<header>
|
||||
<HeaderTop />
|
||||
<HeaderMenu />
|
||||
</header>
|
||||
<div className="grid sm:mx-[50px] sm:my-[25px] sm:gap-y-[25px]">
|
||||
<Banner />
|
||||
{children}
|
||||
</div>
|
||||
|
||||
<footer className="grid w-full grid-cols-1 gap-6 bg-[#2E2F7C] px-5 py-20 text-white sm:grid-cols-5 sm:gap-16 sm:px-16">
|
||||
<FooterNewsletter />
|
||||
<FooterLinks />
|
||||
</footer>
|
||||
|
||||
<PopupModal
|
||||
isOpen={isLoginOpen}
|
||||
onClose={() => setIsLoginOpen(false)}
|
||||
description="Selamat Datang, silakan daftarkan akun Anda untuk melanjutkan!"
|
||||
>
|
||||
<FormLogin
|
||||
setIsRegisterOpen={setIsRegisterOpen}
|
||||
setIsLoginOpen={setIsLoginOpen}
|
||||
/>
|
||||
</PopupModal>
|
||||
|
||||
<PopupModal
|
||||
isOpen={isRegisterOpen}
|
||||
onClose={() => setIsRegisterOpen(false)}
|
||||
description="Selamat Datang, silakan daftarkan akun Anda untuk melanjutkan!"
|
||||
>
|
||||
<FormRegister />
|
||||
</PopupModal>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Link } from 'react-router'
|
||||
|
||||
import { APP } from '~/data/meta'
|
||||
|
||||
import { COPYRIGHT_MENU, FOOTER_MENU } from './menu'
|
||||
|
||||
export const FooterLinks = () => {
|
||||
return (
|
||||
<div className="col-span-1 flex flex-col justify-between sm:col-span-3">
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
||||
{FOOTER_MENU.map(({ group, items }, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex w-full flex-col gap-3"
|
||||
>
|
||||
<h3 className="text-xl font-semibold sm:text-2xl">{group}</h3>
|
||||
<div
|
||||
className={`${group === 'Follow Us' ? 'flex-col-2 flex w-full flex-wrap' : ''}`}
|
||||
>
|
||||
{items.map(({ url, icon: Icon, title }, subIndex) => (
|
||||
<Link
|
||||
key={subIndex}
|
||||
to={url}
|
||||
className="flex w-1/2 items-center gap-3 py-1 sm:w-full"
|
||||
>
|
||||
{Icon && (
|
||||
<Icon
|
||||
color="white"
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
)}
|
||||
{title}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-8 justify-between border-t border-white pt-8 text-center text-xs sm:flex sm:text-sm">
|
||||
<div className="flex gap-6">
|
||||
{COPYRIGHT_MENU.map(({ url, title }, index) => (
|
||||
<Link
|
||||
key={index}
|
||||
to={url}
|
||||
className={`w-full text-white underline`}
|
||||
>
|
||||
{title}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-2 sm:order-first">
|
||||
{new Date().getFullYear()} {APP.title}. All rights reserved.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { Link } from 'react-router'
|
||||
|
||||
import { Button } from '~/components/ui/button'
|
||||
import { APP } from '~/data/meta'
|
||||
|
||||
export const FooterNewsletter = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="col-span-2 hidden gap-y-6 sm:grid">
|
||||
<div className="h-[75px] bg-white p-3">
|
||||
<Link
|
||||
to="/news"
|
||||
className="h-full"
|
||||
>
|
||||
<img
|
||||
src={APP.logo}
|
||||
alt={APP.title}
|
||||
className="h-full w-auto"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<h2 className="text-4xl font-bold">Join Our Newsletter</h2>
|
||||
<p className="text-lg">
|
||||
Tidak ingin ketinggalan Berita Hukum terhangat? ingin mendapat
|
||||
informasi kajian dan networking terbaru? ikuti Newsletter kami and
|
||||
Stay up to Speed!
|
||||
</p>
|
||||
<form className="grid gap-5">
|
||||
<input
|
||||
placeholder="Daftarkan Email Disini"
|
||||
className="h-[50px] flex-1 bg-white text-center text-lg font-light text-black placeholder:text-[#777777] focus:ring-0 focus:outline-none"
|
||||
size={1}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="newsPrimaryOutline"
|
||||
size="block"
|
||||
>
|
||||
Subscribe
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
<div className="block sm:hidden">
|
||||
<div className="h-[60px] bg-white p-3">
|
||||
<Link
|
||||
to="/news"
|
||||
className="h-full"
|
||||
>
|
||||
<img
|
||||
src={APP.logo}
|
||||
alt={APP.title}
|
||||
className="h-full w-auto"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import { useState } from 'react'
|
||||
import { Link } from 'react-router'
|
||||
|
||||
import { CloseIcon } from '~/components/icons/close'
|
||||
import { MenuIcon } from '~/components/icons/menu'
|
||||
import { useNewsContext } from '~/contexts/news'
|
||||
import { HeaderSearch } from '~/layouts/news/header-search'
|
||||
|
||||
import { MENU } from './menu'
|
||||
|
||||
export default function HeaderMenuMobile() {
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false)
|
||||
const { setIsLoginOpen } = useNewsContext()
|
||||
|
||||
const handleToggleMenu = (): void => {
|
||||
setIsMenuOpen(!isMenuOpen)
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div className="relative z-20 flex min-h-[65px] bg-[#2E2F7C] font-[sans-serif] tracking-wide text-white sm:hidden sm:px-10">
|
||||
<div className="mx-auto flex w-full max-w-screen-xl flex-wrap items-center gap-4 align-middle">
|
||||
{/* Menu */}
|
||||
<div
|
||||
className={`z-20 transition-transform duration-300 max-lg:fixed max-lg:top-0 max-lg:left-0 max-lg:h-full max-lg:w-full max-lg:overflow-auto max-lg:bg-[#2E2F7C] max-lg:p-6 max-lg:shadow-md ${
|
||||
isMenuOpen ? 'translate-x-0' : '-translate-x-full'
|
||||
}`}
|
||||
>
|
||||
{/* Tombol Close */}
|
||||
<button
|
||||
onClick={handleToggleMenu}
|
||||
className="fixed top-5 right-5 z-20 flex h-9 w-9 items-center justify-center lg:hidden"
|
||||
>
|
||||
<CloseIcon
|
||||
width={50}
|
||||
height={50}
|
||||
/>
|
||||
</button>
|
||||
|
||||
{/* List Menu */}
|
||||
<ul className="mx-10 mt-10 max-lg:space-y-3 lg:ml-14 lg:flex lg:gap-x-5">
|
||||
{MENU.map((item, index) => (
|
||||
<li
|
||||
key={index}
|
||||
className="px-3 max-lg:border-b max-lg:py-3"
|
||||
>
|
||||
<Link
|
||||
key={item.title}
|
||||
to={item.url}
|
||||
className={
|
||||
'flex h-full items-center justify-center border-white px-[35px] sm:border-r'
|
||||
}
|
||||
>
|
||||
{item.title}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
|
||||
<button
|
||||
className="w-full bg-white px-[35px] py-3 text-center text-[#2E2F7C] sm:hidden"
|
||||
onClick={() => {
|
||||
setIsLoginOpen(true)
|
||||
setIsMenuOpen(false)
|
||||
}}
|
||||
>
|
||||
Akun
|
||||
</button>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Search dan Toggle Button */}
|
||||
<div className="align-center flex w-full justify-center">
|
||||
<button
|
||||
onClick={handleToggleMenu}
|
||||
className="h-[63px] border border-white px-4 lg:hidden"
|
||||
>
|
||||
<MenuIcon
|
||||
width={25}
|
||||
height={25}
|
||||
/>
|
||||
</button>
|
||||
<div className="w-full py-3">
|
||||
<HeaderSearch />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Link } from 'react-router'
|
||||
|
||||
import HeaderMenuMobile from '~/layouts/news/header-menu-mobile'
|
||||
|
||||
import { HeaderSearch } from './header-search'
|
||||
import { MENU } from './menu'
|
||||
|
||||
export const HeaderMenu = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="hidden h-[60px] items-center justify-between bg-[#2E2F7C] text-xl font-medium text-white sm:flex">
|
||||
{MENU.map((item) => (
|
||||
<Link
|
||||
key={item.title}
|
||||
to={item.url}
|
||||
className={
|
||||
'flex h-full items-center justify-center border-r border-white px-[35px]'
|
||||
}
|
||||
>
|
||||
{item.title}
|
||||
</Link>
|
||||
))}
|
||||
<HeaderSearch />
|
||||
</div>
|
||||
<HeaderMenuMobile />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { SearchIcon } from '~/components/icons/search'
|
||||
import { Button } from '~/components/ui/button'
|
||||
|
||||
export const HeaderSearch = () => {
|
||||
return (
|
||||
<form className="flex flex-1 justify-between gap-[15px] px-[35px]">
|
||||
<input
|
||||
placeholder="Cari..."
|
||||
className="flex-1 text-xl placeholder:text-white focus:ring-0 focus:outline-none sm:text-2xl"
|
||||
size={1}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="icon"
|
||||
size="icon"
|
||||
className="[&_svg]:size-[20px] sm:[&_svg]:size-[30px]"
|
||||
>
|
||||
<SearchIcon />
|
||||
</Button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { Link } from 'react-router'
|
||||
|
||||
import { Button } from '~/components/ui/button'
|
||||
import { useNewsContext } from '~/contexts/news'
|
||||
import { APP } from '~/data/meta'
|
||||
|
||||
export const HeaderTop = () => {
|
||||
const { setIsLoginOpen } = useNewsContext()
|
||||
return (
|
||||
<>
|
||||
<div className="flex h-[60px] items-center justify-between bg-white px-5 align-middle sm:h-[100px] sm:gap-[15px] sm:px-[50px] sm:py-[20px]">
|
||||
<Link
|
||||
to="/news"
|
||||
className="mt-2 h-full py-2"
|
||||
>
|
||||
<img
|
||||
src={APP.logo}
|
||||
alt={APP.title}
|
||||
className="h-3/4 w-auto sm:h-full"
|
||||
/>
|
||||
</Link>
|
||||
<div className="hidden h-full items-center py-1.5 font-light whitespace-pre-line sm:flex">
|
||||
{APP.description}
|
||||
</div>
|
||||
<div className="flex items-center gap-[15px]">
|
||||
<Button className="h-8 w-auto rounded-none px-3 text-xs sm:h-[50px] sm:w-[150px] sm:text-lg">
|
||||
About Us
|
||||
</Button>
|
||||
<Button
|
||||
variant="newsSecondary"
|
||||
className="hidden sm:block"
|
||||
onClick={() => setIsLoginOpen(true)}
|
||||
>
|
||||
Akun
|
||||
</Button>
|
||||
<div className="w-[50px] sm:w-[60px]">
|
||||
<img
|
||||
alt="language"
|
||||
src="/flags/id.svg"
|
||||
className="shadow-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
import type { JSX } from 'react'
|
||||
|
||||
import { FacebookIcon } from '~/components/icons/facebook'
|
||||
import { InstagramIcon } from '~/components/icons/instagram'
|
||||
import { LinkedinIcon } from '~/components/icons/linkedin'
|
||||
import { XIcon } from '~/components/icons/x'
|
||||
|
||||
export const MENU = [
|
||||
{
|
||||
title: 'Spotlight',
|
||||
url: '/news/category/spotlight',
|
||||
},
|
||||
{
|
||||
title: 'Berita',
|
||||
url: '/news/category/berita',
|
||||
},
|
||||
{
|
||||
title: 'Kasus',
|
||||
url: '/news/category/kasus',
|
||||
},
|
||||
{
|
||||
title: 'Kajian',
|
||||
url: '/news/category/kajian',
|
||||
},
|
||||
{
|
||||
title: 'Lifestyle',
|
||||
url: '/news/category/lifestyle',
|
||||
},
|
||||
{
|
||||
title: 'Event',
|
||||
url: '/news/category/event',
|
||||
},
|
||||
{
|
||||
title: 'Travel',
|
||||
url: '/news/category/travel',
|
||||
},
|
||||
]
|
||||
|
||||
type FooterMenu = {
|
||||
group: string
|
||||
items: Array<{
|
||||
title: string
|
||||
url: string
|
||||
icon?: (
|
||||
properties: JSX.IntrinsicAttributes & React.SVGProps<SVGSVGElement>,
|
||||
) => JSX.Element
|
||||
}>
|
||||
}
|
||||
export const FOOTER_MENU: FooterMenu[] = [
|
||||
{
|
||||
group: 'About Us',
|
||||
items: [
|
||||
{
|
||||
title: 'Popular',
|
||||
url: '/news/list?sort=popular',
|
||||
},
|
||||
{
|
||||
title: 'Trending',
|
||||
url: '/news/list?sort=trending',
|
||||
},
|
||||
{
|
||||
title: 'Contact',
|
||||
url: '/news/contact',
|
||||
},
|
||||
{
|
||||
title: 'Support/Help',
|
||||
url: '/news/support',
|
||||
},
|
||||
{
|
||||
title: 'Rquest Topic',
|
||||
url: '/news/request-topic',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
group: 'Column Two',
|
||||
items: [
|
||||
{
|
||||
title: 'FAQs',
|
||||
url: '/news/faq',
|
||||
},
|
||||
{
|
||||
title: 'Terms and Condition',
|
||||
url: '/news/terms-condition',
|
||||
},
|
||||
{
|
||||
title: 'Support',
|
||||
url: '/news/support',
|
||||
},
|
||||
{
|
||||
title: 'Link Nine',
|
||||
url: '/news',
|
||||
},
|
||||
{
|
||||
title: 'Link Ten',
|
||||
url: '/news',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
group: 'Follow Us',
|
||||
items: [
|
||||
{
|
||||
title: 'Facebook',
|
||||
icon: FacebookIcon,
|
||||
url: 'https://facebook.com',
|
||||
},
|
||||
{
|
||||
title: 'Instagram',
|
||||
icon: InstagramIcon,
|
||||
url: 'https://instagram.com',
|
||||
},
|
||||
{
|
||||
title: 'X',
|
||||
icon: XIcon,
|
||||
url: 'https://x.com',
|
||||
},
|
||||
{
|
||||
title: 'Linkedin',
|
||||
icon: LinkedinIcon,
|
||||
url: 'https://linkedin.com',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
export const COPYRIGHT_MENU = [
|
||||
{
|
||||
title: 'Privacy Policy',
|
||||
url: '/news/privacy-policy',
|
||||
},
|
||||
{
|
||||
title: 'Terms of Service',
|
||||
url: '/news/terms-of-service',
|
||||
},
|
||||
{
|
||||
title: 'Cookies Settings',
|
||||
url: '/news/cookies-settings',
|
||||
},
|
||||
]
|
||||
Reference in New Issue
Block a user