mirror of
https://github.com/logos-co/logos-web.git
synced 2026-08-27 12:11:14 +00:00
* 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
366 lines
13 KiB
TypeScript
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>
|
|
)
|
|
}
|