feat: implement news layout components and context for improved structure and functionality

This commit is contained in:
Ardeman
2025-02-22 17:34:25 +08:00
parent d9c36c5ff0
commit 77c9843267
11 changed files with 110 additions and 49 deletions
+54
View File
@@ -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>
)
}
+58
View File
@@ -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>
)
}
+60
View File
@@ -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>
</>
)
}
+89
View File
@@ -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>
</>
)
}
+28
View File
@@ -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 />
</>
)
}
+22
View File
@@ -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>
)
}
+47
View File
@@ -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>
</>
)
}
+140
View File
@@ -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',
},
]