Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 89 additions & 42 deletions studio/src/components/auth/auth-components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
MagnifyingGlassIcon,
RocketLaunchIcon,
} from "@heroicons/react/24/outline";
import { getSignupContent, type SignupVariant } from "@/lib/signup-content";

/**
* Auth Card - The card container for auth forms
Expand Down Expand Up @@ -129,23 +130,37 @@ export const TrustedCompanies = () => {
/**
* Marketing Header - Title and description for the right side
*/
export const MarketingHeader = () => {
export const MarketingHeader = ({
title,
description,
}: {
title?: string;
description?: string;
}) => {
const defaultTitle = "Cosmo: Open-Source\nGraphQL Federation Solution";
const defaultDescription =
"Unify distributed APIs into one federated graph. Platform teams get observability and control. Service teams ship independently.";

const displayTitle = title || defaultTitle;
const displayDescription = description || defaultDescription;

return (
<div className="text-center">
<h1 className="bg-[linear-gradient(180deg,#FFFFFF_50%,#999999_100%)] bg-clip-text text-2xl font-bold leading-[130%] text-transparent sm:text-[32px]">
Cosmo: Open-Source
<br />
GraphQL Federation Solution
{displayTitle.split("\n").map((line, index) => (
<span key={index}>
{line}
{index < displayTitle.split("\n").length - 1 && <br />}
</span>
))}
</h1>
<p className="mx-auto mt-4 max-w-md text-sm text-white/85">
Unify distributed APIs into one federated graph. Platform teams get observability and control. Service teams ship independently.
</p>
<p className="mx-auto mt-4 max-w-md text-sm text-white/85">{displayDescription}</p>
</div>
);
};

/**
* Feature Item - Individual feature with icon box
* Feature Item - Individual feature with icon tile (glossy, border highlight)
*/
const FeatureItem = ({
icon,
Expand All @@ -156,9 +171,25 @@ const FeatureItem = ({
title: string;
description: string;
}) => (
<div className="flex gap-4">
<div className="flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-lg bg-white/10">
{icon}
<div className="flex items-center gap-6">
<div
className="feature-icon-tile relative flex h-16 w-16 flex-shrink-0 items-center justify-center overflow-hidden rounded-lg border border-white/25 bg-black/50"
style={{
boxShadow:
"inset 0 0 0 1px rgba(255,255,255,0.08), inset 0 1px 0 rgba(255,255,255,0.06)",
}}
>
{/* Glossy highlight (top edge) */}
<div
className="pointer-events-none absolute inset-0 rounded-lg"
style={{
background:
"linear-gradient(165deg, rgba(255,255,255,0.12) 0%, rgba(255,255,255,0.04) 25%, transparent 50%)",
}}
/>
{/* Border highlight (static tilt) */}
<div className="feature-icon-tile-border-highlight pointer-events-none absolute inset-0 rounded-lg" />
<span className="relative z-[1]">{icon}</span>
</div>
<div>
<h4 className="text-lg font-semibold text-white">{title}</h4>
Expand All @@ -170,60 +201,76 @@ const FeatureItem = ({
/**
* Product Cosmo Stack - Marketing content with features list
*/
export const ProductCosmoStack = ({ variant = "login" }: { variant?: "login" | "signup" }) => {
export const ProductCosmoStack = ({
variant = "login",
signupVariant,
}: {
variant?: "login" | "signup";
signupVariant?: "default" | "apollo";
}) => {
// Icon mapping function (larger icons for feature tiles)
const getIcon = (iconName: string) => {
const iconClass = "h-8 w-8 text-purple-400";
switch (iconName) {
case "bolt":
return <BoltIcon className={iconClass} />;
case "code-bracket":
return <CodeBracketIcon className={iconClass} />;
case "shield-check":
return <ShieldCheckIcon className={iconClass} />;
case "share":
return <ShareIcon className={iconClass} />;
case "magnifying-glass":
return <MagnifyingGlassIcon className={iconClass} />;
case "rocket-launch":
return <RocketLaunchIcon className={iconClass} />;
default:
return <BoltIcon className={iconClass} />;
}
};

const loginFeatures = [
{
icon: <BoltIcon className="h-6 w-6 text-purple-500" />,
icon: <BoltIcon className="h-8 w-8 text-purple-400" />,
title: "Real time subscriptions without new infrastructure",
description:
"Cosmo Streams turns existing event streams into GraphQL subscriptions by handling authorization, filtering, and fan out in the Cosmo Router, keeping subgraphs stateless and avoiding a separate service.",
},
{
icon: <CodeBracketIcon className="h-6 w-6 text-purple-500" />,
icon: <CodeBracketIcon className="h-8 w-8 text-purple-400" />,
title: "Extend the router with TypeScript",
description:
"With TypeScript plugin support in Cosmo Connect, you can extend the Cosmo Router using TypeScript and run custom logic directly inside the router, without deploying separate services.",
},
{
icon: <ShieldCheckIcon className="h-6 w-6 text-purple-500" />,
icon: <ShieldCheckIcon className="h-8 w-8 text-purple-400" />,
title: "Enforce custom schema rules before deploy",
description:
"With Subgraph Check Extensions, you can run your own validation logic as part of Cosmo's subgraph checks, enforcing custom schema rules before changes are deployed.",
},
];

const signupFeatures = [
{
icon: <ShareIcon className="h-6 w-6 text-purple-500" />,
title: "Federate Any API, Not Just GraphQL",
description:
"Connect REST, gRPC, and GraphQL services without rewrites. Cosmo Connect wraps existing APIs into your graph without forcing migrations.",
},
{
icon: <MagnifyingGlassIcon className="h-6 w-6 text-purple-500" />,
title: "Track Every Query Across Your Entire Graph",
description:
"Native OpenTelemetry tracing from gateway to subgraph. Find slow queries and failing services in seconds with zero instrumentation required.",
},
{
icon: <ShieldCheckIcon className="h-6 w-6 text-purple-500" />,
title: "Catch Breaking Changes Before Deployment",
description:
"Schema checks run automatically in CI/CD. Service teams ship on their own schedule, while platform teams prevent breaking changes from reaching production.",
},
{
icon: <RocketLaunchIcon className="h-6 w-6 text-purple-500" />,
title: "Built for Scale and Performance",
description:
"Go router with sub-millisecond overhead. Deploy with built-in caching, rate limiting, and security controls wherever your infrastructure lives.",
},
];
// Signup content always from content map (single source of truth)
let marketingTitle: string | undefined;
let marketingDescription: string | undefined;
const signupContent =
variant === "signup" ? getSignupContent(signupVariant ?? "default") : null;
if (signupContent) {
marketingTitle = signupContent.marketingTitle;
marketingDescription = signupContent.marketingDescription;
}
const signupFeatures =
signupContent?.features.map((feature) => ({
icon: getIcon(feature.icon),
title: feature.title,
description: feature.description,
})) ?? [];

const features = variant === "login" ? loginFeatures : signupFeatures;

return (
<div className="flex w-full flex-col px-2 sm:max-w-[43.2rem] sm:px-8">
<MarketingHeader />
<MarketingHeader title={marketingTitle} description={marketingDescription} />
<div className="mt-10 flex flex-col gap-6">
{features.map((feature, index) => (
<FeatureItem
Expand Down
110 changes: 110 additions & 0 deletions studio/src/lib/signup-content.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/**
* Signup page content configuration
* Defines different content variants for the signup page based on URL parameters
*/

export interface MarketingFeature {
icon: string; // Icon name/key to identify which icon to use
title: string;
description: string;
}

export interface SignupContent {
heading: string;
description: string;
marketingTitle: string;
marketingDescription: string;
features: MarketingFeature[];
}

export type SignupVariant = "default" | "apollo";

const signupContentMap: Record<SignupVariant, SignupContent> = {
default: {
heading: "Sign up for free",
description: "Try Cosmo as Managed Service. No card required.",
marketingTitle: "Cosmo: Open-Source\nGraphQL Federation Solution",
marketingDescription:
"Unify distributed APIs into one federated graph. Platform teams get observability and control. Service teams ship independently.",
features: [
{
icon: "share",
title: "Federate Any API, Not Just GraphQL",
description:
"Connect REST, gRPC, and GraphQL services without rewrites. Cosmo Connect wraps existing APIs into your graph without forcing migrations.",
},
{
icon: "magnifying-glass",
title: "Track Every Query Across Your Entire Graph",
description:
"Native OpenTelemetry tracing from gateway to subgraph. Find slow queries and failing services in seconds with zero instrumentation required.",
},
{
icon: "shield-check",
title: "Catch Breaking Changes Before Deployment",
description:
"Schema checks run automatically in CI/CD. Service teams ship on their own schedule, while platform teams prevent breaking changes from reaching production.",
},
{
icon: "rocket-launch",
title: "Built for Scale and Performance",
description:
"Go router with sub-millisecond overhead. Deploy with built-in caching, rate limiting, and security controls wherever your infrastructure lives.",
},
],
},
apollo: {
heading: "Sign up for free",
description: "Try Cosmo managed and migrate from Apollo GraphOS in minutes. No credit card required.",
marketingTitle: "Migrate to Cosmo\nfrom Apollo GraphOS",
marketingDescription:
"Escape the vendor lock. 100% open source GraphQL Federation with full control. A drop-in GraphOS replacement.",
features: [
{
icon: "code-bracket",
title: "Schema design and governance as it should be",
description:
"Get linting, breaking-change detection, schema contracts, and PR-based checks from day one. Cosmo doesn't gate governance behind tiers.",
},
{
icon: "rocket-launch",
title: "Cosmo delivers value, not traffic bills",
description:
"Enjoy predictable, transparent pricing for what drives value in your organization, as well as world-class support.",
},
{
icon: "share",
title: "Connect legacy services without a proprietary lock-in",
description:
"Wrap REST, gRPC, SOAP and other existing APIs into your supergraph without rewriting backends. No schema changes required.",
},
{
icon: "bolt",
title: "Build real-time event-driven subscriptions that scale",
description:
"Turn Kafka, NATS, or Redis into GraphQL subscriptions. Subgraphs stay stateless. Scale to tens of thousands of clients effortlessly.",
},
],
},
};

/**
* Get signup content based on the variant
* @param variant - The signup variant (from URL parameter)
* @returns The content configuration for the variant
*/
export const getSignupContent = (variant: SignupVariant = "default"): SignupContent => {
return signupContentMap[variant] || signupContentMap.default;
};

/**
* Parse the 'uc' (use case) parameter from URL query
* @param ucParam - The 'uc' query parameter value
* @returns The signup variant or 'default' if not recognized
*/
export const parseSignupVariant = (ucParam?: string): SignupVariant => {
if (ucParam?.toLowerCase() === "apollo") {
return "apollo";
}
return "default";
};
22 changes: 17 additions & 5 deletions studio/src/pages/signup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@ import Link from "next/link";
import { useRouter } from "next/router";
import { FaGoogle } from "react-icons/fa";
import { z } from "zod";
import { getSignupContent, parseSignupVariant } from "@/lib/signup-content";

const signupUrl = `${process.env.NEXT_PUBLIC_COSMO_CP_URL}/v1/auth/signup`;

const querySchema = z.object({
redirectURL: z.string().url().optional(),
uc: z.string().optional(),
});

const constructSignupURL = ({
Expand All @@ -37,10 +39,20 @@ const constructSignupURL = ({
return signupUrl + (queryString.length ? "?" + queryString : "");
};

function getUcFromUrl(): string | undefined {
if (typeof window === "undefined") return undefined;
return new URLSearchParams(window.location.search).get("uc") ?? undefined;
}

const SignupPage: NextPageWithLayout = () => {
const router = useRouter();

const { redirectURL } = querySchema.parse(router.query);
// Parse query safely so invalid params (e.g. redirectURL from OAuth) don't crash the page
const parseResult = querySchema.safeParse(router.query);
const query = parseResult.success ? parseResult.data : { redirectURL: undefined, uc: undefined };
const uc = router.isReady ? query.uc : getUcFromUrl() ?? query.uc;
const variant = parseSignupVariant(uc);
const content = getSignupContent(variant);
const redirectURL = query.redirectURL;

return (
<div className="flex min-h-full flex-col">
Expand All @@ -50,7 +62,7 @@ const SignupPage: NextPageWithLayout = () => {
{/* Left section - Marketing */}
<div className="flex w-full flex-col items-center justify-center px-4 py-10 lg:min-h-screen lg:w-1/2 lg:items-start lg:px-14 lg:pb-40 lg:pt-2">
<div className="lg:mt-8">
<ProductCosmoStack variant="signup" />
<ProductCosmoStack variant="signup" signupVariant={variant} />
</div>
</div>

Expand All @@ -64,10 +76,10 @@ const SignupPage: NextPageWithLayout = () => {

<div className="mt-8 lg:mt-12">
<h2 className="text-center text-2xl font-normal leading-[120%] text-white lg:text-[32px]">
Sign up for free
{content.heading}
</h2>
<p className="mt-2 text-center text-sm text-white/85 lg:text-base">
Try Cosmo as Managed Service. No card required.
{content.description}
</p>

<div className="mt-6 space-y-3 lg:mt-8 lg:space-y-4">
Expand Down
Loading
Loading