Hive Docs x Nextra 4 (#6089)

Co-authored-by: Dimitri POSTOLOV <dmytropostolov@gmail.com>
This commit is contained in:
Piotr Monwid-Olechnowicz 2025-01-27 15:58:08 +01:00 committed by GitHub
parent 7d480563cf
commit 38eaa1d0ea
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
256 changed files with 2256 additions and 2225 deletions

View file

@ -83,6 +83,7 @@
"eslint-plugin-tailwindcss": "npm:@hasparus/eslint-plugin-tailwindcss@3.17.5",
"fs-extra": "11.2.0",
"graphql": "16.9.0",
"gray-matter": "4.0.3",
"jest-snapshot-serializer-raw": "2.0.0",
"pg": "8.13.1",
"prettier": "3.4.2",

View file

@ -3,3 +3,4 @@ build
temp
public/sitemap.xml
public/changelog.json
public/_pagefind/

View file

@ -0,0 +1 @@
export { useHiveMDXComponents as useMDXComponents } from '@theguild/components/server';

View file

@ -1,6 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference types="next/navigation-types/compat/navigation" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View file

@ -1,16 +1,18 @@
import { withGuildDocs } from '@theguild/components/next.config';
export default withGuildDocs({
nextraConfig: /** @satisfies import("nextra").NextraConfig*/ ({
themeConfig: './src/theme.config.tsx',
autoImportThemeStyle: false,
}),
output: 'export',
basePath: process.env.NEXT_BASE_PATH,
eslint: {
ignoreDuringBuilds: true,
},
experimental: {
turbo: {
treeShaking: true,
},
},
nextraConfig: {
contentDirBasePath: '/docs',
},
redirects: async () => [
{
source: '/docs/get-started/organizations',
@ -245,7 +247,9 @@ export default withGuildDocs({
permanent: true,
},
],
swcMinify: true,
env: {
SITE_URL: 'https://the-guild.dev/graphql/hive',
},
webpack: (config, { webpack }) => {
config.externals['node:fs'] = 'commonjs node:fs';
config.externals['node:path'] = 'commonjs node:path';

View file

@ -4,7 +4,8 @@
"private": true,
"scripts": {
"build": "next build && next-sitemap",
"dev": "next",
"dev": "next --turbopack",
"postbuild": "pagefind --site .next/server/app --output-path out/_pagefind",
"validate-mdx-links": "pnpx validate-mdx-links@1.0.6 --files 'src/**/*.mdx'"
},
"dependencies": {
@ -12,28 +13,28 @@
"@radix-ui/react-icons": "1.3.2",
"@radix-ui/react-tabs": "1.1.2",
"@radix-ui/react-tooltip": "1.1.6",
"@tailwindcss/typography": "0.5.16",
"@theguild/components": "7.6.3",
"clsx": "2.1.1",
"@theguild/components": "9.2.0",
"date-fns": "4.1.0",
"next": "14.2.23",
"react": "18.3.1",
"next": "15.1.0",
"react": "19.0.0",
"react-avatar": "5.0.3",
"react-countup": "6.5.3",
"react-dom": "18.3.1",
"react-dom": "19.0.0",
"react-icons": "5.4.0",
"tailwind-merge": "2.6.0",
"tailwindcss-animate": "1.0.7",
"tailwindcss-radix": "3.0.5"
"tailwind-merge": "2.6.0"
},
"devDependencies": {
"@tailwindcss/typography": "0.5.16",
"@theguild/tailwind-config": "0.6.2",
"@types/react": "18.3.18",
"@types/rss": "^0.0.32",
"next-sitemap": "4.2.3",
"pagefind": "^1.2.0",
"postcss": "8.4.49",
"postcss-nesting": "^13.0.1",
"rss": "1.2.2",
"tailwindcss": "3.4.17"
"tailwindcss": "3.4.17",
"tailwindcss-animate": "1.0.7",
"tailwindcss-radix": "3.0.5"
}
}

View file

@ -1,20 +0,0 @@
<svg width="52" height="52" viewBox="0 0 52 52" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#hive-icon)" fill="currentColor">
<path
d="M36.902.081H16.097L.5 15.678v20.806L16.097 52.08h20.805L52.5 36.484V15.678L36.902.081ZM49.18 28.918 29.334 48.762a4.01 4.01 0 0 1-5.67 0L3.82 28.918a4.01 4.01 0 0 1 0-5.671L23.663 3.402a4.01 4.01 0 0 1 5.671 0L49.18 23.247a4.01 4.01 0 0 1 0 5.67Z"
/>
<path d="m26.499 20.637-5.444 5.443 5.444 5.444 5.443-5.444-5.443-5.443Z" />
</g>
<defs>
<clipPath id="hive-icon">
<path fill="#fff" d="M0 0h52v52H0z" />
</clipPath>
</defs>
<style>
@media (prefers-color-scheme: dark) {
svg {
color: #fff;
}
}
</style>
</svg>

Before

Width:  |  Height:  |  Size: 739 B

View file

@ -6,58 +6,40 @@ const meta: Record<string, DeepPartial<Item | MenuItem | PageItem>> = {
title: 'Home',
type: 'page',
display: 'hidden',
theme: {
layout: 'raw',
},
},
federation: {
title: 'Federation',
type: 'page',
display: 'hidden',
theme: {
layout: 'raw',
},
},
hive: {
title: 'Get Started',
type: 'page',
href: 'https://app.graphql-hive.com',
newWindow: true,
},
'contact-us': {
title: 'Contact Us',
type: 'page',
href: 'https://the-guild.dev/contact',
newWindow: true,
},
status: {
title: 'Status',
type: 'page',
href: 'https://status.graphql-hive.com',
newWindow: true,
},
docs: {
title: 'Documentation',
type: 'page',
theme: {
toc: true,
},
},
partners: {
title: 'Partners',
type: 'page',
display: 'hidden',
theme: {
layout: 'raw',
},
},
ecosystem: {
title: 'Ecosystem',
type: 'page',
display: 'hidden',
theme: {
layout: 'raw',
},
},
products: {
title: 'Products',
@ -67,9 +49,6 @@ const meta: Record<string, DeepPartial<Item | MenuItem | PageItem>> = {
pricing: {
title: 'Pricing',
type: 'page',
theme: {
layout: 'raw',
},
},
'product-updates': {
type: 'page',
@ -85,21 +64,16 @@ const meta: Record<string, DeepPartial<Item | MenuItem | PageItem>> = {
type: 'page',
title: 'Our Open Source Friends',
display: 'hidden',
theme: {
layout: 'raw',
},
},
blog: {
title: 'Blog',
type: 'page',
href: 'https://the-guild.dev/blog',
newWindow: true,
},
github: {
title: 'GitHub',
type: 'page',
href: 'https://github.com/graphql-hive/platform',
newWindow: true,
},
'the-guild': {
title: 'The Guild',
@ -108,12 +82,10 @@ const meta: Record<string, DeepPartial<Item | MenuItem | PageItem>> = {
'about-us': {
title: 'About Us',
href: 'https://the-guild.dev/about-us',
newWindow: true,
},
'brand-assets': {
title: 'Brand Assets',
href: 'https://the-guild.dev/logos',
newWindow: true,
},
},
},
@ -121,7 +93,6 @@ const meta: Record<string, DeepPartial<Item | MenuItem | PageItem>> = {
title: 'GraphQL Foundation',
type: 'page',
href: 'https://graphql.org/community/foundation/',
newWindow: true,
},
};

View file

@ -0,0 +1,36 @@
/* eslint-disable import/no-extraneous-dependencies */
import { ResolvingMetadata } from 'next';
import { generateStaticParamsFor, importPage } from 'nextra/pages';
import { NextPageProps } from '@theguild/components';
import { useMDXComponents } from '../../../../mdx-components.js';
import { ConfiguredGiscus } from '../../../components/configured-giscus';
export const generateStaticParams = generateStaticParamsFor('mdxPath');
export async function generateMetadata(
props: NextPageProps<'...mdxPath'>,
_parent: ResolvingMetadata,
) {
const { mdxPath } = await props.params;
const { metadata } = await importPage(mdxPath);
return {
...metadata,
...(mdxPath?.[0] === 'gateway' && {
title: { absolute: `${metadata.title} | Hive Gateway` },
}),
};
}
const Wrapper = useMDXComponents().wrapper!;
export default async function Page(props: NextPageProps<'...mdxPath'>) {
const params = await props.params;
const result = await importPage(params.mdxPath);
const { default: MDXContent, toc, metadata } = result;
return (
<Wrapper toc={toc} metadata={metadata}>
<MDXContent {...props} params={params} />
<ConfiguredGiscus />
</Wrapper>
);
}

View file

@ -0,0 +1,63 @@
'use client';
import { usePathname } from 'next/navigation';
import { normalizePages } from '@theguild/components';
function ensureAbsolute(url: string) {
if (url.startsWith('/')) {
return `https://the-guild.dev/graphql/hive${url.replace(/\/$/, '')}`;
}
return url;
}
type NormalizedResult = ReturnType<typeof normalizePages>;
function createBreadcrumb(normalizedResult: NormalizedResult) {
const activePaths = normalizedResult.activePath.slice();
if (activePaths[0]?.route !== '/') {
// Add the home page to all pages except the home page
activePaths.unshift({
route: '/',
title: 'Hive',
name: 'index',
type: 'page',
display: 'hidden',
children: [],
frontMatter: {},
});
}
return {
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: activePaths.map((path, index) => {
return {
'@type': 'ListItem',
position: index + 1,
name: path.route === '/' ? 'Hive' : path.title,
item: ensureAbsolute(path.route),
};
}),
};
}
export function DynamicMetaTags({ pageMap }: { pageMap: any[] }) {
const pathname = usePathname()!;
if (pathname === '/_not-found') {
return;
}
const normalizePagesResult = normalizePages({
list: pageMap,
route: pathname,
});
return (
<script
type="application/ld+json"
id="breadcrumb"
dangerouslySetInnerHTML={{
__html: JSON.stringify(createBreadcrumb(normalizePagesResult), null, 2),
}}
/>
);
}

View file

@ -1,8 +1,13 @@
import { GotAnIdeaSection } from '../got-an-idea-section';
import { Page as LandingPageContainer } from '../page';
import { GotAnIdeaSection } from '../../components/got-an-idea-section';
import { LandingPageContainer } from '../../components/landing-page-container';
import { components } from './components';
import EcosystemPageContent from './content.mdx';
export const metadata = {
title: 'The Ecosystem',
description: 'Everything you need to scale your API infrastructure',
};
export default function EcosystemPage() {
return (
<LandingPageContainer className="text-green-1000 light mx-auto max-w-[90rem] overflow-hidden px-4 [&>:not(header)]:px-4 lg:[&>:not(header)]:px-8 xl:[&>:not(header)]:px-[120px]">

View file

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 96 KiB

View file

@ -1,22 +1,27 @@
import { ReactElement, ReactNode } from 'react';
import Image from 'next/image';
import { Anchor, CallToAction, ContactButton, Heading } from '@theguild/components';
import { cn } from '../lib';
import { ArrowIcon } from './arrow-icon';
import { FrequentlyAskedFederationQuestions } from './frequently-asked-questions';
import { Hero, HeroLinks } from './hero';
import { InfoCard } from './info-card';
import { Page } from './page';
import federationDiagram from '../../public/federation-diagram.png';
import queryResultImage from '../../public/federation/query-result.png';
import queryImage from '../../public/federation/query.png';
import subgraphsProductsImage from '../../public/federation/subgraphs-products.png';
import subgraphsReviewsImage from '../../public/federation/subgraphs-reviews.png';
import supergraphSchemaImage from '../../public/federation/supergraph-schema.png';
import { Anchor, CallToAction, cn, ContactButton, Heading } from '@theguild/components';
import { ArrowIcon } from '../../components/arrow-icon';
import { FrequentlyAskedFederationQuestions } from '../../components/frequently-asked-questions';
import { Hero, HeroLinks } from '../../components/hero';
import { InfoCard } from '../../components/info-card';
import { LandingPageContainer } from '../../components/landing-page-container';
import federationDiagram from '../../../public/federation-diagram.png';
import queryResultImage from '../../../public/federation/query-result.png';
import queryImage from '../../../public/federation/query.png';
import subgraphsProductsImage from '../../../public/federation/subgraphs-products.png';
import subgraphsReviewsImage from '../../../public/federation/subgraphs-reviews.png';
import supergraphSchemaImage from '../../../public/federation/supergraph-schema.png';
export function FederationPage(): ReactElement {
export const metadata = {
title: 'What is GraphQL Federation?',
description:
'Discover what GraphQL Federation is, how it unifies multiple APIs into a Supergraph, its core benefits, and the building blocks like subgraphs, schema composition and gateway.',
};
export default function FederationPage(): ReactElement {
return (
<Page className="text-green-1000 light mx-auto max-w-[90rem] overflow-hidden">
<LandingPageContainer className="text-green-1000 light mx-auto max-w-[90rem] overflow-hidden">
<Hero className="mx-4 max-sm:mt-2 md:mx-6">
<Heading
as="h1"
@ -52,7 +57,7 @@ export function FederationPage(): ReactElement {
<WhyHive className="mx-4 md:mx-6" />
<FrequentlyAskedFederationQuestions className="mx-4 md:mx-6" />
<GetStarted className="mx-4 md:mx-6" />
</Page>
</LandingPageContainer>
);
}
@ -210,6 +215,7 @@ function HowFederationWorksSection(props: {
callToActionTitle: string;
index: keyof typeof HowFederationWorksVariants;
children?: ReactNode;
className?: string;
}) {
const variant = HowFederationWorksVariants[props.index];
@ -231,6 +237,7 @@ function HowFederationWorksSection(props: {
variant.afterClassName,
]
: null,
props.className,
)}
>
<div className="mx-auto flex max-w-full shrink flex-col flex-wrap justify-center gap-x-2">
@ -476,6 +483,7 @@ function HowFederationWorks(props: { className?: string }) {
<HowFederationWorksSection
index="third"
heading="GraphQL Gateway (Router)"
className="prose-p:text-white/80 prose-li:text-white/80"
description={
<>
<p>
@ -613,7 +621,7 @@ function WhyHive({ className }: { className?: string }) {
<Heading as="h2" size="md" className="text-balance sm:px-6 sm:text-center">
Why Choose Hive for GraphQL Federation?
</Heading>
<ul className="flex flex-row flex-wrap justify-center divide-y divide-solid sm:mt-6 sm:divide-x sm:divide-y-0 md:mt-16 md:px-6 xl:px-16">
<ul className="flex flex-row flex-wrap justify-center divide-y divide-solid divide-gray-200 sm:mt-6 sm:divide-x sm:divide-y-0 md:mt-16 md:px-6 xl:px-16">
<InfoCard
as="li"
heading="Complete Federation Stack"

View file

@ -24,3 +24,6 @@ export async function GET() {
},
});
}
export const dynamic = 'force-static';
export const config = { runtime: 'edge' };

View file

@ -0,0 +1,13 @@
<svg viewBox="0 0 52 52" xmlns="http://www.w3.org/2000/svg">
<style>
@media (prefers-color-scheme: dark) {
svg {
fill: #fff;
}
}
</style>
<path
d="M36.902.081H16.097L.5 15.678v20.806L16.097 52.08h20.805L52.5 36.484V15.678L36.902.081ZM49.18 28.918 29.334 48.762a4.01 4.01 0 0 1-5.67 0L3.82 28.918a4.01 4.01 0 0 1 0-5.671L23.663 3.402a4.01 4.01 0 0 1 5.671 0L49.18 23.247a4.01 4.01 0 0 1 0 5.67Z"
/>
<path d="m26.499 20.637-5.444 5.443 5.444 5.444 5.443-5.444-5.443-5.443Z" />
</svg>

After

Width:  |  Height:  |  Size: 524 B

View file

@ -0,0 +1,108 @@
import { ReactNode } from 'react';
import localFont from 'next/font/local';
import {
GitHubIcon,
GraphQLConfCard,
HiveFooter,
HiveNavigation,
PaperIcon,
PencilIcon,
PRODUCTS,
RightCornerIcon,
TargetIcon,
} from '@theguild/components';
import { getDefaultMetadata, getPageMap, HiveLayout } from '@theguild/components/server';
import { DynamicMetaTags } from './dynamic-meta-tags';
import graphQLConfLocalImage from '../components/graphql-conf-image.webp';
import '@theguild/components/style.css';
import '../selection-styles.css';
import '../mermaid.css';
import { NarrowPages } from './narrow-pages';
export const metadata = getDefaultMetadata({
productName: PRODUCTS.HIVE.name,
websiteName: 'Hive',
description:
'Fully Open-source schema registry, analytics and gateway for GraphQL federation and other GraphQL APIs',
});
const neueMontreal = localFont({
src: [
{ path: '../fonts/PPNeueMontreal-Regular.woff2', weight: '400' },
{ path: '../fonts/PPNeueMontreal-Medium.woff2', weight: '500' },
{ path: '../fonts/PPNeueMontreal-Medium.woff2', weight: '600' },
],
});
export default async function HiveDocsLayout({ children }: { children: ReactNode }) {
const pageMap = await getPageMap();
const lightOnlyPages = [
'/',
'/pricing',
'/federation',
'/oss-friends',
'/ecosystem',
'/partners',
];
return (
<HiveLayout
lightOnlyPages={lightOnlyPages}
head={<DynamicMetaTags pageMap={pageMap} />}
docsRepositoryBase="https://github.com/graphql-hive/platform/tree/main/packages/web/docs"
fontFamily={neueMontreal.style.fontFamily}
navbar={
<HiveNavigation
companyMenuChildren={<GraphQLConfCard image={graphQLConfLocalImage} />}
productName={PRODUCTS.HIVE.name}
developerMenu={[
{
href: '/docs',
icon: <PaperIcon />,
children: 'Documentation',
},
{ href: 'https://status.graphql-hive.com/', icon: <TargetIcon />, children: 'Status' },
{
href: '/product-updates',
icon: <RightCornerIcon />,
children: 'Product Updates',
},
{ href: 'https://the-guild.dev/blog', icon: <PencilIcon />, children: 'Blog' },
{
href: 'https://github.com/graphql-hive/console',
icon: <GitHubIcon />,
children: 'GitHub',
},
]}
/>
}
footer={
<HiveFooter
items={{
resources: [
{
children: 'Privacy Policy',
href: 'https://the-guild.dev/graphql/hive/privacy-policy.pdf',
title: 'Privacy Policy',
},
{
children: 'Terms of Use',
href: 'https://the-guild.dev/graphql/hive/terms-of-use.pdf',
title: 'Terms of Use',
},
{
children: 'Partners',
href: '/partners',
title: 'Partners',
},
],
}}
/>
}
>
{children}
<NarrowPages pages={lightOnlyPages} />
</HiveLayout>
);
}

View file

@ -0,0 +1,19 @@
'use client';
import { usePathname } from 'next/navigation';
import { HiveLayoutConfig } from '@theguild/components';
/**
* All light mode only pages are narrower, but we also have
* narrow pages that support dark mode.
*/
export function NarrowPages({ pages }: { pages: string[] }) {
const pathname = usePathname();
const isLightOnlyPage = pages.includes(pathname);
return isLightOnlyPage ? (
<div className="absolute size-0">
<HiveLayoutConfig widths="landing-narrow" />
</div>
) : null;
}

View file

@ -0,0 +1,9 @@
import { NotFoundPage } from '@theguild/components';
export default function Page() {
return (
<NotFoundPage>
<h1 className="text-5xl">404 This page could not be found</h1>
</NotFoundPage>
);
}

View file

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 118 KiB

View file

@ -7,37 +7,34 @@ import {
HighlightDecoration,
LargeHiveIconDecoration,
} from '@theguild/components';
import { Page } from './page';
import { LandingPageContainer } from '../../components/landing-page-container';
export async function getStaticProps() {
const res = await fetch('https://formbricks.com/api/oss-friends');
export const metadata = {
title: 'Our Open Source Friends',
description: 'We love open source. Meet our friends who share the same passion.',
};
async function fetchFriends() {
const response = await fetch('https://formbricks.com/api/oss-friends');
const body: {
data: Array<{
data: {
name: string;
description: string;
href: string;
}>;
} = await res.json();
}[];
} = await response.json();
return {
props: {
ssg: { friends: body.data },
},
};
return body.data;
}
export function OSSFriendsPage(props: {
friends: Array<{
name: string;
description: string;
href: string;
}>;
}) {
export default async function OSSFriendsPage() {
const friends = await fetchFriends();
return (
<Page className="text-green-1000 light mx-auto max-w-[90rem] overflow-hidden">
<div className="bg-beige-100 relative isolate mx-4 flex max-w-[90rem] flex-col gap-6 overflow-hidden rounded-3xl px-4 py-6 max-sm:mt-2 sm:py-12 md:mx-6 md:gap-8 lg:py-24">
<LandingPageContainer className="text-green-1000 light mx-auto max-w-[90rem] overflow-hidden">
<div className="bg-beige-100 relative isolate mx-4 flex flex-col gap-6 overflow-hidden rounded-3xl px-4 py-6 max-sm:mt-2 sm:py-12 md:mx-6 md:gap-8 lg:py-24">
<DecorationIsolation>
<ArchDecoration className="pointer-events-none absolute left-[-46px] top-[-20px] size-[200px] rotate-180 md:left-[-60px] md:top-[-188px] md:size-auto" />
<ArchDecoration className="pointer-events-none absolute -top-5 left-[-46px] size-[200px] rotate-180 md:left-[-60px] md:top-[-188px] md:size-auto" />
<ArchDecoration className="pointer-events-none absolute bottom-0 right-[-53px] size-[200px] md:-bottom-32 md:size-auto lg:bottom-[-188px] lg:right-0" />
<svg width="432" height="432" viewBox="0 0 432 432" className="absolute -z-10">
<defs>
@ -79,7 +76,7 @@ export function OSSFriendsPage(props: {
<div className="flex grow flex-col gap-12 px-4 md:px-0 lg:w-[650px]">
<div className="mx-auto leading-6 text-green-800">
<dl className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
{props.friends.map((friend, i) => (
{friends.map((friend, i) => (
<NextLink
href={friend.href}
key={i}
@ -119,7 +116,7 @@ export function OSSFriendsPage(props: {
Start building now
</CallToAction>
</section>
</Page>
</LandingPageContainer>
);
}

View file

@ -1,8 +1,10 @@
import { ReactElement } from 'react';
import { Metadata } from 'next';
import {
Anchor,
ArchDecoration,
CallToAction,
cn,
DecorationIsolation,
GetYourAPIGameRightSection,
Heading,
@ -10,23 +12,45 @@ import {
LargeHiveIconDecoration,
ToolsAndLibrariesCards,
} from '@theguild/components';
import { cn } from '../lib';
import { CheckIcon } from './check-icon';
import { CommunitySection } from './community-section';
import { AligentLogo, KarrotLogo, LinktreeLogo, MeetupLogo, SoundYXZLogo } from './company-logos';
import { CompanyTestimonialsSection } from './company-testimonials';
import { EcosystemManagementSection } from './ecosystem-management';
import { FeatureTabs } from './feature-tabs';
import { FrequentlyAskedQuestions } from './frequently-asked-questions';
import { Hero, HeroFeatures, HeroLinks, TrustedBy } from './hero';
import { InfoCard } from './info-card';
import { Page } from './page';
import { StatsItem, StatsList } from './stats';
import { TeamSection } from './team-section';
import { CheckIcon } from '../components/check-icon';
import { CommunitySection } from '../components/community-section';
import {
AligentLogo,
KarrotLogo,
LinktreeLogo,
MeetupLogo,
SoundYXZLogo,
} from '../components/company-logos';
import { CompanyTestimonialsSection } from '../components/company-testimonials';
import { EcosystemManagementSection } from '../components/ecosystem-management';
import { FeatureTabs } from '../components/feature-tabs';
import { FrequentlyAskedQuestions } from '../components/frequently-asked-questions';
import { Hero, HeroFeatures, HeroLinks, TrustedBy } from '../components/hero';
import { InfoCard } from '../components/info-card';
import { LandingPageContainer } from '../components/landing-page-container';
import { StatsItem, StatsList } from '../components/stats';
import { TeamSection } from '../components/team-section';
import { metadata as rootMetadata } from './layout';
export function IndexPage(): ReactElement {
export const metadata: Metadata = {
title: 'Open-Source GraphQL Federation Platform',
description:
'Fully Open-Source schema registry, analytics and gateway for GraphQL federation and other GraphQL APIs',
alternates: {
// to remove leading slash
canonical: '.',
},
openGraph: {
...rootMetadata.openGraph,
// to remove leading slash
url: '.',
images: [new URL('./opengraph-image.png', import.meta.url).toString()],
},
};
export default function IndexPage(): ReactElement {
return (
<Page className="text-green-1000 light mx-auto max-w-[90rem] overflow-hidden">
<LandingPageContainer className="text-green-1000 light mx-auto max-w-[90rem] overflow-hidden">
<Hero className="mx-4 max-sm:mt-2 md:mx-6">
<Heading
as="h1"
@ -94,7 +118,7 @@ export function IndexPage(): ReactElement {
<ToolsAndLibrariesCards isHive className="mx-4 mt-6 md:mx-6" />
<FrequentlyAskedQuestions className="mx-4 md:mx-6" />
<GetYourAPIGameRightSection className="mx-4 sm:mb-6 md:mx-6" />
</Page>
</LandingPageContainer>
);
}

View file

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

View file

@ -8,9 +8,15 @@ import {
Heading,
InfoCard,
} from '@theguild/components';
import { FrequentlyAskedPartnersQuestions } from './frequently-asked-questions';
import { Hero, HeroLinks } from './hero';
import { Page } from './page';
import { FrequentlyAskedPartnersQuestions } from '../../components/frequently-asked-questions';
import { Hero, HeroLinks } from '../../components/hero';
import { LandingPageContainer } from '../../components/landing-page-container';
export const metadata = {
title: 'Partnerships',
description:
'Accelerate GraphQL Federation adoption with the Hive Partner Network. Access enterprise-grade tools and expertise to build scalable, unified APIs across distributed systems. Join our network of federation experts.',
};
function WhyUs({ className }: { className?: string }) {
return (
@ -116,10 +122,10 @@ function SolutionsPartner({ className }: { className?: string }) {
);
}
export function PartnersPage() {
export default function PartnersPage() {
return (
<Page className="text-green-1000 light mx-auto max-w-[90rem] overflow-hidden">
<Hero className="mx-4 h-[22%] max-sm:mt-2 md:mx-6">
<LandingPageContainer className="text-green-1000 light mx-auto max-w-[90rem] overflow-hidden">
<Hero className="mx-4 h-[22%] max-sm:mt-2 md:mx-6 lg:py-24">
<Heading
as="h1"
size="xl"
@ -148,6 +154,6 @@ export function PartnersPage() {
<SolutionsPartner />
<FrequentlyAskedPartnersQuestions />
<GetYourAPIGameRightSection className="mx-4 mt-6 !overflow-visible sm:mb-6 md:mx-6 md:mt-16" />
</Page>
</LandingPageContainer>
);
}

View file

@ -5,15 +5,21 @@ import {
GetYourAPIGameRightSection,
Heading,
} from '@theguild/components';
import { CompanyTestimonialsSection } from './company-testimonials';
import { FrequentlyAskedQuestions } from './frequently-asked-questions';
import { Page } from './page';
import { PlanComparison } from './plan-comparison';
import { Pricing } from './pricing';
import { CompanyTestimonialsSection } from '../../components/company-testimonials';
import { FrequentlyAskedQuestions } from '../../components/frequently-asked-questions';
import { LandingPageContainer } from '../../components/landing-page-container';
import { PlanComparison } from '../../components/plan-comparison';
import { Pricing } from '../../components/pricing';
export function PricingPage() {
export const metadata = {
title: 'Hive Platform Pricing',
description:
'Honest pricing plans for GraphQL Federation and other GraphQL APIs, supporting developers to enterprise with Open-Source schema registry, analytics, and gateway solutions',
};
export default function PricingPage() {
return (
<Page className="text-green-1000 light mx-auto max-w-[90rem] overflow-hidden">
<LandingPageContainer className="text-green-1000 light mx-auto max-w-[90rem] overflow-hidden">
<PricingPageHero className="mx-4 max-sm:mt-2 md:mx-6" />
<Pricing className="mt-4" />
@ -26,7 +32,7 @@ export function PricingPage() {
<FrequentlyAskedQuestions className="mx-4 md:mx-6" />
<GetYourAPIGameRightSection className="mx-4 sm:mb-6 md:mx-6" />
</Page>
</LandingPageContainer>
);
}

View file

@ -10,8 +10,8 @@ authors: [kamil]
When developing a Supergraph, you may want to test your local subgraph or subgraphs against the rest
of the GraphQL schema.
This is now possible with the new `dev` command in the [Hive CLI](../docs/api-reference/cli.mdx)
(_only available for Federation projects_).
This is now possible with the new `dev` command in the [Hive CLI](/docs/api-reference/cli) (_only
available for Federation projects_).
```bash
hive dev --service reviews --url http://localhost:3001/graphql

View file

@ -13,7 +13,7 @@ This eliminates the need to set up
Apollo Federation v2, as it's now the default behavior.
In October 2023 we announced an
[early access to Native Apollo Federation v2](./2023-10-10-native-federation-2.mdx). Since then, we
[early access to Native Apollo Federation v2](./2023-10-10-native-federation-2). Since then, we
gathered feedback and observed the adoption.
> Thank you 🙏 early adopters!

View file

@ -31,7 +31,7 @@ seamlessly integrate with your GraphQL server of choice, improving the developer
### Migration guide
<Tabs items={['GraphQL Yoga', 'Apollo Server', 'Envelop', 'Custom']}>
<Tabs.Tab>
1. Remove `@graphql-hive/client` from your dependencies.

View file

@ -87,7 +87,7 @@ Furthermore, you get analytics specific to your deployed app version, so you can
decisions on deprecating and removing fields from your GraphQL API schema and avoid breaking ancient
versions of your app.
import pendingAppImage from '../../../public/changelog/2024-07-30-persisted-documents-app-deployments-preview/app-deployments-overview.png'
import pendingAppImage from '../../../../../public/changelog/2024-07-30-persisted-documents-app-deployments-preview/app-deployments-overview.png'
<NextImage
alt="App Deployments on Hive Dashboard"

View file

@ -6,9 +6,7 @@ date: 2024-10-11
authors: [dimitri]
---
import fullScreenMode from './full-screen-mode.mp4'
import queryBuilder from './query-builder.mp4'
import newTabs from './tabs-new.mp4'
import { addBasePath } from 'next/dist/client/add-base-path'
export function Caption({ children }) {
return <p className="text-center text-sm italic">{children}</p>
@ -18,7 +16,7 @@ export function Video({ src, alt }) {
return (
<>
<video autoPlay="autoplay" loop muted playsInline>
<source src={src} type="video/mp4" />
<source src={addBasePath(src)} type="video/mp4" />
Your browser does not support the video tag.
</video>
<Caption>{alt}</Caption>
@ -46,7 +44,7 @@ query" were removed, "Prettify query" button was moved to the end of the toolbar
Users with wide screens can now benefit from the fullscreen mode. The button is located in place of
the GraphiQL logo.
<Video src={fullScreenMode} alt="Full screen mode demo" />
<Video src="/product-updates/full-screen-mode.mp4" alt="Full screen mode demo" />
### Tabs Support
@ -54,7 +52,7 @@ Laboratory now supports multiple tabs, you can test as many queries as needed at
There is no longer a requirement to mandatory save the query in one of the collections, you can have
a draft query which will be stored in local storage.
<Video src={newTabs} alt="Tabs demo" />
<Video src="/product-updates/tabs-new.mp4" alt="Tabs demo" />
The UI of the tabs is inspired by the functionality of Google Chrome tabs, tabs will be shown even
if there is only a single tab as per Chrome tabs UX.
@ -64,7 +62,7 @@ if there is only a single tab as per Chrome tabs UX.
The laboratory now also has a query builder, which you can use to explore schema and easily
construct GraphQL queries.
<Video src={queryBuilder} alt="Query builder demo" />
<Video src="/product-updates/query-builder.mp4" alt="Query builder demo" />
## GraphiQL v4 Alpha

View file

@ -8,7 +8,6 @@ authors: [dotan]
---
import NextImage from 'next/image'
import { Callout } from '@theguild/components'
We're excited to announce that the [**App Deployments**](/docs/other-integrations/apollo-router)
feature is now available for Apollo Router!
@ -19,7 +18,7 @@ App Deployments (persisted documents) are a way to group and publish your GraphQ
single app version to the Hive Registry. This allows you to keep track of your different app
versions, their operations usage, and performance.
import pendingAppImage from '../../../../public/docs/pages/features/app-deployments/pending-app.png'
import pendingAppImage from '../../../../../public/docs/pages/features/app-deployments/pending-app.png'
<NextImage
alt="Pending App Deployment"

View file

@ -7,10 +7,6 @@ date: 2024-12-27
authors: [dimitri]
---
import { Callout } from '@theguild/components'
export const figcaptionClass = 'text-center text-sm mt-2'
We've added Preflight Scripts our GraphQL IDE - Laboratory!
**Preflight Scripts** is a feature that enables you to automatically execute custom code before each
@ -18,7 +14,7 @@ GraphQL request is made. They're especially useful for handling authentication f
where you may need to claim or refresh an access token, validate credentials, or set up custom
headers - all before the request is sent.
![Demo of Preflight Scripts](../../docs/dashboard/laboratory/editing.png)
![Demo of Preflight Scripts](private-next-content-dir/dashboard/laboratory/editing.png)
Check out [the documentation on Preflight Scripts](/docs/dashboard/laboratory/preflight-scripts) for
information on how to configure, edit, and use them.

View file

@ -0,0 +1,19 @@
'use client';
import type { ReactElement, ReactNode } from 'react';
import { ConfiguredGiscus } from '../../../components/configured-giscus';
import { ProductUpdateHeader } from './product-update-header';
const Layout = ({ children }: { children: ReactNode }): ReactElement => {
return (
<>
<ProductUpdateHeader />
{children}
<div className="container !max-w-[65rem]">
<ConfiguredGiscus />
</div>
</>
);
};
export default Layout;

View file

@ -1,8 +1,9 @@
import type { ReactElement } from 'react';
'use client';
import { format } from 'date-fns';
import { Anchor } from '@theguild/components';
import { authors } from '../authors';
import { SocialAvatar } from './social-avatar';
import { Anchor, useConfig } from '@theguild/components';
import { authors } from '../../../authors';
import { SocialAvatar } from '../../../components/social-avatar';
type Meta = {
authors: string[];
@ -11,7 +12,7 @@ type Meta = {
description: string;
};
const Authors = ({ meta }: { meta: Meta }): ReactElement => {
const Authors = ({ meta }: { meta: Meta }) => {
const date = meta.date ? new Date(meta.date) : new Date();
if (meta.authors.length === 1) {
@ -63,11 +64,14 @@ const Authors = ({ meta }: { meta: Meta }): ReactElement => {
);
};
export const ProductUpdateBlogPostHeader = ({ meta }: { meta: Meta }): ReactElement => {
export const ProductUpdateHeader = () => {
const { normalizePagesResult } = useConfig();
const metadata = normalizePagesResult.activeMetadata as Meta;
return (
<>
<h1>{meta.title}</h1>
<Authors meta={meta} />
</>
<div className="x:max-w-[90rem] mx-auto">
<h1 className="mt-12 text-center text-4xl">{metadata.title}</h1>
<Authors meta={metadata} />
</div>
);
};

View file

@ -0,0 +1,15 @@
---
title: Product Updates
description: The most recent developments from GraphQL Hive.
---
import { ProductUpdatesPage } from '../../components/product-updates'
<div className="[&>h1]:!text-left [&>h1]:!mb-0 [&>p]:!mt-0">
# Product Updates
The most recent developments from GraphQL Hive.
</div>
<ProductUpdatesPage />

View file

@ -1,6 +1,5 @@
/* eslint-disable tailwindcss/no-custom-classname */
import { ReactElement } from 'react';
import type { GetStaticProps } from 'next';
import { Callout, Code } from '@theguild/components';
type CLIError = {
@ -16,7 +15,7 @@ export function ErrorDetails(props: CLIError): ReactElement {
<>
<h3
id={`errors-${props.code}`}
className="_font-semibold _tracking-tight _text-slate-900 _mt-8 _text-2xl dark:_text-slate-100"
className="mt-8 text-2xl font-semibold tracking-tight text-slate-900 dark:text-slate-100"
>
{props.code} "{props.title}"{' '}
<a
@ -25,15 +24,15 @@ export function ErrorDetails(props: CLIError): ReactElement {
aria-label="Permalink for this error code"
/>
</h3>
<h4 className="_font-semibold _tracking-tight _text-slate-900 _mt-8 _text-xl dark:_text-slate-100">
Example: <Code contentEditable="false">{props.example}</Code>
<h4 className="mt-8 text-xl font-semibold tracking-tight text-slate-900 dark:text-slate-100">
Example: <Code>{props.example}</Code>
</h4>
<Callout type="default" emoji=">">
<pre>{props.exampleOutput}</pre>
</Callout>
<h4
id={`errors-${props.code}-fix`}
className="_font-semibold _tracking-tight _text-slate-900 _mt-8 _text-xl dark:_text-slate-100"
className="mt-8 text-xl font-semibold tracking-tight text-slate-900 dark:text-slate-100"
>
Suggested Fix
</h4>
@ -386,16 +385,13 @@ export async function getErrorDescriptions(): Promise<CLIError[]> {
);
}
export const getStaticProps: GetStaticProps<{ ssg: { cliErrors: CLIError[] } }> = async () => {
return {
props: {
__nextra_dynamic_opts: {
title: 'CLI Errors',
frontMatter: {
description: 'GraphQL Hive CLI Error Codes and Fixes.',
},
},
ssg: { cliErrors: await getErrorDescriptions() },
},
};
};
export async function CLIErrorsSection() {
const cliErrors = await getErrorDescriptions();
return (
<>
{cliErrors.map(item => (
<ErrorDetails key={item.code} {...item} />
))}
</>
);
}

View file

@ -1,7 +1,13 @@
import Image, { StaticImageData } from 'next/image';
import { GlobeIcon } from '@radix-ui/react-icons';
import { CallToAction, DiscordIcon, GitHubIcon, Heading, TwitterIcon } from '@theguild/components';
import { cn } from '../lib';
import {
CallToAction,
cn,
DiscordIcon,
GitHubIcon,
Heading,
TwitterIcon,
} from '@theguild/components';
import { MaskingScrollview } from './masking-scrollview';
import Achrafash from './community-section/achrafash_.png';
import ChimameRt from './community-section/chimame_rt.png';

View file

@ -1,9 +1,10 @@
'use client';
import React, { Fragment, useRef } from 'react';
import Head from 'next/head';
import Image, { StaticImageData } from 'next/image';
import * as Tabs from '@radix-ui/react-tabs';
import { CallToAction, Heading } from '@theguild/components';
import { cn } from '../../lib';
import { CallToAction, cn, Heading } from '@theguild/components';
import { ArrowIcon } from '../arrow-icon';
import {
KarrotLogo,
@ -64,8 +65,8 @@ const testimonials: Testimonial[] = [
},
{
company: 'Prodigy',
logo: props => (
<div className={cn('flex h-8 items-center', props.className)}>
logo: ({ className, ...props }) => (
<div className={cn('flex h-8 w-min items-center justify-center', className)}>
<ProdigyLogo {...props} className="" height={37} />
</div>
),
@ -121,13 +122,17 @@ export function CompanyTestimonialsSection({ className }: { className?: string }
</Heading>
<Tabs.Root
defaultValue={testimonials[0].company}
className="flex flex-col"
className="flex flex-col overflow-hidden"
// we need scrolling for mobile, so this can't be changed to a state-driven opacity transition
onValueChange={value => {
const id = getTestimonialId(value);
const element = document.getElementById(id);
if (element) {
element.scrollIntoView({ behavior: 'instant', block: 'nearest', inline: 'nearest' });
}
const element = document.getElementById(id)?.parentElement;
const scrollview = scrollviewRef.current;
if (!scrollview || !element) return;
// we don't use scrollIntoView because it will also scroll vertically
scrollview.scrollTo({ left: element.offsetLeft, behavior: 'instant' });
}}
>
<Tabs.List
@ -140,14 +145,7 @@ export function CompanyTestimonialsSection({ className }: { className?: string }
<Tabs.Trigger
key={testimonial.company}
value={testimonial.company}
className={
'hive-focus flex-grow-0 [&[data-state="active"]>:first-child]:bg-blue-400' +
' lg:rdx-state-active:bg-white lg:flex-grow lg:bg-transparent' +
' justify-center p-0.5 lg:p-4' +
' rdx-state-active:text-green-1000 lg:rdx-state-active:border-beige-600' +
' border-transparent font-medium leading-6 text-green-800 lg:border' +
' flex flex-1 items-center justify-center rounded-[15px]'
}
className='hive-focus lg:rdx-state-active:bg-white rdx-state-active:text-green-1000 lg:rdx-state-active:border-beige-600 flex flex-1 grow-0 items-center justify-center rounded-[15px] border-transparent p-0.5 font-medium leading-6 text-green-800 lg:grow lg:border lg:bg-transparent lg:p-4 [&[data-state="active"]>:first-child]:bg-blue-400'
>
<div className="size-2 rounded-full bg-blue-200 transition-colors lg:hidden" />
<Logo title={testimonial.company} height={32} className="max-lg:sr-only" />
@ -169,9 +167,9 @@ export function CompanyTestimonialsSection({ className }: { className?: string }
value={company}
tabIndex={-1}
className={cn(
'relative flex w-full shrink-0 snap-center flex-col' +
' gap-6 md:flex-row lg:gap-12' +
' lg:data-[state="inactive"]:hidden',
'relative flex w-full shrink-0 snap-center flex-col',
'gap-6 md:flex-row lg:gap-12',
'lg:data-[state="inactive"]:hidden',
caseStudyHref
? 'data-[state="active"]:pb-[72px] lg:data-[state="active"]:pb-0'
: 'max-lg:pb-8',
@ -220,12 +218,7 @@ export function CompanyTestimonialsSection({ className }: { className?: string }
{data.map(({ numbers, description }, i) => (
<Fragment key={i}>
<li>
<span
className={
'block text-[40px] leading-[1.2] tracking-[-0.2px]' +
' md:text-6xl md:leading-[1.1875] md:tracking-[-0.64px]'
}
>
<span className="block text-[40px] leading-[1.2] tracking-[-0.2px] md:text-6xl md:leading-[1.1875] md:tracking-[-0.64px]">
{numbers}
</span>
<span className="mt-2">{description}</span>

View file

@ -0,0 +1,20 @@
'use client';
import { usePathname } from 'next/navigation';
import { Giscus } from '@theguild/components';
export function ConfiguredGiscus() {
const route = usePathname();
return (
<Giscus
// ensure giscus is reloaded when client side route is changed
key={route}
repo="graphql-hive/platform"
repoId="R_kgDOHWr5kA"
category="Docs Discussions"
categoryId="DIC_kwDOHWr5kM4CSDSS"
mapping="pathname"
/>
);
}

View file

@ -0,0 +1,351 @@
'use client';
import { ReactNode, useEffect, useLayoutEffect, useRef, useState } from 'react';
import { cn, CodegenIcon, HiveGatewayIcon, HiveIcon, YogaIcon } from '@theguild/components';
import { GraphQLLogo } from './graphql-logo';
import { IconGradientDefs } from './icon-gradient-defs';
import styles from './ecosystem-management.module.css';
const edgeTexts = [
'Apps send requests to Hive Gateway that acts as the api gateway to data from your federated graph.',
'Developers that build the apps/api clients will use GraphQL Codegen for generating type-safe code that makes writing apps safer and faster.',
'Codegen uses Hive to pull the GraphQL schema for generating the code.',
'Hive Gateway pulls the supergraph from the Hive Schema Registry that gives it all the information about the subgraphs and available data to serve to the outside world.',
'Hive Gateway delegates GraphQL requests to the corresponding Yoga subgraphs within your internal network.',
'Check the subgraph schema against the Hive Schema Registry before deployment to ensure integrity. After deploying a new subgraph version, publish its schema to Hive, to generate the supergraph used by Gateway.',
];
const longestEdgeText = edgeTexts.reduce((a, b) => (a.length > b.length ? a : b));
const useIsomorphicLayoutEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect;
const EDGE_HOVER_INTERVAL_TIME = 5000;
const EDGE_HOVER_RESET_TIME = 10_000;
export function EcosystemIllustration(props: { className?: string }) {
const [highlightedEdge, setHighlightedEdge] = useState<number | null>(4);
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
useIsomorphicLayoutEffect(() => {
intervalRef.current = setInterval(() => {
setHighlightedEdge(prev => (prev! % 6) + 1);
}, EDGE_HOVER_INTERVAL_TIME);
return () => clearInterval(intervalRef.current || undefined);
}, []);
const highlightEdge = (edgeNumber: number) => {
clearInterval(intervalRef.current || undefined);
setHighlightedEdge(edgeNumber);
// after 10 seconds, we'll start stepping through edges again
intervalRef.current = setTimeout(() => {
intervalRef.current = setInterval(() => {
setHighlightedEdge(prev => (prev! % 6) + 1);
}, EDGE_HOVER_INTERVAL_TIME);
}, EDGE_HOVER_RESET_TIME);
};
const onPointerOverEdge = (event: React.PointerEvent<HTMLElement>) => {
const edgeNumber = parseInt(event.currentTarget.textContent!);
if (Number.isNaN(edgeNumber) || edgeNumber < 1 || edgeNumber > 6) {
return;
}
highlightEdge(edgeNumber);
};
const highlightIterators = useRef<{ node: number[]; index: number }>({ node: [], index: -1 });
const onHighlightNode = (edges: number[]) => {
clearInterval(intervalRef.current || undefined);
let previousIndex: number;
if (highlightIterators.current.node.every((x, i) => edges[i] === x)) {
previousIndex = highlightIterators.current.index;
} else {
highlightIterators.current.node = edges;
previousIndex = -1;
}
let index = (previousIndex + 1) % edges.length;
// if edge under index is already highlighted, we move forward
if (highlightedEdge === edges[index]) {
index = (index + 1) % edges.length;
}
highlightIterators.current.index = index;
highlightEdge(edges[index]);
};
return (
<div
className={cn(
'relative flex min-h-[400px] flex-1 flex-col items-center',
props.className,
styles.container,
)}
>
<IconGradientDefs />
<div className={'flex flex-row ' + styles.vars}>
<Edge top bottom left highlighted={highlightedEdge === 5}>
<div
style={{
height:
'calc(var(--node-h) / 2 + var(--gap) + var(--big-node-h) / 2 - var(--label-h) / 2)',
}}
className="ml-[calc(1rem+1px-var(--bw))] mt-[calc(var(--node-h)/2)] w-10 rounded-tl-xl"
/>
<EdgeLabel onPointerOver={onPointerOverEdge}>5</EdgeLabel>
<div
style={{
height:
'calc(var(--node-h) / 2 + var(--gap) + var(--big-node-h) / 2 - var(--label-h) / 2)',
}}
className="ml-[calc(1rem+1px-var(--bw))] box-content w-10 rounded-bl-xl"
/>
</Edge>
<div>
<Node
title={
<>
<span className={styles.smHidden}>Hive</span> Gateway
</>
}
description="Gateway"
edges={[1, 4, 5]}
highlightedEdge={highlightedEdge}
onHighlight={onHighlightNode}
>
<HiveGatewayIcon className="size-12 [&>path]:fill-[url(#linear-blue)] [&>path]:stroke-[url(#linear-white)] [&>path]:stroke-[0.5px]" />
</Node>
<Edge
left
className="ml-[calc(var(--node-w)/2-var(--label-h)/2-4px)]"
highlighted={highlightedEdge === 4}
>
<div className="ml-[calc(var(--label-h)/2-.5px)] h-[calc((var(--gap)-var(--label-h))/2)]" />
<EdgeLabel onPointerOver={onPointerOverEdge}>4</EdgeLabel>
<div className="ml-[calc(var(--label-h)/2-.5px)] h-[calc((var(--gap)-var(--label-h))/2)]" />
</Edge>
<Node
className="h-[var(--big-node-h)] w-[var(--node-w)] flex-col text-center"
title="Hive"
description="Registry and CDN"
edges={[3, 4, 6]}
highlightedEdge={highlightedEdge}
onHighlight={onHighlightNode}
>
<HiveIcon className="size-[var(--big-logo-size)] [&>g]:fill-[url(#linear-blue)] [&>g]:stroke-[url(#linear-white)] [&>g]:stroke-[0.2px]" />
</Node>
<Edge
left
className="ml-[calc(var(--node-w)/2-var(--label-h)/2-4px)]"
highlighted={highlightedEdge === 6}
>
<div className="ml-[calc(var(--label-h)/2-.5px)] h-6" />
<EdgeLabel onPointerOver={onPointerOverEdge}>6</EdgeLabel>
<div className="ml-[calc(var(--label-h)/2-.5px)] h-6" />
</Edge>
<Node
title="Yoga"
description="GraphQL Subgraph"
edges={[5, 6]}
highlightedEdge={highlightedEdge}
onHighlight={onHighlightNode}
>
<YogaIcon className="size-12 [&>path]:fill-[url(#linear-blue)] [&>path]:stroke-[url(#linear-white)] [&>path]:stroke-[0.5px]" />
</Node>
</div>
<div>
<Edge
top
className="flex h-[var(--node-h)] flex-row items-center"
highlighted={highlightedEdge === 1}
>
<div className="w-[calc(var(--label-h)/1.6)]" />
<EdgeLabel onPointerOver={onPointerOverEdge}>1</EdgeLabel>
<div className="w-[calc(var(--label-h)/1.6)]" />
</Edge>
<div className="h-[var(--gap)]" />
<Edge
top
highlighted={highlightedEdge === 3}
className="flex h-[var(--big-node-h)] flex-row items-center"
>
<div className="w-[calc(var(--label-h)/1.6)]" />
<EdgeLabel onPointerOver={onPointerOverEdge}>3</EdgeLabel>
<div className="w-[calc(var(--label-h)/1.6)]" />
</Edge>
</div>
<div>
<Node
title="Client"
description={
<span className="[@media(max-width:1438px)]:hidden">GraphQL client of choice</span>
}
edges={[1, 2]}
highlightedEdge={highlightedEdge}
onHighlight={onHighlightNode}
>
<GraphQLLogo className="size-12" />
</Node>
<Edge
left
className="flex h-[calc(var(--gap)+var(--big-node-h)/2-var(--node-h)/2)] flex-col items-center"
highlighted={highlightedEdge === 2}
>
<div className="flex-1" />
<EdgeLabel onPointerOver={onPointerOverEdge}>2</EdgeLabel>
<div className="flex-1" />
</Edge>
<Node
title="Codegen"
description={
<span className="[@media(max-width:1438px)]:hidden">GraphQL Code Generation</span>
}
edges={[2, 3]}
highlightedEdge={highlightedEdge}
onHighlight={onHighlightNode}
>
<CodegenIcon className="size-12 fill-[url(#linear-blue)] stroke-[url(#linear-white)] stroke-[0.5px]" />
</Node>
</div>
</div>
<p className={cn('relative text-white/80', styles.text)}>
{/* We use the longest text to ensure we have enough space. */}
<span className="invisible">{longestEdgeText}</span>
{edgeTexts.map((text, i) => {
return (
<span
key={i}
className={cn(
'absolute inset-0',
// Makes it accessible by crawlers.
highlightedEdge !== null && highlightedEdge - 1 === i ? 'visible' : 'invisible',
)}
>
{text}
</span>
);
})}
</p>
</div>
);
}
interface EdgeProps extends React.HTMLAttributes<HTMLElement> {
highlighted: boolean;
top?: boolean;
left?: boolean;
bottom?: boolean;
}
function Edge({ highlighted, top, bottom, left, className, ...rest }: EdgeProps) {
return (
<div
style={{ '--bw': highlighted ? '2px' : '1px' }}
className={cn(
className,
'[&>*]:transition-colors [&>*]:duration-500 [&>:nth-child(odd)]:border-green-700',
top &&
(bottom
? '[&>:nth-child(1)]:border-t-[length:var(--bw)] [&>:nth-child(3)]:border-b-[length:var(--bw)]'
: '[&>:nth-child(odd)]:border-t-[length:var(--bw)]'),
left && '[&>:nth-child(odd)]:border-l-[length:var(--bw)]',
highlighted &&
'[&>*]:text-green-1000 [&>:nth-child(even)]:bg-green-300 [&>:nth-child(odd)]:border-green-300',
)}
{...rest}
/>
);
}
interface EdgeLabelProps extends React.HTMLAttributes<HTMLElement> {
onPointerOver: React.PointerEventHandler<HTMLElement>;
}
function EdgeLabel(props: EdgeLabelProps) {
return (
<div
className={
'flex size-8 h-[var(--label-h)] items-center justify-center' +
' cursor-default rounded bg-green-700 text-sm font-medium leading-5' +
' hover:ring-2 hover:ring-green-700'
}
{...props}
/>
);
}
interface NodeProps extends Omit<React.HTMLAttributes<HTMLElement>, 'title'> {
title: ReactNode;
description?: ReactNode;
edges: number[];
highlightedEdge: number | null;
onHighlight: (edges: number[]) => void;
}
function Node({
title,
description,
children,
edges,
highlightedEdge,
className,
onHighlight,
...rest
}: NodeProps) {
const highlighted = edges.includes(highlightedEdge!);
const hovered = useRef(false);
return (
<div
onPointerOver={event => {
if (hovered.current || event.currentTarget !== event.target) {
return;
}
hovered.current = true;
if (edges.includes(highlightedEdge!)) return;
onHighlight([edges[0]]);
}}
onPointerOut={event => {
if (
!event.currentTarget.contains(event.relatedTarget as Node) &&
event.currentTarget === event.target
) {
hovered.current = false;
}
}}
onClick={() => onHighlight(edges)}
className={cn(
styles.node,
'relative z-10 flex h-[var(--node-h)] items-center gap-2 rounded-2xl p-4 xl:gap-4 xl:p-[22px]' +
' bg-[linear-gradient(135deg,rgb(255_255_255/0.10),rgb(255_255_255/0.20))]' +
' cursor-pointer transition-colors duration-500 [&>svg]:flex-shrink-0',
// todo: linear gradients don't transition, so we should add white/10 background layer'
highlighted &&
'bg-[linear-gradient(135deg,rgb(255_255_255_/0.2),rgb(255_255_255/0.3))] ring ring-green-300',
className,
)}
{...rest}
>
{children}
<div>
<div className="font-medium text-green-100">{title}</div>
{description && (
<div
className="mt-0.5 text-sm leading-5 text-green-200"
style={{
display: 'var(--node-desc-display)',
}}
>
{description}
</div>
)}
</div>
</div>
);
}

View file

@ -0,0 +1,19 @@
export function GraphQLLogo(props: { className?: string }) {
return (
<svg className={props.className} viewBox="0 0 100 100">
<g fill="url(#linear-blue)">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M50 6.90308L87.323 28.4515V71.5484L50 93.0968L12.677 71.5484V28.4515L50 6.90308ZM16.8647 30.8693V62.5251L44.2795 15.0414L16.8647 30.8693ZM50 13.5086L18.3975 68.2457H81.6025L50 13.5086ZM77.4148 72.4334H22.5852L50 88.2613L77.4148 72.4334ZM83.1353 62.5251L55.7205 15.0414L83.1353 30.8693V62.5251Z"
/>
<circle cx="50" cy="9.3209" r="8.82" />
<circle cx="85.2292" cy="29.6605" r="8.82" />
<circle cx="85.2292" cy="70.3396" r="8.82" />
<circle cx="50" cy="90.6791" r="8.82" />
<circle cx="14.7659" cy="70.3396" r="8.82" />
<circle cx="14.7659" cy="29.6605" r="8.82" />
</g>
</svg>
);
}

View file

@ -0,0 +1,33 @@
/**
* This must be included in one of the SVGs here so they work nicely in Safari.
*/
export function IconGradientDefs() {
return (
<svg className="absolute size-0">
<defs>
<linearGradient
id="linear-blue"
x1="0"
y1="0"
x2="100%"
y2="100%"
gradientUnits="objectBoundingBox"
>
<stop stopColor="#8CBEB3" />
<stop offset="1" stopColor="#68A8B6" />
</linearGradient>
<linearGradient
id="linear-white"
x1="0"
y1="0"
x2="100%"
y2="100%"
gradientUnits="objectBoundingBox"
>
<stop stopColor="white" stopOpacity="0.5" />
<stop offset="1" stopColor="white" stopOpacity="0.15" />
</linearGradient>
</defs>
</svg>
);
}

View file

@ -1,27 +1,22 @@
import { ReactNode, useEffect, useLayoutEffect, useRef, useState } from 'react';
import NextLink from 'next/link';
import {
CallToAction,
CodegenIcon,
cn,
DecorationIsolation,
Heading,
HighlightDecoration,
HiveGatewayIcon,
HiveIcon,
YogaIcon,
} from '@theguild/components';
import { cn } from '../../lib';
import { ArrowIcon } from '../arrow-icon';
import { BookIcon } from '../book-icon';
import { CheckIcon } from '../check-icon';
import styles from './ecosystem-management.module.css';
import { EcosystemIllustration } from './ecosystem-illustration';
export function EcosystemManagementSection({ className }: { className?: string }) {
return (
<section
className={cn(
'bg-green-1000 relative isolate overflow-hidden rounded-3xl text-white' +
' p-8 pb-[160px] sm:pb-[112px] md:p-[72px] md:pb-[112px] lg:pb-[72px]',
'bg-green-1000 relative isolate overflow-hidden rounded-3xl text-white',
'p-8 pb-[160px] sm:pb-[112px] md:p-[72px] md:pb-[112px] lg:pb-[72px]',
className,
)}
>
@ -112,7 +107,7 @@ export function EcosystemManagementSection({ className }: { className?: string }
</CallToAction>
</div>
</div>
<Illustration />
<EcosystemIllustration />
</div>
<DecorationIsolation>
<HighlightDecoration className="pointer-events-none absolute right-0 top-[-22px] overflow-visible" />
@ -120,401 +115,3 @@ export function EcosystemManagementSection({ className }: { className?: string }
</section>
);
}
const edgeTexts = [
'Apps send requests to Hive Gateway that acts as the api gateway to data from your federated graph.',
'Developers that build the apps/api clients will use GraphQL Codegen for generating type-safe code that makes writing apps safer and faster.',
'Codegen uses Hive to pull the GraphQL schema for generating the code.',
'Hive Gateway pulls the supergraph from the Hive Schema Registry that gives it all the information about the subgraphs and available data to serve to the outside world.',
'Hive Gateway delegates GraphQL requests to the corresponding Yoga subgraphs within your internal network.',
'Check the subgraph schema against the Hive Schema Registry before deployment to ensure integrity. After deploying a new subgraph version, publish its schema to Hive, to generate the supergraph used by Gateway.',
];
const longestEdgeText = edgeTexts.reduce((a, b) => (a.length > b.length ? a : b));
const useIsomorphicLayoutEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect;
const EDGE_HOVER_INTERVAL_TIME = 5000;
const EDGE_HOVER_RESET_TIME = 10_000;
function Illustration(props: { className?: string }) {
const [highlightedEdge, setHighlightedEdge] = useState<number | null>(4);
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
useIsomorphicLayoutEffect(() => {
intervalRef.current = setInterval(() => {
setHighlightedEdge(prev => (prev! % 6) + 1);
}, EDGE_HOVER_INTERVAL_TIME);
return () => clearInterval(intervalRef.current || undefined);
}, []);
const highlightEdge = (edgeNumber: number) => {
clearInterval(intervalRef.current || undefined);
setHighlightedEdge(edgeNumber);
// after 10 seconds, we'll start stepping through edges again
intervalRef.current = setTimeout(() => {
intervalRef.current = setInterval(() => {
setHighlightedEdge(prev => (prev! % 6) + 1);
}, EDGE_HOVER_INTERVAL_TIME);
}, EDGE_HOVER_RESET_TIME);
};
const onPointerOverEdge = (event: React.PointerEvent<HTMLElement>) => {
const edgeNumber = parseInt(event.currentTarget.textContent!);
if (Number.isNaN(edgeNumber) || edgeNumber < 1 || edgeNumber > 6) {
return;
}
highlightEdge(edgeNumber);
};
const highlightIterators = useRef<{ node: number[]; index: number }>({ node: [], index: -1 });
const onHighlightNode = (edges: number[]) => {
clearInterval(intervalRef.current || undefined);
let previousIndex: number;
if (highlightIterators.current.node.every((x, i) => edges[i] === x)) {
previousIndex = highlightIterators.current.index;
} else {
highlightIterators.current.node = edges;
previousIndex = -1;
}
let index = (previousIndex + 1) % edges.length;
// if edge under index is already highlighted, we move forward
if (highlightedEdge === edges[index]) {
index = (index + 1) % edges.length;
}
highlightIterators.current.index = index;
highlightEdge(edges[index]);
};
return (
<div
className={cn(
'relative flex min-h-[400px] flex-1 flex-col items-center',
props.className,
styles.container,
)}
>
<IconGradientDefs />
<div className={'flex flex-row ' + styles.vars}>
<Edge top bottom left highlighted={highlightedEdge === 5}>
<div
style={{
height:
'calc(var(--node-h) / 2 + var(--gap) + var(--big-node-h) / 2 - var(--label-h) / 2)',
}}
className="ml-[calc(1rem+1px-var(--bw))] mt-[calc(var(--node-h)/2)] w-10 rounded-tl-xl"
/>
<EdgeLabel onPointerOver={onPointerOverEdge}>5</EdgeLabel>
<div
style={{
height:
'calc(var(--node-h) / 2 + var(--gap) + var(--big-node-h) / 2 - var(--label-h) / 2)',
}}
className="ml-[calc(1rem+1px-var(--bw))] box-content w-10 rounded-bl-xl"
/>
</Edge>
<div>
<Node
title={
<>
<span className={styles.smHidden}>Hive</span> Gateway
</>
}
description="Gateway"
edges={[1, 4, 5]}
highlightedEdge={highlightedEdge}
onHighlight={onHighlightNode}
>
<HiveGatewayIcon className="size-12 [&>path]:fill-[url(#linear-blue)] [&>path]:stroke-[url(#linear-white)] [&>path]:stroke-[0.5px]" />
</Node>
<Edge
left
className="ml-[calc(var(--node-w)/2-var(--label-h)/2-4px)]"
highlighted={highlightedEdge === 4}
>
<div className="ml-[calc(var(--label-h)/2-.5px)] h-[calc((var(--gap)-var(--label-h))/2)]" />
<EdgeLabel onPointerOver={onPointerOverEdge}>4</EdgeLabel>
<div className="ml-[calc(var(--label-h)/2-.5px)] h-[calc((var(--gap)-var(--label-h))/2)]" />
</Edge>
<Node
className="h-[var(--big-node-h)] w-[var(--node-w)] flex-col text-center"
title="Hive"
description="Registry and CDN"
edges={[3, 4, 6]}
highlightedEdge={highlightedEdge}
onHighlight={onHighlightNode}
>
<HiveIcon className="size-[var(--big-logo-size)] [&>g]:fill-[url(#linear-blue)] [&>g]:stroke-[url(#linear-white)] [&>g]:stroke-[0.2px]" />
</Node>
<Edge
left
className="ml-[calc(var(--node-w)/2-var(--label-h)/2-4px)]"
highlighted={highlightedEdge === 6}
>
<div className="ml-[calc(var(--label-h)/2-.5px)] h-6" />
<EdgeLabel onPointerOver={onPointerOverEdge}>6</EdgeLabel>
<div className="ml-[calc(var(--label-h)/2-.5px)] h-6" />
</Edge>
<Node
title="Yoga"
description="GraphQL Subgraph"
edges={[5, 6]}
highlightedEdge={highlightedEdge}
onHighlight={onHighlightNode}
>
<YogaIcon className="size-12 [&>path]:fill-[url(#linear-blue)] [&>path]:stroke-[url(#linear-white)] [&>path]:stroke-[0.5px]" />
</Node>
</div>
<div>
<Edge
top
className="flex h-[var(--node-h)] flex-row items-center"
highlighted={highlightedEdge === 1}
>
<div className="w-[calc(var(--label-h)/1.6)]" />
<EdgeLabel onPointerOver={onPointerOverEdge}>1</EdgeLabel>
<div className="w-[calc(var(--label-h)/1.6)]" />
</Edge>
<div className="h-[var(--gap)]" />
<Edge
top
highlighted={highlightedEdge === 3}
className="flex h-[var(--big-node-h)] flex-row items-center"
>
<div className="w-[calc(var(--label-h)/1.6)]" />
<EdgeLabel onPointerOver={onPointerOverEdge}>3</EdgeLabel>
<div className="w-[calc(var(--label-h)/1.6)]" />
</Edge>
</div>
<div>
<Node
title="Client"
description={
<span className="[@media(max-width:1438px)]:hidden">GraphQL client of choice</span>
}
edges={[1, 2]}
highlightedEdge={highlightedEdge}
onHighlight={onHighlightNode}
>
<GraphQLLogo className="size-12" />
</Node>
<Edge
left
className="flex h-[calc(var(--gap)+var(--big-node-h)/2-var(--node-h)/2)] flex-col items-center"
highlighted={highlightedEdge === 2}
>
<div className="flex-1" />
<EdgeLabel onPointerOver={onPointerOverEdge}>2</EdgeLabel>
<div className="flex-1" />
</Edge>
<Node
title="Codegen"
description={
<span className="[@media(max-width:1438px)]:hidden">GraphQL Code Generation</span>
}
edges={[2, 3]}
highlightedEdge={highlightedEdge}
onHighlight={onHighlightNode}
>
<CodegenIcon className="size-12 fill-[url(#linear-blue)] stroke-[url(#linear-white)] stroke-[0.5px]" />
</Node>
</div>
</div>
<p className={cn('relative text-white/80', styles.text)}>
{/* We use the longest text to ensure we have enough space. */}
<span className="invisible">{longestEdgeText}</span>
{edgeTexts.map((text, i) => {
return (
<span
key={i}
className={cn(
'absolute inset-0',
// Makes it accessible by crawlers.
highlightedEdge !== null && highlightedEdge - 1 === i ? 'visible' : 'invisible',
)}
>
{text}
</span>
);
})}
</p>
</div>
);
}
interface EdgeProps extends React.HTMLAttributes<HTMLElement> {
highlighted: boolean;
top?: boolean;
left?: boolean;
bottom?: boolean;
}
function Edge({ highlighted, top, bottom, left, className, ...rest }: EdgeProps) {
return (
<div
style={{ '--bw': highlighted ? '2px' : '1px' }}
className={cn(
className,
'[&>*]:transition-colors [&>*]:duration-500 [&>:nth-child(odd)]:border-green-700',
top &&
(bottom
? '[&>:nth-child(1)]:border-t-[length:var(--bw)] [&>:nth-child(3)]:border-b-[length:var(--bw)]'
: '[&>:nth-child(odd)]:border-t-[length:var(--bw)]'),
left && '[&>:nth-child(odd)]:border-l-[length:var(--bw)]',
highlighted &&
'[&>*]:text-green-1000 [&>:nth-child(even)]:bg-green-300 [&>:nth-child(odd)]:border-green-300',
)}
{...rest}
/>
);
}
interface EdgeLabelProps extends React.HTMLAttributes<HTMLElement> {
onPointerOver: React.PointerEventHandler<HTMLElement>;
}
function EdgeLabel(props: EdgeLabelProps) {
return (
<div
className={
'flex size-8 h-[var(--label-h)] items-center justify-center' +
' cursor-default rounded bg-green-700 text-sm font-medium leading-5' +
' hover:ring-2 hover:ring-green-700'
}
{...props}
/>
);
}
interface NodeProps extends Omit<React.HTMLAttributes<HTMLElement>, 'title'> {
title: ReactNode;
description?: ReactNode;
edges: number[];
highlightedEdge: number | null;
onHighlight: (edges: number[]) => void;
}
function Node({
title,
description,
children,
edges,
highlightedEdge,
className,
onHighlight,
...rest
}: NodeProps) {
const highlighted = edges.includes(highlightedEdge!);
const hovered = useRef(false);
return (
<div
onPointerOver={event => {
if (hovered.current || event.currentTarget !== event.target) {
return;
}
hovered.current = true;
if (edges.includes(highlightedEdge!)) return;
onHighlight([edges[0]]);
}}
onPointerOut={event => {
if (
!event.currentTarget.contains(event.relatedTarget as Node) &&
event.currentTarget === event.target
) {
hovered.current = false;
}
}}
onClick={() => onHighlight(edges)}
className={cn(
styles.node,
'relative z-10 flex h-[var(--node-h)] items-center gap-2 rounded-2xl p-4 xl:gap-4 xl:p-[22px]' +
' bg-[linear-gradient(135deg,rgb(255_255_255/0.10),rgb(255_255_255/0.20))]' +
' cursor-pointer transition-colors duration-500 [&>svg]:flex-shrink-0',
// todo: linear gradients don't transition, so we should add white/10 background layer'
highlighted &&
'bg-[linear-gradient(135deg,rgb(255_255_255_/0.2),rgb(255_255_255/0.3))] ring ring-green-300',
className,
)}
{...rest}
>
{children}
<div>
<div className="font-medium text-green-100">{title}</div>
{description && (
<div
className="mt-0.5 text-sm leading-5 text-green-200"
style={{
display: 'var(--node-desc-display)',
}}
>
{description}
</div>
)}
</div>
</div>
);
}
/**
* This must be included in one of the SVGs here so they work nicely in Safari.
*/
function IconGradientDefs() {
return (
<svg className="absolute size-0">
<defs>
<linearGradient
id="linear-blue"
x1="0"
y1="0"
x2="100%"
y2="100%"
gradientUnits="objectBoundingBox"
>
<stop stopColor="#8CBEB3" />
<stop offset="1" stopColor="#68A8B6" />
</linearGradient>
<linearGradient
id="linear-white"
x1="0"
y1="0"
x2="100%"
y2="100%"
gradientUnits="objectBoundingBox"
>
<stop stopColor="white" stopOpacity="0.5" />
<stop offset="1" stopColor="white" stopOpacity="0.15" />
</linearGradient>
</defs>
</svg>
);
}
function GraphQLLogo(props: { className?: string }) {
return (
<svg className={props.className} viewBox="0 0 100 100">
<g fill="url(#linear-blue)">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M50 6.90308L87.323 28.4515V71.5484L50 93.0968L12.677 71.5484V28.4515L50 6.90308ZM16.8647 30.8693V62.5251L44.2795 15.0414L16.8647 30.8693ZM50 13.5086L18.3975 68.2457H81.6025L50 13.5086ZM77.4148 72.4334H22.5852L50 88.2613L77.4148 72.4334ZM83.1353 62.5251L55.7205 15.0414L83.1353 30.8693V62.5251Z"
/>
<circle cx="50" cy="9.3209" r="8.82" />
<circle cx="85.2292" cy="29.6605" r="8.82" />
<circle cx="85.2292" cy="70.3396" r="8.82" />
<circle cx="50" cy="90.6791" r="8.82" />
<circle cx="14.7659" cy="70.3396" r="8.82" />
<circle cx="14.7659" cy="29.6605" r="8.82" />
</g>
</svg>
);
}

View file

@ -1,6 +0,0 @@
---
title: The Ecosystem
description: Everything you need to scale your API infrastructure
---
export { default } from './page'

View file

@ -1,10 +1,11 @@
'use client';
import { useState } from 'react';
import Image, { StaticImageData } from 'next/image';
import NextLink from 'next/link';
import { ChevronDownIcon } from '@radix-ui/react-icons';
import * as Tabs from '@radix-ui/react-tabs';
import { CallToAction, Heading } from '@theguild/components';
import { cn } from '../lib';
import { CallToAction, cn, Heading } from '@theguild/components';
import { ArrowIcon } from './arrow-icon';
import auditImage from '../../public/features/gateway/audit.png';
import observabilityClientsImage from '../../public/features/observability/clients.webp';
@ -105,8 +106,8 @@ export function FeatureTabs({ className }: { className?: string }) {
return (
<section
className={cn(
'border-beige-400 isolate mx-auto w-[1200px] max-w-full rounded-3xl bg-white' +
' sm:max-w-[calc(100%-4rem)] sm:border md:p-6',
'border-beige-400 isolate mx-auto w-[1200px] max-w-full rounded-3xl bg-white',
'sm:max-w-[calc(100%-4rem)] sm:border md:p-6',
className,
)}
>
@ -119,19 +120,13 @@ export function FeatureTabs({ className }: { className?: string }) {
}}
value={currentTab}
>
<Tabs.List
className={
'sm:bg-beige-200 group relative z-10 mx-4 my-6 flex flex-col overflow-hidden focus-within:overflow-visible max-sm:h-[58px] max-sm:focus-within:pointer-events-none max-sm:focus-within:rounded-b-none max-sm:focus-within:has-[>:nth-child(2)[data-state="active"]]:translate-y-[-100%] max-sm:focus-within:has-[>:nth-child(3)[data-state="active"]]:translate-y-[-200%] sm:flex-row sm:rounded-2xl md:mx-0 md:mb-12 md:mt-0'
}
>
<Tabs.List className='sm:bg-beige-200 group relative z-10 mx-4 my-6 flex flex-col overflow-hidden focus-within:overflow-visible max-sm:h-[58px] max-sm:focus-within:pointer-events-none max-sm:focus-within:rounded-b-none max-sm:focus-within:has-[>:nth-child(2)[data-state="active"]]:translate-y-[-100%] max-sm:focus-within:has-[>:nth-child(3)[data-state="active"]]:translate-y-[-200%] sm:flex-row sm:rounded-2xl md:mx-0 md:mb-12 md:mt-0'>
{tabs.map((tab, i) => {
return (
<Tabs.Trigger
key={tab}
value={tab}
className={
'hive-focus rdx-state-active:text-green-1000 rdx-state-active:border-beige-600 rdx-state-active:bg-white max-sm:rdx-state-inactive:hidden group-focus-within:rdx-state-inactive:flex max-sm:bg-beige-200 max-sm:rdx-state-inactive:rounded-none max-sm:border-beige-600 max-sm:group-focus-within:rdx-state-inactive:border-y-beige-200 max-sm:group-focus-within:[&:last-child]:border-t-beige-200 max-sm:group-focus-within:[&[data-state="inactive"]:first-child]:border-t-beige-600 max-sm:group-focus-within:[&:nth-child(2)]:rdx-state-active:rounded-none max-sm:group-focus-within:[&:nth-child(2)]:rdx-state-active:border-y-beige-200 max-sm:group-focus-within:[[data-state="active"]+&:last-child]:border-b-beige-600 max-sm:group-focus-within:[[data-state="inactive"]+&:last-child[data-state="inactive"]]:border-b-beige-600 max-sm:group-focus-within:first:rdx-state-active:border-b-beige-200 max-sm:group-focus-within:first:rdx-state-active:rounded-b-none max-sm:rdx-state-inactive:pointer-events-none max-sm:rdx-state-inactive:group-focus-within:pointer-events-auto z-10 flex flex-1 items-center justify-center gap-2.5 rounded-lg border-transparent p-4 text-base font-medium leading-6 text-green-800 max-sm:border max-sm:group-focus-within:aria-selected:z-20 max-sm:group-focus-within:aria-selected:ring-4 sm:rounded-[15px] sm:border sm:text-xs sm:max-lg:p-3 sm:max-[721px]:p-2 md:text-sm lg:text-base max-sm:group-focus-within:[&:nth-child(3)]:rounded-t-none [&>svg]:shrink-0 max-sm:group-focus-within:[&[data-state="inactive"]:first-child]:rounded-t-lg [&[data-state="inactive"]>:last-child]:invisible max-sm:group-focus-within:[[data-state="active"]+&:last-child]:rounded-b-lg max-sm:group-focus-within:[[data-state="inactive"]+&:last-child[data-state="inactive"]]:rounded-b-lg'
}
className='hive-focus rdx-state-active:text-green-1000 rdx-state-active:border-beige-600 rdx-state-active:bg-white max-sm:rdx-state-inactive:hidden group-focus-within:rdx-state-inactive:flex max-sm:bg-beige-200 max-sm:rdx-state-inactive:rounded-none max-sm:border-beige-600 max-sm:group-focus-within:rdx-state-inactive:border-y-beige-200 max-sm:group-focus-within:[&:last-child]:border-t-beige-200 max-sm:group-focus-within:[&[data-state="inactive"]:first-child]:border-t-beige-600 max-sm:group-focus-within:[&:nth-child(2)]:rdx-state-active:rounded-none max-sm:group-focus-within:[&:nth-child(2)]:rdx-state-active:border-y-beige-200 max-sm:group-focus-within:[[data-state="active"]+&:last-child]:border-b-beige-600 max-sm:group-focus-within:[[data-state="inactive"]+&:last-child[data-state="inactive"]]:border-b-beige-600 max-sm:group-focus-within:first:rdx-state-active:border-b-beige-200 max-sm:group-focus-within:first:rdx-state-active:rounded-b-none max-sm:rdx-state-inactive:pointer-events-none max-sm:rdx-state-inactive:group-focus-within:pointer-events-auto z-10 flex flex-1 items-center justify-center gap-2.5 rounded-lg border-transparent p-4 text-base font-medium leading-6 text-green-800 max-sm:border max-sm:group-focus-within:aria-selected:z-20 max-sm:group-focus-within:aria-selected:ring-4 sm:rounded-[15px] sm:border sm:text-xs sm:max-lg:p-3 sm:max-[721px]:p-2 md:text-sm lg:text-base max-sm:group-focus-within:[&:nth-child(3)]:rounded-t-none [&>svg]:shrink-0 max-sm:group-focus-within:[&[data-state="inactive"]:first-child]:rounded-t-lg [&[data-state="inactive"]>:last-child]:invisible max-sm:group-focus-within:[[data-state="active"]+&:last-child]:rounded-b-lg max-sm:group-focus-within:[[data-state="inactive"]+&:last-child[data-state="inactive"]]:rounded-b-lg'
>
{icons[i]}
{tab}

View file

@ -1,8 +1,8 @@
import { Children, ComponentPropsWithoutRef } from 'react';
import { Children, ComponentPropsWithoutRef, ReactElement, ReactNode } from 'react';
import * as RadixAccordion from '@radix-ui/react-accordion';
import { ChevronDownIcon } from '@radix-ui/react-icons';
import { Anchor, Heading } from '@theguild/components';
import { cn, usePageFAQSchema } from '../../lib';
import { Anchor, cn, Heading } from '@theguild/components';
import { AttachPageFAQSchema } from '../../lib';
import FederationQuestions from './federation-questions.mdx';
import HomeQuestions from './home-questions.mdx';
import PartnersQuestions from './partners-questions.mdx';
@ -21,20 +21,16 @@ const h2 = (props: ComponentPropsWithoutRef<'h2'>) => (
<Heading as="h2" size="md" className="basis-1/2" {...props} />
);
const UnwrapChild = (props: { children?: ReactNode }) => props.children as unknown as ReactElement;
const Accordion = (props: ComponentPropsWithoutRef<'ul'>) => (
<RadixAccordion.Root asChild type="single" collapsible>
<ul className="basis-1/2 divide-y max-xl:grow" {...props} />
<ul className="divide-beige-400 basis-1/2 divide-y max-xl:grow" {...props} />
</RadixAccordion.Root>
);
const AccordionItem = (props: ComponentPropsWithoutRef<'li'>) => {
const texts = Children.toArray(props.children)
.map(child =>
typeof child === 'object' && 'type' in child && child.type === 'p'
? (child.props.children as string)
: null,
)
.filter(Boolean);
const texts = Children.toArray(props.children).filter(child => child !== '\n');
if (texts.length === 0) {
return null;
@ -45,7 +41,14 @@ const AccordionItem = (props: ComponentPropsWithoutRef<'li'>) => {
throw new Error(`Expected a question and an answer, got ${texts.length} items`);
}
const [question, ...answers] = texts;
const [first, ...answers] = texts;
const question =
typeof first === 'string'
? first
: typeof first === 'object' && 'type' in first
? first.props.children
: null;
if (!question) return null;
@ -83,25 +86,25 @@ const AccordionItem = (props: ComponentPropsWithoutRef<'li'>) => {
);
};
const components = {
a,
h2,
ul: Accordion,
li: AccordionItem,
};
export function FrequentlyAskedQuestions({ className }: { className?: string }) {
usePageFAQSchema();
return (
<>
<AttachPageFAQSchema />
<section
className={cn(
className,
'text-green-1000 flex flex-col gap-x-6 gap-y-2 px-4 py-6 md:flex-row md:px-10 lg:gap-x-24 lg:px-[120px] lg:py-24',
)}
>
<HomeQuestions components={components} />
<HomeQuestions
components={{
a,
h2,
p: UnwrapChild,
ul: Accordion,
li: AccordionItem,
}}
/>
</section>
</>
);
@ -112,13 +115,7 @@ const federationUL = (props: ComponentPropsWithoutRef<'ul'>) => {
};
const federationLI = (props: ComponentPropsWithoutRef<'li'>) => {
const texts = Children.toArray(props.children)
.map(child =>
typeof child === 'object' && 'type' in child && child.type === 'p'
? (child.props.children as string)
: null,
)
.filter(Boolean);
const texts = Children.toArray(props.children).filter(child => child !== '\n');
if (texts.length === 0) {
return null;
@ -154,25 +151,25 @@ const federationLI = (props: ComponentPropsWithoutRef<'li'>) => {
);
};
const federationComponents = {
a,
h2,
ul: federationUL,
li: federationLI,
};
export function FrequentlyAskedFederationQuestions({ className }: { className?: string }) {
usePageFAQSchema();
return (
<>
<AttachPageFAQSchema />
<section
className={cn(
className,
'text-green-1000 flex flex-col gap-8 px-4 py-6 md:px-14 lg:flex-row lg:px-[120px] lg:py-24',
)}
>
<FederationQuestions components={federationComponents} />
<FederationQuestions
components={{
a,
h2,
p: UnwrapChild,
ul: federationUL,
li: federationLI,
}}
/>
</section>
</>
);
@ -190,6 +187,7 @@ export function FrequentlyAskedPartnersQuestions({ className }: { className?: st
components={{
a,
h2,
p: UnwrapChild,
ul: Accordion,
li: AccordionItem,
}}

View file

@ -1,5 +1,4 @@
import { ContactButton, DecorationIsolation, Heading } from '@theguild/components';
import { cn } from '../lib';
import { cn, ContactButton, DecorationIsolation, Heading } from '@theguild/components';
export function GotAnIdeaSection({ className }: { className?: string }) {
return (
@ -113,7 +112,7 @@ export function GotAnIdeaSection({ className }: { className?: string }) {
<Heading as="h2" size="md" className="text-white">
Got an idea for a new library?
</Heading>
<p className="mt-4 text-white/80">
<p className="mb-8 mt-4 text-white/80">
Join our community to chat with us and let's build something together!
</p>
<ContactButton variant="primary-inverted" className="mt-8">

View file

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

View file

@ -2,10 +2,10 @@ import { ReactNode } from 'react';
import {
ArchDecoration,
ArchDecorationGradientDefs,
cn,
DecorationIsolation,
HighlightDecoration,
} from '@theguild/components';
import { cn } from '../lib';
export function Hero(props: { children: ReactNode; className?: string }) {
return (

View file

@ -1,5 +1,5 @@
import { ReactNode } from 'react';
import { cn } from '../lib';
import { cn } from '@theguild/components';
import { Stud } from './stud';
export interface InfoCardProps extends React.HTMLAttributes<HTMLElement> {

View file

@ -0,0 +1,15 @@
import { ReactNode } from 'react';
import * as Tooltip from '@radix-ui/react-tooltip';
import { cn, CookiesConsent } from '@theguild/components';
/**
* Adds styles, cookie consent banner and Radix Tooltip provider.
*/
export function LandingPageContainer(props: { children: ReactNode; className?: string }) {
return (
<Tooltip.Provider>
<div className={cn('flex h-full flex-col', props.className)}>{props.children}</div>
<CookiesConsent />
</Tooltip.Provider>
);
}

View file

@ -1,3 +1,5 @@
'use client';
import { useEffect, useLayoutEffect, useReducer, useRef, useState } from 'react';
import { useMounted } from '@theguild/components';

View file

@ -1,27 +0,0 @@
import { ComponentPropsWithoutRef } from 'react';
import { useRouter } from 'next/router';
import { GraphQLConfCard, HiveNavigation, PRODUCTS, type Navbar } from '@theguild/components';
import graphQLConfLocalImage from './graphql-conf-image.webp';
export function NavigationMenu(props: ComponentPropsWithoutRef<typeof Navbar>) {
const { route } = useRouter();
return (
<HiveNavigation
className={isLandingPage(route) ? 'light max-w-[1392px]' : 'max-w-[90rem]'}
companyMenuChildren={<GraphQLConfCard image={graphQLConfLocalImage} />}
productName={PRODUCTS.HIVE.name}
{...props}
/>
);
}
const landingLikePages = [
'/',
'/pricing',
'/federation',
'/oss-friends',
'/ecosystem',
'/partners',
];
export const isLandingPage = (route: string) => landingLikePages.includes(route);

View file

@ -1,16 +0,0 @@
#__next {
--nextra-navbar-height: 90px;
}
@media (min-width: 768px) {
.nextra-sidebar-container.nextra-sidebar-container,
.nextra-toc > .nextra-scrollbar {
top: var(--nextra-navbar-height);
}
}
@media (max-width: 767px) {
#__next {
--nextra-navbar-height: 64px;
}
}

View file

@ -1,37 +0,0 @@
import { ReactNode } from 'react';
import * as Tooltip from '@radix-ui/react-tooltip';
import { CookiesConsent, useMounted } from '@theguild/components';
import { cn, useTheme } from '../lib';
export function Page(props: { children: ReactNode; className?: string }) {
const mounted = useMounted();
useTheme();
return (
<Tooltip.Provider>
<style global jsx>
{`
html {
scroll-behavior: smooth;
color-scheme: light !important;
}
body {
background: #fff;
--nextra-primary-hue: 191deg;
--nextra-primary-saturation: 40%;
--nextra-bg: 255, 255, 255;
}
.nextra-sidebar-footer {
display: none;
}
`}
</style>
<div className={cn('flex h-full flex-col', props.className)}>{props.children}</div>
{mounted && <CookiesConsent />}
{/* position Crisp button below the cookies banner */}
<style jsx global>
{' #crisp-chatbox { z-index: 40 !important; '}
</style>
</Tooltip.Provider>
);
}

View file

@ -1,7 +1,9 @@
import { HTMLAttributes, ReactElement, ReactNode, useState } from 'react';
'use client';
import { HTMLAttributes, ReactElement, ReactNode } from 'react';
import { Arrow, Content, Root, Trigger } from '@radix-ui/react-tooltip';
import { CallToAction, cn, ContactTextLink } from '@theguild/components';
import { Slider } from './slider';
import { CallToAction, cn } from '@theguild/components';
import { PricingSlider } from './pricing-slider';
function Tooltip({ content, children }: { content: string; children: ReactNode }) {
return (
@ -200,42 +202,3 @@ export function Pricing({
</section>
);
}
function PricingSlider({ className, ...rest }: { className?: string }) {
const min = 1;
const max = 300;
const [millionsOfOperations, setMillionsOfOperations] = useState(min);
return (
<label className={cn(className, 'block')} {...rest}>
<div className="text-green-1000 font-medium">Expected monthly operations?</div>
<div className="text-green-1000 flex items-center gap-2 pt-12 text-sm">
<span className="font-medium">{min}M</span>
<Slider
min={min}
max={max}
defaultValue={min}
step={1}
// 10$ base price + 10$ per 1M
style={{ '--ops': min, '--price': 'calc(10 + var(--ops) * 10)' }}
counter="after:content-[''_counter(ops)_'M_operations,_$'_counter(price)_'_/_month'] after:[counter-set:ops_calc(var(--ops))_price_calc(var(--price))]"
onChange={event => {
const value = event.currentTarget.valueAsNumber;
setMillionsOfOperations(value);
event.currentTarget.parentElement!.style.setProperty('--ops', `${value}`);
}}
/>
<span className="font-medium">{max}M</span>
</div>
<p
className="mt-4 rounded-xl bg-green-100 p-3 transition"
style={{ opacity: millionsOfOperations >= max * 0.95 ? 1 : 0 }}
>
<span className="font-medium">Running {max}M+ operations?</span>
<br />
<ContactTextLink>Talk to us</ContactTextLink>
</p>
</label>
);
}

Some files were not shown because too many files have changed in this diff Show more