mirror of
https://github.com/logos-co/logos-web.git
synced 2026-08-27 12:11:14 +00:00
UI updates (#15)
* Refactor technology stack integration and update documentation links - Updated the technology stack links in various components to point to the new GitHub documentation repository. - Refactored the Lambda Prize page to utilize a shared TechStackSection instead of local tech cards. - Removed redundant tech stack message copies from the storage and technology stack pages. - Enhanced the home feature cards section to use Link components for navigation. - Added tests to ensure the integrity of the new tech stack structure and documentation links. * feat: integrate social proof stats into home page and update related components
This commit is contained in:
@@ -7,7 +7,6 @@ import Image from 'next/image'
|
||||
import { getTranslations } from 'next-intl/server'
|
||||
|
||||
import {
|
||||
CardInfo,
|
||||
Footer,
|
||||
GiantSwitch,
|
||||
GiantSwitchTag,
|
||||
@@ -17,155 +16,28 @@ import {
|
||||
TableRow,
|
||||
ViewToggle,
|
||||
} from '@acid-info/logos-ui'
|
||||
import { Button, Card } from '@/components/ui'
|
||||
import type { Language } from '@repo/content/schemas'
|
||||
|
||||
import { TechStackDiagram } from '@/components/sections/shared/tech-stack-diagram'
|
||||
import { Button } from '@/components/ui'
|
||||
import { EXTERNAL_URLS, ROUTES } from '@/constants/routes'
|
||||
|
||||
const cardImages = {
|
||||
storage: '/design-systems/storage.png',
|
||||
messaging: '/design-systems/messaging.png',
|
||||
blockchain: '/design-systems/blockchain.png',
|
||||
userModules: '/design-systems/user-modules.png',
|
||||
networking: '/design-systems/networking.png',
|
||||
kernel: '/design-systems/kernel.png',
|
||||
} as const
|
||||
|
||||
function Thumb({ src, alt }: { src: string; alt: string }) {
|
||||
return <Image src={src} alt={alt} width={46} height={57} />
|
||||
}
|
||||
import { getHomeTechStackOverview } from '@/lib/tech-stack-overview'
|
||||
|
||||
// --- Cards --------------------------------------------------------------
|
||||
|
||||
function CardGrid({ state }: { state: 'default' | 'hover' }) {
|
||||
const isHover = state === 'hover'
|
||||
const networkingTitle = (
|
||||
<>
|
||||
<span className="block">The Networking Stack:</span>
|
||||
<span className="block">Discovery, Peering, and Mix-Net</span>
|
||||
</>
|
||||
)
|
||||
export async function Cards({ locale }: { locale: Language }) {
|
||||
const techStack = await getHomeTechStackOverview(locale)
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-[12px]">
|
||||
{/* Row 1: four small cards */}
|
||||
<div className="grid grid-cols-1 gap-[12px] sm:grid-cols-2 lg:grid-cols-4">
|
||||
<Card
|
||||
height={366}
|
||||
forceHover={isHover}
|
||||
staticDefault={!isHover}
|
||||
image={isHover && <Thumb src={cardImages.storage} alt="" />}
|
||||
title="Storage"
|
||||
description={
|
||||
isHover
|
||||
? 'Decentralised file storage and retrieval, using content-addressed (CID-based) data'
|
||||
: undefined
|
||||
}
|
||||
ctaHref={isHover ? ROUTES.storage : undefined}
|
||||
/>
|
||||
<Card
|
||||
height={366}
|
||||
forceHover={isHover}
|
||||
staticDefault={!isHover}
|
||||
image={isHover && <Thumb src={cardImages.messaging} alt="" />}
|
||||
title="Messaging"
|
||||
description={
|
||||
isHover
|
||||
? 'Private, censorship-resistant communication between parties.'
|
||||
: undefined
|
||||
}
|
||||
ctaHref={isHover ? ROUTES.messaging : undefined}
|
||||
/>
|
||||
<Card
|
||||
height={366}
|
||||
forceHover={isHover}
|
||||
staticDefault={!isHover}
|
||||
image={isHover && <Thumb src={cardImages.blockchain} alt="" />}
|
||||
title="Blockchain"
|
||||
description={
|
||||
isHover ? 'Decentralised compute and consensus.' : undefined
|
||||
}
|
||||
ctaHref={isHover ? ROUTES.blockchain : undefined}
|
||||
>
|
||||
{isHover && (
|
||||
<>
|
||||
<CardInfo
|
||||
height={78}
|
||||
label="Logos Execution Zone (LEZ)"
|
||||
description="Developers can deploy programs, run AMMs, transfer tokens, and build financial primitives with built-in privacy."
|
||||
/>
|
||||
<CardInfo
|
||||
height={78}
|
||||
label="Data Availability and Consensus: Cryptarchia"
|
||||
description="A private proof-of-stake consensus mechanism where validator identities and stake amounts remain hidden."
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
<Card
|
||||
height={366}
|
||||
forceHover={isHover}
|
||||
staticDefault={!isHover}
|
||||
image={isHover && <Thumb src={cardImages.userModules} alt="" />}
|
||||
title="User Modules"
|
||||
description={
|
||||
isHover
|
||||
? 'Anyone can build modules that plug into the same IPC infrastructure.'
|
||||
: undefined
|
||||
}
|
||||
ctaHref={isHover ? ROUTES.technologyStack : undefined}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Row 2: wide band — Networking Stack (no Lambda icon glyph in Figma) */}
|
||||
<Card
|
||||
height={196}
|
||||
forceHover={isHover}
|
||||
staticDefault={!isHover}
|
||||
showIcon={false}
|
||||
image={isHover && <Thumb src={cardImages.networking} alt="" />}
|
||||
title={networkingTitle}
|
||||
description={
|
||||
isHover
|
||||
? 'This layer handles how Logos nodes find each other, establish connections, and communicate.'
|
||||
: undefined
|
||||
}
|
||||
ctaHref={isHover ? ROUTES.networking : undefined}
|
||||
<div className="flex w-full flex-col gap-[32px] bg-white p-[20px]">
|
||||
<h2 className="font-display text-[64px] leading-[1] tracking-[-0.03em] text-brand-dark-green">
|
||||
Cards
|
||||
</h2>
|
||||
<TechStackDiagram
|
||||
data={techStack}
|
||||
networkingHref={ROUTES.networking}
|
||||
foundationHref={ROUTES.technologyStack}
|
||||
/>
|
||||
|
||||
{/* Row 3: wide band — Foundation Kernel (no Lambda icon glyph in Figma) */}
|
||||
<Card
|
||||
height={196}
|
||||
forceHover={isHover}
|
||||
staticDefault={!isHover}
|
||||
showIcon={false}
|
||||
image={isHover && <Thumb src={cardImages.kernel} alt="" />}
|
||||
title="The Foundation: Logos Kernel"
|
||||
description={
|
||||
isHover
|
||||
? 'A microkernel that handles the essential primitives every decentralised application needs.'
|
||||
: undefined
|
||||
}
|
||||
ctaHref={isHover ? ROUTES.technologyStack : undefined}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function Cards() {
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-[80px] bg-white p-[20px]">
|
||||
<div className="flex flex-col gap-[32px]">
|
||||
<h2 className="font-display text-[64px] leading-[1] tracking-[-0.03em] text-brand-dark-green">
|
||||
Default
|
||||
</h2>
|
||||
<CardGrid state="default" />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-[32px]">
|
||||
<h2 className="font-display text-[64px] leading-[1] tracking-[-0.03em] text-brand-dark-green">
|
||||
Hover
|
||||
</h2>
|
||||
<CardGrid state="hover" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -183,7 +55,7 @@ export function Buttons() {
|
||||
<p className="font-mono text-[10px] leading-[1.3] font-medium text-brand-dark-green uppercase opacity-50">
|
||||
Primary
|
||||
</p>
|
||||
<Button variant="primary" href={ROUTES.technologyStack}>
|
||||
<Button variant="primary" href={EXTERNAL_URLS.docs}>
|
||||
View The Docs
|
||||
</Button>
|
||||
</div>
|
||||
@@ -191,7 +63,7 @@ export function Buttons() {
|
||||
<p className="font-mono text-[10px] leading-[1.3] font-medium text-brand-dark-green uppercase opacity-50">
|
||||
Secondary
|
||||
</p>
|
||||
<Button variant="secondary" href={ROUTES.technologyStack}>
|
||||
<Button variant="secondary" href={EXTERNAL_URLS.docs}>
|
||||
View The Docs
|
||||
</Button>
|
||||
</div>
|
||||
@@ -199,7 +71,7 @@ export function Buttons() {
|
||||
<p className="font-mono text-[10px] leading-[1.3] font-medium text-brand-dark-green uppercase opacity-50">
|
||||
Tertiary
|
||||
</p>
|
||||
<Button variant="tertiary" href={ROUTES.technologyStack}>
|
||||
<Button variant="tertiary" href={EXTERNAL_URLS.docs}>
|
||||
View The Docs
|
||||
</Button>
|
||||
</div>
|
||||
@@ -207,7 +79,7 @@ export function Buttons() {
|
||||
<p className="font-mono text-[10px] leading-[1.3] font-medium text-brand-dark-green uppercase opacity-50">
|
||||
Link
|
||||
</p>
|
||||
<Button variant="link" href={ROUTES.technologyStack}>
|
||||
<Button variant="link" href={EXTERNAL_URLS.docs}>
|
||||
View The Docs
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
* - components.tsx → Cards / Buttons / Tables / GiantSwitches /
|
||||
* ViewToggles / Paginations / Footers
|
||||
*/
|
||||
import { isActiveLocale } from '@repo/content/locales'
|
||||
|
||||
import { createDefaultMetadata } from '@/lib/metadata'
|
||||
|
||||
import { ColorPalette } from './_sections/color-palette'
|
||||
@@ -37,12 +39,21 @@ export async function generateMetadata({
|
||||
})
|
||||
}
|
||||
|
||||
export default function DesignSystemsPage() {
|
||||
export default async function DesignSystemsPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ locale: string }>
|
||||
}) {
|
||||
const { locale } = await params
|
||||
if (!isActiveLocale(locale)) {
|
||||
throw new Error(`DesignSystemsPage received non-active locale "${locale}"`)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex w-full max-w-[1440px] flex-col gap-[40px] py-10">
|
||||
<ColorPalette />
|
||||
<TypeStyles />
|
||||
<Cards />
|
||||
<Cards locale={locale} />
|
||||
<Buttons />
|
||||
<Tables />
|
||||
<GiantSwitches />
|
||||
|
||||
@@ -11,10 +11,10 @@ const NAMESPACE = 'pages.getStarted'
|
||||
|
||||
const docs = [
|
||||
{ key: 'architecture', href: EXTERNAL_URLS.docs },
|
||||
{ key: 'blockchain', href: `${EXTERNAL_URLS.docs}/logos-stack/blockchain` },
|
||||
{ key: 'messaging', href: `${EXTERNAL_URLS.docs}/logos-stack/messaging` },
|
||||
{ key: 'storage', href: `${EXTERNAL_URLS.docs}/logos-stack/storage` },
|
||||
{ key: 'api', href: `${EXTERNAL_URLS.docs}/api` },
|
||||
{ key: 'blockchain', href: EXTERNAL_URLS.docs },
|
||||
{ key: 'messaging', href: EXTERNAL_URLS.docs },
|
||||
{ key: 'storage', href: EXTERNAL_URLS.docs },
|
||||
{ key: 'api', href: EXTERNAL_URLS.docs },
|
||||
] as const
|
||||
|
||||
const communityLinks = [
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import Image from 'next/image'
|
||||
import { LogosMark } from '@acid-info/logos-ui'
|
||||
import type { TechStackOverviewSection } from '@repo/content/schemas'
|
||||
|
||||
import TechStackSection from '@/components/sections/home/tech-stack-section'
|
||||
import { Button, ButtonArrowIcon } from '@/components/ui'
|
||||
import { ROUTES } from '@/constants/routes'
|
||||
import { Link } from '@/i18n/navigation'
|
||||
import { DownloadIcon } from '@/components/sections/shared/builder-cta-card'
|
||||
|
||||
interface RowCopy {
|
||||
label: string
|
||||
@@ -18,17 +19,6 @@ interface PrizeCopy {
|
||||
status: string
|
||||
}
|
||||
|
||||
interface TechCardCopy {
|
||||
title: string
|
||||
body: string
|
||||
badges?: string[]
|
||||
}
|
||||
|
||||
interface TechWideCopy {
|
||||
title: string
|
||||
body: string
|
||||
}
|
||||
|
||||
interface SupportRowCopy extends RowCopy {
|
||||
action: string
|
||||
}
|
||||
@@ -62,22 +52,6 @@ interface LambdaPrizePageCopy {
|
||||
secondaryCta: string
|
||||
rows: RowCopy[]
|
||||
}
|
||||
techStack: {
|
||||
disclaimer: string
|
||||
heading: string
|
||||
body: string
|
||||
cta: string
|
||||
basecamp: {
|
||||
title: string
|
||||
body: string
|
||||
cta: string
|
||||
}
|
||||
cards: TechCardCopy[]
|
||||
networking: TechWideCopy
|
||||
foundation: TechWideCopy
|
||||
primaryCta: string
|
||||
secondaryCta: string
|
||||
}
|
||||
support: {
|
||||
heading: string
|
||||
body: string
|
||||
@@ -88,6 +62,7 @@ interface LambdaPrizePageCopy {
|
||||
|
||||
interface LambdaPrizePageProps {
|
||||
copy: LambdaPrizePageCopy
|
||||
techStack: TechStackOverviewSection
|
||||
}
|
||||
|
||||
const prizeImages = [
|
||||
@@ -327,113 +302,6 @@ function AboutProgramme({ copy }: { copy: LambdaPrizePageCopy['about'] }) {
|
||||
)
|
||||
}
|
||||
|
||||
function TechCard({ card }: { card: TechCardCopy }) {
|
||||
return (
|
||||
<div className="flex h-[258px] items-center justify-center rounded-3xl border border-brand-dark-green p-3 text-center md:h-[366px]">
|
||||
<div className="flex w-full flex-col items-center gap-3">
|
||||
<LogosMark size={18} className="hidden md:block" />
|
||||
<h3 className="text-subhead-sans">{card.title}</h3>
|
||||
<p className="text-mono-s max-w-[208px] text-center">{card.body}</p>
|
||||
{card.badges ? (
|
||||
<div className="mt-2 flex w-full max-w-[164px] flex-col gap-1.5">
|
||||
{card.badges.map((badge) => (
|
||||
<div
|
||||
key={badge}
|
||||
className="rounded-[18px] border border-brand-dark-green/50 px-2 py-2 text-center font-mono text-[10px] font-medium uppercase leading-[1.3]"
|
||||
>
|
||||
{badge}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function WideTechCard({
|
||||
title,
|
||||
body,
|
||||
className = '',
|
||||
}: TechWideCopy & { className?: string }) {
|
||||
return (
|
||||
<div
|
||||
className={`flex h-[196px] items-center justify-center rounded-3xl border border-brand-dark-green p-3 text-center ${className}`}
|
||||
>
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<h3 className="text-subhead-sans max-w-[320px] whitespace-pre-line">
|
||||
{title}
|
||||
</h3>
|
||||
<p className="text-mono-s max-w-[208px] text-center">{body}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function TechStack({ copy }: { copy: LambdaPrizePageCopy['techStack'] }) {
|
||||
const headingLines = copy.heading.split('\n')
|
||||
|
||||
return (
|
||||
<section className="mt-[100px] h-[1548px] bg-brand-off-white px-3 pt-3 text-brand-dark-green md:mt-0 md:h-[1507px]">
|
||||
<div className="flex items-start justify-between">
|
||||
<p className="text-mono-s w-[226px]">{copy.disclaimer}</p>
|
||||
<div className="flex flex-col gap-1 md:flex-row">
|
||||
<Button href={ROUTES.technologyStack} className="cursor-pointer">
|
||||
{copy.primaryCta}
|
||||
</Button>
|
||||
<Button
|
||||
href={ROUTES.buildersHub}
|
||||
variant="secondary"
|
||||
className="cursor-pointer"
|
||||
>
|
||||
{copy.secondaryCta}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx-auto mt-[70px] flex w-full max-w-[464px] flex-col items-center text-center md:mt-[52px]">
|
||||
<h2 className="font-display text-[40px] leading-none tracking-[-0.03em] md:text-[56px] md:leading-[0.88]">
|
||||
{headingLines.map((line) => (
|
||||
<span key={line} className="block">
|
||||
{line}
|
||||
</span>
|
||||
))}
|
||||
</h2>
|
||||
<p className="text-mono-s mt-10 w-[359px] max-w-full">{copy.body}</p>
|
||||
<div className="mt-6">
|
||||
<TertiaryCta href={ROUTES.technologyStack}>{copy.cta}</TertiaryCta>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-[100px] flex flex-col gap-3">
|
||||
<div className="flex h-[124px] items-center justify-between rounded-3xl border border-brand-dark-green p-3 md:h-[196px]">
|
||||
<div className="flex flex-col gap-3 md:mx-auto md:items-center">
|
||||
<div className="flex items-center gap-2">
|
||||
<LogosMark size={14} />
|
||||
<h3 className="text-subhead-sans">{copy.basecamp.title}</h3>
|
||||
</div>
|
||||
<p className="text-mono-s w-[208px] md:hidden">
|
||||
{copy.basecamp.body}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
href={ROUTES.basecamp}
|
||||
icon={<DownloadIcon />}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
{copy.basecamp.cta}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3 md:grid-cols-4">
|
||||
{copy.cards.map((card) => (
|
||||
<TechCard key={card.title} card={card} />
|
||||
))}
|
||||
</div>
|
||||
<WideTechCard {...copy.networking} />
|
||||
<WideTechCard {...copy.foundation} />
|
||||
</div>
|
||||
</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]">
|
||||
@@ -469,14 +337,18 @@ function Support({ copy }: { copy: LambdaPrizePageCopy['support'] }) {
|
||||
)
|
||||
}
|
||||
|
||||
export function LambdaPrizePage({ copy }: LambdaPrizePageProps) {
|
||||
export function LambdaPrizePage({ copy, techStack }: LambdaPrizePageProps) {
|
||||
return (
|
||||
<main className="overflow-hidden bg-brand-off-white">
|
||||
<Hero copy={copy.hero} />
|
||||
<HowItWorks copy={copy.howItWorks} evaluation={copy.evaluation} />
|
||||
<FeaturedPrizes copy={copy.featured} />
|
||||
<AboutProgramme copy={copy.about} />
|
||||
<TechStack copy={copy.techStack} />
|
||||
<TechStackSection
|
||||
data={techStack}
|
||||
networkingHref={ROUTES.networking}
|
||||
foundationHref={ROUTES.technologyStack}
|
||||
/>
|
||||
<Support copy={copy.support} />
|
||||
</main>
|
||||
)
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { getTranslations } from 'next-intl/server'
|
||||
|
||||
import { isActiveLocale } from '@repo/content/locales'
|
||||
|
||||
import { ROUTES } from '@/constants/routes'
|
||||
import { getHomeTechStackOverview } from '@/lib/tech-stack-overview'
|
||||
import { createTranslatedPageMetadata } from '@/lib/translated-page-metadata'
|
||||
|
||||
import { LambdaPrizePage } from './_sections/lambda-prize-page'
|
||||
@@ -18,10 +21,15 @@ export default async function Page({
|
||||
params: Promise<{ locale: string }>
|
||||
}) {
|
||||
const { locale } = await params
|
||||
if (!isActiveLocale(locale)) {
|
||||
throw new Error(`LambdaPrizePage received non-active locale "${locale}"`)
|
||||
}
|
||||
const t = await getTranslations({ locale, namespace: NAMESPACE })
|
||||
const techStack = await getHomeTechStackOverview(locale)
|
||||
|
||||
return (
|
||||
<LambdaPrizePage
|
||||
techStack={techStack}
|
||||
copy={{
|
||||
hero: {
|
||||
label: t('hero.label'),
|
||||
@@ -129,49 +137,6 @@ export default async function Page({
|
||||
},
|
||||
],
|
||||
},
|
||||
techStack: {
|
||||
disclaimer: t('techStack.disclaimer'),
|
||||
heading: t('techStack.heading'),
|
||||
body: t('techStack.body'),
|
||||
cta: t('techStack.cta'),
|
||||
basecamp: {
|
||||
title: t('techStack.basecamp.title'),
|
||||
body: t('techStack.basecamp.body'),
|
||||
cta: t('techStack.basecamp.cta'),
|
||||
},
|
||||
cards: [
|
||||
{
|
||||
title: t('techStack.cards.storage.title'),
|
||||
body: t('techStack.cards.storage.body'),
|
||||
},
|
||||
{
|
||||
title: t('techStack.cards.messaging.title'),
|
||||
body: t('techStack.cards.messaging.body'),
|
||||
},
|
||||
{
|
||||
title: t('techStack.cards.blockchain.title'),
|
||||
body: t('techStack.cards.blockchain.body'),
|
||||
badges: [
|
||||
t('techStack.cards.blockchain.badges.1'),
|
||||
t('techStack.cards.blockchain.badges.2'),
|
||||
],
|
||||
},
|
||||
{
|
||||
title: t('techStack.cards.userModules.title'),
|
||||
body: t('techStack.cards.userModules.body'),
|
||||
},
|
||||
],
|
||||
networking: {
|
||||
title: t('techStack.networking.title'),
|
||||
body: t('techStack.networking.body'),
|
||||
},
|
||||
foundation: {
|
||||
title: t('techStack.foundation.title'),
|
||||
body: t('techStack.foundation.body'),
|
||||
},
|
||||
primaryCta: t('techStack.primaryCta'),
|
||||
secondaryCta: t('techStack.secondaryCta'),
|
||||
},
|
||||
support: {
|
||||
heading: t('support.heading'),
|
||||
body: t('support.body'),
|
||||
|
||||
@@ -23,6 +23,7 @@ import { ROUTES } from '@/constants/routes'
|
||||
import { createPageMetadata } from '@/lib/page-metadata'
|
||||
import { createSectionFinder } from '@/lib/page-sections'
|
||||
import { getLatestPressArticles } from '@/lib/press-engine'
|
||||
import { getSocialProofStats } from '@/lib/social-proof-stats'
|
||||
|
||||
const findSection = createSectionFinder('home')
|
||||
|
||||
@@ -42,38 +43,46 @@ export default async function HomePage({
|
||||
const page = await getPageCopy(ROUTE, locale)
|
||||
|
||||
const hero = findSection<HeroSection>(page.sections, 'hero', 'home.atf')
|
||||
|
||||
const techStack = findSection<TechStackOverviewSection>(
|
||||
page.sections,
|
||||
'techStackOverview',
|
||||
'home.techStack'
|
||||
)
|
||||
|
||||
const parallelSocietyHeadline = findSection<FeaturedTextSection>(
|
||||
page.sections,
|
||||
'featuredText',
|
||||
'home.parallelSocietyHeadline'
|
||||
)
|
||||
|
||||
const parallelSocietyGallery = findSection<GallerySection>(
|
||||
page.sections,
|
||||
'gallery',
|
||||
'home.parallelSociety'
|
||||
)
|
||||
|
||||
const press = findSection<RelatedArticlesSection>(
|
||||
page.sections,
|
||||
'relatedArticles',
|
||||
'home.press'
|
||||
)
|
||||
|
||||
const circlesCta = findSection<FeaturedTextSection>(
|
||||
page.sections,
|
||||
'featuredText',
|
||||
'home.circlesCta'
|
||||
)
|
||||
|
||||
const articles = await getLatestPressArticles(press.visibleCount ?? 4)
|
||||
const [articles, socialProofStats] = await Promise.all([
|
||||
getLatestPressArticles(press.visibleCount ?? 4),
|
||||
getSocialProofStats(),
|
||||
])
|
||||
|
||||
return (
|
||||
<>
|
||||
<HeroSectionView data={hero} />
|
||||
<SocialProofSection />
|
||||
<SocialProofSection stats={socialProofStats} />
|
||||
<AboutSection locale={locale} />
|
||||
<BuilderPortalSection locale={locale} />
|
||||
<FeatureCardsSection />
|
||||
|
||||
@@ -33,10 +33,8 @@ const findSection = createSectionFinder('messaging')
|
||||
* - messaging.builderCta → MessagingBuilderCta
|
||||
* - messaging.relatedArticles → MessagingRelatedArticles
|
||||
*
|
||||
* `MessagingTechStack` is deferred (same rationale as `StorageTechStack`):
|
||||
* it composes the global techStackOverview with messaging-specific intro
|
||||
* copy. Migration awaits either a cross-page shared partial or extra fields
|
||||
* on `techStackOverview`.
|
||||
* `MessagingTechStack` composes the shared tech stack explorer so the card
|
||||
* grid stays aligned with `/technology-stack`.
|
||||
*/
|
||||
export default async function MessagingPage({
|
||||
params,
|
||||
|
||||
@@ -34,11 +34,8 @@ const findSection = createSectionFinder('storage')
|
||||
* - storage.builderCta → StorageBuilderCta
|
||||
* - storage.relatedArticles → StorageRelatedArticles
|
||||
*
|
||||
* `StorageTechStack` is deferred: it composes the global techStackOverview
|
||||
* (shared with `/technology-stack`) with a storage-specific intro and per-card
|
||||
* body text (`pages.storage.techStack.*`). Migrating cleanly needs either a
|
||||
* cross-page shared partial or extra fields on `techStackOverview`. Until then
|
||||
* the component continues to read from `messages.en.json`.
|
||||
* `StorageTechStack` composes the shared tech stack explorer so the card grid
|
||||
* stays aligned with `/technology-stack`.
|
||||
*/
|
||||
export default async function StoragePage({
|
||||
params,
|
||||
|
||||
@@ -19,4 +19,17 @@ describe('broadcast network page contract', () => {
|
||||
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'"
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
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}')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,54 @@
|
||||
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',
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -5,8 +5,9 @@ import { useTranslations } from 'next-intl'
|
||||
|
||||
import { LogosMark } from '@acid-info/logos-ui'
|
||||
|
||||
import { Button } from '@/components/ui'
|
||||
import { ButtonArrowIcon } from '@/components/ui'
|
||||
import { ROUTES } from '@/constants/routes'
|
||||
import { Link } from '@/i18n/navigation'
|
||||
|
||||
interface PathCard {
|
||||
key: 'build' | 'operate' | 'activism'
|
||||
@@ -21,8 +22,9 @@ interface PathCard {
|
||||
|
||||
function PathCardView({ card }: { card: PathCard }) {
|
||||
return (
|
||||
<article
|
||||
className="group/path-card relative h-[422px] overflow-hidden rounded-3xl bg-brand-dark-green text-brand-off-white"
|
||||
<Link
|
||||
href={card.href}
|
||||
className="group/path-card relative block h-[422px] cursor-pointer overflow-hidden rounded-3xl bg-brand-dark-green text-brand-off-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-dark-green"
|
||||
>
|
||||
<Image
|
||||
src={card.image}
|
||||
@@ -40,17 +42,21 @@ function PathCardView({ card }: { card: PathCard }) {
|
||||
<h3 className="text-h3-serif">{card.title}</h3>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
href={card.href}
|
||||
className="absolute top-3 right-3 cursor-pointer bg-brand-off-white text-brand-dark-green backdrop-blur-[5px] transition-colors duration-300 ease-out hover:bg-accent-steel-teal focus-visible:bg-accent-steel-teal group-hover/path-card:bg-accent-steel-teal"
|
||||
<span
|
||||
className="absolute top-3 right-3 inline-flex items-center justify-center rounded-xl bg-brand-off-white px-3 py-2 text-brand-dark-green backdrop-blur-[5px] transition-colors duration-300 ease-out group-hover/path-card:bg-accent-steel-teal group-focus-visible/path-card:bg-accent-steel-teal"
|
||||
>
|
||||
{card.cta}
|
||||
</Button>
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<span className="font-mono text-[10px] leading-[1.35] font-semibold whitespace-nowrap uppercase">
|
||||
{card.cta}
|
||||
</span>
|
||||
<ButtonArrowIcon />
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<p className="absolute right-6 bottom-6 left-6 font-sans text-[14px] leading-[1.2] font-medium">
|
||||
{card.body}
|
||||
</p>
|
||||
</article>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
import Image from 'next/image'
|
||||
import { useTranslations } from 'next-intl'
|
||||
|
||||
import type { SocialProofStats } from '@/lib/social-proof-stats'
|
||||
|
||||
interface SocialProofCard {
|
||||
key: 'contributions' | 'nodes' | 'circles' | 'issues'
|
||||
key: 'contributions' | 'contributors' | 'repositories' | 'circles'
|
||||
value: string
|
||||
label: string
|
||||
body: string
|
||||
@@ -49,13 +51,17 @@ function SocialProofCardView({
|
||||
)
|
||||
}
|
||||
|
||||
export default function SocialProofSection() {
|
||||
interface SocialProofSectionProps {
|
||||
stats: SocialProofStats
|
||||
}
|
||||
|
||||
export default function SocialProofSection({ stats }: SocialProofSectionProps) {
|
||||
const t = useTranslations('home.socialProof')
|
||||
|
||||
const cards: SocialProofCard[] = [
|
||||
{
|
||||
key: 'contributions',
|
||||
value: t('contributions.value'),
|
||||
value: stats.contributions,
|
||||
label: t('contributions.label'),
|
||||
body: t('contributions.body'),
|
||||
image: '/images/home/figma-refresh/social-contributions.webp',
|
||||
@@ -63,31 +69,31 @@ export default function SocialProofSection() {
|
||||
imageClassName: 'object-[47%_52%]',
|
||||
},
|
||||
{
|
||||
key: 'nodes',
|
||||
value: t('nodes.value'),
|
||||
label: t('nodes.label'),
|
||||
body: t('nodes.body'),
|
||||
key: 'contributors',
|
||||
value: stats.contributors,
|
||||
label: t('contributors.label'),
|
||||
body: t('contributors.body'),
|
||||
image: '/images/home/figma-refresh/social-node.webp',
|
||||
mobileClassName: 'h-[432px]',
|
||||
imageClassName: 'object-[50%_52%]',
|
||||
},
|
||||
{
|
||||
key: 'repositories',
|
||||
value: stats.repositories,
|
||||
label: t('repositories.label'),
|
||||
body: t('repositories.body'),
|
||||
image: '/images/home/figma-refresh/social-issues.webp',
|
||||
mobileClassName: 'h-[411px]',
|
||||
imageClassName: 'object-[47%_51%]',
|
||||
},
|
||||
{
|
||||
key: 'circles',
|
||||
value: t('circles.value'),
|
||||
value: stats.circles,
|
||||
label: t('circles.label'),
|
||||
body: t('circles.body'),
|
||||
image: '/images/home/figma-refresh/social-circles.webp',
|
||||
mobileClassName: 'h-[411px]',
|
||||
imageClassName: 'object-[50%_50%]',
|
||||
},
|
||||
{
|
||||
key: 'issues',
|
||||
value: t('issues.value'),
|
||||
label: t('issues.label'),
|
||||
body: t('issues.body'),
|
||||
image: '/images/home/figma-refresh/social-issues.webp',
|
||||
mobileClassName: 'h-[432px]',
|
||||
imageClassName: 'object-[47%_51%]',
|
||||
imageClassName: 'object-[50%_50%]',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { getTranslations } from 'next-intl/server'
|
||||
import Image from 'next/image'
|
||||
|
||||
import { Button } from '@/components/ui'
|
||||
import { ROUTES } from '@/constants/routes'
|
||||
import { EXTERNAL_URLS, ROUTES } from '@/constants/routes'
|
||||
|
||||
interface SupportCard {
|
||||
title: string
|
||||
@@ -38,7 +38,7 @@ export default async function StartBuildingSection({
|
||||
{ title: t('lambdaPrize'), href: ROUTES.lambdaPrize, cta: t('cardCta') },
|
||||
{ title: t('rfps'), href: ROUTES.rfps, cta: t('cardCta') },
|
||||
{ title: t('ideas'), href: ROUTES.ideas, cta: t('cardCta') },
|
||||
{ title: t('docs'), href: ROUTES.technologyStack, cta: t('cardCta') },
|
||||
{ title: t('docs'), href: EXTERNAL_URLS.docs, cta: t('cardCta') },
|
||||
]
|
||||
|
||||
return (
|
||||
|
||||
@@ -81,6 +81,9 @@ export default function SiteHeaderClient({
|
||||
normalizedPathname.endsWith(ROUTES.lambdaPrize)
|
||||
const usesTransparentHeader = normalizedPathname.endsWith(ROUTES.press)
|
||||
const usesOverlayHeader = usesHeroHeaderTone || usesTransparentHeader
|
||||
const usesAccentTanHeaderTone =
|
||||
normalizedPathname.endsWith(ROUTES.logosBroadcastNetwork) ||
|
||||
normalizedPathname.endsWith(ROUTES.podcast)
|
||||
const open = () => {
|
||||
setInitialPanelLabel(null)
|
||||
setIsOpen(true)
|
||||
@@ -135,7 +138,8 @@ export default function SiteHeaderClient({
|
||||
<header
|
||||
className={clsx(
|
||||
'left-0 right-0 top-0 z-50',
|
||||
usesOverlayHeader ? 'fixed' : 'sticky'
|
||||
usesOverlayHeader ? 'fixed' : 'sticky',
|
||||
usesAccentTanHeaderTone && 'bg-accent-tan'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -90,7 +90,7 @@ export const EXTERNAL_URLS = {
|
||||
|
||||
// Forum
|
||||
forum: 'https://forum.logos.co/',
|
||||
docs: 'https://docs.logos.co',
|
||||
docs: 'https://github.com/logos-co/logos-docs',
|
||||
|
||||
// Research
|
||||
vacp2p: 'https://vac.dev',
|
||||
|
||||
@@ -3,8 +3,9 @@ import { join, relative } from 'node:path'
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { ROUTES } from '@/constants/routes'
|
||||
import { EXTERNAL_URLS, ROUTES } from '@/constants/routes'
|
||||
|
||||
import buildersHubResources from '../../../../content/builders-hub/resources/en.json' with { type: 'json' }
|
||||
import buildersHubSettings from '../../../../content/builders-hub/settings/en.json' with { type: 'json' }
|
||||
import footer from '../../../../content/site/en/footer.json' with { type: 'json' }
|
||||
import navigation from '../../../../content/site/en/navigation.json' with { type: 'json' }
|
||||
@@ -33,6 +34,8 @@ const repoPressArticlePaths = [
|
||||
].map((path) => join(repoRoot, path))
|
||||
const jobsHref = 'https://free.technology/jobs'
|
||||
const onboardingCalendarHref = 'https://cal.com/team/logos-onboarding/intro'
|
||||
const logosDocsHref = 'https://github.com/logos-co/logos-docs'
|
||||
const docsLabels = new Set(['docs', 'documentation', 'view the docs'])
|
||||
const routeUsageAllowlist = new Set([
|
||||
'apps/web/app/[locale]/work-with-us/page.tsx',
|
||||
'apps/web/app/sitemap.ts',
|
||||
@@ -40,6 +43,31 @@ const routeUsageAllowlist = new Set([
|
||||
'apps/web/lib/__tests__/link-policy.test.ts',
|
||||
])
|
||||
|
||||
const collectDocsLinks = (value: unknown): Array<{ label: string; href: string }> => {
|
||||
if (Array.isArray(value)) {
|
||||
return value.flatMap(collectDocsLinks)
|
||||
}
|
||||
|
||||
if (!value || typeof value !== 'object') return []
|
||||
|
||||
const record = value as Record<string, unknown>
|
||||
const docsText =
|
||||
typeof record.label === 'string' && docsLabels.has(record.label.toLowerCase())
|
||||
? record.label
|
||||
: typeof record.title === 'string' &&
|
||||
docsLabels.has(record.title.toLowerCase())
|
||||
? record.title
|
||||
: null
|
||||
const ownLink =
|
||||
docsText && typeof record.href === 'string'
|
||||
? [{ label: docsText, href: record.href }]
|
||||
: []
|
||||
|
||||
return ownLink.concat(
|
||||
Object.values(record).flatMap((child) => collectDocsLinks(child))
|
||||
)
|
||||
}
|
||||
|
||||
const collectTextFiles = (dir: string): string[] => {
|
||||
return readdirSync(dir).flatMap((entry) => {
|
||||
const path = join(dir, entry)
|
||||
@@ -148,6 +176,26 @@ describe('link policy', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('routes Docs and Documentation links to the Logos docs repository', () => {
|
||||
const contentDocsLinks = [
|
||||
footer,
|
||||
navigation,
|
||||
buildersHubResources,
|
||||
buildersHubSettings,
|
||||
...readdirSync(join(repoRoot, 'content/pages/en'))
|
||||
.filter((entry) => entry.endsWith('.json'))
|
||||
.map((entry) =>
|
||||
JSON.parse(readFileSync(join(repoRoot, 'content/pages/en', entry), 'utf8'))
|
||||
),
|
||||
].flatMap(collectDocsLinks)
|
||||
|
||||
expect(EXTERNAL_URLS.docs).toBe(logosDocsHref)
|
||||
expect(contentDocsLinks.length).toBeGreaterThan(0)
|
||||
expect(contentDocsLinks).toEqual(
|
||||
contentDocsLinks.map((link) => ({ ...link, href: logosDocsHref }))
|
||||
)
|
||||
})
|
||||
|
||||
it('does not use the retired Work With Us route for public CTAs', () => {
|
||||
const offenders = scannedRoots.flatMap(collectTextFiles).flatMap((file) => {
|
||||
const relativePath = relative(repoRoot, file)
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import { afterEach, describe, expect, test, vi } from 'vitest'
|
||||
|
||||
import { getSocialProofStats } from '../social-proof-stats'
|
||||
|
||||
const jsonResponse = (payload: unknown): Response =>
|
||||
({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => payload,
|
||||
}) as unknown as Response
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
describe('getSocialProofStats', () => {
|
||||
test('maps contribution and circle API totals for the home social proof cards', async () => {
|
||||
const fetchMock = vi
|
||||
.spyOn(globalThis, 'fetch')
|
||||
.mockResolvedValueOnce(
|
||||
jsonResponse({
|
||||
stg_external_contributors_agg_total_ext_contributions: [
|
||||
{
|
||||
total_commits: 3441,
|
||||
total_external_collaborators: 218,
|
||||
total_repositories: 341,
|
||||
},
|
||||
],
|
||||
})
|
||||
)
|
||||
.mockResolvedValueOnce(
|
||||
jsonResponse({
|
||||
data: {
|
||||
stg_external_circle_circle_event_aggregate: {
|
||||
aggregate: {
|
||||
count: 34,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
await expect(getSocialProofStats()).resolves.toEqual({
|
||||
contributions: '3,441',
|
||||
contributors: '218',
|
||||
repositories: '341',
|
||||
circles: '34',
|
||||
})
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledTimes(2)
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'https://hasura.bi.status.im/api/rest/contributions/count_all',
|
||||
{ cache: 'force-cache' }
|
||||
)
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'https://hasura.bi.status.im/v1/graphql',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
cache: 'force-cache',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
test('keeps the requested stats as build fallback when the APIs fail', async () => {
|
||||
vi.spyOn(globalThis, 'fetch').mockRejectedValue(new Error('network down'))
|
||||
|
||||
await expect(getSocialProofStats()).resolves.toEqual({
|
||||
contributions: '3,441',
|
||||
contributors: '218',
|
||||
repositories: '341',
|
||||
circles: '34',
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,11 +1,11 @@
|
||||
import { getLatestEventsByCity } from './circle-events'
|
||||
import {
|
||||
BI_GRAPHQL_API_URL,
|
||||
CIRCLES_GRAPHQL_RESPONSE_KEY,
|
||||
LOGOS_GRAPHQL_API_URL,
|
||||
} from './logos-data-api'
|
||||
import { logger } from './logger'
|
||||
|
||||
const LOGOS_GRAPHQL_API_URL = 'https://api.logos.co/v1/graphql'
|
||||
const BI_GRAPHQL_API_URL = 'https://hasura.bi.status.im/v1/graphql'
|
||||
const CIRCLES_GRAPHQL_RESPONSE_KEY =
|
||||
'stg_external_circle_circle_event_aggregate'
|
||||
|
||||
export const ACTIVE_CIRCLES_DAYS = 120
|
||||
|
||||
type GraphqlResponse<TData> = {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { LOGOS_GRAPHQL_API_URL } from './logos-data-api'
|
||||
import { logger } from './logger'
|
||||
|
||||
const HASURA_URL = 'https://api.logos.co/v1/graphql'
|
||||
|
||||
const CIRCLE_EVENTS_QUERY = `query CircleEvents {
|
||||
stg_external_circle_circle_event {
|
||||
event_id
|
||||
@@ -41,7 +40,7 @@ interface HasuraResponse {
|
||||
*/
|
||||
export async function fetchCircleEvents(): Promise<CircleEvent[]> {
|
||||
try {
|
||||
const response = await fetch(HASURA_URL, {
|
||||
const response = await fetch(LOGOS_GRAPHQL_API_URL, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ query: CIRCLE_EVENTS_QUERY }),
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
export const LOGOS_GRAPHQL_API_URL = 'https://api.logos.co/v1/graphql'
|
||||
|
||||
export const BI_GRAPHQL_API_URL = 'https://hasura.bi.status.im/v1/graphql'
|
||||
|
||||
export const CONTRIBUTIONS_API_URL =
|
||||
'https://hasura.bi.status.im/api/rest/contributions/count_all'
|
||||
|
||||
export const CONTRIBUTIONS_API_RESPONSE_KEY =
|
||||
'stg_external_contributors_agg_total_ext_contributions'
|
||||
|
||||
export const CIRCLES_GRAPHQL_RESPONSE_KEY =
|
||||
'stg_external_circle_circle_event_aggregate'
|
||||
@@ -0,0 +1,131 @@
|
||||
import {
|
||||
BI_GRAPHQL_API_URL,
|
||||
CIRCLES_GRAPHQL_RESPONSE_KEY,
|
||||
CONTRIBUTIONS_API_RESPONSE_KEY,
|
||||
CONTRIBUTIONS_API_URL,
|
||||
} from './logos-data-api'
|
||||
import { logger } from './logger'
|
||||
|
||||
const ACTIVE_CIRCLES_DAYS = 120
|
||||
|
||||
const FALLBACK_SOCIAL_PROOF_STATS = {
|
||||
contributions: 3441,
|
||||
contributors: 218,
|
||||
repositories: 341,
|
||||
circles: 34,
|
||||
} as const
|
||||
|
||||
type ContributionsApiData = {
|
||||
total_commits?: number | null
|
||||
total_external_collaborators?: number | null
|
||||
total_repositories?: number | null
|
||||
}
|
||||
|
||||
type ContributionsApiResponse = {
|
||||
[CONTRIBUTIONS_API_RESPONSE_KEY]?: ContributionsApiData[]
|
||||
}
|
||||
|
||||
type CirclesApiResponse = {
|
||||
data?: {
|
||||
[CIRCLES_GRAPHQL_RESPONSE_KEY]?: {
|
||||
aggregate?: {
|
||||
count?: number | null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface SocialProofStats {
|
||||
contributions: string
|
||||
contributors: string
|
||||
repositories: string
|
||||
circles: string
|
||||
}
|
||||
|
||||
function formatStat(
|
||||
value: number | null | undefined,
|
||||
fallback: number
|
||||
): string {
|
||||
return (value ?? fallback).toLocaleString('en-US')
|
||||
}
|
||||
|
||||
function getTodayISODateDaysAgo(days: number): string {
|
||||
const date = new Date()
|
||||
date.setDate(date.getDate() - days)
|
||||
return date.toISOString().split('T')[0] ?? ''
|
||||
}
|
||||
|
||||
function getCirclesGraphqlQuery(dateString: string): string {
|
||||
return `query CountDistinctCities {
|
||||
${CIRCLES_GRAPHQL_RESPONSE_KEY}(where: { end_at: { _gte: "${dateString}" } }) {
|
||||
aggregate {
|
||||
count(distinct: true, columns: location_city)
|
||||
}
|
||||
}
|
||||
}`
|
||||
}
|
||||
|
||||
async function fetchContributionsData(): Promise<ContributionsApiData | null> {
|
||||
try {
|
||||
const response = await fetch(CONTRIBUTIONS_API_URL, {
|
||||
cache: 'force-cache',
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Contributions request failed: ${response.status}`)
|
||||
}
|
||||
|
||||
const data = (await response.json()) as ContributionsApiResponse
|
||||
return data[CONTRIBUTIONS_API_RESPONSE_KEY]?.[0] ?? null
|
||||
} catch (error) {
|
||||
logger.error('Failed to fetch social proof contribution stats', { error })
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchTotalCircles(): Promise<number | null> {
|
||||
try {
|
||||
const activeSinceDate = getTodayISODateDaysAgo(ACTIVE_CIRCLES_DAYS)
|
||||
const response = await fetch(BI_GRAPHQL_API_URL, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
query: getCirclesGraphqlQuery(activeSinceDate),
|
||||
}),
|
||||
cache: 'force-cache',
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Circles request failed: ${response.status}`)
|
||||
}
|
||||
|
||||
const data = (await response.json()) as CirclesApiResponse
|
||||
return data.data?.[CIRCLES_GRAPHQL_RESPONSE_KEY]?.aggregate?.count ?? null
|
||||
} catch (error) {
|
||||
logger.error('Failed to fetch social proof circle stats', { error })
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSocialProofStats(): Promise<SocialProofStats> {
|
||||
const [contributionsData, totalCircles] = await Promise.all([
|
||||
fetchContributionsData(),
|
||||
fetchTotalCircles(),
|
||||
])
|
||||
|
||||
return {
|
||||
contributions: formatStat(
|
||||
contributionsData?.total_commits,
|
||||
FALLBACK_SOCIAL_PROOF_STATS.contributions
|
||||
),
|
||||
contributors: formatStat(
|
||||
contributionsData?.total_external_collaborators,
|
||||
FALLBACK_SOCIAL_PROOF_STATS.contributors
|
||||
),
|
||||
repositories: formatStat(
|
||||
contributionsData?.total_repositories,
|
||||
FALLBACK_SOCIAL_PROOF_STATS.repositories
|
||||
),
|
||||
circles: formatStat(totalCircles, FALLBACK_SOCIAL_PROOF_STATS.circles),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { getPageCopy } from '@repo/content/loaders'
|
||||
import type { Language, TechStackOverviewSection } from '@repo/content/schemas'
|
||||
|
||||
import { ROUTES } from '@/constants/routes'
|
||||
|
||||
import { createSectionFinder } from './page-sections'
|
||||
|
||||
const findHomeSection = createSectionFinder('home')
|
||||
|
||||
export async function getHomeTechStackOverview(
|
||||
locale: Language
|
||||
): Promise<TechStackOverviewSection> {
|
||||
const homePage = await getPageCopy(ROUTES.home, locale)
|
||||
|
||||
return findHomeSection<TechStackOverviewSection>(
|
||||
homePage.sections,
|
||||
'techStackOverview',
|
||||
'home.techStack'
|
||||
)
|
||||
}
|
||||
@@ -38,24 +38,20 @@
|
||||
},
|
||||
"socialProof": {
|
||||
"contributions": {
|
||||
"value": "2262",
|
||||
"label": "Contributions",
|
||||
"label": "Total contributions",
|
||||
"body": "An open source community of builders, researchers, and technologists."
|
||||
},
|
||||
"nodes": {
|
||||
"value": "205",
|
||||
"label": "Node Operators",
|
||||
"contributors": {
|
||||
"label": "Active contributors",
|
||||
"body": "An open source community of builders, researchers, and technologists."
|
||||
},
|
||||
"repositories": {
|
||||
"label": "Repositories",
|
||||
"body": "Open source tools, libraries, and infrastructure to inspect, fork, and build on."
|
||||
},
|
||||
"circles": {
|
||||
"value": "26",
|
||||
"label": "Circles",
|
||||
"body": "Local chapters of activists and change seekers solving real world issues."
|
||||
},
|
||||
"issues": {
|
||||
"value": "26",
|
||||
"label": "Winnable Issues",
|
||||
"body": "Local issues that Circles identify and solve – from privacy tech to community funding."
|
||||
}
|
||||
},
|
||||
"build": {
|
||||
@@ -305,29 +301,7 @@
|
||||
"stack": {
|
||||
"titleLine1": "Explore the Logos",
|
||||
"titleLine2": "Technology Stack.",
|
||||
"body": "The Logos technology stack is a unified ecosystem of distinct modules. The entire stack is modular so developers can assemble their own Logos-based platform with a different selection of modules.",
|
||||
"storage": "Storage",
|
||||
"messaging": "Messaging",
|
||||
"blockchain": "Blockchain",
|
||||
"userModules": "User Modules",
|
||||
"basecamp": "Basecamp",
|
||||
"basecampBody": "Lorem ipsum dolor sit amet consectetur lorem ipsum dolor sit amet consectetur lorem.",
|
||||
"basecampCta": "Install",
|
||||
"networkingLine1": "The Networking Stack:",
|
||||
"networkingLine2": "Discovery, Peering, and Mix-Net",
|
||||
"foundation": "The Foundation: Logos Kernel"
|
||||
}
|
||||
},
|
||||
"storage": {
|
||||
"techStack": {
|
||||
"storageBody": "Decentralised file storage and retrieval, using content-addressed (CID-based) data",
|
||||
"messagingBody": "Private, censorship-resistant communication between parties.",
|
||||
"blockchainBody": "Decentralised compute and consensus.",
|
||||
"userModulesBody": "Anyone can build modules that plug into the same IPC infrastructure.",
|
||||
"networkingBody": "This layer handles how Logos nodes find each other, establish connections, and communicate.",
|
||||
"foundationBody": "A microkernel that handles the essential primitives every decentralised application needs.",
|
||||
"lez": "LOGOS Execution zone (LEZ)",
|
||||
"cryptarchia": "Data Availability and Consensus: Cryptarchia"
|
||||
"body": "The Logos technology stack is a unified ecosystem of distinct modules. The entire stack is modular so developers can assemble their own Logos-based platform with a different selection of modules."
|
||||
}
|
||||
},
|
||||
"ideas": {
|
||||
@@ -447,49 +421,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"techStack": {
|
||||
"disclaimer": "Disclaimer: This diagram oversimplifies the stack.",
|
||||
"heading": "The Logos\nTechnology Stack",
|
||||
"body": "Logos provides developers with a modular, plugin-based runtime for building decentralised applications.",
|
||||
"cta": "See the stack",
|
||||
"primaryCta": "Documentation",
|
||||
"secondaryCta": "Builder Hub",
|
||||
"basecamp": {
|
||||
"title": "Basecamp",
|
||||
"body": "Basecamp bundles the Logos stack into a practical starting point for builders.",
|
||||
"cta": "Install"
|
||||
},
|
||||
"cards": {
|
||||
"storage": {
|
||||
"title": "Storage",
|
||||
"body": "Decentralised file storage and retrieval, using content-addressed (CID-based) data"
|
||||
},
|
||||
"messaging": {
|
||||
"title": "Messaging",
|
||||
"body": "Private, censorship-resistant communication between parties."
|
||||
},
|
||||
"blockchain": {
|
||||
"title": "Blockchain",
|
||||
"body": "Decentralised compute and consensus.",
|
||||
"badges": {
|
||||
"1": "LOGOS Execution zone (LEZ)",
|
||||
"2": "Data Availability and Consensus: Cryptarchia"
|
||||
}
|
||||
},
|
||||
"userModules": {
|
||||
"title": "User Modules",
|
||||
"body": "Anyone can build modules that plug into the same IPC infrastructure."
|
||||
}
|
||||
},
|
||||
"networking": {
|
||||
"title": "The Networking Stack:\nDiscovery, Peering, and Mix-Net",
|
||||
"body": "This layer handles how Logos nodes find each other, establish connections, and communicate."
|
||||
},
|
||||
"foundation": {
|
||||
"title": "The Foundation: Logos Kernel",
|
||||
"body": "A microkernel that handles the essential primitives every decentralised application needs."
|
||||
}
|
||||
},
|
||||
"support": {
|
||||
"heading": "Get Support",
|
||||
"body": "Find examples, tutorials, and community channels for building on Logos.",
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"title": "Documentation",
|
||||
"description": "Reference for the kernel, modules, and UI packages.",
|
||||
"ctaLabel": "Browse",
|
||||
"href": "/technology-stack",
|
||||
"href": "https://github.com/logos-co/logos-docs",
|
||||
"status": "published"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -15,7 +15,11 @@
|
||||
"href": "https://discord.gg/logosnetwork",
|
||||
"external": true
|
||||
},
|
||||
{ "label": "View the Docs", "href": "/technology-stack" }
|
||||
{
|
||||
"label": "View the Docs",
|
||||
"href": "https://github.com/logos-co/logos-docs",
|
||||
"external": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"journey": {
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
"description": "Basecamp is built around a modular architecture. At its core is a lightweight runtime that manages self-contained modules, each providing a distinct capability such as private peer-to-peer messaging or distributed storage.",
|
||||
"cta": {
|
||||
"label": "Documentation",
|
||||
"href": "https://docs.logos.co",
|
||||
"href": "https://github.com/logos-co/logos-docs",
|
||||
"external": true,
|
||||
"variant": "primary"
|
||||
},
|
||||
@@ -176,7 +176,7 @@
|
||||
"description": "Basecamp is an open-source application. If you want to explore the codebase, build from source, or develop your own modules, everything lives on GitHub.",
|
||||
"cta": {
|
||||
"label": "View the docs",
|
||||
"href": "https://docs.logos.co",
|
||||
"href": "https://github.com/logos-co/logos-docs",
|
||||
"external": true,
|
||||
"variant": "primary"
|
||||
}
|
||||
|
||||
@@ -73,7 +73,11 @@
|
||||
"key": "home.techStack",
|
||||
"eyebrow": "A unified ecosystem. Private-by-default. Built for real life.",
|
||||
"title": "The Logos Technology Stack",
|
||||
"cta": { "label": "See the Stack", "href": "/technology-stack" },
|
||||
"cta": {
|
||||
"label": "See the Stack",
|
||||
"href": "https://github.com/logos-co/logos-docs",
|
||||
"external": true
|
||||
},
|
||||
"basecamp": {
|
||||
"title": "Basecamp",
|
||||
"body": "Lorem ipsum dolor sit amet consectetur lorem ipsum dolor sit amet consectetur lorem.",
|
||||
@@ -132,7 +136,8 @@
|
||||
"subheadingExtra": "Privacy-preserving blockchain for sovereign order and decentralised governance.",
|
||||
"cta": {
|
||||
"label": "View the Docs",
|
||||
"href": "/technology-stack"
|
||||
"href": "https://github.com/logos-co/logos-docs",
|
||||
"external": true
|
||||
},
|
||||
"cards": [
|
||||
{
|
||||
|
||||
@@ -55,7 +55,8 @@
|
||||
},
|
||||
{
|
||||
"label": "Docs",
|
||||
"href": "/technology-stack/blockchain",
|
||||
"href": "https://github.com/logos-co/logos-docs",
|
||||
"external": true,
|
||||
"variant": "secondary"
|
||||
}
|
||||
]
|
||||
@@ -68,7 +69,8 @@
|
||||
"description": "Logos Blockchain is a proof-of-stake layer-1 with privacy built into the consensus mechanism itself — not bolted on at the application layer. Block proposers are anonymous. Transactions are private by default. The chain supports the deployment of application-specific execution environments called Sovereign Zones, connected through Mantle Channels for interoperability.",
|
||||
"cta": {
|
||||
"label": "View the docs",
|
||||
"href": "/technology-stack/blockchain"
|
||||
"href": "https://github.com/logos-co/logos-docs",
|
||||
"external": true
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -79,7 +81,8 @@
|
||||
"subheading": "A private proof-of-stake consensus mechanism where validator identities and stake amounts remain hidden.",
|
||||
"cta": {
|
||||
"label": "View the docs",
|
||||
"href": "/technology-stack/blockchain"
|
||||
"href": "https://github.com/logos-co/logos-docs",
|
||||
"external": true
|
||||
},
|
||||
"cards": [
|
||||
{
|
||||
@@ -98,7 +101,8 @@
|
||||
},
|
||||
"secondaryCta": {
|
||||
"label": "Docs",
|
||||
"href": "/technology-stack/blockchain",
|
||||
"href": "https://github.com/logos-co/logos-docs",
|
||||
"external": true,
|
||||
"variant": "secondary"
|
||||
}
|
||||
},
|
||||
@@ -118,7 +122,8 @@
|
||||
},
|
||||
"secondaryCta": {
|
||||
"label": "Docs",
|
||||
"href": "/technology-stack/blockchain",
|
||||
"href": "https://github.com/logos-co/logos-docs",
|
||||
"external": true,
|
||||
"variant": "secondary"
|
||||
}
|
||||
},
|
||||
@@ -138,7 +143,8 @@
|
||||
},
|
||||
"secondaryCta": {
|
||||
"label": "Docs",
|
||||
"href": "/technology-stack/blockchain",
|
||||
"href": "https://github.com/logos-co/logos-docs",
|
||||
"external": true,
|
||||
"variant": "secondary"
|
||||
}
|
||||
}
|
||||
@@ -153,7 +159,8 @@
|
||||
"description": "Lorem ipsum dolor sit amet consectetur",
|
||||
"cta": {
|
||||
"label": "View the docs",
|
||||
"href": "/technology-stack"
|
||||
"href": "https://github.com/logos-co/logos-docs",
|
||||
"external": true
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -47,7 +47,8 @@
|
||||
},
|
||||
{
|
||||
"label": "DOCS",
|
||||
"href": "/technology-stack/messaging",
|
||||
"href": "https://github.com/logos-co/logos-docs",
|
||||
"external": true,
|
||||
"variant": "secondary"
|
||||
}
|
||||
]
|
||||
@@ -60,7 +61,8 @@
|
||||
"description": "Logos Messaging provides the messaging layer for the Logos stack, letting applications send messages, relay data, and coordinate without relying on centralised servers or exposing metadata to surveillance.\n\nIt uses a peer-to-peer relay network in which messages propagate across nodes with no central broker, and it supports private one-to-one messaging, group messaging, and public broadcast channels.\n\nMessages can be encrypted end-to-end, and the relay network is designed to resist traffic analysis, making it difficult for observers to determine who is communicating with whom.",
|
||||
"cta": {
|
||||
"label": "View the docs",
|
||||
"href": "/technology-stack/messaging"
|
||||
"href": "https://github.com/logos-co/logos-docs",
|
||||
"external": true
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -125,7 +127,8 @@
|
||||
"description": "Lorem ipsum dolor sit amet consectetur",
|
||||
"cta": {
|
||||
"label": "View the docs",
|
||||
"href": "/technology-stack"
|
||||
"href": "https://github.com/logos-co/logos-docs",
|
||||
"external": true
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -30,7 +30,8 @@
|
||||
},
|
||||
{
|
||||
"label": "Docs",
|
||||
"href": "/technology-stack/networking",
|
||||
"href": "https://github.com/logos-co/logos-docs",
|
||||
"external": true,
|
||||
"variant": "secondary"
|
||||
}
|
||||
]
|
||||
@@ -42,7 +43,8 @@
|
||||
"description": "The networking layer handles how Logos nodes find each other, establish connections, and communicate. Private-by-default, this layer doesn't care whether modules above it are storing files, sending chat messages, or processing transactions. It provides private, reliable communication as a shared foundation.",
|
||||
"cta": {
|
||||
"label": "View the docs",
|
||||
"href": "/technology-stack/networking"
|
||||
"href": "https://github.com/logos-co/logos-docs",
|
||||
"external": true
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -90,7 +92,8 @@
|
||||
"description": "Reference for the kernel, modules, and UI packages.",
|
||||
"cta": {
|
||||
"label": "View the docs",
|
||||
"href": "/technology-stack"
|
||||
"href": "https://github.com/logos-co/logos-docs",
|
||||
"external": true
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -50,7 +50,8 @@
|
||||
},
|
||||
{
|
||||
"label": "DOCS",
|
||||
"href": "/technology-stack/storage",
|
||||
"href": "https://github.com/logos-co/logos-docs",
|
||||
"external": true,
|
||||
"variant": "secondary"
|
||||
}
|
||||
]
|
||||
@@ -63,7 +64,8 @@
|
||||
"description": "Files are addressed by content rather than by location. When you store a file, the network returns a CID — a content identifier derived from the file itself — and anyone with that CID can retrieve the file from whichever nodes hold it. There is no central server to depend on and no single host to fail. The usage is straightforward: store a file and get back a content identifier; provide a content identifier and get back the file. Storage works both inside the Logos App and from your own applications, through either the high-level module API or the lower-level libstorage library.",
|
||||
"cta": {
|
||||
"label": "View the docs",
|
||||
"href": "/technology-stack/storage"
|
||||
"href": "https://github.com/logos-co/logos-docs",
|
||||
"external": true
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -140,7 +142,8 @@
|
||||
"description": "Lorem ipsum dolor sit amet consectetur",
|
||||
"cta": {
|
||||
"label": "View the docs",
|
||||
"href": "/technology-stack"
|
||||
"href": "https://github.com/logos-co/logos-docs",
|
||||
"external": true
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -42,7 +42,8 @@
|
||||
},
|
||||
{
|
||||
"label": "Documentation",
|
||||
"href": "/technology-stack#open-source",
|
||||
"href": "https://github.com/logos-co/logos-docs",
|
||||
"external": true,
|
||||
"variant": "secondary"
|
||||
}
|
||||
],
|
||||
@@ -70,7 +71,8 @@
|
||||
"eyebrow": "Disclaimer: Abstract representation of the stack.",
|
||||
"cta": {
|
||||
"label": "Docs",
|
||||
"href": "/technology-stack#open-source"
|
||||
"href": "https://github.com/logos-co/logos-docs",
|
||||
"external": true
|
||||
},
|
||||
"architecture": {
|
||||
"eyebrow": "Technology",
|
||||
@@ -81,7 +83,8 @@
|
||||
],
|
||||
"cta": {
|
||||
"label": "View the docs",
|
||||
"href": "/technology-stack#stack"
|
||||
"href": "https://github.com/logos-co/logos-docs",
|
||||
"external": true
|
||||
},
|
||||
"image": {
|
||||
"src": "/images/technology-stack/architecture-overview.jpg",
|
||||
@@ -209,7 +212,7 @@
|
||||
},
|
||||
"secondaryCta": {
|
||||
"label": "Docs",
|
||||
"href": "https://docs.logos.co",
|
||||
"href": "https://github.com/logos-co/logos-docs",
|
||||
"external": true
|
||||
}
|
||||
},
|
||||
@@ -224,7 +227,7 @@
|
||||
},
|
||||
"secondaryCta": {
|
||||
"label": "Docs",
|
||||
"href": "https://docs.waku.org",
|
||||
"href": "https://github.com/logos-co/logos-docs",
|
||||
"external": true
|
||||
}
|
||||
},
|
||||
@@ -239,7 +242,7 @@
|
||||
},
|
||||
"secondaryCta": {
|
||||
"label": "Docs",
|
||||
"href": "https://docs.codex.storage",
|
||||
"href": "https://github.com/logos-co/logos-docs",
|
||||
"external": true
|
||||
}
|
||||
},
|
||||
@@ -254,7 +257,7 @@
|
||||
},
|
||||
"secondaryCta": {
|
||||
"label": "Docs",
|
||||
"href": "https://vac.dev",
|
||||
"href": "https://github.com/logos-co/logos-docs",
|
||||
"external": true
|
||||
}
|
||||
}
|
||||
@@ -268,7 +271,8 @@
|
||||
"subheadingExtra": "Privacy-preserving blockchain for sovereign order and decentralised governance.",
|
||||
"cta": {
|
||||
"label": "View the Docs",
|
||||
"href": "/technology-stack"
|
||||
"href": "https://github.com/logos-co/logos-docs",
|
||||
"external": true
|
||||
},
|
||||
"cards": [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user