mirror of
https://github.com/logos-co/logos-web.git
synced 2026-08-27 12:11:14 +00:00
Refactor: centralize content width with ContentWidth component + align layouts to Figma (#24)
* fix(builders-hub): align ideas page details to Figma Match exact Figma specs for the Builder Ideas page (desktop 121:11657, mobile 121:11581): - idea-row: desktop index weight Regular (was medium); mobile title width 179px; mobile Apply CTA square corners (drop rounded-[4px]) and left-aligned at x=298 (right-[22px]) - bottom CTA: heading H3 (36px desktop / 30px mobile) instead of H2 (56px); restore text-h3-serif line-height 1 / -0.03em; gap 24px * fix(builders-hub): align RFP page details to Figma Match exact Figma specs for the Builder RFPs page (desktop 121:11577, mobile 121:11489): - rfp-card: title letter-spacing -0.01em (H4 Sans) instead of tracking-tight (-0.025em) - rfp-list-row: apply the same Row-entry corrections as the ideas row — desktop index weight Regular (was medium), mobile title width 179px, Learn More CTA square corners (drop rounded-[4px]) and left-aligned at x=298 (right-[22px]) - listing grid (RFP branch only): desktop top margin 138px so the first card lands at Figma y=374 (was 160px -> 396px, 22px too low); ideas and list-view branches untouched Text copy verified against Figma (titles/taglines match, British spelling kept where Figma used American). Grid layout, card geometry (345x317 desktop / 369x317 mobile), colours, borders, 96px images and CTA positions confirmed matching. * fix(get-started): align spacing, colours, and copy with Figma - Correct 04 heading copy to "What you can build today" - Swap community row alternating backgrounds (row 0 = grey-01) - Restyle community row labels to 14px (sans-medium number + serif name) - Underline community row "Learn more" CTA (border-b) - Build card body: Public Sans 14px instead of mono 10px - Fix install/docs section top padding (install pt-24, docs pt-40 on desktop) - Add 6px gap between install card image and text panel * fix(basecamp): align spacing, padding, and text gaps with Figma design - Fixed HowItWorksSection: md:py-3 → md:py-10 (was reducing padding on desktop) - Fixed LocalFirstSection: py-3 → py-10 for proper vertical spacing - Increased gap spacing from 3 to 4 across card components and sections - Added uppercase styling to LocalFirstSection eyebrow - Added opacity-0 to footerLabel for layout consistency - Adjusted ModularSection vertical padding: py-10 → py-12 mobile, py-10 desktop - Added flex flex-col gap-4 to ModularSection body text - CapabilitiesSection: py-6 → py-10 mobile for consistent spacing - ResourcesSection: py-10 → py-12 mobile for alignment with other sections - Improved internal spacing in ResourceCard (gap-3 → gap-4, mt-3 → mt-4) All changes verified against Figma desktop design at node-id=146-15527 * refine(basecamp): fine-tune section spacing and padding alignment - HeroSection: pb-12 mobile, pb-10 desktop (better spacing at section break) - HowItWorks title gap: mb-6 → mb-8 (increased spacing to content) - ModularSection: removed inconsistent py-12, standardized to py-10 - CapabilitiesSection heading: mb-10 → mb-12 (better visual hierarchy) - ResourcesSection: removed inconsistent py-12, standardized to py-10 All sections now maintain consistent padding pattern: py-10 with device-specific overrides where needed * fix(lambda-prize): align spacing, padding, and gap with design - HowItWorks: gap-3 → gap-4 mobile, gap-6 desktop, added md:px-3 md:py-10 - HowItWorks titles: mb-6 → mb-8 for both sections - FeaturedPrizes: mt-6 → mt-10 mobile, mt-12 desktop, gap-3 → gap-4, moved px-3 to section level - AboutProgramme: gap-3 → gap-4 mobile, gap-6 desktop, added md:px-3 md:py-10 - Support: pt-10 → py-10 mobile, py-12 desktop, added px-3, removed redundant px-3 from grid All sections now have consistent spacing patterns aligned with Figma design specifications * fix(lambda-prize): precision spacing refinements - standardize padding and margins - DataRows: increase py-1.5 → py-3 for better vertical spacing - FeaturedPrizes: reduce excessive padding pt-[100px] → pt-10, md:pt-[200px] → md:pt-12 - Hero: standardize margins mb-[60px] → mb-12, mt-[60px] → mt-12 - Support: standardize margin-top mt-[35px] → mt-6, md:mt-[91px] → md:mt-8 * fix(research): align section heights with Figma design - Change hero section from min-h-[447px] to h-[447px] (fixed height) - Change overview section from min-h-[670px] to h-[670px] (desktop) - Change resource rows from min-h-[50px] to h-[50px] (fixed height) - Change contribute section from min-h-[262px] to h-[262px] (desktop) These fixed heights match the Figma design specifications for precise layout control across all breakpoints. * fix(research): revert to min-h for responsive design Responsive design requires minimum heights (min-h) instead of fixed heights (h) to accommodate content overflow on various screen sizes. Maintains Figma design intent while ensuring proper mobile/tablet layout behavior. * fix(press): align PressHero padding with Figma design Change PressHero section padding from pt-6 (24px) to pt-10 (40px) to match Figma design specifications where section starts at y=40. All other spacing, margins, colors, and image ratios already aligned with Figma design. Press page now 100% design-compliant. * fix(podcast): add wrapper padding for consistent page layout spacing * fix(podcast): align top padding with Figma design (pt-20/80px) * fix(broadcast-network): align padding with Figma design specs * fix(technology-stack): align hero padding with design specifications * fix(technology-stack): center hero section on desktop to fix left alignment * fix(technology-stack): make architecture image responsive and prevent clipping * fix(technology-stack): constrain all sections to 1440px max-width on desktop * refactor: centralize max-width layout at page level to eliminate duplication - Add SectionLayout wrapper to all technology-stack pages (main, blockchain, networking, storage, messaging) - Move 1440px max-width constraint from individual section components to page-level layout - Remove mx-auto max-w-360/max-w-[1440px] from tech-stack sections and shared components - Reduces code duplication and centralizes layout management * refactor: simplify layout by inlining max-width constraint - Remove SectionLayout function definitions from each page - Use simple div wrapper at page return level - Reduces code duplication and improves maintainability * refactor: revert page-level layout constraint, keep section-level management - Remove max-width wrapper from page returns - Each section maintains full-width background - Section components manage internal 1440px constraint - Preserves visual hierarchy from Figma design * refactor: centralize content width constraints with ContentWidth component - Create ContentWidth component for 1440px max-width constraint - Remove layout-level max-width from main element to allow full-width sections - Apply ContentWidth to all home page sections for consistent layout - Convert absolute positioning to flex/grid layouts in: * Parallel Society (gallery display) * Tech Stack (title and metadata centering) * Feature Cards (3-column grid) * Circles CTA (vertical layout) * Press Section (flex grid) * Social Proof (4-column grid) * Start Building (2-column layout) * About Section (centered closing text) - Implement responsive sizing with Tailwind classes - Update technology-stack page section for proper centering This enables full-width background colors while constraining content to 1440px max-width, improving design system consistency across all pages. * refactor: make social proof section full-width without 1440px constraint Remove ContentWidth wrapper from the stat cards section to allow it to span full screen width while maintaining proper padding. * fix: add line break in 'The Logos Technology Stack' title Match Figma design by breaking 'The Logos' and 'Technology Stack' onto separate lines. * fix: add line break in Basecamp title Match Figma design by breaking 'Basecamp is our' and 'Exit Guide.' onto separate lines. * fix: add line break to tech stack title on mobile view Use <br /> instead of literal newline for proper HTML rendering of 'The ... Technology Stack' title. * fix: add width constraints to AppInstall paragraph text Match container width (260px mobile, 464px desktop) to ensure text wrapping aligns with Figma design. * Revert "fix: add width constraints to AppInstall paragraph text" This reverts commit 184efed9421b0b8c006d5121f26da72b7d015491. * fix(tech-stack): constrain desktop title to 464px to match Figma Desktop h2 used w-full, so 'The Logos Technology Stack' stayed on one line at wide/4K viewports. Figma node 121:6875 sets it to 464px, which wraps it to two lines. Parent is items-center so the fixed width centers. * refacor: refactor layout components to use ContentWidth for consistent styling - Updated various sections across the application to wrap content in the ContentWidth component for improved layout consistency. - Adjusted padding and margin values in several sections to enhance visual alignment. - Modified text content in the About section to reflect updated messaging. - Improved accessibility and responsiveness by ensuring all sections utilize the ContentWidth component. * feat: integrate ContentWidth component for layout consistency across various sections * feat: update StorageAccess component with new card data and CTA buttons for improved user interaction * feat: update MessagingIntro and related tests for improved layout and responsiveness based on Figma specifications * Refactor code structure for improved readability and maintainability * Refactor code structure for improved readability and maintainability * Refactor technology stack detail layout and hero components - Introduced `TechStackDetailPage` and `TechStackDetailSection` components for consistent layout across technology stack pages. - Updated individual technology stack pages (blockchain, messaging, networking, storage) to utilize the new layout components. - Refactored hero components (BlockchainHero, MessagingHero, NetworkingHero, StorageHero) to use `TechStackDetailHero` for improved structure and reusability. - Adjusted spacing and margin classes to align with design specifications, removing redundant margin classes. - Added `imageFit` property to builders hub settings schema to control image display behavior. - Updated tests to ensure layout consistency and adherence to design specifications. * feat: enhance layout and responsiveness of articles and podcasts sections based on Figma specifications * fix: prevent messaging delivery image clipping * feat: update article and podcast layouts for improved responsiveness and alignment with Figma designs * test: remove ui implementation contracts
This commit is contained in:
@@ -85,6 +85,7 @@ Payload collections: `Pages`, `Circles`, `Ideas`, `Rfps`, `ContentChangeRequests
|
||||
## Testing
|
||||
|
||||
- Vitest lives in `apps/web` (`pnpm test` from root or app). Add tests beside the code in `__tests__/` folders.
|
||||
- Do not add UI implementation contract tests that assert Tailwind class strings, Figma measurements, layout spacing, motion details, hover treatment, or component source structure. UI changes are reviewed through browser verification, not brittle source-string tests.
|
||||
- Playwright is the standard for E2E if/when added.
|
||||
- For UI/frontend changes, **start the dev server and verify in a browser** before reporting done. Type-check passing ≠ feature working.
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# logos-web — Agent Instructions
|
||||
|
||||
## Model Policy
|
||||
|
||||
- **Prefer Opus.** Use an Opus model (e.g. `claude-opus-4-8`) whenever possible.
|
||||
- **Never use Haiku.** Do not run on Haiku 4.5 or any Haiku model under any circumstances.
|
||||
- Do not silently downgrade the model. If a switch happens, flag it to the user instead of proceeding quietly.
|
||||
|
||||
## Commit Attribution
|
||||
|
||||
- **Claude must never appear in commits.** Never add a `Co-Authored-By: Claude ...`
|
||||
trailer, and never list Claude as the author or committer. Commits must be
|
||||
authored solely by the human developer.
|
||||
+2
-1
@@ -42,5 +42,6 @@ pnpm --filter web test
|
||||
|
||||
For UI changes, read the relevant docs in `docs/web-pages.md` or `docs/components.md`, run the dev server, and verify the result in a browser before reporting done.
|
||||
|
||||
After static builds, keep the default-locale stripping step intact: `scripts/strip-default-locale-prefix.sh`.
|
||||
Do not add UI implementation contract tests for Tailwind class strings, Figma measurements, layout spacing, motion details, hover treatment, or component source structure. These tests churn during active UI iteration and should be replaced with browser verification or durable behavioural tests only.
|
||||
|
||||
After static builds, keep the default-locale stripping step intact: `scripts/strip-default-locale-prefix.sh`.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { getTranslations } from 'next-intl/server'
|
||||
import { LogosMark } from '@acid-info/logos-ui'
|
||||
import ContentWidth from '@/components/layout/content-width'
|
||||
import { ROUTES } from '@/constants/routes'
|
||||
import { createTranslatedPageMetadata } from '@/lib/translated-page-metadata'
|
||||
|
||||
@@ -19,10 +20,12 @@ export default async function BlogPage({
|
||||
const t = await getTranslations({ locale, namespace: NAMESPACE })
|
||||
return (
|
||||
<div className="px-3 pt-16 pb-12">
|
||||
<h1 className="text-h2 flex items-center gap-3 text-brand-dark-green">
|
||||
<LogosMark size={40} className="shrink-0" />
|
||||
{t('heading')}
|
||||
</h1>
|
||||
<ContentWidth>
|
||||
<h1 className="text-h2 flex items-center gap-3 text-brand-dark-green">
|
||||
<LogosMark size={40} className="shrink-0" />
|
||||
{t('heading')}
|
||||
</h1>
|
||||
</ContentWidth>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import Image from 'next/image'
|
||||
import { getTranslations } from 'next-intl/server'
|
||||
|
||||
import { IconMask } from '@/components/icons/icon-mask'
|
||||
import ContentWidth from '@/components/layout/content-width'
|
||||
import { BOOK_DOWNLOADS } from '@/constants/book-assets'
|
||||
import { ROUTES } from '@/constants/routes'
|
||||
import { createDefaultMetadata } from '@/lib/metadata'
|
||||
@@ -132,7 +133,8 @@ export default async function BookPage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="grid gap-3 border-t border-brand-dark-green/10 px-3 py-16 md:grid-cols-12 md:py-24">
|
||||
<section className="border-t border-brand-dark-green/10 px-3 py-16 md:py-24">
|
||||
<ContentWidth className="grid gap-3 md:grid-cols-12">
|
||||
<div className="md:col-span-7">
|
||||
<div className="relative aspect-[1588/1436] overflow-hidden rounded">
|
||||
<Image
|
||||
@@ -165,14 +167,16 @@ export default async function BookPage() {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</ContentWidth>
|
||||
</section>
|
||||
|
||||
<section className="border-t border-brand-dark-green/10">
|
||||
{authors.map((author, index) => (
|
||||
<div
|
||||
key={author.name}
|
||||
className="grid gap-3 border-b border-brand-dark-green/10 px-3 py-16 md:grid-cols-12 md:py-24"
|
||||
className="border-b border-brand-dark-green/10 px-3 py-16 md:py-24"
|
||||
>
|
||||
<ContentWidth className="grid gap-3 md:grid-cols-12">
|
||||
<div
|
||||
className={
|
||||
index % 2 === 0
|
||||
@@ -206,6 +210,7 @@ export default async function BookPage() {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</ContentWidth>
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
|
||||
@@ -50,7 +50,7 @@ export default async function DesignSystemsPage({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex w-full max-w-[1440px] flex-col gap-[40px] py-10">
|
||||
<div className="flex w-full flex-col gap-[40px] py-10">
|
||||
<ColorPalette />
|
||||
<TypeStyles />
|
||||
<Cards locale={locale} />
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { ReactNode } from 'react'
|
||||
import { getTranslations } from 'next-intl/server'
|
||||
import { LogosMark } from '@acid-info/logos-ui'
|
||||
|
||||
import ContentWidth from '@/components/layout/content-width'
|
||||
import { Button, ButtonArrowIcon } from '@/components/ui/button'
|
||||
import { EXTERNAL_URLS, ROUTES } from '@/constants/routes'
|
||||
import { createTranslatedPageMetadata } from '@/lib/translated-page-metadata'
|
||||
@@ -125,6 +126,7 @@ export default async function GetStartedPage({
|
||||
return (
|
||||
<div className="bg-brand-off-white text-brand-dark-green">
|
||||
<section className="relative mb-[60px] h-[200px] w-full px-3 md:mb-[100px] md:h-[258px]">
|
||||
<ContentWidth className="relative h-full">
|
||||
<div className="absolute top-[60px] left-3 flex items-center gap-3 md:top-[90px]">
|
||||
<LogosMark size={26} className="w-5 shrink-0" />
|
||||
<h1 className="font-display text-[30px] leading-none tracking-[-0.9px] md:text-[36px] md:tracking-[-1.08px]">
|
||||
@@ -134,18 +136,19 @@ export default async function GetStartedPage({
|
||||
<p className="absolute top-[126px] left-3 max-w-[342px] whitespace-pre-line font-mono text-[10px] leading-[1.3] md:top-[90px] md:left-[calc(50%+6px)] md:max-w-[345px]">
|
||||
{t('intro')}
|
||||
</p>
|
||||
</ContentWidth>
|
||||
</section>
|
||||
|
||||
<section
|
||||
id="install"
|
||||
className="border-t border-brand-dark-green/10 px-3 pt-6 pb-[100px] md:pt-10"
|
||||
className="border-t border-brand-dark-green/10 px-3 pt-6 pb-[100px]"
|
||||
>
|
||||
<div className="flex w-full flex-col gap-10">
|
||||
<ContentWidth className="flex w-full flex-col gap-10">
|
||||
<SectionHeading
|
||||
number={t('sections.install.number')}
|
||||
heading={t('sections.install.heading')}
|
||||
/>
|
||||
<div className="grid overflow-hidden rounded-xl bg-gray-01 p-1.5 md:min-h-[325px] md:grid-cols-2">
|
||||
<div className="grid gap-1.5 overflow-hidden rounded-xl bg-gray-01 p-1.5 md:min-h-[325px] md:grid-cols-2">
|
||||
<div className="relative min-h-[314px] overflow-hidden rounded-md md:min-h-full">
|
||||
<Image
|
||||
src="/images/builders-hub/basecamp-card.png"
|
||||
@@ -170,11 +173,11 @@ export default async function GetStartedPage({
|
||||
</ActionLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ContentWidth>
|
||||
</section>
|
||||
|
||||
<section className="border-t border-brand-dark-green/10 px-3 pt-6 pb-[100px]">
|
||||
<div className="flex w-full flex-col gap-10">
|
||||
<section className="border-t border-brand-dark-green/10 px-3 pt-6 pb-[100px] md:pt-10">
|
||||
<ContentWidth className="flex w-full flex-col gap-10">
|
||||
<div className="grid gap-6 md:grid-cols-12 md:items-start">
|
||||
<div className="md:col-span-5">
|
||||
<SectionHeading
|
||||
@@ -212,11 +215,11 @@ export default async function GetStartedPage({
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</ContentWidth>
|
||||
</section>
|
||||
|
||||
<section className="border-t border-brand-dark-green/10 px-3 pt-6 pb-[100px]">
|
||||
<div className="flex w-full flex-col gap-10">
|
||||
<ContentWidth className="flex w-full flex-col gap-10">
|
||||
<SectionHeading
|
||||
number={t('sections.community.number')}
|
||||
heading={t('sections.community.heading')}
|
||||
@@ -229,25 +232,29 @@ export default async function GetStartedPage({
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={`flex h-[62px] cursor-pointer items-center justify-between px-3 transition-colors hover:bg-accent-light-blue md:h-[50px] md:px-4 ${
|
||||
index % 2 === 0 ? 'bg-brand-dark-green/5' : 'bg-gray-01'
|
||||
index % 2 === 0 ? 'bg-gray-01' : 'bg-brand-dark-green/5'
|
||||
}`}
|
||||
>
|
||||
<span className="font-mono text-[10px] leading-[1.3]">
|
||||
{String(index + 1).padStart(2, '0')}{' '}
|
||||
{t(`sections.community.items.${item.key}`)}
|
||||
<span className="flex items-baseline gap-3 text-[14px] leading-[1.2]">
|
||||
<span className="font-sans font-medium">
|
||||
{String(index + 1).padStart(2, '0')}
|
||||
</span>
|
||||
<span className="font-display">
|
||||
{t(`sections.community.items.${item.key}`)}
|
||||
</span>
|
||||
</span>
|
||||
<span className="inline-flex items-center gap-1 font-mono text-[10px] leading-[1.35] font-semibold whitespace-nowrap uppercase">
|
||||
<span className="inline-flex items-center gap-1 border-b border-brand-dark-green/50 pb-0.5 font-mono text-[10px] leading-[1.35] font-semibold whitespace-nowrap uppercase">
|
||||
{t('sections.community.cta')}
|
||||
<ButtonArrowIcon />
|
||||
</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</ContentWidth>
|
||||
</section>
|
||||
|
||||
<section className="border-t border-brand-dark-green/10 px-3 pt-6 pb-[100px]">
|
||||
<div className="flex w-full flex-col gap-10">
|
||||
<ContentWidth className="flex w-full flex-col gap-10">
|
||||
<SectionHeading
|
||||
number={t('sections.build.number')}
|
||||
heading={t('sections.build.heading')}
|
||||
@@ -264,7 +271,7 @@ export default async function GetStartedPage({
|
||||
{t(`sections.build.items.${item.key}.title`)}
|
||||
</h3>
|
||||
{'hasBody' in item && item.hasBody ? (
|
||||
<p className="max-w-[360px] font-mono text-[10px] leading-[1.3]">
|
||||
<p className="max-w-[329px] font-sans text-[14px] leading-[1.2]">
|
||||
{t(`sections.build.items.${item.key}.body`)}
|
||||
</p>
|
||||
) : null}
|
||||
@@ -280,7 +287,7 @@ export default async function GetStartedPage({
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</ContentWidth>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -2,6 +2,7 @@ import Image from 'next/image'
|
||||
import { LogosMark } from '@acid-info/logos-ui'
|
||||
import type { TechStackOverviewSection } from '@repo/content/schemas'
|
||||
|
||||
import ContentWidth from '@/components/layout/content-width'
|
||||
import TechStackSection from '@/components/sections/home/tech-stack-section'
|
||||
import { Button, ButtonArrowIcon } from '@/components/ui'
|
||||
import { ROUTES } from '@/constants/routes'
|
||||
@@ -101,7 +102,7 @@ function DataRows({ rows }: { rows: RowCopy[] }) {
|
||||
{rows.map((row, index) => (
|
||||
<div
|
||||
key={`${row.label}-${index}`}
|
||||
className="flex min-h-[36px] min-w-0 flex-col border-t border-brand-dark-green/10 py-1.5 last:border-b md:grid md:min-h-[31px] md:grid-cols-2"
|
||||
className="flex min-h-[36px] min-w-0 flex-col border-t border-brand-dark-green/10 py-3 last:border-b md:grid md:min-h-[31px] md:grid-cols-2"
|
||||
>
|
||||
<p className="text-mono-s">{row.label}</p>
|
||||
<p className="text-mono-s min-w-0 pr-2 break-words [overflow-wrap:anywhere]">
|
||||
@@ -127,9 +128,9 @@ function Hero({ copy }: { copy: LambdaPrizePageCopy['hero'] }) {
|
||||
priority
|
||||
/>
|
||||
<div className="absolute inset-0 bg-brand-dark-green/35" />
|
||||
<div className="relative z-10 flex h-full items-center justify-center">
|
||||
<ContentWidth className="relative z-10 flex h-full items-center justify-center">
|
||||
<div className="flex w-full -translate-y-[6px] flex-col items-center text-center">
|
||||
<div className="text-h4-serif mb-[60px] inline-flex items-center gap-3">
|
||||
<div className="text-h4-serif mb-12 inline-flex items-center gap-3">
|
||||
<LogosMark size={20} />
|
||||
<span>{copy.label}</span>
|
||||
</div>
|
||||
@@ -140,7 +141,7 @@ function Hero({ copy }: { copy: LambdaPrizePageCopy['hero'] }) {
|
||||
</span>
|
||||
))}
|
||||
</h1>
|
||||
<p className="text-mono-s mt-[60px] w-full max-w-[345px] md:w-[462px] md:max-w-full">
|
||||
<p className="text-mono-s mt-12 w-full max-w-[345px] md:w-[462px] md:max-w-full">
|
||||
<span className="inline-flex items-baseline gap-1">
|
||||
<LogosMark size={7} className="shrink-0" />
|
||||
<span>{copy.body}</span>
|
||||
@@ -162,7 +163,7 @@ function Hero({ copy }: { copy: LambdaPrizePageCopy['hero'] }) {
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ContentWidth>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -175,15 +176,16 @@ function HowItWorks({
|
||||
evaluation: LambdaPrizePageCopy['evaluation']
|
||||
}) {
|
||||
return (
|
||||
<section className="flex flex-col gap-3 bg-brand-off-white p-3 text-brand-dark-green md:grid md:h-[650px] md:grid-cols-2">
|
||||
<section className="bg-brand-off-white text-brand-dark-green">
|
||||
<ContentWidth className="flex flex-col gap-4 p-3 md:grid md:h-[650px] md:grid-cols-2 md:gap-6 md:px-3 md:py-10">
|
||||
<div className="order-2 flex min-h-[687px] flex-col justify-between md:order-1 md:min-h-0">
|
||||
<div className="flex flex-col gap-10 md:gap-20">
|
||||
<div>
|
||||
<h2 className="text-h4-serif mb-6">{copy.heading}</h2>
|
||||
<h2 className="text-h4-serif mb-8">{copy.heading}</h2>
|
||||
<DataRows rows={copy.rows} />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-h4-serif mb-6">{evaluation.heading}</h2>
|
||||
<h2 className="text-h4-serif mb-8">{evaluation.heading}</h2>
|
||||
<DataRows rows={evaluation.rows} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -209,6 +211,7 @@ function HowItWorks({
|
||||
className="rounded-xl object-cover object-left md:object-center"
|
||||
/>
|
||||
</div>
|
||||
</ContentWidth>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -252,9 +255,10 @@ function PrizeCard({ prize, image }: { prize: PrizeCopy; image: string }) {
|
||||
|
||||
function FeaturedPrizes({ copy }: { copy: LambdaPrizePageCopy['featured'] }) {
|
||||
return (
|
||||
<section className="h-[1931px] bg-brand-off-white pt-[100px] text-brand-dark-green md:h-[1011px] md:pt-[200px]">
|
||||
<h2 className="text-h3-serif px-3">{copy.heading}</h2>
|
||||
<div className="mt-6 grid grid-cols-1 gap-3 md:grid-cols-3 md:px-3">
|
||||
<section className="h-[1931px] bg-brand-off-white px-3 pt-10 text-brand-dark-green md:h-[1011px] md:px-3 md:pt-12">
|
||||
<ContentWidth>
|
||||
<h2 className="text-h3-serif">{copy.heading}</h2>
|
||||
<div className="mt-10 grid grid-cols-1 gap-4 md:grid-cols-3 md:mt-12">
|
||||
{copy.prizes.map((prize, index) => (
|
||||
<PrizeCard
|
||||
key={`${prize.title}-${index}`}
|
||||
@@ -263,49 +267,71 @@ function FeaturedPrizes({ copy }: { copy: LambdaPrizePageCopy['featured'] }) {
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</ContentWidth>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function AboutProgramme({ copy }: { copy: LambdaPrizePageCopy['about'] }) {
|
||||
return (
|
||||
<section className="flex h-[960px] flex-col gap-3 bg-brand-off-white p-3 text-brand-dark-green md:grid md:h-[600px] md:grid-cols-2">
|
||||
<div className="relative h-[405px] overflow-hidden rounded-xl md:h-auto">
|
||||
<Image
|
||||
src="/images/lambda-prize/about.webp"
|
||||
alt=""
|
||||
fill
|
||||
sizes="50vw"
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex min-h-[519px] flex-col justify-between md:min-h-0">
|
||||
<div className="flex flex-col items-start justify-between gap-3 md:flex-row md:gap-0">
|
||||
<h2 className="text-h3-serif">{copy.heading}</h2>
|
||||
<div className="flex gap-1">
|
||||
<Button href={ROUTES.rfps} className="cursor-pointer">
|
||||
{copy.primaryCta}
|
||||
</Button>
|
||||
<Button
|
||||
href={ROUTES.buildersHub}
|
||||
variant="secondary"
|
||||
className="cursor-pointer"
|
||||
>
|
||||
{copy.secondaryCta}
|
||||
</Button>
|
||||
<section className="bg-gray-01 text-brand-dark-green">
|
||||
<ContentWidth className="flex min-h-[960px] flex-col gap-3 p-3 md:h-[600px] md:min-h-0 md:flex-row">
|
||||
<div className="relative h-[405px] overflow-hidden rounded-3xl md:h-full md:w-[702px] md:shrink-0">
|
||||
<Image
|
||||
src="/images/lambda-prize/about.webp"
|
||||
alt=""
|
||||
fill
|
||||
sizes="(min-width: 768px) 702px, 100vw"
|
||||
className="object-cover object-center"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex min-h-[519px] min-w-0 flex-1 flex-col justify-between md:h-full md:min-h-0">
|
||||
<div className="flex flex-col items-start justify-between gap-3 md:h-9 md:flex-row md:gap-0">
|
||||
<h2 className="text-h3-sans whitespace-nowrap">
|
||||
{copy.heading}
|
||||
</h2>
|
||||
<div className="flex gap-1">
|
||||
<Button href={ROUTES.rfps} className="cursor-pointer">
|
||||
{copy.primaryCta}
|
||||
</Button>
|
||||
<Button
|
||||
href={ROUTES.buildersHub}
|
||||
variant="secondary"
|
||||
className="cursor-pointer"
|
||||
>
|
||||
{copy.secondaryCta}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mx-auto w-full max-w-[345px] text-center font-sans text-[12px] leading-[1.2] font-medium text-brand-dark-green">
|
||||
{copy.body}
|
||||
</p>
|
||||
<div className="flex flex-col gap-3 text-brand-dark-green">
|
||||
{copy.rows.map((row, index) => (
|
||||
<div
|
||||
key={`${row.label}-${index}`}
|
||||
className="flex min-w-0 flex-col gap-1.5 border-t border-brand-dark-green/50 pt-1.5 md:flex-row md:gap-3"
|
||||
>
|
||||
<p className="text-eyebrow min-w-0 uppercase md:w-[345px] md:shrink-0">
|
||||
{row.label}
|
||||
</p>
|
||||
<p className="text-mono-s min-w-0 break-words [overflow-wrap:anywhere]">
|
||||
{row.body}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-mono-s mx-auto w-[345px] text-center">{copy.body}</p>
|
||||
<DataRows rows={copy.rows} />
|
||||
</div>
|
||||
</ContentWidth>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function Support({ copy }: { copy: LambdaPrizePageCopy['support'] }) {
|
||||
return (
|
||||
<section className="h-[406px] border-t border-brand-dark-green/10 bg-brand-off-white pt-10 text-brand-dark-green md:h-[421px]">
|
||||
<div className="grid grid-cols-[1fr_auto] gap-y-6 px-3 md:grid-cols-3">
|
||||
<section className="h-[406px] border-t border-brand-dark-green/10 bg-brand-off-white px-3 py-10 text-brand-dark-green md:h-[421px] md:py-12">
|
||||
<ContentWidth>
|
||||
<div className="grid grid-cols-[1fr_auto] gap-y-6 md:grid-cols-3">
|
||||
<h2 className="text-h3-serif">{copy.heading}</h2>
|
||||
<p className="text-mono-s col-start-1 row-start-2 w-[178px] md:col-start-auto md:row-start-auto md:w-[226px]">
|
||||
{copy.body}
|
||||
@@ -314,7 +340,7 @@ function Support({ copy }: { copy: LambdaPrizePageCopy['support'] }) {
|
||||
<TertiaryCta href={ROUTES.faq}>{copy.cta}</TertiaryCta>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-[35px] md:mt-[91px]">
|
||||
<div className="mt-6 md:mt-8">
|
||||
{copy.rows.map((row, index) => (
|
||||
<div
|
||||
key={row.label}
|
||||
@@ -333,6 +359,7 @@ function Support({ copy }: { copy: LambdaPrizePageCopy['support'] }) {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ContentWidth>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { getTranslations } from 'next-intl/server'
|
||||
import { LogosMark } from '@acid-info/logos-ui'
|
||||
import ContentWidth from '@/components/layout/content-width'
|
||||
import { ROUTES } from '@/constants/routes'
|
||||
import { createTranslatedPageMetadata } from '@/lib/translated-page-metadata'
|
||||
|
||||
@@ -19,10 +20,12 @@ export default async function LinksPage({
|
||||
const t = await getTranslations({ locale, namespace: NAMESPACE })
|
||||
return (
|
||||
<div className="px-3 pt-16 pb-12">
|
||||
<h1 className="text-h2 flex items-center gap-3 text-brand-dark-green">
|
||||
<LogosMark size={40} className="shrink-0" />
|
||||
{t('heading')}
|
||||
</h1>
|
||||
<ContentWidth>
|
||||
<h1 className="text-h2 flex items-center gap-3 text-brand-dark-green">
|
||||
<LogosMark size={40} className="shrink-0" />
|
||||
{t('heading')}
|
||||
</h1>
|
||||
</ContentWidth>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
+31
-28
@@ -3,6 +3,7 @@
|
||||
import Image from 'next/image'
|
||||
import { useMemo, useState } from 'react'
|
||||
|
||||
import ContentWidth from '@/components/layout/content-width'
|
||||
import { ExternalLink } from '@/components/ui'
|
||||
import { cn } from '@/lib/cn'
|
||||
import type { BroadcastEventRow, PressPodcastRow } from '@/lib/press-engine'
|
||||
@@ -206,8 +207,8 @@ function UnderlineLabel({ children }: { children: string }) {
|
||||
|
||||
function BroadcastHero({ copy }: { copy: BroadcastNetworkCopy }) {
|
||||
return (
|
||||
<section className="bg-accent-tan px-3 pb-[100px] pt-16 text-brand-dark-green md:pt-6">
|
||||
<div className="flex w-full flex-col gap-10">
|
||||
<section className="bg-accent-tan px-3 pb-25 pt-6 text-brand-dark-green md:pt-6">
|
||||
<ContentWidth className="flex w-full flex-col gap-10">
|
||||
<div className="flex w-full max-w-[1186px] items-start justify-between gap-6">
|
||||
<div className="relative h-[86px] w-[107px] shrink-0 overflow-hidden">
|
||||
<Image
|
||||
@@ -229,7 +230,7 @@ function BroadcastHero({ copy }: { copy: BroadcastNetworkCopy }) {
|
||||
<div className="flex w-full max-w-[1186px] justify-end">
|
||||
<p className="text-mono-s w-[226px]">{copy.introSecondary}</p>
|
||||
</div>
|
||||
</div>
|
||||
</ContentWidth>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -294,7 +295,7 @@ function EventCards({
|
||||
|
||||
return (
|
||||
<section className="bg-accent-tan px-3 pb-10">
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<ContentWidth className="grid grid-cols-3 gap-3">
|
||||
{displayEvents.map((event, index) => (
|
||||
<EventCard
|
||||
key={event.id}
|
||||
@@ -303,7 +304,7 @@ function EventCards({
|
||||
image={FEATURE_IMAGES[index % FEATURE_IMAGES.length]}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</ContentWidth>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -380,8 +381,8 @@ function EventsCalendar({
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="overflow-x-auto bg-accent-tan pb-[100px] text-brand-dark-green">
|
||||
<div className="mx-auto w-[1440px]">
|
||||
<section className="overflow-x-auto bg-accent-tan pt-25 pb-25 text-brand-dark-green">
|
||||
<ContentWidth>
|
||||
<div className="relative h-[160px] px-3">
|
||||
<h2 className="absolute bottom-6 left-3 font-sans text-[36px] leading-none tracking-[-0.02em]">
|
||||
{copy.upcomingEvents}
|
||||
@@ -472,7 +473,7 @@ function EventsCalendar({
|
||||
<CalendarCell key={day.key} day={day} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</ContentWidth>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -492,38 +493,40 @@ function EpisodeRow({
|
||||
<ExternalLink
|
||||
href={podcast.href}
|
||||
className={cn(
|
||||
'group relative block h-[131px] cursor-pointer overflow-hidden text-brand-dark-green transition-colors hover:bg-brand-yellow',
|
||||
'group block h-[107px] cursor-pointer text-brand-dark-green transition-colors hover:bg-brand-yellow',
|
||||
background
|
||||
)}
|
||||
>
|
||||
<div className="absolute left-3 top-3 h-[107px] w-[190px] overflow-hidden">
|
||||
<Image
|
||||
src={podcast.image}
|
||||
alt=""
|
||||
fill
|
||||
sizes="190px"
|
||||
className="object-cover transition-transform duration-700 group-hover:scale-105"
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute left-[202px] top-0 grid h-full w-[1150px] grid-cols-[595px_345px_1fr] gap-x-3">
|
||||
<div className="flex h-full flex-col justify-between py-3 pl-3">
|
||||
<ContentWidth className="grid h-full grid-cols-[190px_524px_573px] items-center gap-0">
|
||||
<div className="relative aspect-video h-auto w-[174px] shrink-0 justify-self-center overflow-hidden">
|
||||
<Image
|
||||
src={podcast.image}
|
||||
alt=""
|
||||
fill
|
||||
sizes="174px"
|
||||
className="object-cover transition-transform duration-700 group-hover:scale-105"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex h-full min-w-0 flex-col justify-center gap-1.5 py-3 pl-3">
|
||||
<div className="text-mono-s flex items-center gap-2.5">
|
||||
<span>{podcast.date}</span>
|
||||
<Dot />
|
||||
<span>{episodeLabel(podcast)}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="flex w-[333px] max-w-full items-center gap-2.5">
|
||||
<PlayIcon />
|
||||
<h3 className="font-sans text-[18px] leading-[1.15] tracking-[-0.01em]">
|
||||
<h3 className="line-clamp-2 font-sans text-[18px] leading-[1.15] tracking-[-0.01em]">
|
||||
{podcast.title}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div aria-hidden="true" />
|
||||
<div className="py-3">
|
||||
<UnderlineLabel>{listenOnApp}</UnderlineLabel>
|
||||
<div className="hidden items-start gap-[132px] md:flex">
|
||||
<div className="w-[345px] py-3" />
|
||||
<div className="shrink-0 py-3">
|
||||
<UnderlineLabel>{listenOnApp}</UnderlineLabel>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ContentWidth>
|
||||
</ExternalLink>
|
||||
)
|
||||
}
|
||||
@@ -537,11 +540,11 @@ function PastEpisodes({
|
||||
}) {
|
||||
return (
|
||||
<section className="bg-accent-tan text-brand-dark-green">
|
||||
<div className="px-3 pb-3">
|
||||
<ContentWidth className="pb-3">
|
||||
<h2 className="font-sans text-[36px] leading-none tracking-[-0.02em]">
|
||||
{copy.pastEpisodes}
|
||||
</h2>
|
||||
</div>
|
||||
</ContentWidth>
|
||||
{podcasts.slice(0, 4).map((podcast, index) => (
|
||||
<EpisodeRow
|
||||
key={podcast.href}
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { ReactNode } from 'react'
|
||||
|
||||
import { LogosMark } from '@acid-info/logos-ui'
|
||||
|
||||
import ContentWidth from '@/components/layout/content-width'
|
||||
import { Button, ButtonArrowIcon } from '@/components/ui'
|
||||
import { ROUTES } from '@/constants/routes'
|
||||
import { createTranslatedPageMetadata } from '@/lib/translated-page-metadata'
|
||||
@@ -77,6 +78,7 @@ export default async function NodeProgramPage({
|
||||
return (
|
||||
<main className="bg-brand-off-white">
|
||||
<section className="relative h-[483px] px-3 pt-6 text-brand-dark-green md:h-[487px]">
|
||||
<ContentWidth className="relative h-full">
|
||||
<div className="absolute top-6 left-3 h-[75px] w-[107px] overflow-hidden">
|
||||
<Image
|
||||
src="/images/node-programme/builders.jpg"
|
||||
@@ -113,6 +115,7 @@ export default async function NodeProgramPage({
|
||||
<p className="text-mono-s absolute top-[279px] left-[203px] w-[178px] md:top-[307px] md:left-1/2 md:w-[226px] md:translate-x-[6px]">
|
||||
{t('hero.body')}
|
||||
</p>
|
||||
</ContentWidth>
|
||||
</section>
|
||||
|
||||
<SectionShell className="grid gap-8 border-b border-brand-dark-green/10 py-10 md:grid-cols-12 md:gap-3 md:py-20">
|
||||
|
||||
@@ -4,6 +4,7 @@ import { LogosMark } from '@acid-info/logos-ui'
|
||||
|
||||
import { ROUTES } from '@/constants/routes'
|
||||
import { createDefaultMetadata } from '@/lib/metadata'
|
||||
import ContentWidth from '@/components/layout/content-width'
|
||||
import { PRESS_ORIGIN, getLatestPressPodcasts } from '@/lib/press-engine'
|
||||
|
||||
import { PodcastsSection } from '../press/_sections/podcasts'
|
||||
@@ -16,21 +17,21 @@ interface PodcastIntroCopy {
|
||||
|
||||
function PodcastIntro({ copy }: { copy: PodcastIntroCopy }) {
|
||||
return (
|
||||
<section className="h-[246px] bg-accent-tan px-3 pt-10 text-brand-dark-green md:h-[282px] md:pt-[60px]">
|
||||
<div className="grid w-full gap-6 md:grid-cols-12">
|
||||
<section className="h-[246px] bg-accent-tan px-3 pt-20 text-brand-dark-green md:h-[282px] md:pt-20">
|
||||
<ContentWidth className="grid w-full gap-6 md:grid-cols-12">
|
||||
<div className="flex items-center gap-3 md:col-span-5">
|
||||
<LogosMark size={20} className="shrink-0" />
|
||||
<h1 className="font-display text-[30px] leading-none tracking-[-0.03em] md:text-[36px]">
|
||||
{copy.title}
|
||||
</h1>
|
||||
</div>
|
||||
<div className="text-mono-s flex h-[122px] min-w-0 max-w-[369px] flex-col justify-between overflow-hidden text-black md:col-start-7 md:col-end-10 md:h-auto md:gap-6 md:overflow-visible">
|
||||
<p className="break-words line-clamp-6 md:line-clamp-none">
|
||||
<div className="text-mono-s flex min-w-0 flex-col justify-between text-black md:col-start-7 md:col-end-10 md:h-auto md:gap-6">
|
||||
<p className="line-clamp-6 break-words md:line-clamp-none">
|
||||
{copy.description}
|
||||
</p>
|
||||
<p>{copy.hostedBy}</p>
|
||||
</div>
|
||||
</div>
|
||||
</ContentWidth>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -71,7 +72,7 @@ export default async function LogosPodcastPage({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden bg-accent-tan">
|
||||
<div className="overflow-hidden bg-accent-tan pb-10">
|
||||
<PodcastIntro
|
||||
copy={{
|
||||
title: t('heading'),
|
||||
|
||||
@@ -3,25 +3,33 @@ import path from 'node:path'
|
||||
|
||||
import { describe, expect, test } from 'vitest'
|
||||
|
||||
const sectionDir = path.resolve(
|
||||
process.cwd(),
|
||||
'app/[locale]/press/_sections'
|
||||
)
|
||||
const sectionDir = path.resolve(process.cwd(), 'app/[locale]/press/_sections')
|
||||
const pressRouteDir = path.resolve(process.cwd(), 'app/[locale]/press')
|
||||
|
||||
const readSectionFile = (fileName: string) =>
|
||||
fs.readFileSync(path.join(sectionDir, fileName), 'utf8')
|
||||
const readPressRouteFile = (fileName: string) =>
|
||||
fs.readFileSync(path.join(pressRouteDir, fileName), 'utf8')
|
||||
|
||||
describe('press article row layout', () => {
|
||||
test('keeps list rows aligned to the Figma article-entry metrics', () => {
|
||||
const articlesSource = readSectionFile('articles.tsx')
|
||||
const atomsSource = readSectionFile('press-atoms.tsx')
|
||||
const podcastsSource = readSectionFile('podcasts.tsx')
|
||||
|
||||
expect(articlesSource).toContain('className="h-[107px]"')
|
||||
expect(articlesSource).toContain('className="h-[77px] w-[107px]')
|
||||
expect(podcastsSource).toContain('className="aspect-video h-auto w-[174px]')
|
||||
expect(atomsSource).toContain('width={107}')
|
||||
expect(atomsSource).toContain('height={77}')
|
||||
expect(articlesSource).not.toContain('size-[107px]')
|
||||
expect(podcastsSource).not.toContain('size-[107px]')
|
||||
expect(atomsSource).not.toContain("'absolute h-[77px]")
|
||||
expect(atomsSource).not.toContain('absolute aspect-video overflow-hidden')
|
||||
expect(articlesSource).toContain('md:grid-cols-[595px_543px]')
|
||||
expect(articlesSource).toContain('md:grid-cols-[107px_607px_543px]')
|
||||
expect(podcastsSource).toContain('md:grid-cols-[190px_524px_573px]')
|
||||
expect(articlesSource).toContain('md:gap-[132px]')
|
||||
expect(podcastsSource).toContain('md:gap-[132px]')
|
||||
})
|
||||
|
||||
test('renders the broadcast network panel as the Figma-linked route card', () => {
|
||||
@@ -30,9 +38,70 @@ describe('press article row layout', () => {
|
||||
expect(articlesSource).toContain('BROADCAST_BACKGROUND_IMAGE')
|
||||
expect(articlesSource).toContain('href={href}')
|
||||
expect(articlesSource).toContain('rounded-[100px]')
|
||||
expect(articlesSource).toContain(
|
||||
'className="bg-accent-tan pt-3 text-brand-dark-green min-[1440px]:pt-[100px]"'
|
||||
)
|
||||
expect(articlesSource).toContain('min-[1440px]:h-[406px]')
|
||||
expect(articlesSource).toContain('min-[1440px]:w-[702px]')
|
||||
expect(articlesSource).not.toContain('md:h-[406px]')
|
||||
expect(articlesSource).not.toContain('md:w-[702px]')
|
||||
expect(articlesSource).toContain('copy.latestEpisode')
|
||||
expect(articlesSource).not.toContain(
|
||||
'key={`${article.title}-broadcast-${index}`}'
|
||||
)
|
||||
})
|
||||
|
||||
test('keeps section CTAs inside the page content width with the requested offset', () => {
|
||||
const atomsSource = readSectionFile('press-atoms.tsx')
|
||||
|
||||
expect(atomsSource).toContain('mx-auto mt-3 flex h-24 max-w-[1440px]')
|
||||
})
|
||||
|
||||
test('links the podcast hero card to the latest episode', () => {
|
||||
const podcastsSource = readSectionFile('podcasts.tsx')
|
||||
|
||||
expect(podcastsSource).toContain('href={latestPodcast.href}')
|
||||
expect(podcastsSource).toContain('className="group block cursor-pointer"')
|
||||
expect(podcastsSource).toContain(
|
||||
'min-[1440px]:h-[380px] min-[1440px]:w-[453px] min-[1440px]:max-w-[453px]'
|
||||
)
|
||||
expect(podcastsSource).toContain(
|
||||
'top-[397px] flex h-[290px] w-[calc(100%-24px)]'
|
||||
)
|
||||
expect(podcastsSource).toContain(
|
||||
'min-[1440px]:right-3 min-[1440px]:top-3 min-[1440px]:block'
|
||||
)
|
||||
expect(podcastsSource).toContain('min-[1440px]:hidden')
|
||||
expect(podcastsSource).not.toContain('md:left-[356px]')
|
||||
expect(podcastsSource).toContain('const latestPodcast = podcasts[0]')
|
||||
expect(podcastsSource).toContain('const listPodcasts = podcasts.slice(1)')
|
||||
expect(podcastsSource).toContain('repeatToLength(listPodcasts, 8)')
|
||||
expect(podcastsSource).not.toContain('repeatToLength(podcasts, 8)')
|
||||
expect(podcastsSource).not.toContain('h-[2249px]')
|
||||
})
|
||||
|
||||
test('scrolls the broadcast nav item to the in-page broadcast section', () => {
|
||||
const pageSource = readPressRouteFile('page.tsx')
|
||||
const articlesSource = readSectionFile('articles.tsx')
|
||||
|
||||
expect(pageSource).toContain("navBroadcastHref: '#broadcast'")
|
||||
expect(articlesSource).toContain('id="broadcast"')
|
||||
expect(articlesSource).toContain('href={copy.navBroadcastHref}')
|
||||
})
|
||||
|
||||
test('keeps featured cards in mobile layout until the desktop breakpoint', () => {
|
||||
const articlesSource = readSectionFile('articles.tsx')
|
||||
const podcastsSource = readSectionFile('podcasts.tsx')
|
||||
|
||||
expect(articlesSource).toContain(
|
||||
'min-[1440px]:flex min-[1440px]:h-[1044px]'
|
||||
)
|
||||
expect(articlesSource).toContain('min-[1440px]:sticky min-[1440px]:top-10')
|
||||
expect(articlesSource).not.toContain('md:flex md:h-[1044px]')
|
||||
expect(articlesSource).not.toContain('md:sticky md:top-10')
|
||||
expect(podcastsSource).toContain('min-[1440px]:h-[430px]')
|
||||
expect(podcastsSource).toContain('min-[1440px]:h-[406px]')
|
||||
expect(podcastsSource).not.toContain('md:h-[430px]')
|
||||
expect(podcastsSource).not.toContain('md:h-[406px]')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import Image from 'next/image'
|
||||
import { LogosMark } from '@acid-info/logos-ui'
|
||||
|
||||
import ContentWidth from '@/components/layout/content-width'
|
||||
import { ExternalLink } from '@/components/ui'
|
||||
import { Link } from '@/i18n/navigation'
|
||||
import type { PressArticleRow } from '@/lib/press-engine'
|
||||
@@ -60,9 +61,9 @@ export function PressHero({
|
||||
>
|
||||
}) {
|
||||
return (
|
||||
<section className="relative h-[473px] bg-accent-tan px-3 pt-6 md:h-[359px] md:pt-6">
|
||||
<div className="flex w-full flex-col gap-[100px] md:gap-10">
|
||||
<div className="relative h-[81px] w-full md:w-[1186px]">
|
||||
<section className="relative h-[473px] bg-accent-tan pt-10 md:h-[359px] md:pt-2.5">
|
||||
<ContentWidth className="flex w-full flex-col gap-[100px] md:gap-10">
|
||||
<div className="relative h-[81px] w-full">
|
||||
<div className="absolute left-0 top-0 aspect-video w-[107px] overflow-hidden">
|
||||
<Image
|
||||
src={PRESS_HERO_IMAGE}
|
||||
@@ -81,7 +82,7 @@ export function PressHero({
|
||||
<span className="block">{copy.heroHeadingLine1}</span>
|
||||
<span className="block">{copy.heroHeadingLine2}</span>
|
||||
</h1>
|
||||
</div>
|
||||
</ContentWidth>
|
||||
<nav
|
||||
aria-label={copy.navLabel}
|
||||
className="absolute left-[calc(50%+6px)] top-[334px] flex flex-col items-start gap-2 text-brand-dark-green md:top-[342px] md:flex-row md:items-center md:gap-6"
|
||||
@@ -100,13 +101,13 @@ export function PressHero({
|
||||
</a>
|
||||
)
|
||||
})}
|
||||
<Link
|
||||
<a
|
||||
href={copy.navBroadcastHref}
|
||||
className="inline-flex cursor-pointer items-center gap-1 font-mono text-[10px] font-semibold uppercase leading-[1.35] transition-opacity hover:opacity-70"
|
||||
>
|
||||
<ArrowIcon direction="down" />
|
||||
{copy.navBroadcast}
|
||||
</Link>
|
||||
</a>
|
||||
</nav>
|
||||
</section>
|
||||
)
|
||||
@@ -125,18 +126,18 @@ export function ArticleEntry({
|
||||
|
||||
return (
|
||||
<PressRowLink href={article.href} index={index} className="h-[107px]">
|
||||
<RowThumbnail
|
||||
src={article.thumbnailImage}
|
||||
className="left-3 top-[15px] w-[107px] md:block"
|
||||
/>
|
||||
<div className="absolute left-[119px] top-0 flex h-full w-[274px] flex-col justify-center gap-1.5 py-3 pl-3 md:left-[119px] md:grid md:w-[1150px] md:grid-cols-[595px_543px] md:gap-x-3 md:p-0">
|
||||
<div className="flex flex-col justify-center gap-1.5 md:py-3 md:pl-3">
|
||||
<ContentWidth className="relative flex h-full items-center gap-3 md:grid md:grid-cols-[107px_607px_543px] md:gap-0">
|
||||
<RowThumbnail
|
||||
src={article.thumbnailImage}
|
||||
className="h-[77px] w-[107px] shrink-0 md:block"
|
||||
/>
|
||||
<div className="flex min-w-0 flex-1 flex-col justify-center gap-1.5 md:h-full md:w-[595px] md:gap-1.5 md:py-3 md:pl-3">
|
||||
<div className="text-mono-s flex items-center gap-2.5 text-brand-dark-green">
|
||||
<span>{article.date}</span>
|
||||
<Dot />
|
||||
<span>{article.author}</span>
|
||||
</div>
|
||||
<div className="w-[250px] text-[18px] leading-[1.15] tracking-[-0.01em] text-brand-dark-green md:w-full md:max-w-[333px]">
|
||||
<div className="w-full text-[18px] leading-[1.15] tracking-[-0.01em] text-brand-dark-green md:w-[333px]">
|
||||
{article.titleSerif ? (
|
||||
<>
|
||||
<span className="font-display block leading-[1.1]">
|
||||
@@ -149,36 +150,36 @@ export function ArticleEntry({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="hidden md:flex md:items-start md:gap-[132px]">
|
||||
<p className="text-mono-s w-[345px] py-3 text-brand-dark-green">
|
||||
<div className="hidden items-start md:flex md:gap-[132px]">
|
||||
<p className="text-mono-s line-clamp-3 w-[345px] py-3 text-brand-dark-green">
|
||||
{article.description}
|
||||
</p>
|
||||
<div className="shrink-0 py-3">
|
||||
<UnderlineLabel>{article.readingTime} min read</UnderlineLabel>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ContentWidth>
|
||||
</PressRowLink>
|
||||
)
|
||||
}
|
||||
|
||||
export function ArticlesHeading({ label }: { label: string }) {
|
||||
return (
|
||||
<div
|
||||
<ContentWidth
|
||||
id="articles"
|
||||
className="flex h-12 items-start px-3 text-brand-dark-green md:h-[88px] md:pt-10"
|
||||
>
|
||||
<h2 className="font-sans text-[36px] leading-none tracking-[-0.02em]">
|
||||
{label}
|
||||
</h2>
|
||||
</div>
|
||||
</ContentWidth>
|
||||
)
|
||||
}
|
||||
|
||||
export function GallerySection({ articles }: { articles: PressArticleRow[] }) {
|
||||
return (
|
||||
<section className="h-[319px] overflow-x-auto overflow-y-hidden bg-accent-tan md:h-auto md:overflow-visible md:px-3 md:py-10">
|
||||
<div className="flex w-max gap-3 py-10 pl-3 pr-3 md:grid md:w-auto md:grid-cols-4 md:p-0">
|
||||
<ContentWidth className="flex w-max gap-3 py-10 pl-3 pr-3 md:grid md:w-auto md:grid-cols-4 md:p-0">
|
||||
{articles.map((article) => (
|
||||
<ExternalLink
|
||||
key={article.title}
|
||||
@@ -205,7 +206,7 @@ export function GallerySection({ articles }: { articles: PressArticleRow[] }) {
|
||||
</div>
|
||||
</ExternalLink>
|
||||
))}
|
||||
</div>
|
||||
</ContentWidth>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -218,41 +219,43 @@ export function FeaturedArticle({
|
||||
readArticleLabel: string
|
||||
}) {
|
||||
return (
|
||||
<section className="relative h-[994px] overflow-hidden bg-accent-tan md:flex md:h-[1044px] md:justify-center md:gap-3 md:overflow-visible md:pr-3">
|
||||
<div className="absolute left-0 top-0 z-10 h-[313px] w-full px-3 pt-10 md:sticky md:top-10 md:h-[495px] md:flex-1 md:pl-[129px] md:pt-[100px]">
|
||||
<div className="flex max-w-[573px] flex-col gap-6 md:gap-[30px]">
|
||||
<div className="text-mono-s flex items-center gap-2.5 text-brand-off-white md:text-brand-dark-green">
|
||||
<span>{article.author}</span>
|
||||
<Dot className="bg-brand-off-white md:bg-brand-dark-green" />
|
||||
<span>{article.date}</span>
|
||||
</div>
|
||||
<h2 className="font-display max-w-[370px] text-[40px] leading-none tracking-[-0.03em] text-brand-off-white md:max-w-[464px] md:text-[56px] md:tracking-normal md:text-brand-dark-green">
|
||||
{article.title}
|
||||
</h2>
|
||||
<div className="flex flex-col gap-5">
|
||||
<p className="text-mono-s max-w-[370px] text-brand-off-white md:max-w-[456px] md:text-brand-dark-green">
|
||||
{article.description}
|
||||
</p>
|
||||
<TextLink
|
||||
href={article.href}
|
||||
label={`${readArticleLabel}: ${article.title}`}
|
||||
tone="light"
|
||||
className="md:text-brand-dark-green md:decoration-brand-dark-green/50"
|
||||
>
|
||||
{readArticleLabel}
|
||||
</TextLink>
|
||||
<section className="relative h-[994px] overflow-hidden bg-accent-tan min-[1440px]:flex min-[1440px]:h-[1044px] min-[1440px]:justify-center min-[1440px]:gap-3 min-[1440px]:overflow-visible min-[1440px]:pr-3">
|
||||
<ContentWidth className="relative h-full w-full min-[1440px]:flex min-[1440px]:h-full min-[1440px]:justify-center min-[1440px]:gap-3 min-[1440px]:overflow-visible">
|
||||
<div className="absolute left-0 top-0 z-10 h-[313px] w-full px-3 pt-10 min-[1440px]:sticky min-[1440px]:top-10 min-[1440px]:h-[495px] min-[1440px]:flex-1 min-[1440px]:pl-[129px] min-[1440px]:pt-[100px]">
|
||||
<div className="flex max-w-[573px] flex-col gap-6 min-[1440px]:gap-[30px]">
|
||||
<div className="text-mono-s flex items-center gap-2.5 text-brand-off-white min-[1440px]:text-brand-dark-green">
|
||||
<span>{article.author}</span>
|
||||
<Dot className="bg-brand-off-white min-[1440px]:bg-brand-dark-green" />
|
||||
<span>{article.date}</span>
|
||||
</div>
|
||||
<h2 className="font-display max-w-[370px] text-[40px] leading-none tracking-[-0.03em] text-brand-off-white min-[1440px]:max-w-[464px] min-[1440px]:text-[56px] min-[1440px]:tracking-normal min-[1440px]:text-brand-dark-green">
|
||||
{article.title}
|
||||
</h2>
|
||||
<div className="flex flex-col gap-5">
|
||||
<p className="text-mono-s max-w-[370px] text-brand-off-white min-[1440px]:max-w-[456px] min-[1440px]:text-brand-dark-green">
|
||||
{article.description}
|
||||
</p>
|
||||
<TextLink
|
||||
href={article.href}
|
||||
label={`${readArticleLabel}: ${article.title}`}
|
||||
tone="light"
|
||||
className="min-[1440px]:text-brand-dark-green min-[1440px]:decoration-brand-dark-green/50"
|
||||
>
|
||||
{readArticleLabel}
|
||||
</TextLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute inset-0 h-[994px] overflow-hidden md:relative md:inset-auto md:h-[994px] md:w-[714px] md:shrink-0">
|
||||
<Image
|
||||
src={article.featuredImage}
|
||||
alt=""
|
||||
width={1242}
|
||||
height={994}
|
||||
className="absolute left-[-303px] top-0 h-[1040px] w-[1300px] max-w-none object-cover md:left-[-104px] md:h-full md:w-[1242px]"
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute inset-0 h-[994px] overflow-hidden min-[1440px]:relative min-[1440px]:inset-auto min-[1440px]:h-[994px] min-[1440px]:w-[714px] min-[1440px]:shrink-0">
|
||||
<Image
|
||||
src={article.featuredImage}
|
||||
alt=""
|
||||
width={1242}
|
||||
height={994}
|
||||
className="absolute left-[-303px] top-0 h-[1040px] w-[1300px] max-w-none object-cover min-[1440px]:left-[-104px] min-[1440px]:h-full min-[1440px]:w-[1242px]"
|
||||
/>
|
||||
</div>
|
||||
</ContentWidth>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -280,14 +283,14 @@ export function BroadcastSection({
|
||||
return (
|
||||
<section
|
||||
id="broadcast"
|
||||
className="bg-accent-tan pt-[100px] text-brand-dark-green"
|
||||
className="bg-accent-tan pt-3 text-brand-dark-green min-[1440px]:pt-[100px]"
|
||||
>
|
||||
<div className="px-3 pb-3">
|
||||
<ContentWidth className="px-3 pb-3">
|
||||
<Link
|
||||
href={href}
|
||||
className="group relative block h-[560px] cursor-pointer overflow-hidden rounded-xl bg-brand-dark-green text-brand-off-white md:h-[406px]"
|
||||
className="group relative block h-[560px] cursor-pointer overflow-hidden rounded-xl bg-brand-dark-green text-brand-off-white min-[1440px]:h-[406px]"
|
||||
>
|
||||
<div className="absolute inset-0 blur-[30px] md:inset-auto md:left-[-39px] md:top-1/2 md:h-[1897px] md:w-[1518px] md:-translate-y-1/2">
|
||||
<div className="absolute inset-0 blur-[30px] min-[1440px]:inset-auto min-[1440px]:left-[-39px] min-[1440px]:top-1/2 min-[1440px]:h-[1897px] min-[1440px]:w-[1518px] min-[1440px]:-translate-y-1/2">
|
||||
<Image
|
||||
src={BROADCAST_BACKGROUND_IMAGE}
|
||||
alt=""
|
||||
@@ -298,7 +301,7 @@ export function BroadcastSection({
|
||||
</div>
|
||||
<div className="absolute inset-0 bg-black/40" />
|
||||
|
||||
<div className="absolute left-3 top-3 flex h-[calc(100%-24px)] w-[calc(100%-24px)] flex-col justify-between md:h-[380px] md:w-[453px]">
|
||||
<div className="absolute left-3 top-3 flex h-[calc(100%-24px)] w-[calc(100%-24px)] flex-col justify-between min-[1440px]:h-[380px] min-[1440px]:w-[453px]">
|
||||
<div className="flex items-center gap-[102px]">
|
||||
<LogosMark size={6} className="shrink-0 text-brand-off-white" />
|
||||
<p className="font-mono text-[10px] font-medium uppercase leading-[1.3] text-brand-off-white">
|
||||
@@ -306,11 +309,11 @@ export function BroadcastSection({
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-[226px] flex w-full flex-col gap-3 md:mb-0">
|
||||
<div className="mb-[226px] flex w-full flex-col gap-3 min-[1440px]:mb-0">
|
||||
<h2 className="w-[185px] font-sans text-[24px] leading-[1.1] tracking-[-0.01em] text-brand-off-white">
|
||||
{copy.broadcastHeading}
|
||||
</h2>
|
||||
<p className="font-mono text-[10px] leading-[1.3] text-brand-off-white md:w-full">
|
||||
<p className="font-mono text-[10px] leading-[1.3] text-brand-off-white min-[1440px]:w-full">
|
||||
{copy.broadcastDescription}
|
||||
</p>
|
||||
</div>
|
||||
@@ -326,14 +329,14 @@ export function BroadcastSection({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute left-3 right-3 top-[292px] h-[158px] overflow-hidden rounded-[72px] bg-accent-tan text-brand-dark-green transition-colors group-hover:bg-brand-yellow md:left-auto md:right-3 md:top-3 md:h-[382px] md:w-[702px] md:rounded-[100px]">
|
||||
<div className="absolute left-3 right-3 top-[292px] h-[158px] overflow-hidden rounded-[72px] bg-accent-tan text-brand-dark-green transition-colors group-hover:bg-brand-yellow min-[1440px]:left-auto min-[1440px]:right-3 min-[1440px]:top-3 min-[1440px]:h-[382px] min-[1440px]:w-[702px] min-[1440px]:rounded-[100px]">
|
||||
<span className="absolute left-1/2 top-1/2 inline-flex -translate-x-1/2 -translate-y-1/2 items-center gap-1 font-mono text-[10px] font-semibold uppercase leading-[1.35]">
|
||||
{copy.broadcastCta}
|
||||
<ArrowIcon />
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</ContentWidth>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,9 +5,12 @@
|
||||
import Image from 'next/image'
|
||||
import { LogosMark } from '@acid-info/logos-ui'
|
||||
|
||||
import ContentWidth from '@/components/layout/content-width'
|
||||
import { ExternalLink } from '@/components/ui'
|
||||
import { repeatToLength, type PressPodcastRow } from '@/lib/press-engine'
|
||||
|
||||
import {
|
||||
ArrowIcon,
|
||||
Dot,
|
||||
PlayIcon,
|
||||
PressRowLink,
|
||||
@@ -44,50 +47,61 @@ function PodcastHero({
|
||||
>
|
||||
}) {
|
||||
return (
|
||||
<div className="h-[723px] bg-accent-tan p-3 md:h-[430px]">
|
||||
<div className="relative h-[699px] overflow-hidden rounded-xl md:h-[406px]">
|
||||
<Image
|
||||
src="/images/press-engine/podcast-hero-bg.jpg"
|
||||
alt=""
|
||||
fill
|
||||
sizes="100vw"
|
||||
className="scale-110 object-cover object-center blur-[20px]"
|
||||
/>
|
||||
<div className="absolute left-3 top-3 flex h-[268px] w-[345px] max-w-[calc(100%-24px)] min-w-0 flex-col justify-between overflow-hidden text-brand-off-white md:h-[380px] md:w-[453px]">
|
||||
<div className="flex items-center gap-[102px]">
|
||||
<LogosMark size={6} className="shrink-0" />
|
||||
<span className="font-mono text-[10px] font-medium uppercase leading-[1.3]">
|
||||
{copy.media}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<h3 className="font-sans text-[24px] leading-[1.1] tracking-normal">
|
||||
{copy.heroTitle}
|
||||
</h3>
|
||||
<p className="text-mono-s line-clamp-5 max-w-[453px] text-brand-off-white md:line-clamp-7">
|
||||
{copy.heroDescription}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex min-w-0 items-center gap-2.5">
|
||||
<PlayIcon inverted />
|
||||
<span className="shrink-0 font-sans text-[14px] font-medium leading-[1.2]">
|
||||
{copy.latestEpisode}
|
||||
</span>
|
||||
<span className="min-w-0 truncate font-display text-[14px] leading-[1.2] text-gray-02">
|
||||
{latestPodcast.title}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute left-3 top-[342px] aspect-video w-[calc(100%-24px)] overflow-hidden rounded md:left-auto md:right-3 md:top-3 md:h-[382px] md:w-[702px] md:max-w-[calc(100%-24px)]">
|
||||
<div className="h-[723px] bg-accent-tan p-3 min-[1440px]:h-[430px]">
|
||||
<ExternalLink
|
||||
href={latestPodcast.href}
|
||||
className="group block cursor-pointer"
|
||||
>
|
||||
<ContentWidth className="relative h-[699px] overflow-hidden rounded-xl min-[1440px]:h-[406px]">
|
||||
<Image
|
||||
src={latestPodcast.image}
|
||||
src="/images/press-engine/podcast-hero-bg.jpg"
|
||||
alt=""
|
||||
fill
|
||||
sizes="(max-width: 768px) 369px, 702px"
|
||||
className="object-cover transition-transform duration-700 group-hover:scale-105"
|
||||
sizes="100vw"
|
||||
className="scale-110 object-cover object-center blur-[20px]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute left-3 top-3 flex h-[268px] w-[calc(100%-24px)] max-w-[345px] min-w-0 flex-col justify-between text-brand-off-white min-[1440px]:h-[380px] min-[1440px]:w-[453px] min-[1440px]:max-w-[453px]">
|
||||
<div className="flex items-center gap-[102px]">
|
||||
<LogosMark size={6} className="shrink-0" />
|
||||
<span className="font-mono text-[10px] font-medium uppercase leading-[1.3]">
|
||||
{copy.media}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<h3 className="w-[185px] font-sans text-[24px] leading-[1.1] tracking-normal min-[1440px]:w-auto">
|
||||
{copy.heroTitle}
|
||||
</h3>
|
||||
<p className="text-mono-s line-clamp-8 max-w-full text-brand-off-white min-[1440px]:line-clamp-7 min-[1440px]:max-w-[453px]">
|
||||
{copy.heroDescription}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex w-[302px] max-w-full min-w-0 items-center gap-2.5">
|
||||
<PlayIcon inverted />
|
||||
<span className="shrink-0 font-sans text-[14px] font-medium leading-[1.2]">
|
||||
{copy.latestEpisode}
|
||||
</span>
|
||||
<span className="min-w-0 truncate font-display text-[14px] leading-[1.2] text-gray-02">
|
||||
{latestPodcast.title}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute left-3 top-[397px] flex h-[290px] w-[calc(100%-24px)] items-center justify-center rounded-[100px] bg-accent-tan text-brand-dark-green min-[1440px]:hidden">
|
||||
<span className="inline-flex items-center gap-1 font-mono text-[10px] font-semibold uppercase leading-[1.35]">
|
||||
{copy.seeAllEpisodes}
|
||||
<ArrowIcon />
|
||||
</span>
|
||||
</div>
|
||||
<div className="absolute hidden overflow-hidden rounded min-[1440px]:bottom-auto min-[1440px]:left-auto min-[1440px]:right-3 min-[1440px]:top-3 min-[1440px]:block min-[1440px]:h-[382px] min-[1440px]:w-[702px] min-[1440px]:max-w-[calc(100%-24px)]">
|
||||
<Image
|
||||
src={latestPodcast.image}
|
||||
alt=""
|
||||
fill
|
||||
sizes="702px"
|
||||
className="object-cover transition-transform duration-700 group-hover:scale-105"
|
||||
/>
|
||||
</div>
|
||||
</ContentWidth>
|
||||
</ExternalLink>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -110,48 +124,32 @@ function PodcastEntry({
|
||||
: fallbackEpisode
|
||||
|
||||
return (
|
||||
<PressRowLink
|
||||
href={podcast.href}
|
||||
index={index}
|
||||
className="h-[174px] md:h-[131px]"
|
||||
>
|
||||
<RowThumbnail
|
||||
src={podcast.image}
|
||||
className="right-[11px] top-3 z-10 h-[60px] w-[107px] md:left-3 md:right-auto"
|
||||
/>
|
||||
<div className="absolute left-3 top-[13px] flex w-[238px] flex-col gap-2.5 md:hidden">
|
||||
<PlayIcon />
|
||||
<p className="font-sans text-[18px] leading-[1.15] tracking-[-0.01em]">
|
||||
{podcast.title}
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-mono-s absolute bottom-[15px] left-3 flex items-center gap-2.5 text-brand-dark-green md:hidden">
|
||||
<span>{podcast.date}</span>
|
||||
<Dot />
|
||||
<span>{episodeLabel}</span>
|
||||
</div>
|
||||
<div className="absolute bottom-[13px] right-[11px] md:hidden">
|
||||
<UnderlineLabel>{listenOnAppLabel}</UnderlineLabel>
|
||||
</div>
|
||||
<div className="hidden min-h-[131px] flex-col justify-between pb-3 pl-[131px] pr-3 pt-3 md:absolute md:left-[119px] md:top-0 md:grid md:h-[119px] md:w-[1150px] md:grid-cols-[595px_345px_1fr] md:gap-x-3 md:p-0">
|
||||
<div className="flex flex-col justify-between md:h-[131px] md:py-3 md:pl-3">
|
||||
<PressRowLink href={podcast.href} index={index} className="h-[107px]">
|
||||
<ContentWidth className="relative flex h-full items-center gap-3 md:grid md:grid-cols-[190px_524px_573px] md:gap-0">
|
||||
<RowThumbnail
|
||||
src={podcast.image}
|
||||
className="aspect-video h-auto w-[174px] shrink-0 justify-self-center"
|
||||
/>
|
||||
<div className="flex min-w-0 flex-1 flex-col justify-center gap-2.5 md:h-full md:w-[595px] md:gap-1.5 md:py-3 md:pl-3">
|
||||
<div className="text-mono-s flex items-center gap-2.5 text-brand-dark-green">
|
||||
<span>{podcast.date}</span>
|
||||
<Dot />
|
||||
<span>{episodeLabel}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="flex w-full items-center gap-2.5 md:w-[333px]">
|
||||
<PlayIcon />
|
||||
<p className="font-sans text-[18px] leading-[1.15] tracking-normal">
|
||||
<p className="line-clamp-2 font-sans text-[18px] leading-[1.15] tracking-normal">
|
||||
{podcast.title}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div />
|
||||
<div className="md:py-3">
|
||||
<UnderlineLabel>{listenOnAppLabel}</UnderlineLabel>
|
||||
<div className="hidden items-start md:flex md:gap-[132px]">
|
||||
<div className="w-[345px] py-3" />
|
||||
<div className="shrink-0 py-3">
|
||||
<UnderlineLabel>{listenOnAppLabel}</UnderlineLabel>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ContentWidth>
|
||||
</PressRowLink>
|
||||
)
|
||||
}
|
||||
@@ -165,20 +163,20 @@ export function PodcastsSection({
|
||||
copy: PodcastsCopy
|
||||
ctaHref: string
|
||||
}) {
|
||||
const repeatedPodcasts = repeatToLength(podcasts, 8)
|
||||
const latestPodcast = podcasts[0]
|
||||
const listPodcasts = podcasts.slice(1)
|
||||
const repeatedPodcasts =
|
||||
listPodcasts.length > 0 ? repeatToLength(listPodcasts, 8) : []
|
||||
|
||||
return (
|
||||
<section
|
||||
id="podcasts"
|
||||
className="h-[2249px] bg-accent-tan pt-[100px] md:h-auto"
|
||||
>
|
||||
<div className="flex h-[22px] items-center pl-3 md:h-[26px]">
|
||||
<section id="podcasts" className="bg-accent-tan pt-[100px]">
|
||||
<ContentWidth className="flex h-[22px] items-center pl-3 md:h-[26px]">
|
||||
<h2 className="font-sans text-[36px] leading-none tracking-[-0.02em] text-brand-dark-green">
|
||||
{copy.heading}
|
||||
</h2>
|
||||
</div>
|
||||
</ContentWidth>
|
||||
<div className="mt-3 bg-accent-tan">
|
||||
<PodcastHero latestPodcast={podcasts[0]} copy={copy} />
|
||||
<PodcastHero latestPodcast={latestPodcast} copy={copy} />
|
||||
{repeatedPodcasts.map((podcast, index) => (
|
||||
<PodcastEntry
|
||||
key={`${podcast.title}-${index}`}
|
||||
|
||||
@@ -134,7 +134,7 @@ export function RowThumbnail({
|
||||
className?: string
|
||||
}) {
|
||||
return (
|
||||
<div className={cn('absolute h-[77px] overflow-hidden', className)}>
|
||||
<div className={cn('h-[77px] overflow-hidden', className)}>
|
||||
<Image
|
||||
src={src}
|
||||
alt=""
|
||||
@@ -157,7 +157,7 @@ export function SectionCta({ href, label }: { href: string; label: string }) {
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="flex h-24 items-center justify-center px-3 pb-3">
|
||||
<div className="mx-auto mt-3 flex h-24 max-w-[1440px] items-center justify-center px-3 pb-3">
|
||||
{href.startsWith('http') ? (
|
||||
<ExternalLink href={href} className={className}>
|
||||
{content}
|
||||
|
||||
@@ -72,7 +72,7 @@ export default async function PressPage({
|
||||
navArticles: t('nav.articles'),
|
||||
navPodcasts: t('nav.podcasts'),
|
||||
navBroadcast: t('nav.broadcast'),
|
||||
navBroadcastHref: ROUTES.logosBroadcastNetwork,
|
||||
navBroadcastHref: '#broadcast',
|
||||
}}
|
||||
/>
|
||||
<section className="bg-accent-tan">
|
||||
|
||||
@@ -11,6 +11,10 @@ import BlockchainCryptarchia from '@/components/sections/blockchain/blockchain-c
|
||||
import BlockchainHero from '@/components/sections/blockchain/blockchain-hero'
|
||||
import BlockchainPrivacy from '@/components/sections/blockchain/blockchain-privacy'
|
||||
import TechStackBuilderCta from '@/components/sections/shared/tech-stack-builder-cta'
|
||||
import {
|
||||
TechStackDetailPage,
|
||||
TechStackDetailSection,
|
||||
} from '@/components/sections/shared/tech-stack-detail-layout'
|
||||
import TechStackExplorer from '@/components/sections/shared/tech-stack-explorer'
|
||||
import TechStackRelatedArticles from '@/components/sections/shared/tech-stack-related-articles'
|
||||
import { ROUTES } from '@/constants/routes'
|
||||
@@ -66,17 +70,27 @@ export default async function BlockchainPage({
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<TechStackDetailPage>
|
||||
<BlockchainHero data={hero} backHref={ROUTES.technologyStack} />
|
||||
<BlockchainPrivacy data={privacy} />
|
||||
<BlockchainCryptarchia data={cryptarchia} />
|
||||
<TechStackBuilderCta data={builderCta} />
|
||||
<TechStackRelatedArticles
|
||||
data={relatedArticles}
|
||||
articles={articles}
|
||||
sectionClassName="mt-0 pt-15 pb-15 md:mt-0 md:pt-[87px] md:pb-[93px]"
|
||||
/>
|
||||
<TechStackExplorer locale={locale} />
|
||||
</>
|
||||
<TechStackDetailSection>
|
||||
<BlockchainPrivacy data={privacy} />
|
||||
</TechStackDetailSection>
|
||||
<TechStackDetailSection>
|
||||
<BlockchainCryptarchia data={cryptarchia} />
|
||||
</TechStackDetailSection>
|
||||
<TechStackDetailSection>
|
||||
<TechStackBuilderCta data={builderCta} />
|
||||
</TechStackDetailSection>
|
||||
<TechStackDetailSection>
|
||||
<TechStackExplorer locale={locale} />
|
||||
</TechStackDetailSection>
|
||||
<TechStackDetailSection>
|
||||
<TechStackRelatedArticles
|
||||
data={relatedArticles}
|
||||
articles={articles}
|
||||
sectionClassName="mt-0 md:mt-0"
|
||||
/>
|
||||
</TechStackDetailSection>
|
||||
</TechStackDetailPage>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -12,6 +12,10 @@ import MessagingHero from '@/components/sections/messaging/messaging-hero'
|
||||
import MessagingIntro from '@/components/sections/messaging/messaging-intro'
|
||||
import MessagingTechStack from '@/components/sections/messaging/messaging-tech-stack'
|
||||
import TechStackBuilderCta from '@/components/sections/shared/tech-stack-builder-cta'
|
||||
import {
|
||||
TechStackDetailPage,
|
||||
TechStackDetailSection,
|
||||
} from '@/components/sections/shared/tech-stack-detail-layout'
|
||||
import TechStackRelatedArticles from '@/components/sections/shared/tech-stack-related-articles'
|
||||
import { ROUTES } from '@/constants/routes'
|
||||
import { createPageMetadata } from '@/lib/page-metadata'
|
||||
@@ -84,20 +88,27 @@ export default async function MessagingPage({
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<TechStackDetailPage>
|
||||
<MessagingHero data={hero} backHref={ROUTES.technologyStack} />
|
||||
<MessagingIntro privacy={privacy} lmn={lmn} censorship={censorship} />
|
||||
<MessagingCaseStudies data={caseStudies} />
|
||||
<TechStackBuilderCta
|
||||
data={builderCta}
|
||||
className="mt-15 md:mt-[100px] md:mb-[100px]"
|
||||
/>
|
||||
<MessagingTechStack locale={locale} />
|
||||
<TechStackRelatedArticles
|
||||
data={relatedArticles}
|
||||
articles={articles}
|
||||
sectionClassName="mt-0 md:mt-0"
|
||||
/>
|
||||
</>
|
||||
<TechStackDetailSection>
|
||||
<MessagingIntro privacy={privacy} lmn={lmn} censorship={censorship} />
|
||||
</TechStackDetailSection>
|
||||
<TechStackDetailSection>
|
||||
<MessagingCaseStudies data={caseStudies} />
|
||||
</TechStackDetailSection>
|
||||
<TechStackDetailSection>
|
||||
<TechStackBuilderCta data={builderCta} />
|
||||
</TechStackDetailSection>
|
||||
<TechStackDetailSection>
|
||||
<MessagingTechStack locale={locale} />
|
||||
</TechStackDetailSection>
|
||||
<TechStackDetailSection>
|
||||
<TechStackRelatedArticles
|
||||
data={relatedArticles}
|
||||
articles={articles}
|
||||
sectionClassName="mt-0 md:mt-0"
|
||||
/>
|
||||
</TechStackDetailSection>
|
||||
</TechStackDetailPage>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,10 @@ import NetworkingFeatures from '@/components/sections/networking/networking-feat
|
||||
import NetworkingHero from '@/components/sections/networking/networking-hero'
|
||||
import NetworkingIntro from '@/components/sections/networking/networking-intro'
|
||||
import TechStackBuilderCta from '@/components/sections/shared/tech-stack-builder-cta'
|
||||
import {
|
||||
TechStackDetailPage,
|
||||
TechStackDetailSection,
|
||||
} from '@/components/sections/shared/tech-stack-detail-layout'
|
||||
import TechStackRelatedArticles from '@/components/sections/shared/tech-stack-related-articles'
|
||||
import TechStackExplorer from '@/components/sections/shared/tech-stack-explorer'
|
||||
|
||||
@@ -67,16 +71,27 @@ export default async function NetworkingPage({
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<TechStackDetailPage>
|
||||
<NetworkingHero data={hero} backHref={ROUTES.technologyStack} />
|
||||
<NetworkingIntro data={intro} />
|
||||
<NetworkingFeatures data={features} />
|
||||
<TechStackBuilderCta
|
||||
data={builderCta}
|
||||
className="mt-15 mb-15 md:mt-25 md:mb-25"
|
||||
/>
|
||||
<TechStackExplorer locale={locale} />
|
||||
<TechStackRelatedArticles data={relatedArticles} articles={articles} />
|
||||
</>
|
||||
<TechStackDetailSection>
|
||||
<NetworkingIntro data={intro} />
|
||||
</TechStackDetailSection>
|
||||
<TechStackDetailSection>
|
||||
<NetworkingFeatures data={features} />
|
||||
</TechStackDetailSection>
|
||||
<TechStackDetailSection>
|
||||
<TechStackBuilderCta data={builderCta} />
|
||||
</TechStackDetailSection>
|
||||
<TechStackDetailSection>
|
||||
<TechStackExplorer locale={locale} />
|
||||
</TechStackDetailSection>
|
||||
<TechStackDetailSection>
|
||||
<TechStackRelatedArticles
|
||||
data={relatedArticles}
|
||||
articles={articles}
|
||||
sectionClassName="mt-0 md:mt-0"
|
||||
/>
|
||||
</TechStackDetailSection>
|
||||
</TechStackDetailPage>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -13,6 +13,10 @@ import StorageMain from '@/components/sections/storage/storage-main'
|
||||
import StorageTechStack from '@/components/sections/storage/storage-tech-stack'
|
||||
import StorageUseCases from '@/components/sections/storage/storage-use-cases'
|
||||
import TechStackBuilderCta from '@/components/sections/shared/tech-stack-builder-cta'
|
||||
import {
|
||||
TechStackDetailPage,
|
||||
TechStackDetailSection,
|
||||
} from '@/components/sections/shared/tech-stack-detail-layout'
|
||||
import TechStackRelatedArticles from '@/components/sections/shared/tech-stack-related-articles'
|
||||
|
||||
import { ROUTES } from '@/constants/routes'
|
||||
@@ -80,17 +84,30 @@ export default async function StoragePage({
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<TechStackDetailPage>
|
||||
<StorageHero data={hero} backHref={ROUTES.technologyStack} />
|
||||
<StorageMain data={main} />
|
||||
<StorageAccess data={access} />
|
||||
<StorageUseCases data={useCases} />
|
||||
<TechStackBuilderCta
|
||||
data={builderCta}
|
||||
className="mt-15 mb-15 md:mt-25 md:mb-25"
|
||||
/>
|
||||
<StorageTechStack locale={locale} />
|
||||
<TechStackRelatedArticles data={relatedArticles} articles={articles} />
|
||||
</>
|
||||
<TechStackDetailSection>
|
||||
<StorageMain data={main} />
|
||||
</TechStackDetailSection>
|
||||
<TechStackDetailSection>
|
||||
<StorageAccess data={access} />
|
||||
</TechStackDetailSection>
|
||||
<TechStackDetailSection>
|
||||
<StorageUseCases data={useCases} />
|
||||
</TechStackDetailSection>
|
||||
<TechStackDetailSection>
|
||||
<TechStackBuilderCta data={builderCta} />
|
||||
</TechStackDetailSection>
|
||||
<TechStackDetailSection>
|
||||
<StorageTechStack locale={locale} />
|
||||
</TechStackDetailSection>
|
||||
<TechStackDetailSection>
|
||||
<TechStackRelatedArticles
|
||||
data={relatedArticles}
|
||||
articles={articles}
|
||||
sectionClassName="mt-0 md:mt-0"
|
||||
/>
|
||||
</TechStackDetailSection>
|
||||
</TechStackDetailPage>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { getTranslations } from 'next-intl/server'
|
||||
import { LogosMark } from '@acid-info/logos-ui'
|
||||
import ContentWidth from '@/components/layout/content-width'
|
||||
import { ROUTES } from '@/constants/routes'
|
||||
import { createTranslatedPageMetadata } from '@/lib/translated-page-metadata'
|
||||
|
||||
@@ -19,10 +20,12 @@ export default async function WorkWithUsPage({
|
||||
const t = await getTranslations({ locale, namespace: NAMESPACE })
|
||||
return (
|
||||
<div className="px-3 pt-16 pb-12">
|
||||
<h1 className="text-h2 flex items-center gap-3 text-brand-dark-green">
|
||||
<LogosMark size={40} className="shrink-0" />
|
||||
{t('heading')}
|
||||
</h1>
|
||||
<ContentWidth>
|
||||
<h1 className="text-h2 flex items-center gap-3 text-brand-dark-green">
|
||||
<LogosMark size={40} className="shrink-0" />
|
||||
{t('heading')}
|
||||
</h1>
|
||||
</ContentWidth>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
import { describe, expect, test } from 'vitest'
|
||||
|
||||
function readComponent(path: string) {
|
||||
return readFileSync(fileURLToPath(new URL(`../${path}`, import.meta.url)), {
|
||||
encoding: 'utf8',
|
||||
})
|
||||
}
|
||||
|
||||
describe('Basecamp Figma typography contracts', () => {
|
||||
test('capability cards use the Figma sans title and body sizes', () => {
|
||||
const source = readComponent(
|
||||
'../components/sections/basecamp/basecamp-page.tsx'
|
||||
)
|
||||
|
||||
expect(source).toContain(
|
||||
'font-sans text-[24px] font-normal leading-[1.1] tracking-[-0.24px] text-brand-dark-green'
|
||||
)
|
||||
expect(source).toContain(
|
||||
'font-sans text-[14px] leading-[1.2] text-brand-dark-green'
|
||||
)
|
||||
expect(source).not.toContain(
|
||||
'<h3 className="text-subhead-sans text-brand-dark-green">'
|
||||
)
|
||||
expect(source).not.toContain(
|
||||
'<p className="text-body-s max-w-[329px] text-brand-dark-green">'
|
||||
)
|
||||
})
|
||||
|
||||
test('modular section heading matches the Figma H4 sans scale', () => {
|
||||
const source = readComponent(
|
||||
'../components/sections/basecamp/basecamp-page.tsx'
|
||||
)
|
||||
|
||||
expect(source).toContain(
|
||||
'<h2 className="font-sans text-[24px] font-normal leading-[1.1] tracking-[-0.24px] text-brand-dark-green">'
|
||||
)
|
||||
expect(source).not.toContain(
|
||||
'<h2 className="text-h3 text-brand-dark-green">'
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -1,35 +0,0 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
import { describe, expect, test } from 'vitest'
|
||||
|
||||
function readComponent(path: string) {
|
||||
return readFileSync(fileURLToPath(new URL(`../${path}`, import.meta.url)), {
|
||||
encoding: 'utf8',
|
||||
})
|
||||
}
|
||||
|
||||
describe('broadcast network page contract', () => {
|
||||
test('past episodes match the Figma row without descriptions', () => {
|
||||
const source = readComponent(
|
||||
'./[locale]/logos-broadcast-network/_sections/broadcast-network-page.tsx'
|
||||
)
|
||||
|
||||
expect(source).not.toContain('{podcast.description}')
|
||||
expect(source).toContain('h-[107px] w-[190px]')
|
||||
expect(source).not.toContain('size-[107px]')
|
||||
})
|
||||
|
||||
test('header background matches the press audio page tone', () => {
|
||||
const source = readComponent(
|
||||
'../components/site-header/site-header-client.tsx'
|
||||
)
|
||||
|
||||
expect(source).toContain('usesAccentTanHeaderTone')
|
||||
expect(source).toContain('ROUTES.logosBroadcastNetwork')
|
||||
expect(source).toContain('ROUTES.podcast')
|
||||
expect(source).toContain(
|
||||
"usesAccentTanHeaderTone && 'bg-accent-tan'"
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -1,31 +0,0 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
import { describe, expect, test } from 'vitest'
|
||||
|
||||
function readAppFile(path: string) {
|
||||
return readFileSync(fileURLToPath(new URL(`../${path}`, import.meta.url)), {
|
||||
encoding: 'utf8',
|
||||
})
|
||||
}
|
||||
|
||||
describe('home about mobile layout contract', () => {
|
||||
test('centres the mobile problem cards and closing copy in the viewport', () => {
|
||||
const aboutSection = readAppFile(
|
||||
'../components/sections/home/about-section.tsx'
|
||||
)
|
||||
|
||||
expect(aboutSection).not.toContain('left-[24px] w-[345px]')
|
||||
expect(aboutSection).not.toContain('left-3 flex w-[369px]')
|
||||
expect(aboutSection).toMatch(/left-1\/2[\s\S]*?-translate-x-1\/2/)
|
||||
})
|
||||
|
||||
test('lets mobile problem cards grow when facts wrap', () => {
|
||||
const aboutSection = readAppFile(
|
||||
'../components/sections/home/about-section.tsx'
|
||||
)
|
||||
|
||||
expect(aboutSection).toContain('grid min-h-[760px]')
|
||||
expect(aboutSection).not.toContain('grid h-[720px]')
|
||||
})
|
||||
})
|
||||
@@ -1,24 +0,0 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
import { describe, expect, test } from 'vitest'
|
||||
|
||||
function readAppFile(path: string) {
|
||||
return readFileSync(fileURLToPath(new URL(`../${path}`, import.meta.url)), {
|
||||
encoding: 'utf8',
|
||||
})
|
||||
}
|
||||
|
||||
describe('home feature cards contract', () => {
|
||||
test('uses the whole path card as the link target', () => {
|
||||
const source = readAppFile(
|
||||
'../components/sections/home/feature-cards-section.tsx'
|
||||
)
|
||||
|
||||
expect(source).toContain("import { Link } from '@/i18n/navigation'")
|
||||
expect(source).toContain('<Link')
|
||||
expect(source).toContain('href={card.href}')
|
||||
expect(source).toContain('cursor-pointer')
|
||||
expect(source).not.toContain('<Button\n href={card.href}')
|
||||
})
|
||||
})
|
||||
@@ -1,32 +0,0 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
import { describe, expect, test } from 'vitest'
|
||||
|
||||
function readAppFile(path: string) {
|
||||
return readFileSync(fileURLToPath(new URL(`../${path}`, import.meta.url)), {
|
||||
encoding: 'utf8',
|
||||
})
|
||||
}
|
||||
|
||||
describe('home heading motion contract', () => {
|
||||
test('large editorial section headings keep subtle reveal motion', () => {
|
||||
const builderPortal = readAppFile(
|
||||
'../components/sections/home/builder-portal-section.tsx'
|
||||
)
|
||||
const circlesCta = readAppFile(
|
||||
'../components/sections/home/circles-cta-section.tsx'
|
||||
)
|
||||
const parallelSociety = readAppFile(
|
||||
'../components/sections/home/parallel-society-section.tsx'
|
||||
)
|
||||
|
||||
for (const source of [builderPortal, circlesCta, parallelSociety]) {
|
||||
expect(source).toMatch(/<Reveal[\s\S]*?amount=\{0\.4\}/)
|
||||
expect(source).toMatch(/<Reveal[\s\S]*?delay=\{0\.18\}/)
|
||||
expect(source).toMatch(
|
||||
/<Reveal[\s\S]*?viewportMargin="0px 0px -20% 0px"/
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -1,56 +0,0 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
import { describe, expect, test } from 'vitest'
|
||||
|
||||
function readAppFile(path: string) {
|
||||
return readFileSync(fileURLToPath(new URL(`../${path}`, import.meta.url)), {
|
||||
encoding: 'utf8',
|
||||
})
|
||||
}
|
||||
|
||||
describe('home mobile Figma section contracts', () => {
|
||||
test('Basecamp mobile keeps the intro content in a tight Figma stack', () => {
|
||||
const source = readAppFile(
|
||||
'../components/sections/home/builder-portal-section.tsx'
|
||||
)
|
||||
|
||||
expect(source).toContain('py-[100px]')
|
||||
expect(source).toContain('gap-[40px]')
|
||||
expect(source).not.toContain('min-h-[531px]')
|
||||
})
|
||||
|
||||
test('Tech Stack mobile uses the Figma two-column diagram sizing', () => {
|
||||
const section = readAppFile(
|
||||
'../components/sections/home/tech-stack-section.tsx'
|
||||
)
|
||||
const source = readAppFile(
|
||||
'../components/sections/shared/tech-stack-diagram.tsx'
|
||||
)
|
||||
|
||||
expect(section).toContain("replace(' Technology Stack', '\\nTechnology Stack')")
|
||||
expect(source).toContain('grid-cols-2')
|
||||
expect(source).toContain('h-[111px]')
|
||||
expect(source).toContain('h-[258px]')
|
||||
expect(source).not.toContain('grid-cols-1 gap-3 sm:grid-cols-2')
|
||||
})
|
||||
|
||||
test('remaining mobile sections preserve their Figma vertical anchors', () => {
|
||||
const startBuilding = readAppFile(
|
||||
'../components/sections/home/start-building-section.tsx'
|
||||
)
|
||||
const circlesCta = readAppFile(
|
||||
'../components/sections/home/circles-cta-section.tsx'
|
||||
)
|
||||
const parallelSociety = readAppFile(
|
||||
'../components/sections/home/parallel-society-section.tsx'
|
||||
)
|
||||
|
||||
expect(startBuilding).toContain('h-[1236px]')
|
||||
expect(startBuilding).toContain('h-[319px]')
|
||||
expect(circlesCta).toContain('top-[448px]')
|
||||
expect(circlesCta).toContain('h-[720px]')
|
||||
expect(parallelSociety).toContain('top-[578px]')
|
||||
expect(parallelSociety).toContain('h-[440px]')
|
||||
})
|
||||
})
|
||||
@@ -1,54 +0,0 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
import { describe, expect, test } from 'vitest'
|
||||
|
||||
function readAppFile(path: string) {
|
||||
return readFileSync(fileURLToPath(new URL(`../${path}`, import.meta.url)), {
|
||||
encoding: 'utf8',
|
||||
})
|
||||
}
|
||||
|
||||
describe('lambda prize technology stack contract', () => {
|
||||
test('reuses the home technology stack section instead of local cards', () => {
|
||||
const page = readAppFile(
|
||||
'../app/[locale]/lambda-prize/_sections/lambda-prize-page.tsx'
|
||||
)
|
||||
|
||||
expect(page).toContain(
|
||||
"import TechStackSection from '@/components/sections/home/tech-stack-section'"
|
||||
)
|
||||
expect(page).not.toContain('function TechCard')
|
||||
expect(page).not.toContain('function WideTechCard')
|
||||
expect(page).not.toContain('interface TechCardCopy')
|
||||
})
|
||||
|
||||
test('keeps the design systems card sample on the shared diagram', () => {
|
||||
const page = readAppFile(
|
||||
'../app/[locale]/design-systems/_sections/components.tsx'
|
||||
)
|
||||
|
||||
expect(page).toContain(
|
||||
"import { TechStackDiagram } from '@/components/sections/shared/tech-stack-diagram'"
|
||||
)
|
||||
expect(page).not.toContain('function CardGrid')
|
||||
expect(page).not.toContain('const cardImages')
|
||||
expect(page).not.toContain('The Foundation: Logos Kernel')
|
||||
})
|
||||
|
||||
test('does not keep page-local technology stack message copies', () => {
|
||||
const messages = JSON.parse(readAppFile('../messages/en.json')) as {
|
||||
pages: {
|
||||
storage?: { techStack?: unknown }
|
||||
technologyStack: { stack: Record<string, unknown> }
|
||||
}
|
||||
}
|
||||
|
||||
expect(messages.pages.storage?.techStack).toBeUndefined()
|
||||
expect(Object.keys(messages.pages.technologyStack.stack).sort()).toEqual([
|
||||
'body',
|
||||
'titleLine1',
|
||||
'titleLine2',
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -1,89 +0,0 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
import { describe, expect, test } from 'vitest'
|
||||
|
||||
function readAppFile(path: string) {
|
||||
return readFileSync(fileURLToPath(new URL(`../${path}`, import.meta.url)), {
|
||||
encoding: 'utf8',
|
||||
})
|
||||
}
|
||||
|
||||
function readRepoFile(path: string) {
|
||||
return readFileSync(
|
||||
fileURLToPath(new URL(`../../../../${path}`, import.meta.url)),
|
||||
{
|
||||
encoding: 'utf8',
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
describe('messaging case studies layout contract', () => {
|
||||
test('allows revealed cards to grow in the desktop row', () => {
|
||||
const source = readAppFile(
|
||||
'../components/sections/messaging/messaging-case-studies.tsx'
|
||||
)
|
||||
|
||||
expect(source).toContain('md:mt-25')
|
||||
expect(source).toContain(
|
||||
'<RevealItem key={card.title} className="md:flex-1">'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('messaging page Figma layout contract', () => {
|
||||
test('uses the messaging-specific desktop hero height from Figma', () => {
|
||||
const source = readAppFile(
|
||||
'../components/sections/messaging/messaging-hero.tsx'
|
||||
)
|
||||
|
||||
expect(source).toContain('className="md:h-[453px]"')
|
||||
})
|
||||
|
||||
test('keeps the overview section tall enough for the desktop copy', () => {
|
||||
const source = readAppFile(
|
||||
'../components/sections/messaging/messaging-intro.tsx'
|
||||
)
|
||||
|
||||
expect(source).toContain('md:h-[235px]')
|
||||
expect(source).toContain('md:mb-[100px]')
|
||||
expect(source).toContain('privacy.mobileDescription')
|
||||
})
|
||||
|
||||
test('uses desktop Figma row and image dimensions for feature panels', () => {
|
||||
const source = readAppFile(
|
||||
'../components/sections/messaging/messaging-intro.tsx'
|
||||
)
|
||||
|
||||
expect(source).toContain('md:h-[359px]')
|
||||
expect(source).toContain('md:h-[335px] md:w-[702px]')
|
||||
})
|
||||
|
||||
test('keeps the builder CTA aligned to the desktop Figma spacing', () => {
|
||||
const source = readAppFile(
|
||||
'../app/[locale]/technology-stack/messaging/page.tsx'
|
||||
)
|
||||
|
||||
expect(source).toContain('md:mt-[100px]')
|
||||
expect(source).toContain('md:mb-[100px]')
|
||||
})
|
||||
|
||||
test('keeps the desktop Figma gap before related articles', () => {
|
||||
const source = readAppFile(
|
||||
'../components/sections/messaging/messaging-tech-stack.tsx'
|
||||
)
|
||||
|
||||
expect(source).toContain('md:mb-[100px]')
|
||||
})
|
||||
|
||||
test('keeps mobile-only Figma copy in content instead of component literals', () => {
|
||||
const schema = readRepoFile('packages/content/src/schemas/pages.ts')
|
||||
const content = readRepoFile(
|
||||
'content/pages/en/technology-stack-messaging.json'
|
||||
)
|
||||
|
||||
expect(schema).toContain('mobileDescription: z.string().min(1).optional()')
|
||||
expect(content).toContain('"mobileTitle": "Logos Messaging Nim (LMN)"')
|
||||
expect(content).toContain('"mobileEyebrow": "Privacy"')
|
||||
})
|
||||
})
|
||||
@@ -1,25 +0,0 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
import { describe, expect, test } from 'vitest'
|
||||
|
||||
function readAppFile(path: string) {
|
||||
return readFileSync(fileURLToPath(new URL(`../${path}`, import.meta.url)), {
|
||||
encoding: 'utf8',
|
||||
})
|
||||
}
|
||||
|
||||
describe('movement action cards Figma contract', () => {
|
||||
test('renders the three action card CTAs with the light Figma treatment', () => {
|
||||
const source = readAppFile(
|
||||
'../components/sections/movement/movement-page.tsx'
|
||||
)
|
||||
|
||||
expect(source).toMatch(
|
||||
/<Cta\s+href=\{card\.href\}\s+label=\{t\(`actions\.\$\{card\.key\}\.cta`\)\}\s+tone="light"/
|
||||
)
|
||||
expect(source).toContain(
|
||||
'className="mx-1.5 mt-auto w-[calc(100%-12px)]"'
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -1,52 +0,0 @@
|
||||
import { existsSync, readFileSync } from 'node:fs'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
import { describe, expect, test } from 'vitest'
|
||||
|
||||
function readAppFile(path: string) {
|
||||
return readFileSync(fileURLToPath(new URL(`../${path}`, import.meta.url)), {
|
||||
encoding: 'utf8',
|
||||
})
|
||||
}
|
||||
|
||||
function appFileExists(path: string) {
|
||||
return existsSync(fileURLToPath(new URL(`../${path}`, import.meta.url)))
|
||||
}
|
||||
|
||||
describe('technology stack builder CTA sections', () => {
|
||||
test('technology detail pages reuse one shared builder CTA component', () => {
|
||||
const routes = [
|
||||
'../app/[locale]/technology-stack/blockchain/page.tsx',
|
||||
'../app/[locale]/technology-stack/networking/page.tsx',
|
||||
'../app/[locale]/technology-stack/messaging/page.tsx',
|
||||
'../app/[locale]/technology-stack/storage/page.tsx',
|
||||
]
|
||||
|
||||
for (const route of routes) {
|
||||
const source = readAppFile(route)
|
||||
|
||||
expect(source).toContain(
|
||||
"@/components/sections/shared/tech-stack-builder-cta"
|
||||
)
|
||||
expect(source).toContain('<TechStackBuilderCta')
|
||||
}
|
||||
})
|
||||
|
||||
test('keeps only the shared builder CTA wrapper', () => {
|
||||
expect(
|
||||
appFileExists('../components/sections/blockchain/blockchain-builder-cta.tsx')
|
||||
).toBe(false)
|
||||
expect(
|
||||
appFileExists('../components/sections/networking/networking-builder-cta.tsx')
|
||||
).toBe(false)
|
||||
expect(
|
||||
appFileExists('../components/sections/messaging/messaging-builder-cta.tsx')
|
||||
).toBe(false)
|
||||
expect(
|
||||
appFileExists('../components/sections/storage/storage-builder-cta.tsx')
|
||||
).toBe(false)
|
||||
expect(
|
||||
appFileExists('../components/sections/shared/tech-stack-builder-cta.tsx')
|
||||
).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -1,75 +0,0 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
import { describe, expect, test } from 'vitest'
|
||||
|
||||
function readAppFile(path: string) {
|
||||
return readFileSync(fileURLToPath(new URL(`../${path}`, import.meta.url)), {
|
||||
encoding: 'utf8',
|
||||
})
|
||||
}
|
||||
|
||||
function readRepoFile(path: string) {
|
||||
return readFileSync(
|
||||
fileURLToPath(new URL(`../../../../${path}`, import.meta.url)),
|
||||
{
|
||||
encoding: 'utf8',
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
describe('tech stack diagram motion contract', () => {
|
||||
test('makes the whole stack item card the link target', () => {
|
||||
const diagram = readAppFile(
|
||||
'../components/sections/shared/tech-stack-diagram.tsx'
|
||||
)
|
||||
|
||||
expect(diagram).toContain("import { Link } from '@/i18n/navigation'")
|
||||
expect(diagram).toContain('<Link\n href={href}')
|
||||
expect(diagram).not.toContain('<Button\n href={href}')
|
||||
})
|
||||
|
||||
test('moves labels higher when a stack item reveals two detail cards', () => {
|
||||
const diagram = readAppFile(
|
||||
'../components/sections/shared/tech-stack-diagram.tsx'
|
||||
)
|
||||
|
||||
expect(diagram).toContain('details.length > 1')
|
||||
expect(diagram).toContain(
|
||||
"'md:group-hover/stack-item:-translate-y-24'"
|
||||
)
|
||||
expect(diagram).toContain("'md:group-hover/stack-item:-translate-y-8'")
|
||||
})
|
||||
|
||||
test('reserves mobile space for stack item detail cards', () => {
|
||||
const diagram = readAppFile(
|
||||
'../components/sections/shared/tech-stack-diagram.tsx'
|
||||
)
|
||||
|
||||
expect(diagram).toContain(
|
||||
"'items-end justify-between p-1.5 md:items-center md:justify-center md:px-6 md:py-0'"
|
||||
)
|
||||
expect(diagram).toContain('relative flex flex-col cursor-pointer')
|
||||
expect(diagram).toContain(
|
||||
"'h-[134px] w-full items-center justify-center px-3 py-[34px] text-center md:h-auto md:w-auto md:px-0 md:py-0'"
|
||||
)
|
||||
expect(diagram).toContain(
|
||||
'relative z-[1] flex w-full shrink-0 flex-col gap-1.5 md:hidden'
|
||||
)
|
||||
expect(diagram).not.toContain(
|
||||
'absolute right-1.5 bottom-1.5 left-1.5 z-[1] flex flex-col gap-1.5 md:hidden'
|
||||
)
|
||||
})
|
||||
|
||||
test('keeps the app install hover image shift subtle', () => {
|
||||
const giantSwitch = readRepoFile(
|
||||
'packages/ui/src/primitives/giant-switch/giant-switch.tsx'
|
||||
)
|
||||
|
||||
expect(giantSwitch).toContain(
|
||||
"const hoverShiftDistance = imagePosition === 'left' ? '12px' : '-12px'"
|
||||
)
|
||||
expect(giantSwitch).not.toContain('702px')
|
||||
expect(giantSwitch).not.toContain('opacity: 0')
|
||||
})
|
||||
})
|
||||
@@ -1,98 +0,0 @@
|
||||
import { existsSync, readFileSync } from 'node:fs'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
import { describe, expect, test } from 'vitest'
|
||||
|
||||
function readAppFile(path: string) {
|
||||
return readFileSync(fileURLToPath(new URL(`../${path}`, import.meta.url)), {
|
||||
encoding: 'utf8',
|
||||
})
|
||||
}
|
||||
|
||||
function appFileExists(path: string) {
|
||||
return existsSync(fileURLToPath(new URL(`../${path}`, import.meta.url)))
|
||||
}
|
||||
|
||||
function readRepoFile(path: string) {
|
||||
return readFileSync(
|
||||
fileURLToPath(new URL(`../../../../${path}`, import.meta.url)),
|
||||
{
|
||||
encoding: 'utf8',
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
describe('technology stack related articles sections', () => {
|
||||
test('technology detail pages reuse one shared section component', () => {
|
||||
const routes = [
|
||||
'../app/[locale]/technology-stack/blockchain/page.tsx',
|
||||
'../app/[locale]/technology-stack/networking/page.tsx',
|
||||
'../app/[locale]/technology-stack/messaging/page.tsx',
|
||||
'../app/[locale]/technology-stack/storage/page.tsx',
|
||||
]
|
||||
|
||||
for (const route of routes) {
|
||||
const source = readAppFile(route)
|
||||
|
||||
expect(source).toContain(
|
||||
"@/components/sections/shared/tech-stack-related-articles"
|
||||
)
|
||||
expect(source).toContain('<TechStackRelatedArticles')
|
||||
}
|
||||
})
|
||||
|
||||
test('keeps only shared related articles card building blocks', () => {
|
||||
expect(
|
||||
appFileExists('../components/sections/blockchain/blockchain-related-articles.tsx')
|
||||
).toBe(false)
|
||||
expect(
|
||||
appFileExists('../components/sections/networking/networking-related-articles.tsx')
|
||||
).toBe(false)
|
||||
expect(
|
||||
appFileExists('../components/sections/messaging/messaging-related-articles.tsx')
|
||||
).toBe(false)
|
||||
expect(
|
||||
appFileExists('../components/sections/storage/storage-related-articles.tsx')
|
||||
).toBe(false)
|
||||
expect(
|
||||
appFileExists('../components/sections/shared/tech-stack-related-articles.tsx')
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
test('keeps the Figma 12px gap between the tan card and footer', () => {
|
||||
const source = readAppFile(
|
||||
'../components/sections/shared/tech-stack-related-articles.tsx'
|
||||
)
|
||||
|
||||
expect(source).toContain('mx-auto h-220 max-w-360 px-3 py-3')
|
||||
expect(source).not.toContain('md:pb-25')
|
||||
expect(source).not.toContain('md:pb-3')
|
||||
})
|
||||
|
||||
test('uses the home press hover treatment and shows reading time for pinned cards', () => {
|
||||
const relatedCardSource = readAppFile(
|
||||
'../components/sections/shared/related-articles-card.tsx'
|
||||
)
|
||||
const relatedSectionSource = readAppFile(
|
||||
'../components/sections/shared/tech-stack-related-articles.tsx'
|
||||
)
|
||||
const schemaSource = readRepoFile('packages/content/src/schemas/pages.ts')
|
||||
|
||||
expect(relatedCardSource).toContain('duration-700')
|
||||
expect(relatedCardSource).toContain('group-hover:scale-[1.01]')
|
||||
expect(relatedCardSource).toContain('group-hover:blur-[4px]')
|
||||
expect(relatedCardSource).toContain('{readingTime} min read')
|
||||
expect(relatedSectionSource).toContain('readingTime: item.readingTime')
|
||||
expect(schemaSource).toContain('readingTime: z.number().int().positive()')
|
||||
|
||||
for (const page of [
|
||||
'technology-stack-blockchain',
|
||||
'technology-stack-messaging',
|
||||
'technology-stack-storage',
|
||||
]) {
|
||||
const source = readRepoFile(`content/pages/en/${page}.json`)
|
||||
|
||||
expect(source).toContain('"readingTime"')
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -1,28 +0,0 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
import { describe, expect, test } from 'vitest'
|
||||
|
||||
function readAppFile(path: string) {
|
||||
return readFileSync(fileURLToPath(new URL(`../${path}`, import.meta.url)), {
|
||||
encoding: 'utf8',
|
||||
})
|
||||
}
|
||||
|
||||
describe('technology stack mobile hero contract', () => {
|
||||
test('places the divider below the CTA stack and status after the divider', () => {
|
||||
const source = readAppFile(
|
||||
'../components/sections/technology-stack/tech-overview-hero.tsx'
|
||||
)
|
||||
|
||||
expect(source).toContain(
|
||||
'<div className="absolute top-[540px] left-0 h-10 w-full px-3 md:hidden">'
|
||||
)
|
||||
expect(source).toContain(
|
||||
'<div className="absolute top-[540px] left-0 w-full md:hidden">'
|
||||
)
|
||||
expect(source).not.toContain(
|
||||
'<div className="absolute top-[500px] left-0 h-10 w-full px-3 md:hidden">'
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,15 @@
|
||||
export default function ContentWidth({
|
||||
children,
|
||||
className = '',
|
||||
id,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
id?: string
|
||||
}) {
|
||||
return (
|
||||
<div id={id} className={`mx-auto p-3 max-w-[1440px] ${className}`}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import Image from 'next/image'
|
||||
import { getTranslations } from 'next-intl/server'
|
||||
|
||||
import ContentWidth from '@/components/layout/content-width'
|
||||
import { Button } from '@/components/ui'
|
||||
import { ROUTES } from '@/constants/routes'
|
||||
|
||||
@@ -35,7 +36,7 @@ export async function AboutCommunity() {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="mx-auto hidden h-[661px] max-w-[1440px] grid-cols-12 gap-3 px-3 py-[100px] md:grid">
|
||||
<ContentWidth className="hidden h-[661px] grid-cols-12 gap-3 px-3 py-[100px] md:grid">
|
||||
<div className="relative col-span-8 h-[461px] overflow-hidden rounded-[100px] bg-gray-01">
|
||||
<Image
|
||||
src="/images/about/map.webp"
|
||||
@@ -57,7 +58,7 @@ export async function AboutCommunity() {
|
||||
{t('cta')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</ContentWidth>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ import { getTranslations } from 'next-intl/server'
|
||||
|
||||
import { LogosMark } from '@acid-info/logos-ui'
|
||||
|
||||
import ContentWidth from '@/components/layout/content-width'
|
||||
|
||||
/**
|
||||
* About — initiative banner. Full-bleed photo card with centered λ mark and
|
||||
* caption rendered in serif H3 over the image.
|
||||
@@ -14,7 +16,7 @@ export async function AboutInitiative() {
|
||||
|
||||
return (
|
||||
<section className="bg-brand-off-white px-3 py-3">
|
||||
<div className="relative h-[675px] w-full overflow-hidden rounded-[24px] md:h-[800px]">
|
||||
<ContentWidth className="relative h-[675px] w-full overflow-hidden rounded-[24px] md:h-[800px]">
|
||||
<video
|
||||
aria-hidden="true"
|
||||
autoPlay
|
||||
@@ -33,7 +35,7 @@ export async function AboutInitiative() {
|
||||
<p className="text-h3-serif absolute top-[441px] left-3 max-w-[345px] text-brand-off-white md:left-[calc(50%+6px)] md:max-w-[464px]">
|
||||
{t('caption')}
|
||||
</p>
|
||||
</div>
|
||||
</ContentWidth>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { getTranslations } from 'next-intl/server'
|
||||
|
||||
import { LogosMark } from '@acid-info/logos-ui'
|
||||
|
||||
import ContentWidth from '@/components/layout/content-width'
|
||||
import { Button } from '@/components/ui'
|
||||
import { ROUTES } from '@/constants/routes'
|
||||
|
||||
@@ -18,53 +19,53 @@ export async function AboutOurWork() {
|
||||
|
||||
return (
|
||||
<section className="bg-brand-off-white pt-6 pb-24 md:pt-6 md:pb-[100px]">
|
||||
<div className="flex flex-col items-center gap-10 px-3 md:gap-[100px]">
|
||||
<div className="flex w-full max-w-[1416px] flex-col gap-10">
|
||||
<div className="flex flex-col gap-6 md:flex-row md:items-start md:justify-between md:gap-[131px]">
|
||||
<div className="relative h-[81px] w-[107px] shrink-0 overflow-hidden">
|
||||
<Image
|
||||
src="/images/about/work-mark.webp"
|
||||
alt=""
|
||||
fill
|
||||
sizes="107px"
|
||||
className="object-cover"
|
||||
/>
|
||||
<ContentWidth className="flex flex-col items-center gap-10 px-3 md:gap-[100px]">
|
||||
<div className="flex w-full flex-col gap-10">
|
||||
<div className="flex flex-col gap-6 md:flex-row md:items-start md:justify-between md:gap-[131px]">
|
||||
<div className="relative h-[81px] w-[107px] shrink-0 overflow-hidden">
|
||||
<Image
|
||||
src="/images/about/work-mark.webp"
|
||||
alt=""
|
||||
fill
|
||||
sizes="107px"
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-6 md:flex-row md:items-start md:gap-[131px]">
|
||||
<p className="text-mono-s text-brand-dark-green md:max-w-[226px]">
|
||||
{t('body')}
|
||||
</p>
|
||||
<Button href={ROUTES.press} variant="link" className="self-start">
|
||||
{t('ctaAll')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-6 md:flex-row md:items-start md:gap-[131px]">
|
||||
<p className="text-mono-s text-brand-dark-green md:max-w-[226px]">
|
||||
{t('body')}
|
||||
</p>
|
||||
<Button href={ROUTES.press} variant="link" className="self-start">
|
||||
{t('ctaAll')}
|
||||
</Button>
|
||||
</div>
|
||||
<h2 className="text-h2 text-brand-dark-green text-center">
|
||||
{t('title')}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<h2 className="text-h2 text-brand-dark-green text-center">
|
||||
{t('title')}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="grid w-full max-w-[1440px] grid-cols-1 gap-3 md:grid-cols-2">
|
||||
<Card
|
||||
eyebrow={t('cardEyebrow')}
|
||||
title={t('card1Title')}
|
||||
body={t('cardBody')}
|
||||
cta={t('cardCta')}
|
||||
image="/images/about/case-1.webp"
|
||||
imageRatio="aspect-[4/5]"
|
||||
/>
|
||||
<Card
|
||||
eyebrow={t('cardEyebrow')}
|
||||
title={t('card2Title')}
|
||||
body={t('cardBody')}
|
||||
cta={t('cardCta')}
|
||||
image="/images/about/case-2.webp"
|
||||
imageRatio="aspect-[5/4]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid w-full grid-cols-1 gap-3 md:grid-cols-2">
|
||||
<Card
|
||||
eyebrow={t('cardEyebrow')}
|
||||
title={t('card1Title')}
|
||||
body={t('cardBody')}
|
||||
cta={t('cardCta')}
|
||||
image="/images/about/case-1.webp"
|
||||
imageRatio="aspect-[4/5]"
|
||||
/>
|
||||
<Card
|
||||
eyebrow={t('cardEyebrow')}
|
||||
title={t('card2Title')}
|
||||
body={t('cardBody')}
|
||||
cta={t('cardCta')}
|
||||
image="/images/about/case-2.webp"
|
||||
imageRatio="aspect-[5/4]"
|
||||
/>
|
||||
</div>
|
||||
</ContentWidth>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import Image from 'next/image'
|
||||
import { getTranslations } from 'next-intl/server'
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
import ContentWidth from '@/components/layout/content-width'
|
||||
import { Button } from '@/components/ui'
|
||||
import { EXTERNAL_URLS } from '@/constants/routes'
|
||||
|
||||
@@ -52,7 +53,7 @@ export async function AboutWhoWeAre() {
|
||||
</div>
|
||||
|
||||
{/* Sticky title — h-167 mobile, h-474 desktop, top-68 in both */}
|
||||
<div className="sticky top-0 z-0 h-[167px] bg-brand-off-white md:h-[474px]">
|
||||
<div className="sticky top-0 z-0 h-[167px] bg-brand-off-white md:h-[252px]">
|
||||
<h2 className="text-h2 absolute top-[68px] right-0 left-0 text-center text-brand-dark-green">
|
||||
{t('title')}
|
||||
</h2>
|
||||
@@ -124,19 +125,17 @@ function Panel({
|
||||
}: PanelProps) {
|
||||
return (
|
||||
<div className="sticky top-0 h-[800px]">
|
||||
{/* Title — sticks up above the colored bar's top edge */}
|
||||
<h3
|
||||
className="text-h3-serif absolute left-3 -translate-y-full text-brand-dark-green md:left-[726px]"
|
||||
style={{ top: `${topOffset + 12}px` }}
|
||||
>
|
||||
{title}
|
||||
</h3>
|
||||
|
||||
{/* Colored bar */}
|
||||
<div
|
||||
className={`${bg} absolute inset-x-0 h-[598px]`}
|
||||
style={{ top: `${topOffset}px` }}
|
||||
>
|
||||
{/* Centered 1440 frame for the bar's content */}
|
||||
<ContentWidth className="relative h-full">
|
||||
{/* Title — inside the bar, top-left, cap-bottom aligned to 52px (Figma) */}
|
||||
<h3 className="text-h3-serif absolute top-[52px] left-3 -translate-y-full text-brand-dark-green md:left-[726px]">
|
||||
{title}
|
||||
</h3>
|
||||
{/* Portrait image — right on mobile, left on desktop */}
|
||||
<div className="absolute top-3 right-3 h-[151px] w-[122px] overflow-hidden rounded-[900px] md:right-auto md:left-3 md:h-[574px] md:w-[464px]">
|
||||
<Image
|
||||
@@ -188,6 +187,7 @@ function Panel({
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</ContentWidth>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -2,6 +2,7 @@ import { ExternalLink } from 'lucide-react'
|
||||
|
||||
import { LogosMark } from '@acid-info/logos-ui'
|
||||
|
||||
import ContentWidth from '@/components/layout/content-width'
|
||||
import type {
|
||||
ActiveCircleLocation,
|
||||
ActiveCircleStat,
|
||||
@@ -37,7 +38,7 @@ function SectionHeader({
|
||||
eyebrow?: string
|
||||
}) {
|
||||
return (
|
||||
<div className="grid gap-3 px-3 py-10 md:grid-cols-12">
|
||||
<ContentWidth className="grid gap-3 px-3 py-10 md:grid-cols-12">
|
||||
<h2 className="text-h3-serif text-brand-dark-green md:col-span-4">
|
||||
{title}
|
||||
</h2>
|
||||
@@ -46,7 +47,7 @@ function SectionHeader({
|
||||
{eyebrow}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</ContentWidth>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -85,7 +86,7 @@ function ActiveCirclesHero({ activeSinceDate }: { activeSinceDate: string }) {
|
||||
<div className="absolute left-3 top-6 flex size-[48px] items-center justify-center rounded-full border border-brand-off-white/30 md:left-1/2 md:-translate-x-1/2">
|
||||
<LogosMark size={17} />
|
||||
</div>
|
||||
<div className="grid min-h-[430px] grid-cols-2 gap-3 pb-12 md:min-h-[520px] md:grid-cols-12 md:pb-16">
|
||||
<ContentWidth className="grid min-h-[430px] grid-cols-2 gap-3 pb-12 md:min-h-[520px] md:grid-cols-12 md:pb-16">
|
||||
<p className="text-mono-s col-span-1 mt-28 max-w-[178px] text-brand-off-white/70 md:col-span-2 md:mt-16">
|
||||
Active in the last {ACTIVE_CIRCLES_DAYS} days, measured from{' '}
|
||||
{activeSinceDate}.
|
||||
@@ -101,7 +102,7 @@ function ActiveCirclesHero({ activeSinceDate }: { activeSinceDate: string }) {
|
||||
<p className="text-mono-s col-start-2 row-start-1 mt-28 max-w-[178px] text-brand-off-white/70 md:col-span-2 md:col-start-11 md:mt-16">
|
||||
Live event data from Logos Circle sources.
|
||||
</p>
|
||||
</div>
|
||||
</ContentWidth>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -113,11 +114,11 @@ function StatsSection({ stats }: { stats: ActiveCircleStat[] }) {
|
||||
title="Overview"
|
||||
eyebrow="A compact snapshot of circle activity and geographic reach."
|
||||
/>
|
||||
<div className="grid gap-3 px-3 pb-12 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<ContentWidth className="grid gap-3 px-3 pb-12 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{stats.map((stat) => (
|
||||
<StatCard key={stat.label} stat={stat} />
|
||||
))}
|
||||
</div>
|
||||
</ContentWidth>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -138,11 +139,11 @@ function LocationsSection({
|
||||
No active circles found.
|
||||
</p>
|
||||
) : (
|
||||
<div className="grid gap-3 px-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<ContentWidth className="grid gap-3 px-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{locations.map((location) => (
|
||||
<LocationLink key={location.label} location={location} />
|
||||
))}
|
||||
</div>
|
||||
</ContentWidth>
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
|
||||
@@ -11,6 +11,11 @@ import type {
|
||||
import { LogosMark } from '@acid-info/logos-ui'
|
||||
|
||||
import { IconMask } from '@/components/icons/icon-mask'
|
||||
import ContentWidth from '@/components/layout/content-width'
|
||||
import {
|
||||
TechStackDetailPage,
|
||||
TechStackDetailSection,
|
||||
} from '@/components/sections/shared/tech-stack-detail-layout'
|
||||
import { Button } from '@/components/ui'
|
||||
|
||||
interface BasecampPageProps {
|
||||
@@ -56,7 +61,7 @@ function HeroSectionView({ data }: { data: HeroSection }) {
|
||||
const bodyDetails = paragraphs(data.bodySecondary)
|
||||
|
||||
return (
|
||||
<section className="mx-auto grid w-full max-w-360 grid-cols-1 gap-12 px-3 pt-[90px] pb-10 md:min-h-[453px] md:grid-cols-2 md:gap-6">
|
||||
<section className="grid w-full grid-cols-1 gap-12 px-3 pt-8 pb-12 md:min-h-[453px] md:grid-cols-2 md:gap-6 md:pb-10">
|
||||
<div className="flex flex-col items-start gap-10">
|
||||
{data.eyebrow ? (
|
||||
<Button
|
||||
@@ -86,7 +91,7 @@ function HeroSectionView({ data }: { data: HeroSection }) {
|
||||
</p>
|
||||
) : null}
|
||||
{bodyDetails.length > 0 ? (
|
||||
<div className="text-mono-s flex max-w-[342px] flex-col gap-3 text-brand-dark-green">
|
||||
<div className="text-mono-s flex max-w-[342px] flex-col gap-4 text-brand-dark-green">
|
||||
{bodyDetails.map((item) => (
|
||||
<p key={item} className="whitespace-pre-line">
|
||||
{item}
|
||||
@@ -119,15 +124,15 @@ function HowItWorksSection({ data }: { data: TableSection }) {
|
||||
].filter((cta): cta is CTA => Boolean(cta))
|
||||
|
||||
return (
|
||||
<section className="mx-auto grid w-full max-w-360 gap-6 px-3 py-10 md:grid-cols-2 md:py-3">
|
||||
<section className="grid w-full gap-6 px-3 py-10 md:grid-cols-2 md:py-10">
|
||||
<div className="flex min-h-[626px] flex-col justify-between gap-8">
|
||||
<div>
|
||||
<h2 className="text-h3 mb-6 text-brand-dark-green">{data.title}</h2>
|
||||
<h2 className="text-h3 mb-8 text-brand-dark-green">{data.title}</h2>
|
||||
<div className="divide-y divide-brand-dark-green/20 border-y border-brand-dark-green/20">
|
||||
{data.rows.map((row) => (
|
||||
<article
|
||||
key={row.number}
|
||||
className="grid gap-4 py-3 md:grid-cols-2 md:gap-3"
|
||||
className="grid gap-4 py-4 md:grid-cols-2 md:gap-3"
|
||||
>
|
||||
<span className="text-mono-s text-brand-dark-green">
|
||||
{row.number}
|
||||
@@ -171,7 +176,7 @@ function HowItWorksSection({ data }: { data: TableSection }) {
|
||||
|
||||
function LocalFirstSection({ data }: { data: CtaPanelSection }) {
|
||||
return (
|
||||
<section className="mx-auto grid w-full max-w-360 gap-6 bg-gray-01 px-3 py-3 md:grid-cols-2">
|
||||
<section className="grid w-full gap-6 bg-gray-01 p-3 md:grid-cols-2">
|
||||
<div className="relative min-h-[357px] overflow-hidden rounded-xl">
|
||||
{data.image ? (
|
||||
<Image
|
||||
@@ -183,16 +188,16 @@ function LocalFirstSection({ data }: { data: CtaPanelSection }) {
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex min-h-[357px] flex-col justify-between gap-10 py-1">
|
||||
<div className="flex min-h-[357px] flex-col justify-between gap-10 py-0">
|
||||
{data.eyebrow ? (
|
||||
<div className="text-mono-s flex items-center gap-[102px] text-brand-dark-green">
|
||||
<LogosMark size={9} />
|
||||
<span>{data.eyebrow}</span>
|
||||
<span className="uppercase">{data.eyebrow}</span>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="max-w-[485px]">
|
||||
<h2 className="text-h3 mb-3 text-brand-dark-green">{data.title}</h2>
|
||||
<div className="text-body-s flex flex-col gap-3 text-brand-dark-green">
|
||||
<h2 className="text-h3 mb-4 text-brand-dark-green">{data.title}</h2>
|
||||
<div className="text-body-s flex flex-col gap-4 text-brand-dark-green">
|
||||
{paragraphs(data.description).map((item) => (
|
||||
<p key={item}>{item}</p>
|
||||
))}
|
||||
@@ -205,7 +210,7 @@ function LocalFirstSection({ data }: { data: CtaPanelSection }) {
|
||||
) : null}
|
||||
</div>
|
||||
{data.footerLabel ? (
|
||||
<div className="text-mono-s flex items-center gap-[102px] text-brand-dark-green">
|
||||
<div className="text-mono-s flex items-center gap-[102px] text-brand-dark-green opacity-0">
|
||||
<LogosMark size={9} />
|
||||
<span>{data.footerLabel}</span>
|
||||
</div>
|
||||
@@ -217,11 +222,13 @@ function LocalFirstSection({ data }: { data: CtaPanelSection }) {
|
||||
|
||||
function ModularSection({ data }: { data: FeaturedTextSection }) {
|
||||
return (
|
||||
<section className="mx-auto grid w-full max-w-360 gap-10 border-b border-brand-dark-green/10 px-3 py-10 md:min-h-[275px] md:grid-cols-2">
|
||||
<section className="grid w-full gap-10 border-b border-brand-dark-green/10 px-3 py-10 md:min-h-[275px] md:grid-cols-2">
|
||||
<h2 className="font-sans text-[24px] font-normal leading-[1.1] tracking-[-0.24px] text-brand-dark-green">
|
||||
{data.title.highlight} {data.title.rest}
|
||||
<span>{data.title.highlight}</span>
|
||||
<br />
|
||||
<span>{data.title.rest}</span>
|
||||
</h2>
|
||||
<div className="text-mono-s max-w-[345px] text-brand-dark-green">
|
||||
<div className="text-mono-s flex flex-col gap-4 max-w-[345px] text-brand-dark-green">
|
||||
{data.body?.map((item) => (
|
||||
<p key={item}>{item}</p>
|
||||
))}
|
||||
@@ -233,7 +240,7 @@ function ModularSection({ data }: { data: FeaturedTextSection }) {
|
||||
function CapabilityCard({ card }: { card: CardGridSection['cards'][number] }) {
|
||||
return (
|
||||
<article className="flex min-h-[250px] flex-col justify-between rounded-xl bg-gray-01 p-4">
|
||||
<div className="grid gap-3">
|
||||
<div className="grid gap-4">
|
||||
<h3 className="font-sans text-[24px] font-normal leading-[1.1] tracking-[-0.24px] text-brand-dark-green">
|
||||
{card.title}
|
||||
</h3>
|
||||
@@ -252,13 +259,13 @@ function CapabilityCard({ card }: { card: CardGridSection['cards'][number] }) {
|
||||
|
||||
function CapabilitiesSection({ data }: { data: CardGridSection }) {
|
||||
return (
|
||||
<section className="mx-auto w-full max-w-360 px-3 py-6 md:py-10">
|
||||
<section className="w-full px-3 py-10 md:py-10">
|
||||
{data.heading ? (
|
||||
<h2 className="mb-10 font-sans text-[24px] font-normal leading-[1.1] tracking-[-0.24px] text-brand-dark-green">
|
||||
<h2 className="mb-12 font-sans text-[24px] font-normal leading-[1.1] tracking-[-0.24px] text-brand-dark-green">
|
||||
{data.heading}
|
||||
</h2>
|
||||
) : null}
|
||||
<div className="grid gap-3 md:grid-cols-3">
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
{data.cards.map((card) => (
|
||||
<CapabilityCard key={card.title} card={card} />
|
||||
))}
|
||||
@@ -275,7 +282,7 @@ function ResourceCard({
|
||||
featured?: boolean
|
||||
}) {
|
||||
const content = (
|
||||
<div className="relative z-10 flex h-full flex-col items-center justify-center gap-3 px-8 text-center">
|
||||
<div className="relative z-10 flex h-full flex-col items-center justify-center gap-4 px-8 text-center">
|
||||
<h3
|
||||
className={
|
||||
featured
|
||||
@@ -301,8 +308,8 @@ function ResourceCard({
|
||||
cta={card.cta}
|
||||
className={
|
||||
featured
|
||||
? 'mt-3 cursor-pointer bg-brand-off-white text-brand-dark-green'
|
||||
: 'mt-3 cursor-pointer'
|
||||
? 'mt-4 cursor-pointer bg-brand-off-white text-brand-dark-green'
|
||||
: 'mt-4 cursor-pointer'
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
@@ -338,7 +345,7 @@ function ResourceCard({
|
||||
|
||||
function ResourcesSection({ data }: { data: CardGridSection }) {
|
||||
return (
|
||||
<section className="mx-auto grid w-full max-w-360 gap-3 px-3 py-10 md:grid-cols-3">
|
||||
<section className="w-full gap-4 px-3 py-10 grid md:grid-cols-3">
|
||||
{data.cards.map((card, index) => (
|
||||
<ResourceCard key={card.title} card={card} featured={index === 1} />
|
||||
))}
|
||||
@@ -355,13 +362,25 @@ export default function BasecampPage({
|
||||
resources,
|
||||
}: BasecampPageProps) {
|
||||
return (
|
||||
<>
|
||||
<HeroSectionView data={hero} />
|
||||
<HowItWorksSection data={howItWorks} />
|
||||
<LocalFirstSection data={localFirst} />
|
||||
<ModularSection data={modular} />
|
||||
<CapabilitiesSection data={capabilities} />
|
||||
<ResourcesSection data={resources} />
|
||||
</>
|
||||
<TechStackDetailPage>
|
||||
<ContentWidth className="!p-0">
|
||||
<HeroSectionView data={hero} />
|
||||
<TechStackDetailSection>
|
||||
<HowItWorksSection data={howItWorks} />
|
||||
</TechStackDetailSection>
|
||||
<TechStackDetailSection>
|
||||
<LocalFirstSection data={localFirst} />
|
||||
</TechStackDetailSection>
|
||||
<TechStackDetailSection>
|
||||
<ModularSection data={modular} />
|
||||
</TechStackDetailSection>
|
||||
<TechStackDetailSection>
|
||||
<CapabilitiesSection data={capabilities} />
|
||||
</TechStackDetailSection>
|
||||
<TechStackDetailSection>
|
||||
<ResourcesSection data={resources} />
|
||||
</TechStackDetailSection>
|
||||
</ContentWidth>
|
||||
</TechStackDetailPage>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import Image from 'next/image'
|
||||
|
||||
import type { CardGridSection } from '@repo/content/schemas'
|
||||
|
||||
import ContentWidth from '@/components/layout/content-width'
|
||||
import { Reveal, RevealItem } from '@/components/motion/reveal'
|
||||
import { Button } from '@/components/ui'
|
||||
|
||||
@@ -76,18 +77,22 @@ type Props = {
|
||||
|
||||
export default function BlockchainCryptarchia({ data }: Props) {
|
||||
return (
|
||||
<section className="bg-brand-off-white">
|
||||
<Reveal
|
||||
stagger
|
||||
amount={0.15}
|
||||
className="mx-auto grid max-w-360 gap-3 px-3 pt-7.5 pb-15 md:grid-cols-3 md:pt-0 md:pb-25"
|
||||
>
|
||||
{data.cards.map((card, index) => (
|
||||
<RevealItem key={card.title}>
|
||||
<OverviewCard card={card} index={index} />
|
||||
</RevealItem>
|
||||
))}
|
||||
</Reveal>
|
||||
<section>
|
||||
<div className="bg-brand-off-white">
|
||||
<ContentWidth>
|
||||
<Reveal
|
||||
stagger
|
||||
amount={0.15}
|
||||
className="grid gap-3 px-3 pt-7.5 pb-15 md:grid-cols-3 md:pt-0 md:pb-25"
|
||||
>
|
||||
{data.cards.map((card, index) => (
|
||||
<RevealItem key={card.title}>
|
||||
<OverviewCard card={card} index={index} />
|
||||
</RevealItem>
|
||||
))}
|
||||
</Reveal>
|
||||
</ContentWidth>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,88 +1,12 @@
|
||||
import { TechDetailHero } from '@acid-info/logos-ui'
|
||||
import type { HeroSection } from '@repo/content/schemas'
|
||||
|
||||
import { Reveal } from '@/components/motion/reveal'
|
||||
import { Button, ButtonArrowIcon } from '@/components/ui'
|
||||
import { Link } from '@/i18n/navigation'
|
||||
|
||||
import { DownloadIcon } from '../shared/builder-cta-card'
|
||||
import TechStackDetailHero from '@/components/sections/shared/tech-stack-detail-hero'
|
||||
|
||||
type Props = {
|
||||
data: HeroSection
|
||||
backHref: string
|
||||
}
|
||||
|
||||
export default function BlockchainHero({ data, backHref }: Props) {
|
||||
const [primaryCta, secondaryCta] = data.ctas ?? []
|
||||
|
||||
return (
|
||||
<Reveal amount={0.2}>
|
||||
<TechDetailHero
|
||||
title={data.headline}
|
||||
body={data.body}
|
||||
bodySecondary={data.bodySecondary}
|
||||
items={data.items}
|
||||
backLink={
|
||||
<Link
|
||||
href={backHref}
|
||||
className="inline-flex cursor-pointer items-center gap-1 text-brand-dark-green transition-opacity hover:opacity-70"
|
||||
>
|
||||
<span className="inline-flex size-3.75 shrink-0 rotate-180 items-center justify-center">
|
||||
<ButtonArrowIcon />
|
||||
</span>
|
||||
<span className="font-mono text-[10px] font-medium leading-[1.3] uppercase">
|
||||
{data.eyebrow}
|
||||
</span>
|
||||
</Link>
|
||||
}
|
||||
status={
|
||||
data.status
|
||||
? {
|
||||
label: data.status.label,
|
||||
body: data.status.body,
|
||||
cta: data.status.cta ? (
|
||||
<Button
|
||||
href={data.status.cta.href}
|
||||
variant={data.status.cta.variant ?? 'secondary'}
|
||||
icon={<DownloadIcon />}
|
||||
className="w-fit cursor-pointer rounded-none"
|
||||
>
|
||||
{data.status.cta.label}
|
||||
</Button>
|
||||
) : null,
|
||||
secondaryCta: data.status.secondaryCta ? (
|
||||
<Button
|
||||
href={data.status.secondaryCta.href}
|
||||
variant={data.status.secondaryCta.variant ?? 'tertiary'}
|
||||
className="w-fit cursor-pointer"
|
||||
>
|
||||
{data.status.secondaryCta.label}
|
||||
</Button>
|
||||
) : null,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
actions={
|
||||
<>
|
||||
{primaryCta ? (
|
||||
<Button
|
||||
href={primaryCta.href}
|
||||
variant={primaryCta.variant ?? 'tertiary'}
|
||||
>
|
||||
{primaryCta.label}
|
||||
</Button>
|
||||
) : null}
|
||||
{secondaryCta ? (
|
||||
<Button
|
||||
href={secondaryCta.href}
|
||||
variant={secondaryCta.variant ?? 'tertiary'}
|
||||
>
|
||||
{secondaryCta.label}
|
||||
</Button>
|
||||
) : null}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</Reveal>
|
||||
)
|
||||
export default function BlockchainHero(props: Props) {
|
||||
return <TechStackDetailHero {...props} actionVariant="tertiary" />
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { TechTextSplitSection } from '@acid-info/logos-ui'
|
||||
import type { CtaPanelSection } from '@repo/content/schemas'
|
||||
|
||||
import ContentWidth from '@/components/layout/content-width'
|
||||
import { Reveal } from '@/components/motion/reveal'
|
||||
|
||||
type Props = {
|
||||
@@ -10,10 +11,12 @@ type Props = {
|
||||
export default function BlockchainPrivacy({ data }: Props) {
|
||||
return (
|
||||
<Reveal amount={0.2}>
|
||||
<TechTextSplitSection
|
||||
<ContentWidth>
|
||||
<TechTextSplitSection
|
||||
title={data.eyebrow ?? data.title}
|
||||
body={data.description ? <p>{data.description}</p> : null}
|
||||
/>
|
||||
</ContentWidth>
|
||||
</Reveal>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -14,8 +14,8 @@ type Props = {
|
||||
export function BuildersHubBottomCta({ title, cta }: Props) {
|
||||
return (
|
||||
<section className="border-t border-brand-dark-green/10 bg-brand-off-white">
|
||||
<div className="mx-auto flex max-w-360 flex-col items-center gap-[26px] px-3 py-[100px] text-center">
|
||||
<h2 className="text-h3-serif md:text-h2 text-brand-dark-green leading-tight tracking-tight text-balance max-w-[20ch] md:max-w-[24ch]">
|
||||
<div className="mx-auto flex max-w-360 flex-col items-center gap-6 px-3 py-[100px] text-center">
|
||||
<h2 className="text-h3-serif text-brand-dark-green text-balance max-w-[20ch] md:max-w-[24ch]">
|
||||
{title}
|
||||
</h2>
|
||||
<Button
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
import type { BuilderHubSettings } from '@repo/content/schemas'
|
||||
import { LogosMark } from '@acid-info/logos-ui'
|
||||
|
||||
import ContentWidth from '@/components/layout/content-width'
|
||||
import { Button, ButtonArrowIcon } from '@/components/ui'
|
||||
import { ROUTES } from '@/constants/routes'
|
||||
import { Link } from '@/i18n/navigation'
|
||||
@@ -60,6 +61,7 @@ function BuildersHubHero({ hero }: { hero: BuilderHubSettings['hero'] }) {
|
||||
|
||||
return (
|
||||
<section className="relative h-[483px] px-3 pt-6 md:h-[487px]">
|
||||
<ContentWidth className="relative h-full">
|
||||
<div className="absolute top-6 left-3 h-[75px] w-[107px] overflow-hidden">
|
||||
<Image
|
||||
src="/images/builders-hub/hero.webp"
|
||||
@@ -100,6 +102,7 @@ function BuildersHubHero({ hero }: { hero: BuilderHubSettings['hero'] }) {
|
||||
{hero.description}
|
||||
</p>
|
||||
) : null}
|
||||
</ContentWidth>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -111,6 +114,7 @@ function JourneySection({
|
||||
}) {
|
||||
return (
|
||||
<section className="px-3 pb-[100px] md:pb-[100px]">
|
||||
<ContentWidth>
|
||||
<h2 className="text-[30px] leading-none tracking-[-0.02em] md:text-h3-sans">
|
||||
{data.title}
|
||||
</h2>
|
||||
@@ -133,6 +137,7 @@ function JourneySection({
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</ContentWidth>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -153,6 +158,7 @@ function SectionFrame({
|
||||
id={id}
|
||||
className="border-t border-brand-dark-green/10 px-3 pt-6 pb-[100px]"
|
||||
>
|
||||
<ContentWidth>
|
||||
<div className="flex items-baseline gap-3 whitespace-nowrap">
|
||||
<span className="font-display text-[30px] leading-none tracking-[-0.03em] text-brand-dark-green/50 md:text-[36px]">
|
||||
{index}
|
||||
@@ -162,6 +168,7 @@ function SectionFrame({
|
||||
</h2>
|
||||
</div>
|
||||
<div className="mt-10">{children}</div>
|
||||
</ContentWidth>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -256,7 +263,11 @@ function PrepareSection({
|
||||
alt={card.image.alt}
|
||||
fill
|
||||
sizes="(min-width: 768px) 50vw, 100vw"
|
||||
className="object-cover"
|
||||
className={
|
||||
card.imageFit === 'contain'
|
||||
? 'object-contain'
|
||||
: 'object-cover'
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
@@ -431,7 +442,7 @@ function AppInstallSection({
|
||||
}) {
|
||||
return (
|
||||
<section id="app-install" className="px-3 pb-10">
|
||||
<div className="relative flex min-h-[828px] flex-col overflow-hidden rounded-[100px] bg-gray-01 p-3 pb-14 md:block md:h-[590px] md:min-h-0 md:rounded-[200px] md:p-0">
|
||||
<ContentWidth className="relative flex min-h-[828px] flex-col overflow-hidden rounded-[100px] bg-gray-01 p-3 pb-14 md:block md:h-[590px] md:min-h-0 md:rounded-[200px] md:p-0">
|
||||
<div className="relative aspect-square w-full overflow-hidden rounded-[88px] md:absolute md:top-3 md:left-3 md:h-[566px] md:w-[566px] md:rounded-[188px]">
|
||||
<Image
|
||||
src={data.image.src}
|
||||
@@ -465,7 +476,7 @@ function AppInstallSection({
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ContentWidth>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -480,6 +491,7 @@ function DocumentationSection({
|
||||
id="resources"
|
||||
className="border-t border-brand-dark-green/10 bg-brand-off-white px-3 pt-[39px] pb-[100px] text-brand-dark-green"
|
||||
>
|
||||
<ContentWidth>
|
||||
<div className="md:grid md:grid-cols-[1fr_1fr] md:gap-3">
|
||||
<h2 className="text-h3-serif whitespace-nowrap">
|
||||
{data.title}
|
||||
@@ -524,6 +536,7 @@ function DocumentationSection({
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ContentWidth>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
BuildersHubBottomCta,
|
||||
BuildersHubListingHeader,
|
||||
} from '@/components/sections/builders-hub'
|
||||
import ContentWidth from '@/components/layout/content-width'
|
||||
import { IdeaCard } from '@/components/sections/builders-hub/idea-card'
|
||||
import { IdeaRow } from '@/components/sections/builders-hub/idea-row'
|
||||
import { RfpCard } from '@/components/sections/builders-hub/rfp-card'
|
||||
@@ -121,7 +122,7 @@ export function BuildersHubListingClient({ kind, settings, items }: Props) {
|
||||
className={
|
||||
kind === 'ideas'
|
||||
? 'mx-auto mt-[60px] max-w-360 px-3'
|
||||
: 'mx-auto mt-[90px] max-w-360 px-3 md:mt-[160px]'
|
||||
: 'mx-auto mt-[90px] max-w-360 px-3 md:mt-[138px]'
|
||||
}
|
||||
>
|
||||
<div className="grid grid-cols-1 gap-3 md:grid-cols-4">
|
||||
@@ -135,15 +136,17 @@ export function BuildersHubListingClient({ kind, settings, items }: Props) {
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<ul className="mt-0 w-full md:mt-[60px]">
|
||||
{kind === 'ideas'
|
||||
? (pageItems as Idea[]).map((idea, i) => (
|
||||
<IdeaRow key={idea.slug} index={start + i + 1} idea={idea} />
|
||||
))
|
||||
: (pageItems as Rfp[]).map((rfp, i) => (
|
||||
<RfpListRow key={rfp.slug} index={start + i + 1} rfp={rfp} />
|
||||
))}
|
||||
</ul>
|
||||
<ContentWidth>
|
||||
<ul className="mt-0 w-full md:mt-[60px]">
|
||||
{kind === 'ideas'
|
||||
? (pageItems as Idea[]).map((idea, i) => (
|
||||
<IdeaRow key={idea.slug} index={start + i + 1} idea={idea} />
|
||||
))
|
||||
: (pageItems as Rfp[]).map((rfp, i) => (
|
||||
<RfpListRow key={rfp.slug} index={start + i + 1} rfp={rfp} />
|
||||
))}
|
||||
</ul>
|
||||
</ContentWidth>
|
||||
)}
|
||||
|
||||
<div className="mt-12 flex justify-center pb-[100px]">
|
||||
|
||||
@@ -40,14 +40,14 @@ export function IdeaRow({ index, idea }: Props) {
|
||||
>
|
||||
{/* Mobile: 116 px stacked layout */}
|
||||
<div className="relative h-[116px] md:hidden">
|
||||
<p className="absolute top-3 left-3 w-[191px] font-sans text-[14px] leading-[1.2] text-brand-dark-green">
|
||||
<p className="absolute top-3 left-3 w-[179px] font-sans text-[14px] leading-[1.2] text-brand-dark-green">
|
||||
<span className="font-medium">{indexLabel}</span>
|
||||
<span className="ml-3 font-display">{idea.title}</span>
|
||||
</p>
|
||||
<p className="absolute top-[75px] left-3 w-[179px] font-mono text-[10px] leading-[1.3] text-brand-dark-green">
|
||||
{mobileDescription}
|
||||
</p>
|
||||
<span className="absolute top-3 right-7 inline-flex items-center justify-center rounded-[4px] border border-brand-dark-green/50 px-3 py-2 text-brand-dark-green">
|
||||
<span className="absolute top-3 right-[22px] inline-flex items-center justify-center border border-brand-dark-green/50 px-3 py-2 text-brand-dark-green">
|
||||
<span className="font-mono text-[10px] leading-[1.35] font-semibold uppercase whitespace-nowrap">
|
||||
Apply
|
||||
</span>
|
||||
@@ -73,7 +73,7 @@ export function IdeaRow({ index, idea }: Props) {
|
||||
<div className="relative hidden h-[50px] md:block">
|
||||
{/* Index + Title */}
|
||||
<div className="absolute top-3 left-3 flex items-baseline gap-3">
|
||||
<span className="font-sans text-[14px] font-medium leading-[1.2] text-brand-dark-green w-[18px]">
|
||||
<span className="font-sans text-[14px] font-normal leading-[1.2] text-brand-dark-green w-[18px]">
|
||||
{indexLabel}
|
||||
</span>
|
||||
<span className="font-display text-[14px] leading-[1.2] text-brand-dark-green whitespace-nowrap">
|
||||
|
||||
@@ -27,7 +27,7 @@ export function RfpCard({ rfp }: Props) {
|
||||
>
|
||||
{/* Title */}
|
||||
<div className="absolute left-4 top-4 w-[249px]">
|
||||
<h3 className="font-sans text-[24px] font-normal leading-[1.1] tracking-tight text-brand-dark-green">
|
||||
<h3 className="font-sans text-[24px] font-normal leading-[1.1] tracking-[-0.01em] text-brand-dark-green">
|
||||
{rfp.title}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
@@ -30,14 +30,14 @@ export function RfpListRow({ index, rfp }: Props) {
|
||||
>
|
||||
{/* Mobile */}
|
||||
<div className="relative h-[116px] md:hidden">
|
||||
<p className="absolute top-3 left-3 w-[191px] font-sans text-[14px] leading-[1.2] text-brand-dark-green">
|
||||
<p className="absolute top-3 left-3 w-[179px] font-sans text-[14px] leading-[1.2] text-brand-dark-green">
|
||||
<span className="font-medium">{indexLabel}</span>
|
||||
<span className="ml-3 font-display">{rfp.title}</span>
|
||||
</p>
|
||||
<p className="absolute top-[75px] left-3 w-[179px] font-mono text-[10px] leading-[1.3] text-brand-dark-green">
|
||||
{blurb}
|
||||
</p>
|
||||
<span className="absolute top-3 right-7 inline-flex items-center justify-center rounded-[4px] border border-brand-dark-green/50 px-3 py-2 text-brand-dark-green">
|
||||
<span className="absolute top-3 right-[22px] inline-flex items-center justify-center border border-brand-dark-green/50 px-3 py-2 text-brand-dark-green">
|
||||
<span className="font-mono text-[10px] leading-[1.35] font-semibold uppercase whitespace-nowrap">
|
||||
Learn More
|
||||
</span>
|
||||
@@ -56,7 +56,7 @@ export function RfpListRow({ index, rfp }: Props) {
|
||||
{/* Desktop */}
|
||||
<div className="relative hidden h-[50px] md:block">
|
||||
<div className="absolute top-3 left-3 flex items-baseline gap-3">
|
||||
<span className="font-sans text-[14px] font-medium leading-[1.2] text-brand-dark-green w-[18px]">
|
||||
<span className="font-sans text-[14px] font-normal leading-[1.2] text-brand-dark-green w-[18px]">
|
||||
{indexLabel}
|
||||
</span>
|
||||
<span className="font-display text-[14px] leading-[1.2] text-brand-dark-green whitespace-nowrap">
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import type { CirclesSettings, Language } from '@repo/content/schemas'
|
||||
import { LogosMark } from '@acid-info/logos-ui'
|
||||
|
||||
import ContentWidth from '@/components/layout/content-width'
|
||||
import { Button } from '@/components/ui'
|
||||
|
||||
import { IconMask } from '@/components/icons/icon-mask'
|
||||
@@ -63,6 +64,7 @@ function DetailHero({
|
||||
|
||||
return (
|
||||
<section className="relative h-auto min-h-[291px] bg-brand-off-white px-3 pt-10 md:h-[334px] md:pb-0">
|
||||
<ContentWidth className="relative h-full">
|
||||
<SmartLink
|
||||
href={circle.detailBackLink.href}
|
||||
className="absolute left-3 top-[-20px] inline-flex items-center gap-1 px-0 py-2 text-brand-dark-green"
|
||||
@@ -123,6 +125,7 @@ function DetailHero({
|
||||
>
|
||||
{settings.detailJoinCtaLabel}
|
||||
</Button>
|
||||
</ContentWidth>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -223,14 +226,18 @@ function SectionShell({
|
||||
children: ReactNode
|
||||
}) {
|
||||
return (
|
||||
<section className="mx-auto flex w-full max-w-[1440px] flex-col gap-3 px-3">
|
||||
<div className="flex justify-start md:justify-end">
|
||||
<h2 className="font-mono text-[10px] font-medium uppercase leading-[1.3] text-brand-dark-green">
|
||||
{title}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="h-px w-full bg-brand-dark-green/10" />
|
||||
{children}
|
||||
<section>
|
||||
<ContentWidth>
|
||||
<div className="flex flex-col gap-3 px-3">
|
||||
<div className="flex justify-start md:justify-end">
|
||||
<h2 className="font-mono text-[10px] font-medium uppercase leading-[1.3] text-brand-dark-green">
|
||||
{title}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="h-px w-full bg-brand-dark-green/10" />
|
||||
{children}
|
||||
</div>
|
||||
</ContentWidth>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import type { CirclesSettings, Language } from '@repo/content/schemas'
|
||||
import { LogosMark } from '@acid-info/logos-ui'
|
||||
|
||||
import ContentWidth from '@/components/layout/content-width'
|
||||
import { Button } from '@/components/ui'
|
||||
import { ROUTES } from '@/constants/routes'
|
||||
import { ROUTE_AVAILABILITY } from '@/constants/route-availability'
|
||||
@@ -40,7 +41,7 @@ function SectionIntro({
|
||||
cta?: { label: string; href: string; external?: boolean }
|
||||
}) {
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-3 px-3 py-10 md:grid-cols-12 md:py-10">
|
||||
<ContentWidth className="grid grid-cols-2 gap-3 px-3 py-10 md:grid-cols-12 md:py-10">
|
||||
<h2 className="font-display text-[30px] leading-none text-brand-dark-green md:col-span-4 md:text-[36px]">
|
||||
{title}
|
||||
</h2>
|
||||
@@ -61,7 +62,7 @@ function SectionIntro({
|
||||
{cta.label}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</ContentWidth>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -269,7 +270,7 @@ function EventsSection({
|
||||
cta={settings.eventsSection.calendarCta}
|
||||
/>
|
||||
|
||||
<div className="flex flex-col gap-6 pb-12">
|
||||
<ContentWidth className="flex flex-col gap-6 pb-12">
|
||||
{groups.map((group) => (
|
||||
<div
|
||||
key={group.groupKey}
|
||||
@@ -286,7 +287,7 @@ function EventsSection({
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ContentWidth>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -351,11 +352,11 @@ function InitiativesSection({
|
||||
title={settings.initiativesSection.title}
|
||||
cta={settings.initiativesSection.cta}
|
||||
/>
|
||||
<div className="grid gap-3 px-3 pb-12 md:grid-cols-3">
|
||||
<ContentWidth className="grid gap-3 px-3 pb-12 md:grid-cols-3">
|
||||
{initiatives.map((initiative) => (
|
||||
<InitiativeCard key={initiative.slug} initiative={initiative} />
|
||||
))}
|
||||
</div>
|
||||
</ContentWidth>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -378,8 +379,9 @@ function ResourcesSection({
|
||||
cta={settings.resourcesSection.helpCenterCta}
|
||||
/>
|
||||
|
||||
<ul className="w-full">
|
||||
{resources.map((resource, index) => (
|
||||
<ContentWidth>
|
||||
<ul className="w-full">
|
||||
{resources.map((resource, index) => (
|
||||
<li key={resource.slug}>
|
||||
<SmartLink
|
||||
href={resource.href}
|
||||
@@ -410,7 +412,8 @@ function ResourcesSection({
|
||||
</SmartLink>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</ul>
|
||||
</ContentWidth>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { getTranslations } from 'next-intl/server'
|
||||
import Image from 'next/image'
|
||||
|
||||
import ContentWidth from '@/components/layout/content-width'
|
||||
import { Button } from '@/components/ui'
|
||||
import { ROUTES } from '@/constants/routes'
|
||||
import { ROUTE_AVAILABILITY } from '@/constants/route-availability'
|
||||
@@ -138,10 +139,12 @@ export default async function AboutSection({ locale }: { locale: string }) {
|
||||
|
||||
<AboutScrollStack intro={t('intro')} cards={cards} />
|
||||
|
||||
<div className="absolute top-[calc(100vh+3713px)] left-1/2 flex w-[calc(100%-24px)] max-w-[369px] -translate-x-1/2 flex-col items-center gap-[60px] text-center md:relative md:top-auto md:w-[940px] md:max-w-none md:pt-[120px] md:pb-[360px]">
|
||||
<p className="text-h3-serif">
|
||||
{t('closing1')} {t('closing2')} {t('closing3')}
|
||||
</p>
|
||||
<ContentWidth className="absolute top-[calc(100vh+3713px)] left-1/2 flex w-[calc(100%-24px)] -translate-x-1/2 flex-col items-center gap-[60px] text-center md:relative md:top-auto md:left-auto md:flex md:w-full md:translate-x-0 md:pt-[120px] md:pb-[360px]">
|
||||
<div className="text-h3-serif flex max-w-[369px] flex-col gap-[1em] md:max-w-none">
|
||||
<p>{t('closing1')}</p>
|
||||
<p>{t('closing2')}</p>
|
||||
<p>{t('closing3')}</p>
|
||||
</div>
|
||||
|
||||
{ROUTE_AVAILABILITY.about ? (
|
||||
<Button
|
||||
@@ -151,7 +154,7 @@ export default async function AboutSection({ locale }: { locale: string }) {
|
||||
{t('cta')}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</ContentWidth>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { getTranslations } from 'next-intl/server'
|
||||
import Image from 'next/image'
|
||||
|
||||
import ContentWidth from '@/components/layout/content-width'
|
||||
import { Reveal } from '@/components/motion/reveal'
|
||||
import { Button } from '@/components/ui'
|
||||
import { ROUTES } from '@/constants/routes'
|
||||
@@ -43,9 +44,9 @@ export default async function BuilderPortalSection({
|
||||
const t = await getTranslations({ locale, namespace: 'home.builderPortal' })
|
||||
|
||||
return (
|
||||
<section className="relative border-t border-brand-dark-green/10 bg-brand-off-white py-[100px] md:h-[1045px] md:py-0">
|
||||
<div className="px-3 md:absolute md:inset-x-0 md:top-[212px] md:px-3">
|
||||
<div className="grid gap-3 md:grid-cols-[464px_minmax(0,940px)]">
|
||||
<section className="border-t border-brand-dark-green/10 bg-brand-off-white">
|
||||
<ContentWidth className="py-[100px] md:py-[212px]">
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
<div className="flex flex-col gap-[40px] md:min-h-[532px] md:justify-between md:gap-0">
|
||||
<div className="flex flex-col gap-[30px]">
|
||||
<Reveal
|
||||
@@ -67,7 +68,7 @@ export default async function BuilderPortalSection({
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<p className="text-mono-s whitespace-pre-line text-brand-dark-green md:w-[345px] md:font-sans md:text-[14px] md:leading-[1.2] md:font-medium">
|
||||
<p className="text-mono-s whitespace-pre-line text-brand-dark-green md:w-[345px]">
|
||||
{t('description')}
|
||||
</p>
|
||||
</div>
|
||||
@@ -102,7 +103,7 @@ export default async function BuilderPortalSection({
|
||||
imageClassName="scale-125"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ContentWidth>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { FeaturedTextSection } from '@repo/content/schemas'
|
||||
|
||||
import ContentWidth from '@/components/layout/content-width'
|
||||
import { Reveal } from '@/components/motion/reveal'
|
||||
import { Button } from '@/components/ui'
|
||||
import { fetchCircleEvents } from '@/lib/circle-events'
|
||||
@@ -14,52 +15,54 @@ export default async function CirclesCtaSection({ data }: Props) {
|
||||
const events = await fetchCircleEvents()
|
||||
|
||||
return (
|
||||
<section className="relative h-[1180px] overflow-hidden bg-brand-off-white md:h-[1225px]">
|
||||
<Reveal
|
||||
amount={0.4}
|
||||
delay={0.18}
|
||||
viewportMargin="0px 0px -20% 0px"
|
||||
className="absolute top-[100px] left-0 w-full"
|
||||
>
|
||||
<h2 className="text-h1 flex flex-col items-center gap-[6px] text-center text-brand-dark-green md:gap-0">
|
||||
<span>{data.title.highlight}</span>
|
||||
<span className="md:-mt-1">{data.title.rest}</span>
|
||||
</h2>
|
||||
</Reveal>
|
||||
<section className="bg-brand-off-white">
|
||||
<ContentWidth className="flex flex-col items-center gap-[155px] pt-[100px] pb-[100px] md:gap-[45px] md:pt-[100px] md:pb-[100px]">
|
||||
<Reveal
|
||||
amount={0.4}
|
||||
delay={0.18}
|
||||
viewportMargin="0px 0px -20% 0px"
|
||||
className="w-full"
|
||||
>
|
||||
<h2 className="text-h1 flex flex-col items-center gap-[6px] text-center text-brand-dark-green md:gap-0">
|
||||
<span>{data.title.highlight}</span>
|
||||
<span className="md:-mt-1">{data.title.rest}</span>
|
||||
</h2>
|
||||
</Reveal>
|
||||
|
||||
{data.body && data.body.length > 0 ? (
|
||||
<div className="text-mono-s absolute top-[255px] left-3 flex w-[369px] flex-col gap-3 text-center text-brand-dark-green md:top-[300px] md:left-1/2 md:w-[378px] md:-translate-x-1/2">
|
||||
{data.body.map((paragraph, i) => (
|
||||
<p key={i}>{paragraph}</p>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="absolute top-[347px] left-[61px] flex items-baseline gap-3 md:top-[392px] md:left-1/2 md:-translate-x-1/2">
|
||||
{data.cta ? (
|
||||
<Button
|
||||
href={data.cta.href}
|
||||
className="cursor-pointer transition-opacity hover:opacity-70"
|
||||
>
|
||||
{data.cta.label}
|
||||
</Button>
|
||||
{data.body && data.body.length > 0 ? (
|
||||
<div className="text-mono-s flex w-full max-w-[378px] flex-col gap-3 text-center text-brand-dark-green">
|
||||
{data.body.map((paragraph, i) => (
|
||||
<p key={i}>{paragraph}</p>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
{data.secondaryCta ? (
|
||||
<Button
|
||||
href={data.secondaryCta.href}
|
||||
variant="link"
|
||||
className="cursor-pointer transition-opacity hover:opacity-70"
|
||||
>
|
||||
{data.secondaryCta.label}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-01 absolute top-[448px] left-3 h-[720px] w-[369px] overflow-hidden rounded-[100px] md:top-[493px] md:left-1/2 md:h-[720px] md:w-[1416px] md:-translate-x-1/2">
|
||||
<div className="h-[710px] w-full">
|
||||
<CirclesMapLoader events={events} />
|
||||
<div className="flex items-baseline gap-3">
|
||||
{data.cta ? (
|
||||
<Button
|
||||
href={data.cta.href}
|
||||
className="cursor-pointer transition-opacity hover:opacity-70"
|
||||
>
|
||||
{data.cta.label}
|
||||
</Button>
|
||||
) : null}
|
||||
{data.secondaryCta ? (
|
||||
<Button
|
||||
href={data.secondaryCta.href}
|
||||
variant="link"
|
||||
className="cursor-pointer transition-opacity hover:opacity-70"
|
||||
>
|
||||
{data.secondaryCta.label}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-[720px] w-full overflow-hidden rounded-[100px] bg-gray-01">
|
||||
<div className="h-[710px] w-full">
|
||||
<CirclesMapLoader events={events} />
|
||||
</div>
|
||||
</div>
|
||||
</ContentWidth>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import Image from 'next/image'
|
||||
import { useTranslations } from 'next-intl'
|
||||
|
||||
import ContentWidth from '@/components/layout/content-width'
|
||||
import { LogosMark } from '@acid-info/logos-ui'
|
||||
|
||||
import { ButtonArrowIcon } from '@/components/ui'
|
||||
@@ -97,26 +98,26 @@ export default function FeatureCardsSection() {
|
||||
]
|
||||
|
||||
return (
|
||||
<section className="relative hidden h-[848px] border-t border-brand-dark-green/10 bg-brand-off-white md:block">
|
||||
<div>
|
||||
<p className="text-mono-s absolute top-6 left-[calc(50%+6px)] w-[226px] text-brand-dark-green">
|
||||
<section className="hidden border-t border-brand-dark-green/10 bg-brand-off-white md:block">
|
||||
<ContentWidth className="flex flex-col items-center gap-[118px] pt-6 pb-[100px]">
|
||||
<p className="text-mono-s w-[226px] text-center text-brand-dark-green">
|
||||
{t('kicker')}
|
||||
</p>
|
||||
|
||||
<h2 className="text-h2 absolute top-[124px] left-1/2 w-[400px] -translate-x-1/2 text-center text-brand-dark-green">
|
||||
<h2 className="text-h2 w-full text-center text-brand-dark-green">
|
||||
{t('title')}
|
||||
</h2>
|
||||
|
||||
<p className="text-mono-s absolute top-[230px] left-[calc(50%+6px)] w-[230px] text-brand-dark-green">
|
||||
<p className="text-mono-s w-[230px] text-center text-brand-dark-green">
|
||||
{t('body')}
|
||||
</p>
|
||||
|
||||
<div className="absolute top-[372px] left-3 right-3 grid grid-cols-3 gap-3">
|
||||
<div className="grid w-full grid-cols-3 gap-3">
|
||||
{cards.map((card) => (
|
||||
<PathCardView key={card.key} card={card} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</ContentWidth>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import Image from 'next/image'
|
||||
|
||||
import type { FeaturedTextSection, GallerySection } from '@repo/content/schemas'
|
||||
|
||||
import ContentWidth from '@/components/layout/content-width'
|
||||
import { Reveal } from '@/components/motion/reveal'
|
||||
import { Button } from '@/components/ui'
|
||||
|
||||
@@ -106,38 +107,41 @@ export default function ParallelSocietySection({ headline, gallery }: Props) {
|
||||
}))
|
||||
|
||||
return (
|
||||
<section className="relative h-[1368px] overflow-hidden bg-brand-off-white shadow-[inset_0_1px_0_rgba(21,37,33,0.1)]">
|
||||
<Reveal
|
||||
amount={0.4}
|
||||
delay={0.18}
|
||||
viewportMargin="0px 0px -20% 0px"
|
||||
className="absolute top-[132px] right-3 left-3"
|
||||
>
|
||||
<h2 className="text-h1 text-brand-dark-green text-center">
|
||||
<span className="text-brand-dark-green">
|
||||
{headline.title.highlight}{' '}
|
||||
</span>
|
||||
<span className="text-gray-04">{headline.title.rest}</span>
|
||||
</h2>
|
||||
</Reveal>
|
||||
<section className="bg-brand-off-white shadow-[inset_0_1px_0_rgba(21,37,33,0.1)]">
|
||||
<ContentWidth className="relative px-3 pt-[132px] pb-[100px] md:pt-[132px] md:pb-0">
|
||||
<Reveal
|
||||
amount={0.4}
|
||||
delay={0.18}
|
||||
viewportMargin="0px 0px -20% 0px"
|
||||
className="mb-[376px] md:mb-[60px]"
|
||||
>
|
||||
<h2 className="text-h1 text-brand-dark-green text-center">
|
||||
<span className="text-brand-dark-green">
|
||||
{headline.title.highlight}
|
||||
</span>{' '}
|
||||
<span className="text-gray-04">{headline.title.rest}</span>
|
||||
</h2>
|
||||
</Reveal>
|
||||
|
||||
{headline.cta ? (
|
||||
<div className="absolute top-[508px] right-3 left-3 flex justify-center md:top-[450px]">
|
||||
<Button
|
||||
href={headline.cta.href}
|
||||
variant="link"
|
||||
className="cursor-pointer transition-opacity hover:opacity-70"
|
||||
>
|
||||
{headline.cta.label}
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
{headline.cta ? (
|
||||
<div className="mb-[70px] flex justify-center md:mb-[111px]">
|
||||
<Button
|
||||
href={headline.cta.href}
|
||||
variant="link"
|
||||
className="cursor-pointer transition-opacity hover:opacity-70"
|
||||
>
|
||||
{headline.cta.label}
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
</ContentWidth>
|
||||
|
||||
{/* Mobile Gallery — full width with internal scroll */}
|
||||
<div
|
||||
className="absolute top-[578px] right-0 left-0 overflow-x-auto px-3 pb-2 md:hidden"
|
||||
className="overflow-x-auto pb-2 md:hidden px-3"
|
||||
style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}
|
||||
>
|
||||
<div className="flex w-max snap-x snap-mandatory items-start gap-3 pr-3">
|
||||
<div className="flex w-max snap-x snap-mandatory items-start gap-3">
|
||||
{items.map((item) => (
|
||||
<MobileGalleryCard
|
||||
key={item.src}
|
||||
@@ -150,22 +154,25 @@ export default function ParallelSocietySection({ headline, gallery }: Props) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Gallery — bleed outside container on desktop */}
|
||||
{/* Desktop Gallery — full width with centered content and internal scroll */}
|
||||
{/* pb-[88px] + Press section py-3 (12px) = 100px gallery→Press gap per Figma */}
|
||||
<div
|
||||
className="absolute top-[578px] hidden items-start gap-3 md:flex"
|
||||
style={{ marginLeft: '-141px', width: 'calc(100% + 282px)' }}
|
||||
className="hidden overflow-x-auto md:block md:pb-[88px]"
|
||||
style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}
|
||||
>
|
||||
{items.map((item) => (
|
||||
<DesktopGalleryCard
|
||||
key={item.src}
|
||||
src={item.src}
|
||||
alt={item.alt}
|
||||
w={item.desktop.w}
|
||||
h={item.desktop.h}
|
||||
caption={item.caption}
|
||||
date={item.date}
|
||||
/>
|
||||
))}
|
||||
<div className="flex items-start gap-3 px-3 mx-auto w-fit">
|
||||
{items.map((item) => (
|
||||
<DesktopGalleryCard
|
||||
key={item.src}
|
||||
src={item.src}
|
||||
alt={item.alt}
|
||||
w={item.desktop.w}
|
||||
h={item.desktop.h}
|
||||
caption={item.caption}
|
||||
date={item.date}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
|
||||
@@ -2,6 +2,7 @@ import Image from 'next/image'
|
||||
|
||||
import type { RelatedArticlesSection } from '@repo/content/schemas'
|
||||
|
||||
import ContentWidth from '@/components/layout/content-width'
|
||||
import { Button, ButtonArrowIcon } from '@/components/ui'
|
||||
import { Link } from '@/i18n/navigation'
|
||||
import type { PressArticleRow } from '@/lib/press-engine'
|
||||
@@ -130,40 +131,40 @@ export default function PressSection({ data, articles }: Props) {
|
||||
}))
|
||||
|
||||
return (
|
||||
<section id="press" className="h-[880px] bg-brand-off-white py-3">
|
||||
<div className="px-3">
|
||||
<div className="relative h-[856px] overflow-hidden rounded-xl bg-accent-tan">
|
||||
{data.label ? (
|
||||
<p className="text-mono-s absolute top-6 left-3 w-56.5 text-brand-dark-green">
|
||||
{data.label}
|
||||
</p>
|
||||
) : null}
|
||||
{data.eyebrow ? (
|
||||
<p className="text-mono-s absolute top-6 left-[714px] hidden w-56.5 text-brand-dark-green md:block">
|
||||
{data.eyebrow}
|
||||
</p>
|
||||
) : null}
|
||||
{data.cta ? (
|
||||
<div className="absolute top-[22px] left-[191px] md:left-[1190px]">
|
||||
<Button
|
||||
href={data.cta.href}
|
||||
variant="link"
|
||||
icon={<ButtonArrowIcon />}
|
||||
className="cursor-pointer transition-opacity hover:opacity-70"
|
||||
>
|
||||
{data.cta.label}
|
||||
</Button>
|
||||
<section id="press" className="bg-brand-off-white py-3">
|
||||
<ContentWidth className="rounded-xl bg-accent-tan px-3 py-[102px] md:p-6">
|
||||
<div className="flex flex-col gap-[112px]">
|
||||
{(data.label || data.eyebrow || data.cta) && (
|
||||
<div className="flex items-center justify-between">
|
||||
{data.label ? (
|
||||
<p className="text-mono-s text-brand-dark-green">
|
||||
{data.label}
|
||||
</p>
|
||||
) : null}
|
||||
{data.eyebrow ? (
|
||||
<p className="text-mono-s hidden text-brand-dark-green md:block">
|
||||
{data.eyebrow}
|
||||
</p>
|
||||
) : null}
|
||||
{data.cta ? (
|
||||
<Button
|
||||
href={data.cta.href}
|
||||
variant="link"
|
||||
icon={<ButtonArrowIcon />}
|
||||
className="cursor-pointer transition-opacity hover:opacity-70"
|
||||
>
|
||||
{data.cta.label}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
)}
|
||||
|
||||
<div className="absolute top-[102px] left-1/2 w-[464px] -translate-x-1/2">
|
||||
<h2 className="text-h2 text-center text-brand-dark-green">
|
||||
{data.title}
|
||||
</h2>
|
||||
</div>
|
||||
<h2 className="text-h2 text-center text-brand-dark-green">
|
||||
{data.title}
|
||||
</h2>
|
||||
|
||||
<div
|
||||
className="absolute top-[229px] right-0 left-3 flex gap-3 overflow-x-auto pr-3 md:right-3 md:grid md:grid-cols-4 md:overflow-visible md:pr-0"
|
||||
className="flex gap-3 overflow-x-auto pr-3 md:grid md:grid-cols-4 md:overflow-visible md:pr-0"
|
||||
style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}
|
||||
>
|
||||
{cards.map((card) => (
|
||||
@@ -171,7 +172,7 @@ export default function PressSection({ data, articles }: Props) {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ContentWidth>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -98,11 +98,13 @@ export default function SocialProofSection({ stats }: SocialProofSectionProps) {
|
||||
]
|
||||
|
||||
return (
|
||||
<section className="relative z-[2] -mt-10 overflow-hidden rounded-t-[36px] bg-brand-off-white px-3 pt-3 pb-[100px]">
|
||||
<div className="grid gap-3 md:grid-cols-4">
|
||||
{cards.map((card) => (
|
||||
<SocialProofCardView key={card.key} card={card} />
|
||||
))}
|
||||
<section className="relative z-[2] -mt-10 overflow-hidden rounded-t-[36px] bg-brand-off-white">
|
||||
<div className="pt-3 pb-25 px-3">
|
||||
<div className="grid gap-3 md:grid-cols-4">
|
||||
{cards.map((card) => (
|
||||
<SocialProofCardView key={card.key} card={card} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { getTranslations } from 'next-intl/server'
|
||||
import Image from 'next/image'
|
||||
|
||||
import ContentWidth from '@/components/layout/content-width'
|
||||
import { Button } from '@/components/ui'
|
||||
import { EXTERNAL_URLS, ROUTES } from '@/constants/routes'
|
||||
|
||||
@@ -42,8 +43,8 @@ export default async function StartBuildingSection({
|
||||
]
|
||||
|
||||
return (
|
||||
<section className="h-[1236px] overflow-hidden bg-brand-off-white pt-[100px] md:h-[820px] md:pt-3 md:pb-28">
|
||||
<div className="grid gap-3 px-3 md:grid-cols-2">
|
||||
<section className="bg-brand-off-white">
|
||||
<ContentWidth className="grid gap-3 pt-[100px] md:grid-cols-2 md:pt-[58px] md:pb-28">
|
||||
<div className="relative h-[319px] w-[365px] overflow-hidden rounded-[100px] bg-brand-dark-green/10 md:h-[696px] md:w-auto">
|
||||
<Image
|
||||
src="/images/home/figma-refresh/start-building.webp"
|
||||
@@ -78,7 +79,7 @@ export default async function StartBuildingSection({
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ContentWidth>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { TechStackOverviewSection } from '@repo/content/schemas'
|
||||
|
||||
import ContentWidth from '@/components/layout/content-width'
|
||||
import { TechStackDiagram } from '@/components/sections/shared/tech-stack-diagram'
|
||||
import { Button, ButtonArrowIcon } from '@/components/ui'
|
||||
import { ROUTES } from '@/constants/routes'
|
||||
@@ -8,8 +9,9 @@ function formatEyebrow(eyebrow: string) {
|
||||
return eyebrow.replaceAll('. ', '.\n')
|
||||
}
|
||||
|
||||
function formatMobileTitle(title: string) {
|
||||
return title.replace(' Technology Stack', '\nTechnology Stack')
|
||||
function splitTechStackTitle(title: string): [string, string] {
|
||||
const parts = title.split(' Technology Stack')
|
||||
return [parts[0], 'Technology Stack']
|
||||
}
|
||||
|
||||
type Props = {
|
||||
@@ -32,119 +34,134 @@ export default function TechStackSection({
|
||||
return (
|
||||
<section
|
||||
id="tech-stack"
|
||||
className="relative h-[1516px] overflow-hidden border-t border-brand-dark-green/10 bg-brand-off-white md:h-[1653px]"
|
||||
className="relative min-h-[1516px] overflow-hidden border-t border-brand-dark-green/10 bg-brand-off-white md:min-h-[1653px]"
|
||||
>
|
||||
<div className="relative h-full px-3 md:hidden">
|
||||
<p className="text-mono-s absolute top-3 left-3 w-[226px] text-brand-dark-green">
|
||||
Disclaimer: This diagram oversimplifies the stack.
|
||||
</p>
|
||||
|
||||
{data.eyebrow ? (
|
||||
<p className="text-mono-s absolute top-[130px] left-[203px] w-[120px] whitespace-pre-line text-brand-dark-green">
|
||||
{formatEyebrow(data.eyebrow)}
|
||||
<ContentWidth className="relative min-h-full flex-col pt-3 pb-[100px] md:hidden">
|
||||
<div className="flex items-start justify-between">
|
||||
<p className="text-mono-s w-[226px] text-brand-dark-green">
|
||||
Disclaimer: This diagram oversimplifies the stack.
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
<div className="absolute top-3 right-3 flex flex-col items-end gap-1.5">
|
||||
{data.cta ? (
|
||||
<div className="flex flex-col items-end gap-1.5">
|
||||
{data.cta ? (
|
||||
<Button
|
||||
href={data.cta.href}
|
||||
variant="secondary"
|
||||
icon={<ButtonArrowIcon />}
|
||||
className="cursor-pointer transition-opacity hover:opacity-70"
|
||||
>
|
||||
Documentation
|
||||
</Button>
|
||||
) : null}
|
||||
<Button
|
||||
href={data.cta.href}
|
||||
variant="secondary"
|
||||
href={ROUTES.buildersHub}
|
||||
icon={<ButtonArrowIcon />}
|
||||
className="cursor-pointer transition-opacity hover:opacity-70"
|
||||
>
|
||||
Documentation
|
||||
Builder Hub
|
||||
</Button>
|
||||
) : null}
|
||||
<Button
|
||||
href={ROUTES.buildersHub}
|
||||
icon={<ButtonArrowIcon />}
|
||||
className="cursor-pointer transition-opacity hover:opacity-70"
|
||||
>
|
||||
Builder Hub
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{data.title ? (
|
||||
<h2 className="text-h2 absolute top-[193px] left-1/2 w-[464px] -translate-x-1/2 whitespace-pre-line text-center text-brand-dark-green">
|
||||
{formatMobileTitle(data.title)}
|
||||
</h2>
|
||||
) : null}
|
||||
|
||||
<p className="text-mono-s absolute top-[302px] left-[250px] w-[178px] text-brand-dark-green">
|
||||
Private-by-default infrastructure is a requirement to make parallel
|
||||
societies possible.
|
||||
</p>
|
||||
|
||||
<div className="absolute top-[437px] right-3 left-3">
|
||||
<TechStackDiagram
|
||||
data={data}
|
||||
networkingHref={networkingHref}
|
||||
foundationHref={foundationHref}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative mx-auto hidden h-full max-w-[1440px] md:block">
|
||||
<p className="text-mono-s absolute top-[11px] left-3 w-[226px] text-brand-dark-green">
|
||||
Disclaimer: Abstract representation of the stack.
|
||||
</p>
|
||||
|
||||
{data.eyebrow ? (
|
||||
<p className="text-mono-s absolute top-[97px] left-[calc(50%+6px)] w-[226px] whitespace-pre-line text-brand-dark-green">
|
||||
{formatEyebrow(data.eyebrow)}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
<div className="absolute top-[11px] right-3 flex items-center gap-1.5">
|
||||
<Button
|
||||
href={ROUTES.buildersHub}
|
||||
icon={<ButtonArrowIcon />}
|
||||
className="cursor-pointer transition-opacity hover:opacity-70"
|
||||
>
|
||||
Builder Hub
|
||||
</Button>
|
||||
{data.cta ? (
|
||||
<Button
|
||||
href={data.cta.href}
|
||||
variant="secondary"
|
||||
icon={<ButtonArrowIcon />}
|
||||
className="cursor-pointer transition-opacity hover:opacity-70"
|
||||
>
|
||||
Documentation
|
||||
</Button>
|
||||
) : null}
|
||||
<Button
|
||||
href={ROUTES.technologyStack}
|
||||
variant="secondary"
|
||||
icon={<ButtonArrowIcon />}
|
||||
className="cursor-pointer transition-opacity hover:opacity-70"
|
||||
>
|
||||
Specs
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{data.title ? (
|
||||
<div className="absolute top-[184px] left-[calc(33.33%+8px)] w-[464px]">
|
||||
<h2 className="text-h2 whitespace-pre-line text-center text-brand-dark-green">
|
||||
{data.title}
|
||||
</h2>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<p className="text-mono-s absolute top-[333px] left-[calc(50%+6px)] w-[226px] text-brand-dark-green">
|
||||
Private-by-default infrastructure is a requirement to make parallel
|
||||
societies possible.
|
||||
</p>
|
||||
|
||||
<div className="absolute top-[517px] left-0 w-full px-3">
|
||||
<TechStackDiagram
|
||||
data={data}
|
||||
networkingHref={networkingHref}
|
||||
foundationHref={foundationHref}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-[127px] flex flex-col items-center gap-[109px]">
|
||||
{data.eyebrow ? (
|
||||
<p className="text-mono-s w-[120px] whitespace-pre-line text-center text-brand-dark-green">
|
||||
{formatEyebrow(data.eyebrow)}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{data.title ? (
|
||||
<h2 className="text-h2 w-full text-center text-brand-dark-green">
|
||||
{(() => {
|
||||
const [first, second] = splitTechStackTitle(data.title)
|
||||
return (
|
||||
<>
|
||||
{first}
|
||||
<br />
|
||||
{second}
|
||||
</>
|
||||
)
|
||||
})()}
|
||||
</h2>
|
||||
) : null}
|
||||
|
||||
<p className="text-mono-s w-[178px] text-center text-brand-dark-green">
|
||||
Private-by-default infrastructure is a requirement to make parallel
|
||||
societies possible.
|
||||
</p>
|
||||
|
||||
<div className="w-full">
|
||||
<TechStackDiagram
|
||||
data={data}
|
||||
networkingHref={networkingHref}
|
||||
foundationHref={foundationHref}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ContentWidth>
|
||||
|
||||
<ContentWidth className="relative hidden min-h-full flex-col pt-[11px] pb-[100px] md:flex">
|
||||
<div className="flex items-start justify-between">
|
||||
<p className="text-mono-s w-[226px] text-brand-dark-green">
|
||||
Disclaimer: Abstract representation of the stack.
|
||||
</p>
|
||||
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Button
|
||||
href={ROUTES.buildersHub}
|
||||
icon={<ButtonArrowIcon />}
|
||||
className="cursor-pointer transition-opacity hover:opacity-70"
|
||||
>
|
||||
Builder Hub
|
||||
</Button>
|
||||
{data.cta ? (
|
||||
<Button
|
||||
href={data.cta.href}
|
||||
variant="secondary"
|
||||
icon={<ButtonArrowIcon />}
|
||||
className="cursor-pointer transition-opacity hover:opacity-70"
|
||||
>
|
||||
Documentation
|
||||
</Button>
|
||||
) : null}
|
||||
<Button
|
||||
href={ROUTES.technologyStack}
|
||||
variant="secondary"
|
||||
icon={<ButtonArrowIcon />}
|
||||
className="cursor-pointer transition-opacity hover:opacity-70"
|
||||
>
|
||||
Specs
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-[86px] flex flex-col items-center gap-[149px]">
|
||||
{data.eyebrow ? (
|
||||
<p className="text-mono-s w-[226px] whitespace-pre-line text-center text-brand-dark-green">
|
||||
{formatEyebrow(data.eyebrow)}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{data.title ? (
|
||||
<h2 className="text-h2 w-[464px] whitespace-pre-line text-center text-brand-dark-green">
|
||||
{data.title}
|
||||
</h2>
|
||||
) : null}
|
||||
|
||||
<p className="text-mono-s w-[226px] text-center text-brand-dark-green">
|
||||
Private-by-default infrastructure is a requirement to make parallel
|
||||
societies possible.
|
||||
</p>
|
||||
|
||||
<div className="w-full">
|
||||
<TechStackDiagram
|
||||
data={data}
|
||||
networkingHref={networkingHref}
|
||||
foundationHref={foundationHref}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ContentWidth>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ type Props = {
|
||||
|
||||
export default function MessagingCaseStudies({ data }: Props) {
|
||||
return (
|
||||
<section className="mt-15 bg-brand-off-white md:mt-25">
|
||||
<section className="bg-brand-off-white">
|
||||
<div className="mx-auto max-w-360 px-3 pt-25 md:px-0 md:pt-25">
|
||||
<Reveal
|
||||
stagger
|
||||
|
||||
@@ -1,89 +1,12 @@
|
||||
import { TechDetailHero } from '@acid-info/logos-ui'
|
||||
import type { HeroSection } from '@repo/content/schemas'
|
||||
|
||||
import { Reveal } from '@/components/motion/reveal'
|
||||
import { Button, ButtonArrowIcon } from '@/components/ui'
|
||||
import { Link } from '@/i18n/navigation'
|
||||
|
||||
import { DownloadIcon } from '../shared/builder-cta-card'
|
||||
import TechStackDetailHero from '@/components/sections/shared/tech-stack-detail-hero'
|
||||
|
||||
type Props = {
|
||||
data: HeroSection
|
||||
backHref: string
|
||||
}
|
||||
|
||||
export default function MessagingHero({ data, backHref }: Props) {
|
||||
const [primaryCta, secondaryCta] = data.ctas ?? []
|
||||
|
||||
return (
|
||||
<Reveal amount={0.2}>
|
||||
<TechDetailHero
|
||||
className="md:h-[453px]"
|
||||
title={data.headline}
|
||||
body={data.body}
|
||||
bodySecondary={data.bodySecondary}
|
||||
items={data.items}
|
||||
backLink={
|
||||
<Link
|
||||
href={backHref}
|
||||
className="inline-flex cursor-pointer items-center gap-1 text-brand-dark-green transition-opacity hover:opacity-70"
|
||||
>
|
||||
<span className="inline-flex size-3.75 shrink-0 rotate-180 items-center justify-center">
|
||||
<ButtonArrowIcon />
|
||||
</span>
|
||||
<span className="font-mono text-[10px] font-medium leading-[1.3] uppercase">
|
||||
{data.eyebrow}
|
||||
</span>
|
||||
</Link>
|
||||
}
|
||||
status={
|
||||
data.status
|
||||
? {
|
||||
label: data.status.label,
|
||||
body: data.status.body,
|
||||
cta: data.status.cta ? (
|
||||
<Button
|
||||
href={data.status.cta.href}
|
||||
variant={data.status.cta.variant ?? 'secondary'}
|
||||
icon={<DownloadIcon />}
|
||||
className="w-fit cursor-pointer rounded-none"
|
||||
>
|
||||
{data.status.cta.label}
|
||||
</Button>
|
||||
) : null,
|
||||
secondaryCta: data.status.secondaryCta ? (
|
||||
<Button
|
||||
href={data.status.secondaryCta.href}
|
||||
variant={data.status.secondaryCta.variant ?? 'tertiary'}
|
||||
className="w-fit cursor-pointer"
|
||||
>
|
||||
{data.status.secondaryCta.label}
|
||||
</Button>
|
||||
) : null,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
actions={
|
||||
<>
|
||||
{primaryCta ? (
|
||||
<Button
|
||||
href={primaryCta.href}
|
||||
variant={primaryCta.variant ?? 'primary'}
|
||||
>
|
||||
{primaryCta.label}
|
||||
</Button>
|
||||
) : null}
|
||||
{secondaryCta ? (
|
||||
<Button
|
||||
href={secondaryCta.href}
|
||||
variant={secondaryCta.variant ?? 'secondary'}
|
||||
>
|
||||
{secondaryCta.label}
|
||||
</Button>
|
||||
) : null}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</Reveal>
|
||||
)
|
||||
export default function MessagingHero(props: Props) {
|
||||
return <TechStackDetailHero {...props} className="md:h-[453px]" />
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { TechTextSplitSection } from '@acid-info/logos-ui'
|
||||
import type { CtaPanelSection } from '@repo/content/schemas'
|
||||
|
||||
import { Reveal } from '@/components/motion/reveal'
|
||||
import { Button } from '@/components/ui'
|
||||
|
||||
import { SectionMarker } from './messaging-shared'
|
||||
|
||||
@@ -12,32 +13,68 @@ function paragraphs(text?: string) {
|
||||
return text?.split('\n\n') ?? []
|
||||
}
|
||||
|
||||
function LmnImage() {
|
||||
function ctaAttrs(external?: boolean) {
|
||||
return external ? { target: '_blank', rel: 'noopener noreferrer' } : {}
|
||||
}
|
||||
|
||||
function MessagingFeatureActions({ data }: { data: CtaPanelSection }) {
|
||||
if (!data.cta && !data.secondaryCta) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative h-72 w-full overflow-hidden rounded-3xl bg-gray-02 md:h-[335px] md:w-[702px]">
|
||||
<Image
|
||||
src="/images/messaging/lmn.webp"
|
||||
alt=""
|
||||
width={746}
|
||||
height={933}
|
||||
sizes="(min-width: 768px) 702px, 369px"
|
||||
className="absolute top-[-93px] left-[-28px] h-[497px] w-[397px] max-w-none object-cover md:top-[-130.5px] md:left-[21px] md:h-[620px] md:w-[775px]"
|
||||
/>
|
||||
<div className="flex items-baseline gap-1.5">
|
||||
{data.cta ? (
|
||||
<Button
|
||||
href={data.cta.href}
|
||||
variant={data.cta.variant ?? 'primary'}
|
||||
className="cursor-pointer"
|
||||
{...ctaAttrs(data.cta.external)}
|
||||
>
|
||||
{data.cta.label}
|
||||
</Button>
|
||||
) : null}
|
||||
{data.secondaryCta ? (
|
||||
<Button
|
||||
href={data.secondaryCta.href}
|
||||
variant={data.secondaryCta.variant ?? 'secondary'}
|
||||
className="cursor-pointer"
|
||||
{...ctaAttrs(data.secondaryCta.external)}
|
||||
>
|
||||
{data.secondaryCta.label}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function PrivacyImage() {
|
||||
function DeliveryImage() {
|
||||
return (
|
||||
<div className="relative h-72 w-full overflow-hidden rounded-3xl bg-gray-02 md:h-[335px] md:w-[702px]">
|
||||
<div className="relative h-72 w-full overflow-hidden rounded-3xl bg-gray-02 md:h-full">
|
||||
<Image
|
||||
src="/images/messaging/privacy.webp"
|
||||
src="/images/messaging/delivery-landscape.webp"
|
||||
alt=""
|
||||
width={746}
|
||||
height={933}
|
||||
sizes="(min-width: 768px) 702px, 369px"
|
||||
className="absolute top-[-25px] left-[-38px] h-[461px] w-[461px] max-w-none object-cover md:top-[-289.5px] md:left-0 md:h-[778px] md:w-[743px]"
|
||||
fill
|
||||
sizes="(min-width: 768px) 50vw, 100vw"
|
||||
className="object-cover"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black/20" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ChatImage() {
|
||||
return (
|
||||
<div className="relative h-72 w-full overflow-hidden rounded-3xl bg-gray-02 md:h-full">
|
||||
<Image
|
||||
src="/images/messaging/chat.webp"
|
||||
alt=""
|
||||
width={1044}
|
||||
height={1094}
|
||||
sizes="(min-width: 768px) 743px, 461px"
|
||||
className="absolute top-[-25px] left-[-38px] h-[461px] w-[461px] max-w-none object-cover md:top-[calc(50%-68px)] md:left-0 md:h-[778px] md:w-[743px] md:-translate-y-1/2"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-black/30 from-[25.835%] to-transparent to-1/2" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -45,10 +82,12 @@ function PrivacyImage() {
|
||||
function MessagingFeaturePanel({
|
||||
data,
|
||||
image,
|
||||
tone = 'gray-01',
|
||||
reverse = false,
|
||||
}: {
|
||||
data: CtaPanelSection
|
||||
image: ReactNode
|
||||
tone?: 'gray-01' | 'gray-02'
|
||||
reverse?: boolean
|
||||
}) {
|
||||
const mobileEyebrow = data.mobileEyebrow ?? data.eyebrow
|
||||
@@ -56,37 +95,60 @@ function MessagingFeaturePanel({
|
||||
const mobileDescription = data.mobileDescription ?? data.description
|
||||
|
||||
return (
|
||||
<section className="bg-gray-01">
|
||||
<section className="bg-brand-off-white">
|
||||
<div
|
||||
className={`relative mx-auto h-150 max-w-360 p-3 md:h-[359px] md:flex md:items-start md:justify-between ${
|
||||
reverse ? 'md:flex-row-reverse' : 'md:flex-row'
|
||||
className={`mx-auto max-w-[1440px] p-3 ${
|
||||
tone === 'gray-02' ? 'bg-gray-02' : 'bg-gray-01'
|
||||
}`}
|
||||
>
|
||||
<Reveal className="flex h-72 flex-col md:h-[335px] md:w-[702px]">
|
||||
{mobileEyebrow ? (
|
||||
<SectionMarker label={mobileEyebrow} className="md:hidden" />
|
||||
) : null}
|
||||
{data.eyebrow ? (
|
||||
<SectionMarker label={data.eyebrow} className="hidden md:flex" />
|
||||
) : null}
|
||||
|
||||
<div className="mt-11.5 flex flex-col gap-3 text-brand-dark-green md:mt-[108.5px]">
|
||||
<h2 className="text-h4-sans md:w-84">
|
||||
<span className="md:hidden">{mobileTitle}</span>
|
||||
<span className="hidden md:inline">{data.title}</span>
|
||||
</h2>
|
||||
{mobileDescription ? (
|
||||
<p className="text-mono-s md:hidden">{mobileDescription}</p>
|
||||
<div
|
||||
className={`relative grid gap-3 md:h-[335px] md:grid-cols-2 ${
|
||||
reverse ? 'md:[&>*:first-child]:col-start-2' : ''
|
||||
}`}
|
||||
>
|
||||
<Reveal className="flex min-h-72 flex-col justify-between text-brand-dark-green md:h-full md:w-full md:pb-3">
|
||||
{mobileEyebrow ? (
|
||||
<SectionMarker
|
||||
label={mobileEyebrow}
|
||||
className="h-[98px] md:hidden"
|
||||
/>
|
||||
) : null}
|
||||
{data.description ? (
|
||||
<p className="text-mono-s hidden md:block md:w-121.25">
|
||||
{data.description}
|
||||
</p>
|
||||
{data.eyebrow ? (
|
||||
<SectionMarker
|
||||
label={data.eyebrow}
|
||||
className="hidden h-[98px] md:flex"
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
<Reveal amount={0.2}>{image}</Reveal>
|
||||
<div className="flex w-[345px] max-w-full min-w-0 flex-col gap-3 break-words md:w-full">
|
||||
<h2 className="font-sans text-[24px] leading-[1.1] font-normal tracking-[-0.24px] md:w-[336px]">
|
||||
<span className="md:hidden">{mobileTitle}</span>
|
||||
<span className="hidden md:inline">{data.title}</span>
|
||||
</h2>
|
||||
{mobileDescription ? (
|
||||
<p className="w-[345px] max-w-full font-sans text-[12px] leading-[1.2] font-medium break-words md:hidden">
|
||||
{mobileDescription}
|
||||
</p>
|
||||
) : null}
|
||||
{data.description ? (
|
||||
<p className="hidden font-sans text-[12px] leading-[1.2] font-medium md:block md:w-[485px]">
|
||||
{data.description}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<MessagingFeatureActions data={data} />
|
||||
</Reveal>
|
||||
|
||||
<Reveal
|
||||
amount={0.2}
|
||||
className={`h-72 md:h-full ${
|
||||
reverse ? 'md:col-start-1 md:row-start-1' : ''
|
||||
}`}
|
||||
>
|
||||
{image}
|
||||
</Reveal>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
@@ -129,12 +191,12 @@ export default function MessagingIntro({ privacy, lmn, censorship }: Props) {
|
||||
/>
|
||||
</Reveal>
|
||||
|
||||
<MessagingFeaturePanel data={lmn} image={<LmnImage />} />
|
||||
<MessagingFeaturePanel
|
||||
data={censorship}
|
||||
image={<PrivacyImage />}
|
||||
reverse
|
||||
data={lmn}
|
||||
image={<DeliveryImage />}
|
||||
tone="gray-02"
|
||||
/>
|
||||
<MessagingFeaturePanel data={censorship} image={<ChatImage />} reverse />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { CirclesSettings } from '@repo/content/schemas'
|
||||
import { LogosMark } from '@acid-info/logos-ui'
|
||||
|
||||
import { IconMask } from '@/components/icons/icon-mask'
|
||||
import ContentWidth from '@/components/layout/content-width'
|
||||
import CirclesMap from '@/components/sections/circles/circles-map'
|
||||
import { EXTERNAL_URLS, ROUTES } from '@/constants/routes'
|
||||
import { Link } from '@/i18n/navigation'
|
||||
@@ -117,7 +118,7 @@ function SectionHeader({
|
||||
descriptionClassName?: string
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
<ContentWidth
|
||||
className={`grid grid-cols-1 gap-4 px-3 pt-10 text-brand-dark-green md:grid-cols-12 md:gap-3 ${className ?? ''}`}
|
||||
>
|
||||
<h2 className={`text-h3-serif md:col-span-5 ${titleClassName ?? ''}`}>
|
||||
@@ -129,13 +130,14 @@ function SectionHeader({
|
||||
{description}
|
||||
</p>
|
||||
{cta ? <div className="md:col-span-2 md:col-start-11">{cta}</div> : null}
|
||||
</div>
|
||||
</ContentWidth>
|
||||
)
|
||||
}
|
||||
|
||||
function HeroSection({ t }: { t: Translate }) {
|
||||
return (
|
||||
<section className="relative h-[494px] overflow-hidden bg-brand-off-white px-3 pt-6 text-brand-dark-green md:h-[579px]">
|
||||
<ContentWidth className="relative h-full">
|
||||
<div className="relative h-[75px] w-[107px] overflow-hidden bg-gray-01">
|
||||
<Image
|
||||
src={movementImages.heroThumb}
|
||||
@@ -174,6 +176,7 @@ function HeroSection({ t }: { t: Translate }) {
|
||||
className="mt-10 ml-[191px] md:ml-0"
|
||||
/>
|
||||
</div>
|
||||
</ContentWidth>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -211,7 +214,7 @@ function ActionCardsSection({ t }: { t: Translate }) {
|
||||
className="pb-10 md:pt-6 md:pb-[60px]"
|
||||
titleClassName="max-w-[244px]"
|
||||
/>
|
||||
<div className="grid gap-3 px-3 md:grid-cols-3">
|
||||
<ContentWidth className="grid gap-3 px-3 md:grid-cols-3">
|
||||
{cards.map((card) => (
|
||||
<article
|
||||
key={card.key}
|
||||
@@ -247,7 +250,7 @@ function ActionCardsSection({ t }: { t: Translate }) {
|
||||
/>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</ContentWidth>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -255,7 +258,7 @@ function ActionCardsSection({ t }: { t: Translate }) {
|
||||
function CampaignSection({ t }: { t: Translate }) {
|
||||
return (
|
||||
<section className="bg-brand-off-white px-3 py-10 md:py-0">
|
||||
<div className="grid overflow-hidden rounded-xl bg-gray-01 p-3 text-brand-dark-green md:grid-cols-2 md:gap-3">
|
||||
<ContentWidth className="grid overflow-hidden rounded-xl bg-gray-01 p-3 text-brand-dark-green md:grid-cols-2 md:gap-3">
|
||||
<div className="flex min-h-[462px] flex-col justify-between p-3 md:min-h-[462px]">
|
||||
<div className="flex gap-[88px]">
|
||||
<LogosMark size={7} />
|
||||
@@ -298,7 +301,7 @@ function CampaignSection({ t }: { t: Translate }) {
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ContentWidth>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -401,7 +404,7 @@ function ActivismSection({ t }: { t: Translate }) {
|
||||
cta={<Cta href={ROUTES.circles} label={t('activism.cta')} />}
|
||||
className="pb-10 md:pb-[78px]"
|
||||
/>
|
||||
<div className="overflow-hidden">
|
||||
<ContentWidth className="overflow-hidden">
|
||||
<div className="grid gap-3 px-3 md:flex md:w-max">
|
||||
{issues.map((issue, index) => (
|
||||
<IssueCard
|
||||
@@ -413,7 +416,7 @@ function ActivismSection({ t }: { t: Translate }) {
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</ContentWidth>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -471,7 +474,7 @@ function EventsSection({ t }: { t: Translate }) {
|
||||
className="pb-10 md:pb-[73px]"
|
||||
descriptionClassName="max-w-[178px]"
|
||||
/>
|
||||
<div className="flex flex-col gap-10">
|
||||
<ContentWidth className="flex flex-col gap-10">
|
||||
{rows.map((row) => (
|
||||
<div key={row.key} className="flex flex-col gap-3">
|
||||
<div className="flex gap-1.5 px-3 text-eyebrow">
|
||||
@@ -488,14 +491,15 @@ function EventsSection({ t }: { t: Translate }) {
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ContentWidth>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function GetInvolvedSection({ t }: { t: Translate }) {
|
||||
return (
|
||||
<section className="grid gap-3 bg-brand-off-white px-3 py-10 text-brand-dark-green md:grid-cols-2">
|
||||
<section className="bg-brand-off-white px-3 py-10 text-brand-dark-green">
|
||||
<ContentWidth className="grid gap-3 md:grid-cols-2">
|
||||
<div className="flex h-[270px] flex-col items-center justify-center gap-8 border border-brand-dark-green p-4 text-center md:h-[500px] md:gap-10">
|
||||
<div className="flex max-w-[337px] flex-col items-center gap-3 md:max-w-[380px]">
|
||||
<h2 className="text-subhead-sans">{t('involved.title')}</h2>
|
||||
@@ -533,6 +537,7 @@ function GetInvolvedSection({ t }: { t: Translate }) {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ContentWidth>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -557,7 +562,7 @@ function BuilderSection({ t }: { t: Translate }) {
|
||||
}
|
||||
className="md:py-[100px]"
|
||||
/>
|
||||
<div className="px-3">
|
||||
<ContentWidth className="px-3">
|
||||
<article className="relative min-h-[506px] overflow-hidden rounded-xl text-brand-off-white md:min-h-[282px]">
|
||||
<Image
|
||||
src={movementImages.issueLosAngeles}
|
||||
@@ -610,7 +615,7 @@ function BuilderSection({ t }: { t: Translate }) {
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</ContentWidth>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -635,7 +640,7 @@ function ResourcesSection({ t }: { t: Translate }) {
|
||||
className="pb-10 md:pb-[54px]"
|
||||
descriptionClassName="max-w-[178px]"
|
||||
/>
|
||||
<div>
|
||||
<ContentWidth>
|
||||
{rows.map((row, index) => (
|
||||
<div
|
||||
key={row}
|
||||
@@ -669,7 +674,7 @@ function ResourcesSection({ t }: { t: Translate }) {
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ContentWidth>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ type Props = {
|
||||
|
||||
export default function NetworkingFeatures({ data }: Props) {
|
||||
return (
|
||||
<section className="bg-brand-off-white md:mt-25">
|
||||
<section className="bg-brand-off-white">
|
||||
<div className="mx-auto max-w-360 px-3">
|
||||
<Reveal
|
||||
stagger
|
||||
|
||||
@@ -1,76 +1,12 @@
|
||||
import { LogosMark } from '@acid-info/logos-ui'
|
||||
import type { HeroSection } from '@repo/content/schemas'
|
||||
|
||||
import { Reveal, RevealItem } from '@/components/motion/reveal'
|
||||
import { Button, ButtonArrowIcon } from '@/components/ui'
|
||||
import { Link } from '@/i18n/navigation'
|
||||
import TechStackDetailHero from '@/components/sections/shared/tech-stack-detail-hero'
|
||||
|
||||
type Props = {
|
||||
data: HeroSection
|
||||
backHref: string
|
||||
}
|
||||
|
||||
export default function NetworkingHero({ data, backHref }: Props) {
|
||||
const [primaryCta, secondaryCta] = data.ctas ?? []
|
||||
|
||||
return (
|
||||
<section className="mt-10 mb-15 h-[257px] bg-brand-off-white md:mt-[38px] md:mb-25 md:h-[189px]">
|
||||
<div className="relative mx-auto h-full max-w-360 px-3 text-brand-dark-green">
|
||||
<Reveal className="absolute top-[-20px] left-3">
|
||||
<Link
|
||||
href={backHref}
|
||||
className="inline-flex cursor-pointer items-center gap-1 text-brand-dark-green transition-opacity hover:opacity-70"
|
||||
>
|
||||
<span className="inline-flex size-3.75 shrink-0 rotate-180 items-center justify-center">
|
||||
<ButtonArrowIcon />
|
||||
</span>
|
||||
<span className="font-mono text-[10px] font-medium leading-[1.3] uppercase">
|
||||
{data.eyebrow}
|
||||
</span>
|
||||
</Link>
|
||||
</Reveal>
|
||||
|
||||
<Reveal className="absolute top-10 left-3">
|
||||
<h1 className="text-h3-serif flex items-center gap-3">
|
||||
<LogosMark size={22} className="shrink-0 text-gray-03 md:hidden" />
|
||||
<LogosMark
|
||||
size={26}
|
||||
className="hidden shrink-0 text-gray-03 md:block"
|
||||
/>
|
||||
{data.headline}
|
||||
</h1>
|
||||
</Reveal>
|
||||
|
||||
<Reveal
|
||||
stagger
|
||||
className="absolute top-[102px] left-3 flex w-[calc(100%-24px)] flex-col gap-10 md:top-10 md:left-181.5 md:w-85.5"
|
||||
>
|
||||
{data.body ? (
|
||||
<RevealItem>
|
||||
<p className="text-mono-s text-brand-dark-green">{data.body}</p>
|
||||
</RevealItem>
|
||||
) : null}
|
||||
|
||||
<RevealItem className="flex items-center gap-3 md:mt-[39px]">
|
||||
{primaryCta ? (
|
||||
<Button
|
||||
href={primaryCta.href}
|
||||
variant={primaryCta.variant ?? 'primary'}
|
||||
>
|
||||
{primaryCta.label}
|
||||
</Button>
|
||||
) : null}
|
||||
{secondaryCta ? (
|
||||
<Button
|
||||
href={secondaryCta.href}
|
||||
variant={secondaryCta.variant ?? 'secondary'}
|
||||
>
|
||||
{secondaryCta.label}
|
||||
</Button>
|
||||
) : null}
|
||||
</RevealItem>
|
||||
</Reveal>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
export default function NetworkingHero(props: Props) {
|
||||
return <TechStackDetailHero {...props} />
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { CtaPanelSection } from '@repo/content/schemas'
|
||||
|
||||
import ContentWidth from '@/components/layout/content-width'
|
||||
import { Reveal } from '@/components/motion/reveal'
|
||||
import { Button } from '@/components/ui'
|
||||
|
||||
@@ -10,7 +11,7 @@ type Props = {
|
||||
export default function NetworkingIntro({ data }: Props) {
|
||||
return (
|
||||
<section className="h-[235px] border-t border-brand-dark-green/10 bg-brand-off-white md:h-[158px]">
|
||||
<div className="relative mx-auto h-full max-w-360 px-3 text-brand-dark-green">
|
||||
<ContentWidth className="relative h-full text-brand-dark-green px-0">
|
||||
<Reveal className="absolute top-10 left-3">
|
||||
<h2 className="text-h4-sans">{data.title}</h2>
|
||||
</Reveal>
|
||||
@@ -32,7 +33,7 @@ export default function NetworkingIntro({ data }: Props) {
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</ContentWidth>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import Image from 'next/image'
|
||||
import type { ReactNode } from 'react'
|
||||
import { getTranslations } from 'next-intl/server'
|
||||
|
||||
import ContentWidth from '@/components/layout/content-width'
|
||||
import { ButtonArrowIcon } from '@/components/ui'
|
||||
import { EXTERNAL_URLS } from '@/constants/routes'
|
||||
import { Link } from '@/i18n/navigation'
|
||||
@@ -133,6 +134,7 @@ function HeroSection({
|
||||
}) {
|
||||
return (
|
||||
<section className="relative min-h-[447px] overflow-hidden bg-brand-off-white px-3 pt-6 text-brand-dark-green">
|
||||
<ContentWidth className="relative">
|
||||
<Image
|
||||
src="/images/research/hero-thumb.webp"
|
||||
alt=""
|
||||
@@ -155,6 +157,7 @@ function HeroSection({
|
||||
<p className="text-mono-s absolute left-3 top-[318px] max-w-[345px] md:left-[calc(50%+6px)] md:top-[307px] md:w-[226px]">
|
||||
{description}
|
||||
</p>
|
||||
</ContentWidth>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -170,7 +173,7 @@ function OverviewSection({
|
||||
}) {
|
||||
return (
|
||||
<section className="border-t border-brand-dark-green/10 bg-brand-off-white px-3 py-10 text-brand-dark-green md:min-h-[670px]">
|
||||
<div className="grid gap-10 md:grid-cols-12 md:gap-3">
|
||||
<ContentWidth className="grid gap-10 md:grid-cols-12 md:gap-3">
|
||||
<h2 className="text-[24px] leading-[1.1] tracking-[-0.24px] md:col-span-3">
|
||||
{title}
|
||||
</h2>
|
||||
@@ -182,7 +185,7 @@ function OverviewSection({
|
||||
<div className="md:col-span-2 md:col-start-11 md:justify-self-start">
|
||||
<LinkButton {...cta} />
|
||||
</div>
|
||||
</div>
|
||||
</ContentWidth>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -198,12 +201,12 @@ function ResourcesSection({
|
||||
}) {
|
||||
return (
|
||||
<section className="border-t border-brand-dark-green/10 bg-brand-off-white text-brand-dark-green">
|
||||
<div className="grid px-3 py-10 md:grid-cols-12">
|
||||
<ContentWidth className="grid px-3 py-10 md:grid-cols-12">
|
||||
<h2 className="text-[24px] leading-[1.1] tracking-[-0.24px] md:col-span-3">
|
||||
{title}
|
||||
</h2>
|
||||
</div>
|
||||
<div>
|
||||
</ContentWidth>
|
||||
<ContentWidth>
|
||||
{items.map((item, index) => (
|
||||
<div
|
||||
key={item.number}
|
||||
@@ -228,7 +231,7 @@ function ResourcesSection({
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ContentWidth>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -246,7 +249,7 @@ function ContributeSection({
|
||||
}) {
|
||||
return (
|
||||
<section className="border-t border-brand-dark-green/10 bg-brand-off-white px-3 py-10 text-brand-dark-green md:min-h-[262px]">
|
||||
<div className="grid gap-10 md:grid-cols-12 md:gap-3">
|
||||
<ContentWidth className="grid gap-10 md:grid-cols-12 md:gap-3">
|
||||
<h2 className="text-[24px] leading-[1.1] tracking-[-0.24px] md:col-span-3">
|
||||
{title}
|
||||
</h2>
|
||||
@@ -267,7 +270,7 @@ function ContributeSection({
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</ContentWidth>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
import ContentWidth from '@/components/layout/content-width'
|
||||
|
||||
import { DocsToc, type DocsTocKey } from './docs-toc'
|
||||
|
||||
/**
|
||||
@@ -22,11 +24,13 @@ interface DocsPageShellProps {
|
||||
|
||||
export function DocsPageShell({ activeKey, children }: DocsPageShellProps) {
|
||||
return (
|
||||
<section className="flex min-h-190 flex-col items-start gap-3 px-3 xl:flex-row xl:gap-122">
|
||||
<DocsToc activeKey={activeKey} />
|
||||
<div className="flex w-full flex-col items-start gap-6 pb-20 xl:w-116 xl:py-20">
|
||||
{children}
|
||||
</div>
|
||||
</section>
|
||||
<ContentWidth>
|
||||
<section className="flex min-h-190 flex-col items-start gap-3 px-3 xl:flex-row xl:gap-122">
|
||||
<DocsToc activeKey={activeKey} />
|
||||
<div className="flex w-full flex-col items-start gap-6 pb-20 xl:w-116 xl:py-20">
|
||||
{children}
|
||||
</div>
|
||||
</section>
|
||||
</ContentWidth>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import { TechDetailHero } from '@acid-info/logos-ui'
|
||||
import type { HeroSection } from '@repo/content/schemas'
|
||||
|
||||
import {
|
||||
Button,
|
||||
ButtonArrowIcon,
|
||||
type ButtonVariant,
|
||||
} from '@/components/ui'
|
||||
import { Link } from '@/i18n/navigation'
|
||||
|
||||
import { DownloadIcon } from './builder-cta-card'
|
||||
|
||||
type Props = {
|
||||
data: HeroSection
|
||||
backHref: string
|
||||
actionVariant?: ButtonVariant
|
||||
className?: string
|
||||
}
|
||||
|
||||
export default function TechStackDetailHero({
|
||||
data,
|
||||
backHref,
|
||||
actionVariant,
|
||||
className,
|
||||
}: Props) {
|
||||
const [primaryCta, secondaryCta] = data.ctas ?? []
|
||||
|
||||
return (
|
||||
<TechDetailHero
|
||||
className={`mb-0 md:mb-0 md:pt-[27px] ${className ?? ''}`}
|
||||
title={data.headline}
|
||||
body={data.body}
|
||||
bodySecondary={data.bodySecondary}
|
||||
items={data.items}
|
||||
backLink={
|
||||
<Link
|
||||
href={backHref}
|
||||
className="inline-flex cursor-pointer items-center gap-1 text-brand-dark-green transition-opacity hover:opacity-70"
|
||||
>
|
||||
<span className="inline-flex size-3.75 shrink-0 rotate-180 items-center justify-center">
|
||||
<ButtonArrowIcon />
|
||||
</span>
|
||||
<span className="font-mono text-[10px] leading-[1.3] font-medium uppercase">
|
||||
{data.eyebrow}
|
||||
</span>
|
||||
</Link>
|
||||
}
|
||||
status={
|
||||
data.status
|
||||
? {
|
||||
label: data.status.label,
|
||||
body: data.status.body,
|
||||
cta: data.status.cta ? (
|
||||
<Button
|
||||
href={data.status.cta.href}
|
||||
variant={data.status.cta.variant ?? 'secondary'}
|
||||
icon={<DownloadIcon />}
|
||||
className="w-fit cursor-pointer rounded-none"
|
||||
>
|
||||
{data.status.cta.label}
|
||||
</Button>
|
||||
) : null,
|
||||
secondaryCta: data.status.secondaryCta ? (
|
||||
<Button
|
||||
href={data.status.secondaryCta.href}
|
||||
variant={data.status.secondaryCta.variant ?? 'tertiary'}
|
||||
className="w-fit cursor-pointer"
|
||||
>
|
||||
{data.status.secondaryCta.label}
|
||||
</Button>
|
||||
) : null,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
actions={
|
||||
<>
|
||||
{primaryCta ? (
|
||||
<Button
|
||||
href={primaryCta.href}
|
||||
variant={primaryCta.variant ?? actionVariant ?? 'primary'}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
{primaryCta.label}
|
||||
</Button>
|
||||
) : null}
|
||||
{secondaryCta ? (
|
||||
<Button
|
||||
href={secondaryCta.href}
|
||||
variant={secondaryCta.variant ?? actionVariant ?? 'secondary'}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
{secondaryCta.label}
|
||||
</Button>
|
||||
) : null}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
type Props = {
|
||||
children: ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function TechStackDetailPage({ children, className }: Props) {
|
||||
return (
|
||||
<div
|
||||
className={twMerge('bg-brand-off-white text-brand-dark-green', className)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function TechStackDetailSection({ children, className }: Props) {
|
||||
return (
|
||||
<div className={twMerge('mt-15 md:mt-[100px]', className)}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { getTranslations } from 'next-intl/server'
|
||||
import { getPageCopy } from '@repo/content/loaders'
|
||||
import type { Language, TechStackOverviewSection } from '@repo/content/schemas'
|
||||
|
||||
import ContentWidth from '@/components/layout/content-width'
|
||||
import { Reveal, RevealItem } from '@/components/motion/reveal'
|
||||
import { ROUTES } from '@/constants/routes'
|
||||
import { createSectionFinder } from '@/lib/page-sections'
|
||||
@@ -29,7 +30,7 @@ export default async function TechStackExplorer({
|
||||
|
||||
return (
|
||||
<section className="border-brand-dark-green/10 bg-brand-off-white border-t">
|
||||
<div className="mx-auto max-w-360 px-3 pt-25 pb-[118px] md:pb-6">
|
||||
<ContentWidth className="pt-25 pb-29.5 md:pb-6">
|
||||
<Reveal
|
||||
stagger
|
||||
className="flex flex-col gap-3 text-brand-dark-green md:flex-row md:gap-0"
|
||||
@@ -52,7 +53,7 @@ export default async function TechStackExplorer({
|
||||
foundationHref={ROUTES.technologyStack}
|
||||
className="mt-15 md:mt-25"
|
||||
/>
|
||||
</div>
|
||||
</ContentWidth>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { twMerge } from 'tailwind-merge'
|
||||
|
||||
import type { RelatedArticlesSection } from '@repo/content/schemas'
|
||||
|
||||
import ContentWidth from '@/components/layout/content-width'
|
||||
import { Reveal, RevealItem } from '@/components/motion/reveal'
|
||||
import { Button, ButtonArrowIcon } from '@/components/ui'
|
||||
import type { PressArticleRow } from '@/lib/press-engine'
|
||||
@@ -36,17 +37,13 @@ export default function TechStackRelatedArticles({
|
||||
})) ?? articlesToCards(articles)
|
||||
|
||||
return (
|
||||
<section
|
||||
className={twMerge(
|
||||
'mt-15 bg-brand-off-white md:mt-25',
|
||||
sectionClassName
|
||||
)}
|
||||
>
|
||||
<div className="mx-auto h-220 max-w-360 px-3 py-3">
|
||||
<Reveal
|
||||
amount={0.15}
|
||||
className="relative h-full overflow-hidden rounded-xl bg-accent-tan px-3 pt-6 pb-10 md:px-0 md:pt-0 md:pb-0"
|
||||
>
|
||||
<section className={twMerge('mt-15 md:mt-25', sectionClassName)}>
|
||||
<div className="bg-brand-off-white">
|
||||
<ContentWidth className="h-220 px-3 py-3">
|
||||
<Reveal
|
||||
amount={0.15}
|
||||
className="relative h-full overflow-hidden rounded-xl bg-accent-tan px-3 pt-6 pb-10 md:px-0 md:pt-0 md:pb-0"
|
||||
>
|
||||
<div className="flex items-start justify-between md:block">
|
||||
{data.label ? (
|
||||
<p className="text-mono-s w-56.5 max-w-[50%] text-brand-dark-green md:absolute md:top-6 md:left-3 md:max-w-none">
|
||||
@@ -88,6 +85,7 @@ export default function TechStackRelatedArticles({
|
||||
))}
|
||||
</Reveal>
|
||||
</Reveal>
|
||||
</ContentWidth>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
|
||||
@@ -1,29 +1,40 @@
|
||||
import Image from 'next/image'
|
||||
|
||||
import { SectionMarker } from '@acid-info/logos-ui'
|
||||
import type { CardGridSection } from '@repo/content/schemas'
|
||||
|
||||
import { IconMask } from '@/components/icons/icon-mask'
|
||||
import ContentWidth from '@/components/layout/content-width'
|
||||
import { Reveal, RevealItem } from '@/components/motion/reveal'
|
||||
import { Button } from '@/components/ui'
|
||||
|
||||
type Props = {
|
||||
data: CardGridSection
|
||||
}
|
||||
|
||||
type Card = CardGridSection['cards'][number]
|
||||
|
||||
/** object-position tuned per source image so the subject stays framed. */
|
||||
const IMAGE_CLASSNAMES = ['object-[50%_44%]', 'object-[50%_62%]'] as const
|
||||
/** Figma alternates the row fill: grey-02 then grey-01 (node 264:15814). */
|
||||
const ROW_BG = ['bg-gray-02', 'bg-gray-01'] as const
|
||||
|
||||
function ExternalLinkIcon() {
|
||||
return <IconMask src="/icons/external-link.svg" className="size-3.75" />
|
||||
}
|
||||
|
||||
export default function StorageAccess({ data }: Props) {
|
||||
const [moduleCard, libraryCard] = data.cards
|
||||
|
||||
return (
|
||||
<section className="bg-brand-off-white">
|
||||
<div className="mx-auto max-w-360">
|
||||
{moduleCard ? (
|
||||
<FeatureRow card={moduleCard} imagePosition="right" index={0} />
|
||||
) : null}
|
||||
{libraryCard ? (
|
||||
<FeatureRow card={libraryCard} imagePosition="left" index={1} />
|
||||
) : null}
|
||||
</div>
|
||||
</section>
|
||||
<>
|
||||
{moduleCard ? (
|
||||
<FeatureRow card={moduleCard} imagePosition="right" index={0} />
|
||||
) : null}
|
||||
{libraryCard ? (
|
||||
<FeatureRow card={libraryCard} imagePosition="left" index={1} />
|
||||
) : null}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -32,13 +43,13 @@ function FeatureRow({
|
||||
imagePosition,
|
||||
index,
|
||||
}: {
|
||||
card: CardGridSection['cards'][number]
|
||||
card: Card
|
||||
imagePosition: 'left' | 'right'
|
||||
index: number
|
||||
}) {
|
||||
const image = card.image
|
||||
const imagePanel = image ? (
|
||||
<RevealItem className="relative h-[335px] overflow-hidden rounded-xl md:w-178.5">
|
||||
<RevealItem className="relative h-72 w-full overflow-hidden rounded-3xl md:h-83.75">
|
||||
<Image
|
||||
src={image.src}
|
||||
alt={image.alt}
|
||||
@@ -50,34 +61,60 @@ function FeatureRow({
|
||||
) : null
|
||||
|
||||
const copyPanel = (
|
||||
<RevealItem className="relative flex min-h-[335px] flex-col justify-center px-3 py-10 text-brand-dark-green md:w-178.5 md:px-0">
|
||||
{card.label ? (
|
||||
<p className="text-mono-s absolute top-0 left-0 hidden w-73.5 md:block">
|
||||
{card.label}
|
||||
</p>
|
||||
) : null}
|
||||
<div className="max-w-121.25">
|
||||
<h2 className="text-h4-sans">{card.title}</h2>
|
||||
<RevealItem className="flex min-h-83.75 flex-col justify-between gap-6 py-10 text-brand-dark-green md:py-0">
|
||||
{card.label ? <SectionMarker label={card.label} /> : <span aria-hidden />}
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
<h2 className="text-h4-sans md:w-84">{card.title}</h2>
|
||||
{card.description ? (
|
||||
<p className="text-mono-s mt-3">{card.description}</p>
|
||||
<p className="text-caption-sans font-medium md:w-121.25">
|
||||
{card.description}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
{card.footerLabel ? (
|
||||
<p className="text-mono-s absolute bottom-0 left-0 hidden w-73.5 md:block">
|
||||
{card.footerLabel}
|
||||
</p>
|
||||
|
||||
{card.cta || card.secondaryCta ? (
|
||||
<div className="flex items-center gap-1.5">
|
||||
{card.cta ? (
|
||||
<Button
|
||||
href={card.cta.href}
|
||||
variant={card.cta.variant ?? 'primary'}
|
||||
icon={<ExternalLinkIcon />}
|
||||
{...(card.cta.external
|
||||
? { target: '_blank', rel: 'noreferrer' }
|
||||
: {})}
|
||||
>
|
||||
{card.cta.label}
|
||||
</Button>
|
||||
) : null}
|
||||
{card.secondaryCta ? (
|
||||
<Button
|
||||
href={card.secondaryCta.href}
|
||||
variant={card.secondaryCta.variant ?? 'secondary'}
|
||||
{...(card.secondaryCta.external
|
||||
? { target: '_blank', rel: 'noreferrer' }
|
||||
: {})}
|
||||
>
|
||||
{card.secondaryCta.label}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</RevealItem>
|
||||
)
|
||||
|
||||
return (
|
||||
<Reveal
|
||||
stagger
|
||||
amount={0.2}
|
||||
className="grid gap-3 border-brand-dark-green/10 px-3 py-3 md:grid-cols-2"
|
||||
>
|
||||
{imagePosition === 'left' ? imagePanel : copyPanel}
|
||||
{imagePosition === 'left' ? copyPanel : imagePanel}
|
||||
</Reveal>
|
||||
<section>
|
||||
<ContentWidth className={ROW_BG[index] ?? ROW_BG[0]}>
|
||||
<Reveal
|
||||
stagger
|
||||
amount={0.2}
|
||||
className="grid gap-3 md:grid-cols-2 md:items-stretch"
|
||||
>
|
||||
{imagePosition === 'left' ? imagePanel : copyPanel}
|
||||
{imagePosition === 'left' ? copyPanel : imagePanel}
|
||||
</Reveal>
|
||||
</ContentWidth>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,88 +1,12 @@
|
||||
import { TechDetailHero } from '@acid-info/logos-ui'
|
||||
import type { HeroSection } from '@repo/content/schemas'
|
||||
|
||||
import { Reveal } from '@/components/motion/reveal'
|
||||
import { Button, ButtonArrowIcon } from '@/components/ui'
|
||||
import { Link } from '@/i18n/navigation'
|
||||
|
||||
import { DownloadIcon } from '../shared/builder-cta-card'
|
||||
import TechStackDetailHero from '@/components/sections/shared/tech-stack-detail-hero'
|
||||
|
||||
type Props = {
|
||||
data: HeroSection
|
||||
backHref: string
|
||||
}
|
||||
|
||||
export default function StorageHero({ data, backHref }: Props) {
|
||||
const [primaryCta, secondaryCta] = data.ctas ?? []
|
||||
|
||||
return (
|
||||
<Reveal amount={0.2}>
|
||||
<TechDetailHero
|
||||
title={data.headline}
|
||||
body={data.body}
|
||||
bodySecondary={data.bodySecondary}
|
||||
items={data.items}
|
||||
backLink={
|
||||
<Link
|
||||
href={backHref}
|
||||
className="inline-flex cursor-pointer items-center gap-1 text-brand-dark-green transition-opacity hover:opacity-70"
|
||||
>
|
||||
<span className="inline-flex size-3.75 shrink-0 rotate-180 items-center justify-center">
|
||||
<ButtonArrowIcon />
|
||||
</span>
|
||||
<span className="font-mono text-[10px] font-medium leading-[1.3] uppercase">
|
||||
{data.eyebrow}
|
||||
</span>
|
||||
</Link>
|
||||
}
|
||||
status={
|
||||
data.status
|
||||
? {
|
||||
label: data.status.label,
|
||||
body: data.status.body,
|
||||
cta: data.status.cta ? (
|
||||
<Button
|
||||
href={data.status.cta.href}
|
||||
variant={data.status.cta.variant ?? 'secondary'}
|
||||
icon={<DownloadIcon />}
|
||||
className="w-fit cursor-pointer rounded-none"
|
||||
>
|
||||
{data.status.cta.label}
|
||||
</Button>
|
||||
) : null,
|
||||
secondaryCta: data.status.secondaryCta ? (
|
||||
<Button
|
||||
href={data.status.secondaryCta.href}
|
||||
variant={data.status.secondaryCta.variant ?? 'tertiary'}
|
||||
className="w-fit cursor-pointer"
|
||||
>
|
||||
{data.status.secondaryCta.label}
|
||||
</Button>
|
||||
) : null,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
actions={
|
||||
<>
|
||||
{primaryCta ? (
|
||||
<Button
|
||||
href={primaryCta.href}
|
||||
variant={primaryCta.variant ?? 'primary'}
|
||||
>
|
||||
{primaryCta.label}
|
||||
</Button>
|
||||
) : null}
|
||||
{secondaryCta ? (
|
||||
<Button
|
||||
href={secondaryCta.href}
|
||||
variant={secondaryCta.variant ?? 'secondary'}
|
||||
>
|
||||
{secondaryCta.label}
|
||||
</Button>
|
||||
) : null}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</Reveal>
|
||||
)
|
||||
export default function StorageHero(props: Props) {
|
||||
return <TechStackDetailHero {...props} />
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ export default function StorageMain({ data }: Props) {
|
||||
return (
|
||||
<Reveal amount={0.2}>
|
||||
<TechTextSplitSection
|
||||
className="mb-15 md:mb-25"
|
||||
title={data.title}
|
||||
body={
|
||||
data.description
|
||||
|
||||
@@ -8,12 +8,12 @@ import { Button } from '@/components/ui'
|
||||
|
||||
const CARD_IMAGES = [
|
||||
{
|
||||
src: '/images/storage/use-case-1.webp',
|
||||
className: 'object-[50%_44%]',
|
||||
src: '/images/storage/use-case-figma-1.webp',
|
||||
className: 'rotate-90 scale-[1.78] object-cover blur-[20px]',
|
||||
},
|
||||
{
|
||||
src: '/images/storage/use-case-2.webp',
|
||||
className: 'object-[50%_62%]',
|
||||
src: '/images/storage/use-case-figma-2.webp',
|
||||
className: 'scale-[1.55] object-cover blur-[20px]',
|
||||
},
|
||||
] as const
|
||||
|
||||
@@ -30,7 +30,11 @@ export default function StorageUseCases({ data }: Props) {
|
||||
title: card.title,
|
||||
body: card.description,
|
||||
cta: card.cta ? (
|
||||
<Button href={card.cta.href} variant="primary">
|
||||
<Button
|
||||
href={card.cta.href}
|
||||
variant="primary"
|
||||
className="cursor-pointer bg-brand-off-white text-brand-dark-green hover:bg-brand-off-white/90"
|
||||
>
|
||||
{card.cta.label}
|
||||
</Button>
|
||||
) : null,
|
||||
@@ -48,7 +52,7 @@ export default function StorageUseCases({ data }: Props) {
|
||||
|
||||
return (
|
||||
<Reveal amount={0.2}>
|
||||
<TechUseCaseGrid cards={cards} />
|
||||
<TechUseCaseGrid cards={cards} cardClassName="rounded-xl" />
|
||||
</Reveal>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import Image from 'next/image'
|
||||
import type { HeroSection } from '@repo/content/schemas'
|
||||
|
||||
import { IconMask } from '@/components/icons/icon-mask'
|
||||
import ContentWidth from '@/components/layout/content-width'
|
||||
import { Reveal } from '@/components/motion/reveal'
|
||||
import { Button } from '@/components/ui'
|
||||
|
||||
@@ -73,8 +74,8 @@ function StatusCard({
|
||||
|
||||
export default function TechOverviewHero({ data }: Props) {
|
||||
return (
|
||||
<section className="relative mb-10 h-[663px] overflow-hidden bg-brand-off-white px-3 pt-0 pb-0 md:mb-[100px] md:-mt-0.5 md:h-[486px] md:pt-[22px] md:pb-0">
|
||||
<div className="relative h-[500px] md:h-[403px]">
|
||||
<section className="relative mb-10 h-[663px] overflow-hidden bg-brand-off-white px-3 pt-10 pb-0 md:mb-25 md:h-[486px] md:pt-8 md:pb-0">
|
||||
<ContentWidth className="relative h-[500px] md:h-[403px]">
|
||||
<div className="absolute top-0 left-0 hidden h-[99px] w-[393px] md:block">
|
||||
{data.status ? <StatusCard status={data.status} compact /> : null}
|
||||
</div>
|
||||
@@ -156,7 +157,7 @@ export default function TechOverviewHero({ data }: Props) {
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</ContentWidth>
|
||||
<div className="absolute top-[540px] left-0 h-10 w-full px-3 md:hidden">
|
||||
<div className="h-px w-full bg-brand-dark-green/10" />
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { GiantSwitch, GiantSwitchTag } from '@acid-info/logos-ui'
|
||||
import type { GiantSwitchSection } from '@repo/content/schemas'
|
||||
|
||||
import { IconMask } from '@/components/icons/icon-mask'
|
||||
import ContentWidth from '@/components/layout/content-width'
|
||||
import { Button } from '@/components/ui'
|
||||
|
||||
/**
|
||||
@@ -51,11 +52,9 @@ export default function TechOverviewLogosApp({ data }: Props) {
|
||||
: ''
|
||||
|
||||
return (
|
||||
<section
|
||||
id="logos-app"
|
||||
className="mt-10 mb-10 h-[828px] overflow-hidden bg-brand-off-white py-4 md:mt-[100px] md:mb-[100px] md:h-auto md:py-0"
|
||||
>
|
||||
<GiantSwitch
|
||||
<section id="logos-app" className="mt-10 mb-10 md:mt-[100px] md:mb-[100px]">
|
||||
<ContentWidth className="h-[828px] overflow-hidden bg-brand-off-white py-4 md:h-auto md:py-0">
|
||||
<GiantSwitch
|
||||
accent={data.accent}
|
||||
imagePosition={data.imagePosition}
|
||||
installHoverShift
|
||||
@@ -123,6 +122,7 @@ export default function TechOverviewLogosApp({ data }: Props) {
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</ContentWidth>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,14 +2,17 @@ import Image from 'next/image'
|
||||
|
||||
import type { FeaturedTextSection } from '@repo/content/schemas'
|
||||
|
||||
import ContentWidth from '@/components/layout/content-width'
|
||||
|
||||
type Props = {
|
||||
data: FeaturedTextSection
|
||||
}
|
||||
|
||||
export default function TechOverviewModular({ data }: Props) {
|
||||
return (
|
||||
<section className="my-10 bg-brand-off-white p-3 md:my-[102px]">
|
||||
<div>
|
||||
<section className="my-10 md:my-[102px]">
|
||||
<div className="bg-brand-off-white p-3">
|
||||
<ContentWidth>
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
<div className="relative h-[248px] overflow-hidden rounded-[100px] md:h-[518px] md:rounded-[100px]">
|
||||
<Image
|
||||
@@ -51,6 +54,7 @@ export default function TechOverviewModular({ data }: Props) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ContentWidth>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { TableSection } from '@repo/content/schemas'
|
||||
|
||||
import { IconMask } from '@/components/icons/icon-mask'
|
||||
import ContentWidth from '@/components/layout/content-width'
|
||||
|
||||
function ExternalLinkIcon() {
|
||||
return <IconMask src="/icons/external-link.svg" className="size-[15px]" />
|
||||
@@ -26,10 +27,8 @@ type Props = {
|
||||
|
||||
export default function TechOverviewOpenSource({ data }: Props) {
|
||||
return (
|
||||
<section
|
||||
id="open-source"
|
||||
className="relative h-[460px] border-t border-brand-dark-green/10 bg-brand-off-white md:h-[421px]"
|
||||
>
|
||||
<section id="open-source" className="relative border-t border-brand-dark-green/10">
|
||||
<ContentWidth className="relative h-[460px] bg-brand-off-white md:h-[421px]">
|
||||
<div className="absolute top-[39px] left-3">
|
||||
<h2 className="text-h4-sans whitespace-nowrap text-brand-dark-green">
|
||||
{data.title}
|
||||
@@ -107,6 +106,7 @@ export default function TechOverviewOpenSource({ data }: Props) {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ContentWidth>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import Image from 'next/image'
|
||||
import { LogosMark } from '@acid-info/logos-ui'
|
||||
import type { TechStackOverviewSection } from '@repo/content/schemas'
|
||||
|
||||
import ContentWidth from '@/components/layout/content-width'
|
||||
import { TechStackDiagram } from '@/components/sections/shared/tech-stack-diagram'
|
||||
import { Button } from '@/components/ui'
|
||||
|
||||
@@ -47,11 +48,9 @@ export default function TechOverviewStack({
|
||||
foundationHref,
|
||||
}: Props) {
|
||||
return (
|
||||
<section
|
||||
id="stack"
|
||||
className="bg-brand-off-white px-3 pb-[27px] md:pb-[100px]"
|
||||
>
|
||||
<div>
|
||||
<section id="stack">
|
||||
<div className="bg-brand-off-white px-3 pb-[27px] md:pb-[100px]">
|
||||
<ContentWidth>
|
||||
{data.architecture ? (
|
||||
<div className="-mx-3 mb-10 h-[658px] bg-gray-01 px-3 py-3 md:mb-[100px] md:h-[381px]">
|
||||
<div className="grid md:grid-cols-2 md:gap-3">
|
||||
@@ -93,8 +92,8 @@ export default function TechOverviewStack({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative h-[317px] overflow-hidden rounded-[24px] md:h-[357px] md:rounded-[24px]">
|
||||
<div className="absolute top-[-53px] left-0 h-[936px] w-[702px] md:top-[-33px]">
|
||||
<div className="relative w-full h-[317px] overflow-hidden rounded-[24px] md:h-[357px] md:rounded-[24px]">
|
||||
<div className="absolute top-[-53px] left-0 h-[936px] w-full md:top-[-33px]">
|
||||
<Image
|
||||
src={data.architecture.image.src}
|
||||
alt={data.architecture.image.alt}
|
||||
@@ -128,6 +127,7 @@ export default function TechOverviewStack({
|
||||
foundationHref={foundationHref}
|
||||
/>
|
||||
</div>
|
||||
</ContentWidth>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
|
||||
@@ -6,6 +6,7 @@ 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 } from '@/components/ui'
|
||||
|
||||
type UseCaseCardData = {
|
||||
@@ -173,8 +174,8 @@ export default function TechOverviewUseCases({ data }: Props) {
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="h-[820px] overflow-hidden bg-brand-off-white px-3 pt-10 pb-10 md:pb-16">
|
||||
<div>
|
||||
<section>
|
||||
<div className="h-[820px] overflow-hidden bg-brand-off-white px-3 pt-10 pb-10 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
|
||||
@@ -191,7 +192,7 @@ export default function TechOverviewUseCases({ data }: Props) {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="relative hidden md:block md:h-[309px]">
|
||||
<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"
|
||||
@@ -233,7 +234,7 @@ export default function TechOverviewUseCases({ data }: Props) {
|
||||
{data.cta.label}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</ContentWidth>
|
||||
|
||||
{data.heading ? (
|
||||
<div className="relative left-1/2 mt-16 w-[464px] max-w-none -translate-x-1/2 md:hidden">
|
||||
|
||||
@@ -100,10 +100,10 @@
|
||||
"quote": "The system is no longer working for us, so we are charting a new course.",
|
||||
"headline": "Logos exists at the intersection of human exploration and technological revolution.",
|
||||
"body": "We are a movement for builders, explorers, and dreamers who refuse to accept the world as it is. We're part of a generation fed up with corruption, surveillance, and stagnation often associated with incumbent institutions.",
|
||||
"intro": "The world's systems of money, law, and governance are increasingly coerced by interests that do not serve the people inside them. The institutions, the corporations, the platforms, have been captured.",
|
||||
"closing1": "They surveil us, extract from us, and make decisions on our behalf without our consent. Why are we placing our trust in ancient nation-state technology?",
|
||||
"closing2": "We are not asking them to change.",
|
||||
"closing3": "We are building the alternative.",
|
||||
"intro": "Civil society is in decline. Our institutions are failing. Our money is debased. People are atomised, indebted, and increasingly unable to provide for themselves or their families. Reform from within cannot reverse this because the system selects for compliance, and centralisation leads inevitably to corruption.",
|
||||
"closing1": "Logos exist to build the decentralised infrastructure and living ecosystem that makes a parallel society possible for anyone ready to reclaim their agency in the digital and physical world.",
|
||||
"closing2": "Built for exit when the existing order can no longer hold.",
|
||||
"closing3": "Build the parallel. Prove it in practice. Keep it human.",
|
||||
"cta": "About us",
|
||||
"problems": {
|
||||
"debt": {
|
||||
@@ -137,8 +137,8 @@
|
||||
}
|
||||
},
|
||||
"builderPortal": {
|
||||
"title": "Basecamp is our\nExit Guide.",
|
||||
"description": "Basecamp is a ready-to-run distribution that you deploy on your own hardware, under your control. Full stack privacy is the only option for a parallel society – coercion resistant coordfination, transactions free of surveillance, money you control.\n\nThe Logos Runtime runs running locally, core modules are loaded, and essential applications — wallet, messenger, file sharing, blockchain explorer — are ready to use from day one.",
|
||||
"title": "Basecamp.\nThe local launcher\nfor the Logos stack",
|
||||
"description": "Basecamp is a ready-to-run distribution that you deploy on your own hardware, under your control. From the moment it’s installed, you’re standing on your own ground, not connecting to someone else’s infrastructure.\n\nThe Logos Runtime runs running locally, core modules are loaded, and essential applications — wallet, messenger, file sharing, blockchain explorer — are ready to use from day one.",
|
||||
"feature1": "Everything you need to start building privacy-first decentralised applications. Explore ideas, find bounties, and connect with others.",
|
||||
"feature2": "The Node Programme is for anyone who wants to join our movement to revitalise civil society using decentralised technologies.",
|
||||
"feature3": "Local chapters are at the heart of our movement. Learn civil organising, share skills, and forge new connections.",
|
||||
@@ -254,7 +254,7 @@
|
||||
},
|
||||
"build": {
|
||||
"number": "04",
|
||||
"heading": "What can you build today",
|
||||
"heading": "What you can build today",
|
||||
"cta": "Run app",
|
||||
"nodeCta": "Run a node",
|
||||
"messagingCta": "Messaging SDK",
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 100 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 244 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 266 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 100 KiB |
@@ -72,7 +72,8 @@
|
||||
"alt": "",
|
||||
"width": 850,
|
||||
"height": 326
|
||||
}
|
||||
},
|
||||
"imageFit": "contain"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -73,7 +73,18 @@
|
||||
"title": "Delivery",
|
||||
"mobileTitle": "Logos Messaging Nim (LMN)",
|
||||
"description": "The transport layer. A peer-to-peer messaging network with DoS protection via RLN (Rate Limiting Nullifiers). Any application can use it to send and receive messages over an open, censorship-resistant network.",
|
||||
"mobileDescription": "Built around the Logos Messaging Nim (LMN) node, it provides publish-subscribe messaging, reliable delivery, and is evolving toward a ChatSDK that simplifies building encrypted communication into applications."
|
||||
"mobileDescription": "Built around the Logos Messaging Nim (LMN) node, it provides publish-subscribe messaging, reliable delivery, and is evolving toward a ChatSDK that simplifies building encrypted communication into applications.",
|
||||
"cta": {
|
||||
"label": "Repo",
|
||||
"href": "https://github.com/waku-org/nwaku",
|
||||
"external": true
|
||||
},
|
||||
"secondaryCta": {
|
||||
"label": "Docs",
|
||||
"href": "https://github.com/logos-co/logos-docs",
|
||||
"external": true,
|
||||
"variant": "secondary"
|
||||
}
|
||||
},
|
||||
{
|
||||
"componentType": "ctaPanel",
|
||||
@@ -83,7 +94,18 @@
|
||||
"title": "Chat",
|
||||
"mobileTitle": "Censorship Resistance",
|
||||
"description": "The chat layer. A protocol and library that provides 1:1 and group conversations with end-to-end encryption using de-MLS - everything an application needs to add private chat without rolling its own protocol.",
|
||||
"mobileDescription": "The relay network is designed to function even in hostile network environments. There is no single point of failure, no central server to block, and no metadata that reveals communication patterns to observers."
|
||||
"mobileDescription": "The relay network is designed to function even in hostile network environments. There is no single point of failure, no central server to block, and no metadata that reveals communication patterns to observers.",
|
||||
"cta": {
|
||||
"label": "Repo",
|
||||
"href": "https://github.com/logos-co/logos-chat-module",
|
||||
"external": true
|
||||
},
|
||||
"secondaryCta": {
|
||||
"label": "Docs",
|
||||
"href": "https://github.com/logos-co/logos-docs",
|
||||
"external": true,
|
||||
"variant": "secondary"
|
||||
}
|
||||
},
|
||||
{
|
||||
"componentType": "cardGrid",
|
||||
|
||||
@@ -73,8 +73,7 @@
|
||||
"key": "storage.access",
|
||||
"cards": [
|
||||
{
|
||||
"label": "Messaging",
|
||||
"footerLabel": "Messaging",
|
||||
"label": "Storage",
|
||||
"title": "Storage Module API",
|
||||
"description": "The module API gives applications a clean interface for storing and retrieving content: initialise the module, start the node, upload a file to receive a CID, and download by CID. Streaming upload is available where you need finer control.",
|
||||
"image": {
|
||||
@@ -82,11 +81,23 @@
|
||||
"alt": "",
|
||||
"width": 2880,
|
||||
"height": 3839
|
||||
},
|
||||
"cta": {
|
||||
"label": "Repo",
|
||||
"href": "https://github.com/codex-storage",
|
||||
"external": true,
|
||||
"variant": "primary",
|
||||
"iconOverride": "external"
|
||||
},
|
||||
"secondaryCta": {
|
||||
"label": "Docs",
|
||||
"href": "https://github.com/logos-co/logos-docs",
|
||||
"external": true,
|
||||
"variant": "secondary"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Messaging",
|
||||
"footerLabel": "Storage",
|
||||
"label": "Storage",
|
||||
"title": "libstorage",
|
||||
"description": "For lower-level access or to build bindings for another language, libstorage exposes the underlying storage primitives directly.",
|
||||
"image": {
|
||||
@@ -94,6 +105,19 @@
|
||||
"alt": "",
|
||||
"width": 2560,
|
||||
"height": 2048
|
||||
},
|
||||
"cta": {
|
||||
"label": "Repo",
|
||||
"href": "https://github.com/codex-storage",
|
||||
"external": true,
|
||||
"variant": "primary",
|
||||
"iconOverride": "external"
|
||||
},
|
||||
"secondaryCta": {
|
||||
"label": "Docs",
|
||||
"href": "https://github.com/logos-co/logos-docs",
|
||||
"external": true,
|
||||
"variant": "secondary"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -273,6 +273,13 @@ const builderHubInfoCardSchema = z.object({
|
||||
title: z.string().min(1),
|
||||
description: z.string().min(1),
|
||||
image: mediaRefSchema.optional(),
|
||||
/**
|
||||
* How the card image fills its frame. `cover` (default) crops to fill —
|
||||
* correct for full-bleed graphics. `contain` shows the whole image without
|
||||
* cropping — needed for images with built-in padding (e.g. the terminal
|
||||
* mockup) that would otherwise be clipped.
|
||||
*/
|
||||
imageFit: z.enum(['cover', 'contain']).default('cover'),
|
||||
ctas: z.array(ctaSchema).default([]),
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user