Merge pull request #1 from ardeman/feature/slicing

style: slicing home page, adjust for mobile view
This commit is contained in:
Ardeman 2025-02-20 07:02:53 +08:00 committed by GitHub
commit cb5255111c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 395 additions and 141 deletions

View File

@ -0,0 +1,25 @@
import { Link } from 'react-router'
import { APP } from '~/data/meta'
export const Banner = () => {
return (
<div className="min-h-[65px] bg-amber-400 sm:mx-10">
<div className="relative">
<Link
to="/#"
className="mt-2 h-full py-2"
>
<img
src={'https://placehold.co/1200x70.png'}
alt={APP.title}
className="h-[70px] w-auto sm:h-full"
/>
</Link>
<p className="absolute top-2 mx-10">
Lorem ipsum dolor sit, amet consectetur
</p>
</div>
</div>
)
}

View File

@ -1,10 +1,5 @@
import { cva, type VariantProps } from 'class-variance-authority'
import type {
ButtonHTMLAttributes,
HTMLAttributes,
MouseEventHandler,
ReactNode,
} from 'react'
import type { ReactNode, ElementType, ComponentPropsWithoutRef } from 'react'
import { twMerge } from 'tailwind-merge'
const buttonVariants = cva(
@ -16,21 +11,13 @@ const buttonVariants = cva(
newsPrimaryOutline: 'border-[3px] border-white text-white text-lg',
newsSecondary: 'border-[3px] border-[#2E2F7C] text-[#2E2F7C] text-lg',
icon: '',
// destructive:
// 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
// outline:
// 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
// secondary:
// 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
// ghost: 'hover:bg-accent hover:text-accent-foreground',
// link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: 'h-[50px] w-[150px]',
block: 'h-[50px] w-full',
icon: 'h-9 w-9',
// sm: 'h-8 rounded-md px-3 text-xs',
// lg: 'h-10 rounded-md px-8',
sm: 'h-8 rounded-md px-3 text-xs',
lg: 'h-10 rounded-md px-8',
},
},
defaultVariants: {
@ -40,34 +27,38 @@ const buttonVariants = cva(
},
)
type TProperties = {
type?: ButtonHTMLAttributes<HTMLButtonElement>['type']
onClick?: MouseEventHandler<HTMLButtonElement>
className?: HTMLAttributes<HTMLButtonElement>['className']
disabled?: boolean
type ButtonBaseProperties = {
children: ReactNode
variant?: VariantProps<typeof buttonVariants>['variant']
size?: VariantProps<typeof buttonVariants>['size']
className?: string
}
export const Button = (properties: TProperties) => {
const {
type = 'button',
onClick,
className,
disabled,
type PolymorphicReference<C extends ElementType> =
ComponentPropsWithoutRef<C>['ref']
type ButtonProperties<C extends ElementType> = ButtonBaseProperties & {
as?: C
ref?: PolymorphicReference<C>
} & Omit<ComponentPropsWithoutRef<C>, keyof ButtonBaseProperties>
export const Button = <C extends ElementType = 'button'>({
as,
children,
variant,
size,
} = properties
className,
...properties
}: ButtonProperties<C>) => {
const Component = as || 'button'
const classes = twMerge(buttonVariants({ variant, size, className }))
return (
<button
type={type}
onClick={onClick}
className={twMerge(buttonVariants({ variant, size, className }))}
disabled={disabled}
<Component
className={classes}
{...properties}
>
{children}
</button>
</Component>
)
}

View File

@ -1,3 +1,4 @@
import { Link } from 'react-router'
import { twMerge } from 'tailwind-merge'
import { CarouselNextIcon } from '~/components/icons/carousel-next'
@ -9,37 +10,44 @@ import { Button } from './button'
export const Carousel = (properties: TNews) => {
const { title, description, items, type } = properties
return (
<div>
<div className="mb-[30px] flex items-center justify-between border-b border-black pb-[30px]">
<div className="">
<div className="mt-3 mb-3 flex items-center justify-between border-b border-black pb-3 sm:mb-[30px] sm:pb-[30px]">
<div className="grid">
<h2 className="text-4xl font-extrabold text-[#2E2F7C]">{title}</h2>
<p className="text-2xl font-light text-[#777777] italic">
<h2 className="text-2xl font-extrabold text-[#2E2F7C] sm:text-4xl">
{title}
</h2>
<p className="text-xl font-light text-[#777777] italic sm:text-2xl">
{description}
</p>
</div>
<div className="flex gap-2.5">
<CarouselPreviousIcon
color="#DCDCDC"
className="cursor-pointer"
width={45}
height={45}
/>
<CarouselNextIcon
color="#2E2F7C"
className="cursor-pointer"
width={45}
height={45}
/>
</div>
</div>
<div
className={twMerge(
'grid gap-x-8',
type === 'hero' ? 'grid-cols-1' : 'grid-cols-3',
'grid sm:grid sm:gap-x-8',
type === 'hero' ? 'grid-cols-1' : 'sm:grid-cols-3',
)}
>
{items.map(({ image, title, content }, index) => (
{items.map(({ featured, title, content, tag, slug }, index) => (
<div
key={index}
className={twMerge(
'grid gap-x-8',
type === 'hero' ? 'grid-cols-3' : '',
'grid sm:gap-x-8',
type === 'hero' ? 'grid-cols-1 sm:grid-cols-3' : '',
)}
>
<img
@ -47,9 +55,9 @@ export const Carousel = (properties: TNews) => {
'w-full object-cover',
type === 'hero'
? 'col-span-2 aspect-[174/100]'
: 'aspect-[5/4]',
: 'aspect-[5/4] rounded-md',
)}
src={image}
src={featured}
alt={title}
/>
<div
@ -58,18 +66,36 @@ export const Carousel = (properties: TNews) => {
type === 'hero' ? 'gap-7' : 'gap-4',
)}
>
<div className={`${type === 'hero' ? 'hidden' : ''} `}>
{tag?.map((item) => (
<span className="my-3 mr-2 inline-block rounded bg-gray-200 px-3 py-1">
{item}
</span>
))}
</div>
<div>
<h3
className={twMerge(
'font-bold',
type === 'hero' ? 'text-4xl' : 'text-2xl',
'mt-2 w-full font-bold lg:mt-0',
type === 'hero'
? 'text-2xl sm:text-4xl'
: 'text-xl sm:text-2xl',
)}
>
{title}
</h3>
<p className="text-xl text-[#777777]">{content}</p>
<p className="text-md mt-5 text-[#777777] sm:text-xl">
{content}
</p>
</div>
<Button size="block">View More</Button>
<Button
size="block"
as={Link}
to={slug}
>
View More
</Button>
</div>
</div>
))}

View File

@ -0,0 +1,44 @@
import { Button } from '~/components/ui/button'
import { APP } from '~/data/meta'
export const Newsletter = () => {
return (
<>
<div className="relative col-span-2 my-5 grid max-h-[400px] gap-y-6 bg-[#2E2F7C] p-5 px-10 text-white sm:grid-cols-2 sm:px-10">
<div className="grid-1">
<h2 className="text-2xl font-bold sm:text-4xl">
Join Our Newsletter
</h2>
<p className="text:md sm:text-lg">
Tidak ingin ketinggalan Berita Hukum terhangat? ingin mendapat
informasi kajian dan networking terbaru? ikuti Newsletter kami and
Stay up to Speed!
</p>
</div>
<div className="w-max-h-[400px] absolute right-0 bottom-0 w-auto sm:top-0">
<img
src={'https://placehold.co/800x200.png'}
alt={APP.title}
className="h-full w-auto"
/>
</div>
<div className="z-10">
<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="newsPrimary"
size="block"
>
Subscribe
</Button>
</form>
</div>
</div>
</>
)
}

View File

@ -6,20 +6,22 @@ import { COPYRIGHT_MENU, FOOTER_MENU } from './menu'
export const FooterLinks = () => {
return (
<div className="col-span-3 flex flex-col justify-between">
<div className="grid grid-cols-3 gap-4">
<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 flex-col gap-3"
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' : ''}`}
>
<h3 className="text-2xl font-semibold">{group}</h3>
<div>
{items.map(({ url, icon: Icon, title }, subIndex) => (
<Link
key={subIndex}
to={url}
className="flex items-center gap-3 py-2"
className="flex w-1/2 items-center gap-3 py-1 sm:w-full"
>
{Icon && (
<Icon
@ -35,21 +37,21 @@ export const FooterLinks = () => {
</div>
))}
</div>
<div className="flex justify-between border-t border-white pt-8 text-sm">
<div>
{new Date().getFullYear()} {APP.title}. All rights reserved.
</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="text-white underline"
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>
)

View File

@ -5,7 +5,8 @@ import { APP } from '~/data/meta'
export const FooterNewsletter = () => {
return (
<div className="col-span-2 grid gap-y-6">
<>
<div className="col-span-2 hidden gap-y-6 sm:grid">
<div className="h-[75px] bg-white p-3">
<Link
to="/news"
@ -18,11 +19,12 @@ export const FooterNewsletter = () => {
/>
</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!
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
@ -39,5 +41,20 @@ export const FooterNewsletter = () => {
</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>
</>
)
}

View File

@ -0,0 +1,88 @@
import { useState } from 'react'
import { Link } from 'react-router'
import { HeaderSearch } from '~/layouts/header-search'
import { MENU } from './menu'
export default function HeaderMenuMobile() {
const [isMenuOpen, setIsMenuOpen] = useState(false)
const handleToggleMenu = (): void => {
setIsMenuOpen(!isMenuOpen)
}
return (
<>
<div className="relative z-50 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-50 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-2 right-4 z-[100] flex h-9 w-9 items-center justify-center lg:hidden"
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-3.5 w-3.5 fill-white"
viewBox="0 0 320.591 320.591"
>
<path d="M30.391 318.583a30.37 30.37 0 0 1-21.56-7.288c-11.774-11.844-11.774-30.973 0-42.817L266.643 10.665c12.246-11.459 31.462-10.822 42.921 1.424 10.362 11.074 10.966 28.095 1.414 39.875L51.647 311.295a30.366 30.366 0 0 1-21.256 7.288z" />
<path d="M287.9 318.583a30.37 30.37 0 0 1-21.257-8.806L8.83 51.963C-2.078 39.225-.595 20.055 12.143 9.146c11.369-9.736 28.136-9.736 39.504 0l259.331 257.813c12.243 11.462 12.876 30.679 1.414 42.922-.456.487-.927.958-1.414 1.414a30.368 30.368 0 0 1-23.078 7.288z" />
</svg>
</button>
{/* List Menu */}
<ul className="mx-10 max-lg:space-y-3 lg:ml-14 lg:flex lg:gap-x-5">
{MENU.map((item) => (
<li 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">
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"
>
<svg
className="h-7 w-7"
fill="#fff"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 15a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z"
clipRule="evenodd"
/>
</svg>
</button>
<div className="w-full py-3">
<HeaderSearch></HeaderSearch>
</div>
</div>
</div>
</div>
</>
)
}

View File

@ -1,11 +1,14 @@
import { Link } from 'react-router'
import HeaderMenuMobile from '~/layouts/header-menu-mobile'
import { HeaderSearch } from './header-search'
import { MENU } from './menu'
export const HeaderMenu = () => {
return (
<div className="flex h-[60px] items-center justify-between bg-[#2E2F7C] text-xl font-medium text-white">
<>
<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}
@ -19,5 +22,7 @@ export const HeaderMenu = () => {
))}
<HeaderSearch />
</div>
<HeaderMenuMobile />
</>
)
}

View File

@ -6,14 +6,14 @@ export const HeaderSearch = () => {
<form className="flex flex-1 justify-between gap-[15px] px-[35px]">
<input
placeholder="Cari..."
className="flex-1 placeholder:text-white focus:ring-0 focus:outline-none"
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-[30px]"
className="[&_svg]:size-[20px] sm:[&_svg]:size-[30px]"
>
<SearchIcon />
</Button>

View File

@ -5,24 +5,31 @@ import { APP } from '~/data/meta'
export const HeaderTop = () => {
return (
<div className="flex h-[100px] items-center justify-between gap-[15px] bg-white px-[50px] py-[20px]">
<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="h-full py-[5px]"
className="mt-2 h-full py-2"
>
<img
src={APP.logo}
alt={APP.title}
className="h-full w-auto"
className="h-3/4 w-auto sm:h-full"
/>
</Link>
<div className="flex h-full items-center py-1.5 font-light whitespace-pre-line">
<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>About Us</Button>
<Button variant="newsSecondary">Akun</Button>
<div className="w-[60px]">
<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"
>
Akun
</Button>
<div className="w-[50px] sm:w-[60px]">
<img
alt="language"
src="/flags/id.svg"

View File

@ -0,0 +1,11 @@
import type { TNewsDetail } from '~/types/news'
export const CONTENT: TNewsDetail = {
title: 'Hotman Paris Membuka Perpustakaan di tengah Diskotik',
content:
'Pengacara Kondang, Hotman Paris Hutapea, membuka sebuah perpustakaan baru di dalam diskotik nya yang berlokasi di daerah Jakarta Pusat, Hotman berkata Perpustakaan ini dibuka dengan harapan untuk meningkatkan gairah membaca masyarakat Indonesia, namun sayangnya..',
featured: '/images/news-1.jpg',
slug: 'hotman-paris-membuka-perpustakaan-di-tengah-diskotik',
author: 'John Doe',
date: new Date(),
}

View File

@ -0,0 +1,17 @@
import { Card } from '~/components/ui/card'
import { CONTENT } from './data'
export const NewsDetailPage = () => {
const { title } = CONTENT
return (
<div className="relative">
<Card>
<h2 className="text-2xl font-extrabold text-[#2E2F7C] sm:text-4xl">
{title}
</h2>
News Detail
</Card>
</div>
)
}

View File

@ -9,7 +9,8 @@ export const SPOTLIGHT: TNews = {
title: 'Hotman Paris Membuka Perpustakaan di tengah Diskotik',
content:
'Pengacara Kondang, Hotman Paris Hutapea, membuka sebuah perpustakaan baru di dalam diskotik nya yang berlokasi di daerah Jakarta Pusat, Hotman berkata Perpustakaan ini dibuka dengan harapan untuk meningkatkan gairah membaca masyarakat Indonesia, namun sayangnya..',
image: '/images/news-1.jpg',
featured: '/images/news-1.jpg',
slug: 'hotman-paris-membuka-perpustakaan-di-tengah-diskotik',
},
],
}
@ -23,19 +24,25 @@ export const BERITA: TNews = {
title: 'Travelling as a way of self-discovery and progress',
content:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros.',
image: '/images/news-2.jpg',
featured: '/images/news-2.jpg',
tag: ['Hukum Property'],
slug: 'travelling-as-a-way-of-self-discovery-and-progress',
},
{
title: 'How does writing influence your personal brand?',
content:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros.',
image: '/images/news-3.jpg',
featured: '/images/news-3.jpg',
tag: ['Hukum'],
slug: 'how-does-writing-influence-your-personal-brand',
},
{
title: 'Helping a local business reinvent itself',
content:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros.',
image: '/images/news-4.jpg',
featured: '/images/news-4.jpg',
tag: ['Hukum Property', 'PREMIUM CONTENT'],
slug: 'helping-a-local-business-reinvent-itself',
},
],
}
@ -49,19 +56,25 @@ export const KAJIAN: TNews = {
title: 'Travelling as a way of self-discovery and progress',
content:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros.',
image: '/images/news-2.jpg',
featured: '/images/news-2.jpg',
tag: ['Hukum Property', 'PREMIUM CONTENT'],
slug: 'travelling-as-a-way-of-self-discovery-and-progress',
},
{
title: 'How does writing influence your personal brand?',
content:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros.',
image: '/images/news-3.jpg',
featured: '/images/news-3.jpg',
tag: ['Hukum Property', 'PREMIUM CONTENT'],
slug: 'how-does-writing-influence-your-personal-brand',
},
{
title: 'Helping a local business reinvent itself',
content:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros.',
image: '/images/news-4.jpg',
featured: '/images/news-4.jpg',
tag: ['Hukum Property', 'PREMIUM CONTENT'],
slug: 'helping-a-local-business-reinvent-itself',
},
],
}

View File

@ -1,8 +1,8 @@
import { Card } from '~/components/ui/card'
import { Carousel } from '~/components/ui/carousel'
import { Newsletter } from '~/components/ui/newsletter'
import { BERITA, KAJIAN, SPOTLIGHT } from './data'
import { Newsletter } from './newsletter'
export const NewsPage = () => {
return (

View File

@ -1,3 +0,0 @@
export const Newsletter = () => {
return <div>Newsletter</div>
}

View File

@ -0,0 +1,7 @@
import { NewsDetailPage } from '~/pages/news-detail'
const NewsDetailLayout = () => {
return <NewsDetailPage />
}
export default NewsDetailLayout

View File

@ -1,5 +1,6 @@
import { Outlet } from 'react-router'
import { Banner } from '~/components/ui/banner'
import { FooterLinks } from '~/layouts/footer-links'
import { FooterNewsletter } from '~/layouts/footer-newsletter'
import { HeaderMenu } from '~/layouts/header-menu'
@ -12,15 +13,12 @@ const NewsLayout = () => {
<HeaderTop />
<HeaderMenu />
</header>
<div className="mx-[50px] my-[25px] grid gap-y-[25px]">
<img
src="/images/banner.png"
alt="banner"
className="h-[100px] w-full object-contain"
/>
<div className="grid sm:mx-[50px] sm:my-[25px] sm:gap-y-[25px]">
<Banner />
<Outlet />
</div>
<footer className="grid w-full grid-cols-5 gap-16 bg-[#2E2F7C] px-16 py-20 text-white">
<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>

View File

@ -2,9 +2,15 @@ export type TNews = {
title: string
description: string
type: 'hero' | 'grid'
items: {
items: Pick<TNewsDetail, 'title' | 'content' | 'featured' | 'slug' | 'tag'>[]
}
export type TNewsDetail = {
title: string
content: string
image: string
}[]
featured: string
author: string
date: Date
slug: string
tag?: Array<string>
}