Hive Docs x Nextra 4 (#6089)
Co-authored-by: Dimitri POSTOLOV <dmytropostolov@gmail.com>
|
|
@ -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",
|
||||
|
|
|
|||
1
packages/web/docs/.gitignore
vendored
|
|
@ -3,3 +3,4 @@ build
|
|||
temp
|
||||
public/sitemap.xml
|
||||
public/changelog.json
|
||||
public/_pagefind/
|
||||
|
|
|
|||
1
packages/web/docs/mdx-components.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { useHiveMDXComponents as useMDXComponents } from '@theguild/components/server';
|
||||
3
packages/web/docs/next-env.d.ts
vendored
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 |
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
36
packages/web/docs/src/app/docs/[[...mdxPath]]/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
63
packages/web/docs/src/app/dynamic-meta-tags.tsx
Normal 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),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -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]">
|
||||
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 96 KiB |
|
|
@ -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"
|
||||
|
|
@ -24,3 +24,6 @@ export async function GET() {
|
|||
},
|
||||
});
|
||||
}
|
||||
|
||||
export const dynamic = 'force-static';
|
||||
export const config = { runtime: 'edge' };
|
||||
|
|
|
|||
13
packages/web/docs/src/app/icon.svg
Normal 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 |
108
packages/web/docs/src/app/layout.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
19
packages/web/docs/src/app/narrow-pages.tsx
Normal 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;
|
||||
}
|
||||
9
packages/web/docs/src/app/not-found.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 118 KiB |
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -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
|
||||
|
|
@ -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!
|
||||
|
|
@ -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.
|
||||
|
|
@ -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"
|
||||
|
Before Width: | Height: | Size: 292 KiB After Width: | Height: | Size: 292 KiB |
|
|
@ -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
|
||||
|
||||
|
|
@ -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"
|
||||
|
|
@ -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.
|
||||
|
||||

|
||||

|
||||
|
||||
Check out [the documentation on Preflight Scripts](/docs/dashboard/laboratory/preflight-scripts) for
|
||||
information on how to configure, edit, and use them.
|
||||
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB |
19
packages/web/docs/src/app/product-updates/(posts)/layout.tsx
Normal 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;
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
15
packages/web/docs/src/app/product-updates/page.mdx
Normal 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 />
|
||||
|
|
@ -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} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
20
packages/web/docs/src/components/configured-giscus.tsx
Normal 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"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
---
|
||||
title: The Ecosystem
|
||||
description: Everything you need to scale your API infrastructure
|
||||
---
|
||||
|
||||
export { default } from './page'
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
15
packages/web/docs/src/components/landing-page-container.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
'use client';
|
||||
|
||||
import { useEffect, useLayoutEffect, useReducer, useRef, useState } from 'react';
|
||||
import { useMounted } from '@theguild/components';
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||