Files
logos-web/apps/web/components/sections/shared/tech-stack-diagram.tsx
T
Jinho Jang a9d3dcdd8a Rebrand blog to Logos Media, refresh copy, and polish stack/hero UI (#43)
* fix(content): update description for storage overview section

* fix(content): update references from "The Logos Blog" to "Logos Media" across multiple files

* feat: Refactor blog section to media section

- Created new components for articles and podcasts under the media section.
- Updated routing constants to replace blog with media.
- Adjusted all references in the codebase from blog to media, including links, buttons, and navigation.
- Implemented new layout and design for articles and podcasts to enhance user experience.
- Updated translations and content references to align with the new media section.

* feat(about): replace h1 with AnimatedHeading component in AboutHero section

* fix: update import paths for PodcastsSection and adjust LambdaBadge properties in multiple components

* fix: adjust hover translation values for desktop stack items
2026-06-11 18:56:03 +09:00

366 lines
13 KiB
TypeScript

import Image from 'next/image'
import type { ReactNode } from 'react'
import { LogosMark } from '@acid-info/logos-ui'
import type { TechStackOverviewSection } from '@repo/content/schemas'
import { ButtonArrowIcon } from '@/components/ui'
import { Link } from '@/i18n/navigation'
import { DownloadIcon } from './builder-cta-card'
const STACK_THUMBNAILS: Record<string, string> = {
basecamp: '/images/technology-stack/thumb-networking.webp',
storage: '/images/technology-stack/thumb-storage.webp',
messaging: '/images/technology-stack/thumb-messaging.webp',
blockchain: '/images/technology-stack/thumb-blockchain.webp',
userModules: '/images/technology-stack/thumb-user-modules.webp',
networking: '/images/technology-stack/thumb-networking.webp',
foundation: '/images/technology-stack/thumb-foundation.webp',
}
function formatNetworkingTitle(title: string) {
return title.replace(': ', ':\n')
}
function StackItemCta({
children,
icon,
className,
}: {
children: string
icon?: ReactNode
className: string
}) {
return (
<span
aria-hidden="true"
className={`inline-flex items-center justify-center rounded-xl bg-brand-dark-green px-3 py-2 text-brand-off-white backdrop-blur-[5px] ${className}`}
>
<span className="inline-flex items-center gap-1">
<span className="font-mono text-[10px] leading-[1.35] font-semibold whitespace-nowrap uppercase">
{children}
</span>
{icon}
</span>
</span>
)
}
function HoverStackItem({
title,
description,
href,
target,
rel,
ctaLabel = 'Learn More',
className,
labelClassName,
ctaIcon,
ctaVisibleByDefault = false,
details,
mobileFeatured = false,
desktopAt1367 = false,
comingSoon = false,
hideCta = false,
thumbnailSrc,
}: {
title: string
description?: string
href: string
target?: '_blank'
rel?: 'noopener noreferrer'
ctaLabel?: string
className: string
labelClassName?: string
ctaIcon?: ReactNode
ctaVisibleByDefault?: boolean
details?: ReadonlyArray<{
title: string
body: string
}>
mobileFeatured?: boolean
desktopAt1367?: boolean
/**
* When true the item is rendered as a non-interactive card (no link) and its
* CTA shows "Soon" instead of "Learn More". Used for pillars whose
* destination is not yet available.
*/
comingSoon?: boolean
/**
* When true the item is rendered as a non-interactive card (no link) with no
* CTA button at all. Used for pillars that have no destination to link to.
*/
hideCta?: boolean
thumbnailSrc: string
}) {
const hasDetails = details !== undefined && details.length > 0
const desktopHoverLarge = desktopAt1367
? 'min-[1367px]:group-hover/stack-item:-translate-y-20'
: 'md:group-hover/stack-item:-translate-y-20'
const desktopHoverSmall = desktopAt1367
? 'min-[1367px]:group-hover/stack-item:-translate-y-8'
: 'md:group-hover/stack-item:-translate-y-8'
const desktopBlock = desktopAt1367 ? 'min-[1367px]:block' : 'md:block'
const desktopHidden = desktopAt1367 ? 'min-[1367px]:hidden' : 'md:hidden'
const desktopFlex = desktopAt1367 ? 'min-[1367px]:flex' : 'md:flex'
const desktopGrid = desktopAt1367 ? 'min-[1367px]:grid' : 'md:grid'
// Hover reveals are desktop-only: in the mobile/tablet layout the details
// are already rendered inline, so an unscoped group-hover would paint the
// desktop overlay on top of them when a desktop user resizes and hovers.
const desktopHoverReveal = desktopAt1367
? 'min-[1367px]:group-hover/stack-item:opacity-100'
: 'md:group-hover/stack-item:opacity-100'
const desktopCtaPosition = desktopAt1367
? 'min-[1367px]:top-3 min-[1367px]:right-3'
: 'md:top-3 md:right-3'
const contentHoverOffset =
details !== undefined && details.length > 1
? desktopHoverLarge
: hasDetails
? desktopHoverSmall
: ''
const stackItemLayoutClass = mobileFeatured
? desktopAt1367
? 'items-start justify-start p-3 min-[1367px]:items-center min-[1367px]:justify-center min-[1367px]:px-6 min-[1367px]:py-0'
: 'items-start justify-start p-3 md:items-center md:justify-center md:px-6 md:py-0'
: hasDetails
? desktopAt1367
? 'items-center justify-between p-1.5 min-[1367px]:justify-center min-[1367px]:px-6 min-[1367px]:py-0'
: 'items-center justify-between p-1.5 md:justify-center md:px-6 md:py-0'
: desktopAt1367
? 'items-center justify-center px-3 min-[1367px]:px-6'
: 'items-center justify-center px-3 md:px-6'
const contentLayoutClass = mobileFeatured
? desktopAt1367
? 'items-start text-left min-[1367px]:items-center min-[1367px]:text-center'
: 'items-start text-left md:items-center md:text-center'
: hasDetails
? desktopAt1367
? 'h-[134px] w-full items-center justify-center px-3 py-8.5 text-center min-[1367px]:h-auto min-[1367px]:w-auto min-[1367px]:px-0 min-[1367px]:py-0'
: 'h-[134px] w-full items-center justify-center px-3 py-8.5 text-center md:h-auto md:w-auto md:px-0 md:py-0'
: 'items-center'
const mobileDescriptionClass = mobileFeatured
? 'text-left'
: hasDetails
? 'w-full text-center'
: 'text-center'
const containerClassName = `group/stack-item relative flex flex-col ${
comingSoon || hideCta ? '' : 'cursor-pointer '
}overflow-hidden rounded-3xl border border-brand-dark-green text-center text-brand-dark-green transition-[border-color] duration-200 ease-out hover:border-brand-dark-green/30 focus-visible:border-brand-dark-green/30 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-dark-green ${stackItemLayoutClass} ${className}`
const content = (
<>
<span className="pointer-events-none absolute inset-0 rounded-3xl bg-accent-light-blue opacity-0 transition-opacity duration-200 ease-out group-hover/stack-item:opacity-100" />
<span
className={`pointer-events-none absolute top-3 left-3 z-[1] hidden h-[57px] w-[46px] overflow-hidden opacity-0 transition-opacity duration-200 ease-out group-hover/stack-item:opacity-100 ${desktopBlock}`}
>
<Image
src={thumbnailSrc}
alt=""
fill
sizes="46px"
className="object-cover"
/>
</span>
{hideCta ? null : (
<StackItemCta
icon={comingSoon ? undefined : (ctaIcon ?? <ButtonArrowIcon />)}
className={`absolute top-2.5 right-2.5 z-10 transition-opacity duration-200 ease-out ${
comingSoon ? '' : 'cursor-pointer '
}${desktopCtaPosition} ${
ctaVisibleByDefault
? ''
: `pointer-events-none opacity-0 ${desktopHoverReveal} group-focus-visible/stack-item:opacity-100`
}`}
>
{comingSoon ? 'Soon' : ctaLabel}
</StackItemCta>
)}
<div
className={`relative z-[1] flex max-w-[222px] flex-col gap-3 transition-transform duration-200 ease-out ${contentLayoutClass} ${contentHoverOffset}`}
>
<span
className={`text-subhead-sans flex items-start gap-2.5 ${labelClassName ?? ''}`}
>
{/* h-[1.15em] = one line of text-subhead-sans, so the mark centers
on the FIRST line (looks centered for single-line titles, and
top-aligned when the title wraps to multiple lines). */}
<span
className={`hidden h-[1.15em] shrink-0 items-center ${desktopFlex}`}
>
{/* -translate-y-px seats the mark on the text baseline; centering
in the line box alone leaves it dipping slightly below. */}
<LogosMark size={14} className="-translate-y-px text-gray-03" />
</span>
{title}
</span>
{description ? (
<>
<p
className={`text-mono-s ${desktopHidden} ${mobileDescriptionClass}`}
>
{description}
</p>
{/* Collapse the hover description to 0 height until hover so the
title sits dead-center in the card (identically across every
pillar, incl. the details card). On hover the grid row expands
(0fr -> 1fr); because the content block is centered, growing it
pushes the title upward and reveals the copy below. The -mt-3
cancels the parent gap-3 while collapsed so the title is truly
centered (gap restored on hover via mt-0). */}
<span
className={`-mt-3 hidden grid-rows-[0fr] opacity-0 transition-all duration-200 ease-out group-hover/stack-item:mt-0 group-hover/stack-item:grid-rows-[1fr] group-hover/stack-item:opacity-100 ${desktopGrid}`}
>
<span className="text-mono-s overflow-hidden text-center">
{description}
</span>
</span>
</>
) : null}
</div>
{hasDetails ? (
<>
<div
className={`relative z-[1] flex w-full shrink-0 flex-col gap-1.5 ${desktopHidden}`}
>
{details.map((detail) => (
<div
key={detail.title}
className="flex h-[50px] items-center justify-center rounded-[18px] border border-brand-dark-green/50 px-2 text-brand-dark-green"
>
<p className="text-center font-mono text-[10px] leading-[1.3] font-medium uppercase">
{detail.title}
</p>
</div>
))}
</div>
<div
className={`absolute right-3 bottom-3 left-3 z-[1] hidden flex-col gap-3 opacity-0 transition-opacity duration-200 ease-out ${desktopHoverReveal} ${desktopFlex}`}
>
{details.map((detail) => (
<div
key={detail.title}
className="rounded-[18px] border border-brand-dark-green/50 px-4 py-3 text-left text-brand-dark-green"
>
<p className="font-mono text-[10px] leading-[1.25] font-semibold tracking-[0.08em] uppercase">
{detail.title}
</p>
<p className="mt-1.5 font-mono text-[10px] leading-[1.25]">
{detail.body}
</p>
</div>
))}
</div>
</>
) : null}
</>
)
if (comingSoon || hideCta) {
return <div className={containerClassName}>{content}</div>
}
return (
<Link
href={href}
target={target}
rel={rel}
className={containerClassName}
>
{content}
</Link>
)
}
export function TechStackDiagram({
data,
networkingHref,
foundationHref,
className,
desktopAt1367 = false,
}: {
data: TechStackOverviewSection
networkingHref: string
foundationHref: string
className?: string
desktopAt1367?: boolean
}) {
const basecampClass = desktopAt1367
? 'h-[111px] w-full border-brand-dark-green min-[1367px]:h-[196px]'
: 'h-[111px] w-full border-brand-dark-green md:h-[196px]'
const pillarsGridClass = desktopAt1367
? 'mt-3 grid grid-cols-2 gap-3 min-[1367px]:grid-cols-4'
: 'mt-3 grid grid-cols-2 gap-3 md:grid-cols-4'
const pillarClass = desktopAt1367
? 'h-[258px] w-full min-[1367px]:h-[366px]'
: 'h-[258px] w-full md:h-[366px]'
const rowClass = desktopAt1367
? 'h-[196px] w-full min-[1367px]:h-[196px]'
: 'h-[196px] w-full md:h-[196px]'
return (
<div className={className}>
{data.basecamp ? (
<HoverStackItem
title={data.basecamp.title}
description={data.basecamp.body}
href={data.basecamp.cta?.href ?? data.basecamp.href}
ctaLabel={data.basecamp.cta?.label ?? 'Install'}
ctaIcon={<DownloadIcon />}
ctaVisibleByDefault
mobileFeatured
desktopAt1367={desktopAt1367}
className={basecampClass}
thumbnailSrc={STACK_THUMBNAILS.basecamp}
/>
) : null}
<div className={pillarsGridClass}>
{data.pillars.map((pillar) => (
<HoverStackItem
key={pillar.id}
title={pillar.title}
description={pillar.body}
href={pillar.href}
className={pillarClass}
details={pillar.details}
desktopAt1367={desktopAt1367}
hideCta={pillar.id === 'userModules'}
thumbnailSrc={STACK_THUMBNAILS[pillar.id]}
/>
))}
</div>
<div className="mt-3 space-y-3">
<HoverStackItem
title={formatNetworkingTitle(data.networkingTitle)}
description={data.networkingDescription}
href={networkingHref}
className={rowClass}
labelClassName="whitespace-pre-line"
desktopAt1367={desktopAt1367}
thumbnailSrc={STACK_THUMBNAILS.networking}
/>
<HoverStackItem
title={data.foundationTitle}
description={data.foundationDescription}
href={foundationHref}
className={rowClass}
desktopAt1367={desktopAt1367}
hideCta
thumbnailSrc={STACK_THUMBNAILS.foundation}
/>
</div>
</div>
)
}