refactor(services-detailed): redesign to asymmetric bento grid and align types
This commit is contained in:
@@ -1,4 +1,6 @@
|
|||||||
|
'use client'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
import {
|
import {
|
||||||
FileText,
|
FileText,
|
||||||
Calculator,
|
Calculator,
|
||||||
@@ -9,6 +11,7 @@ import {
|
|||||||
Coins,
|
Coins,
|
||||||
LucideIcon,
|
LucideIcon,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
|
import type { Page } from '../../payload-types'
|
||||||
|
|
||||||
const iconMap: Record<string, LucideIcon> = {
|
const iconMap: Record<string, LucideIcon> = {
|
||||||
fileText: FileText,
|
fileText: FileText,
|
||||||
@@ -20,62 +23,82 @@ const iconMap: Record<string, LucideIcon> = {
|
|||||||
coins: Coins,
|
coins: Coins,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ServicesDetailedProps {
|
type Props = Extract<NonNullable<Page['layout']>[0], { blockType: 'servicesDetailed' }>
|
||||||
eyebrow?: string | null
|
|
||||||
title: string
|
|
||||||
subtitle?: string | null
|
|
||||||
services?: Array<{
|
|
||||||
id?: string | null
|
|
||||||
title: string
|
|
||||||
description: string
|
|
||||||
icon?: string | null
|
|
||||||
}> | null
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ServicesDetailedBlock: React.FC<ServicesDetailedProps> = ({ eyebrow, title,
|
export const ServicesDetailedBlock: React.FC<Props> = ({
|
||||||
|
title,
|
||||||
subtitle,
|
subtitle,
|
||||||
services,
|
services,
|
||||||
}) => {
|
}) => {
|
||||||
|
const serviceList = services || []
|
||||||
|
const hasBento = serviceList.length >= 3
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="py-12 md:py-16">
|
<section className="py-16 md:py-24 bg-background text-foreground">
|
||||||
<div className="container-premium">
|
<div className="container-premium">
|
||||||
<div className="max-w-3xl mb-12">
|
<div className="max-w-3xl mb-14 md:mb-16">
|
||||||
{eyebrow && (
|
|
||||||
<div className="font-mono text-sm md:text-base font-bold text-accent tracking-widest uppercase mb-4">
|
|
||||||
// {eyebrow}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<h2 className="font-heading text-3xl sm:text-4xl md:text-5xl font-extrabold text-foreground tracking-tight text-balance">
|
<h2 className="font-heading text-3xl sm:text-4xl md:text-5xl font-extrabold text-foreground tracking-tight text-balance">
|
||||||
{title}
|
{title}
|
||||||
</h2>
|
</h2>
|
||||||
{subtitle && (
|
{subtitle && (
|
||||||
<p className="mt-3 text-base sm:text-lg text-foreground/70 leading-relaxed">
|
<p className="mt-4 text-lg sm:text-xl text-foreground/75 leading-relaxed font-light">
|
||||||
{subtitle}
|
{subtitle}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 md:gap-8">
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-12 gap-6 md:gap-8">
|
||||||
{(services || []).map((service, index) => {
|
{serviceList.map((service, index) => {
|
||||||
const Icon = (service.icon && iconMap[service.icon]) || FileText
|
const Icon = (service.icon && iconMap[service.icon]) || FileText
|
||||||
|
const isFirst = hasBento && index === 0
|
||||||
|
const isSecondOrThird = hasBento && (index === 1 || index === 2)
|
||||||
|
|
||||||
|
// Asymmetric Bento grid layout:
|
||||||
|
// 1st: col-span-6 on desktop, dark card
|
||||||
|
// 2nd & 3rd: col-span-3 on desktop, light card
|
||||||
|
// Remaining (4+): col-span-4 on desktop, light card
|
||||||
|
// Fallback (< 3 items): col-span-4 on desktop, light card
|
||||||
|
let colSpanClasses = 'col-span-1 sm:col-span-1 lg:col-span-4'
|
||||||
|
if (isFirst) {
|
||||||
|
colSpanClasses = 'col-span-1 sm:col-span-2 lg:col-span-6'
|
||||||
|
} else if (isSecondOrThird) {
|
||||||
|
colSpanClasses = 'col-span-1 sm:col-span-1 lg:col-span-3'
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<motion.div
|
||||||
key={service.id || index}
|
key={service.id || index}
|
||||||
className="bg-card border border-border rounded-2xl p-6 md:p-8 flex flex-col justify-between hover:border-accent/50 hover:shadow-md transition-all duration-200"
|
whileHover={{ y: -4 }}
|
||||||
|
transition={{ type: 'spring', stiffness: 400, damping: 25 }}
|
||||||
|
className={`${colSpanClasses} ${
|
||||||
|
isFirst
|
||||||
|
? 'bg-foreground text-background border-0 shadow-md p-8 md:p-10'
|
||||||
|
: 'bg-card text-foreground border border-border shadow-xs p-6 md:p-8'
|
||||||
|
} rounded-none min-h-[240px] flex flex-col justify-between`}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<div className="w-12 h-12 rounded-xl bg-accent/10 text-accent flex items-center justify-center mb-6">
|
<div className="text-accent mb-6">
|
||||||
<Icon size={24} />
|
<Icon size={isFirst ? 32 : 28} />
|
||||||
</div>
|
</div>
|
||||||
<h3 className="font-heading text-xl sm:text-2xl font-bold text-foreground mb-3 leading-snug">
|
<h3
|
||||||
|
className={`font-heading font-bold mb-3 leading-snug ${
|
||||||
|
isFirst
|
||||||
|
? 'text-2xl sm:text-3xl text-background'
|
||||||
|
: 'text-xl sm:text-2xl text-foreground'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
{service.title}
|
{service.title}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-foreground/75 text-sm sm:text-base leading-relaxed">
|
</div>
|
||||||
|
|
||||||
|
<p
|
||||||
|
className={`mt-4 text-sm sm:text-base leading-relaxed ${
|
||||||
|
isFirst ? 'text-background/80' : 'text-foreground/75'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
{service.description}
|
{service.description}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</motion.div>
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user