mirror of
https://github.com/logos-co/logos-web.git
synced 2026-08-30 21:51:09 +00:00
250 lines
7.8 KiB
TypeScript
250 lines
7.8 KiB
TypeScript
'use client'
|
|
|
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
import Image from 'next/image'
|
|
|
|
import type { CardGridSection } from '@repo/content/schemas'
|
|
|
|
import { IconMask } from '@/components/icons/icon-mask'
|
|
import ContentWidth from '@/components/layout/content-width'
|
|
import { Button, useDragScroll } from '@/components/ui'
|
|
import {
|
|
getTechOverviewUseCasesInitialScrollLeft,
|
|
getTechOverviewUseCaseCards,
|
|
isTechOverviewUseCasesScrollable,
|
|
type TechOverviewUseCaseCard,
|
|
} from '@/lib/technology-stack-use-cases'
|
|
import { cn } from '@/lib/cn'
|
|
|
|
const DOC_LINK_CLASSNAME =
|
|
'transition-opacity hover:opacity-70 [&>span>span]:border-b-0 [&>span>span]:underline [&>span>span]:decoration-brand-dark-green/50 [&>span>span]:underline-offset-[2px]'
|
|
|
|
function ArrowIcon({ direction }: { direction: 'left' | 'right' }) {
|
|
return (
|
|
<IconMask
|
|
src={
|
|
direction === 'left'
|
|
? '/icons/arrow-left.svg'
|
|
: '/icons/arrow-right.svg'
|
|
}
|
|
className="size-3.5"
|
|
/>
|
|
)
|
|
}
|
|
|
|
function ScrollControl({
|
|
direction,
|
|
onClick,
|
|
}: {
|
|
direction: 'left' | 'right'
|
|
onClick: () => void
|
|
}) {
|
|
return (
|
|
<button
|
|
type="button"
|
|
onClick={onClick}
|
|
aria-label={direction === 'left' ? 'Scroll left' : 'Scroll right'}
|
|
className="bg-brand-off-white/10 inline-flex size-10 cursor-pointer items-center justify-center rounded-[4px] text-brand-dark-green backdrop-blur-[5px] transition-opacity hover:opacity-70"
|
|
>
|
|
<ArrowIcon direction={direction} />
|
|
</button>
|
|
)
|
|
}
|
|
|
|
function UseCaseCard({
|
|
title,
|
|
description,
|
|
href,
|
|
ctaLabel,
|
|
imageSrc,
|
|
imageAlt,
|
|
imageClassName,
|
|
}: TechOverviewUseCaseCard) {
|
|
return (
|
|
<a
|
|
href={href}
|
|
className="border-brand-dark-green/50 relative block h-[317px] w-[345px] shrink-0 cursor-pointer overflow-hidden rounded-xl border bg-brand-off-white text-brand-dark-green transition-opacity hover:opacity-90"
|
|
>
|
|
<h3 className="text-h4-sans absolute left-4 top-4 w-[249px] text-brand-dark-green">
|
|
{title}
|
|
</h3>
|
|
|
|
<div className="absolute left-4 top-[83px]">
|
|
<span className="font-mono text-[13px] leading-[1.1] font-bold tracking-normal uppercase underline decoration-brand-dark-green/50 underline-offset-[4px]">
|
|
{ctaLabel}
|
|
</span>
|
|
</div>
|
|
|
|
<p className="text-mono-s absolute bottom-4 left-4 w-[186px] text-brand-dark-green">
|
|
{description}
|
|
</p>
|
|
|
|
<div
|
|
className={`absolute bottom-[11px] right-[10px] overflow-hidden ${imageClassName}`}
|
|
>
|
|
<Image
|
|
src={imageSrc}
|
|
alt={imageAlt}
|
|
fill
|
|
sizes="96px"
|
|
className="object-cover"
|
|
/>
|
|
</div>
|
|
</a>
|
|
)
|
|
}
|
|
|
|
type Props = {
|
|
data: CardGridSection
|
|
}
|
|
|
|
export default function TechOverviewUseCases({ data }: Props) {
|
|
const scrollRef = useRef<HTMLDivElement>(null)
|
|
const dragHandlers = useDragScroll()
|
|
const [canScroll, setCanScroll] = useState(true)
|
|
|
|
const updateCanScroll = useCallback(() => {
|
|
const scrollElement = scrollRef.current
|
|
if (!scrollElement) return false
|
|
|
|
const nextCanScroll = isTechOverviewUseCasesScrollable({
|
|
scrollWidth: scrollElement.scrollWidth,
|
|
clientWidth: scrollElement.clientWidth,
|
|
})
|
|
|
|
setCanScroll(nextCanScroll)
|
|
if (!nextCanScroll) scrollElement.scrollLeft = 0
|
|
|
|
return nextCanScroll
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
const scrollElement = scrollRef.current
|
|
if (!scrollElement) return
|
|
|
|
if (updateCanScroll()) {
|
|
scrollElement.scrollLeft = getTechOverviewUseCasesInitialScrollLeft()
|
|
}
|
|
|
|
const resizeObserver = new ResizeObserver(updateCanScroll)
|
|
resizeObserver.observe(scrollElement)
|
|
window.addEventListener('resize', updateCanScroll)
|
|
|
|
return () => {
|
|
resizeObserver.disconnect()
|
|
window.removeEventListener('resize', updateCanScroll)
|
|
}
|
|
}, [updateCanScroll])
|
|
|
|
const scroll = (direction: 'left' | 'right') => {
|
|
if (!scrollRef.current) return
|
|
|
|
const amount = direction === 'left' ? -357 : 357
|
|
scrollRef.current.scrollBy({ left: amount, behavior: 'smooth' })
|
|
}
|
|
|
|
const cards = getTechOverviewUseCaseCards(data.cards)
|
|
|
|
return (
|
|
<section className="h-[820px] overflow-hidden bg-brand-off-white md:h-auto md:overflow-visible md:bg-transparent">
|
|
<div className="h-[290px] overflow-hidden bg-brand-off-white px-3 pt-10 pb-0 md:h-auto md:overflow-visible md:pb-16">
|
|
<div className="flex items-start justify-between gap-4 md:hidden">
|
|
<div className="relative h-[81px] w-[107px] shrink-0 overflow-hidden">
|
|
<Image
|
|
src="/images/technology-stack/use-cases-top.jpg"
|
|
alt=""
|
|
fill
|
|
sizes="107px"
|
|
className="object-cover"
|
|
/>
|
|
</div>
|
|
|
|
<p className="text-mono-s w-[178px] text-brand-dark-green">
|
|
{[data.subheading, data.subheadingExtra].filter(Boolean).join(' ')}
|
|
</p>
|
|
</div>
|
|
|
|
<ContentWidth className="relative hidden md:block md:h-[309px]">
|
|
<div className="absolute left-0 top-6 h-[81px] w-[107px] overflow-hidden">
|
|
<Image
|
|
src="/images/technology-stack/use-cases-top.jpg"
|
|
alt=""
|
|
fill
|
|
sizes="107px"
|
|
className="object-cover"
|
|
/>
|
|
</div>
|
|
|
|
{data.subheading ? (
|
|
<p className="text-mono-s absolute top-6 left-[calc(50%+6px)] w-[calc(25%-18px)] min-w-[178px] max-w-[226px] text-brand-dark-green xl:left-[714px] xl:w-[226px] xl:max-w-none">
|
|
{data.subheading}
|
|
</p>
|
|
) : null}
|
|
{data.subheadingExtra ? (
|
|
<p className="text-mono-s absolute top-6 left-[calc(75%+3px)] w-[calc(25%-15px)] min-w-[178px] max-w-[226px] text-brand-dark-green xl:left-[1071px] xl:w-[226px] xl:max-w-none">
|
|
{data.subheadingExtra}
|
|
</p>
|
|
) : null}
|
|
|
|
{data.heading ? (
|
|
<h2 className="text-h2 absolute top-[140px] left-1/2 w-[464px] -translate-x-1/2 text-center text-brand-dark-green xl:left-[476px] xl:translate-x-0">
|
|
{data.heading}
|
|
</h2>
|
|
) : null}
|
|
|
|
{canScroll ? (
|
|
<div className="absolute left-0 top-[269px] flex gap-2.5">
|
|
<ScrollControl direction="left" onClick={() => scroll('left')} />
|
|
<ScrollControl direction="right" onClick={() => scroll('right')} />
|
|
</div>
|
|
) : null}
|
|
|
|
{data.cta ? (
|
|
<Button
|
|
href={data.cta.href}
|
|
variant="link"
|
|
className={`absolute top-[272px] left-[calc(50%+6px)] cursor-pointer xl:left-[714px] ${DOC_LINK_CLASSNAME}`}
|
|
>
|
|
{data.cta.label}
|
|
</Button>
|
|
) : null}
|
|
</ContentWidth>
|
|
|
|
{data.heading ? (
|
|
<div className="relative left-1/2 mt-16 w-[464px] max-w-none -translate-x-1/2 md:hidden">
|
|
<h2 className="text-h2 text-center text-brand-dark-green">
|
|
{data.heading}
|
|
</h2>
|
|
</div>
|
|
) : null}
|
|
|
|
{data.cta ? (
|
|
<div className="mt-13 flex items-center justify-center md:hidden">
|
|
<Button
|
|
href={data.cta.href}
|
|
variant="link"
|
|
className={DOC_LINK_CLASSNAME}
|
|
>
|
|
{data.cta.label}
|
|
</Button>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
|
|
<div
|
|
ref={scrollRef}
|
|
className={cn(
|
|
'mt-19 flex cursor-pointer gap-3 overflow-x-auto px-3 pb-4 select-none md:mt-12.5',
|
|
canScroll ? 'justify-start' : 'justify-center'
|
|
)}
|
|
style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}
|
|
{...dragHandlers}
|
|
>
|
|
{cards.map((card, index) => (
|
|
<UseCaseCard key={`${card.title}-${index}`} {...card} />
|
|
))}
|
|
</div>
|
|
</section>
|
|
)
|
|
}
|