diff --git a/CHANGELOG.md b/CHANGELOG.md index d4a3942..e555dc5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,73 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added — web / marketing page v2 — 2026-04-19 + +- **New marketing page at `/`.** Terminal-hero with `pnpm dlx create-helpbase` + typing into a shadcn-framed Terminal (via `@magicui/terminal`), click-to-copy + primary CTA, `See the live demo →` secondary CTA. Announces OSS + live today. +- **Dual-front comparator.** A vertical table compares Helpbase against two + real status-quos side by side: rolling your own Next.js help center vs. a + hosted docs SaaS. Checks the ownership, MCP, llms.txt, cost, and "maintained + without you" rows. +- **How-it-works strip.** Scaffold → Preview → Deploy, 3 steps with + illustrations, mapped to the real CLI flow. +- **AI-native bento.** Five-cell grid explaining the MCP server, llms.txt, + structured agent output, `helpbase sync` (code-grounded doc diffs), and the + hosted tier escape hatch. +- **Demo cross-link.** A section that points visitors at `demo.helpbase.dev` + with a `curl /llms.txt` teaser of the structured agent output. +- **Pricing — open-core, three tiers.** Self-host (free, MIT), Hosted free + (one site on `{slug}.helpbase.dev`, hosted MCP included), and Hosted Pro + (coming soon — custom domain, team roles, analytics, priority support). +- **Grouped FAQ.** General / Hosted tier / MCP & AI, 9 questions covering OSS + vs hosted, ownership, MCP, and migration. +- **Real footer.** GitHub, Docs, Pricing, Privacy, newsletter form. Named the + built-on stack in the copyright line. + +### Added — web / analytics + +- **Supabase-native page analytics.** New `public.marketing_events` table + (insert-only for anon via RLS, service-role read only). New `track` edge + function validates an event allowlist, caps metadata at 2 KB, and derives a + session hash as `sha256(ip|ua|yyyy-mm-dd)` without storing raw PII. +- **Client helper `apps/web/lib/analytics.ts`.** `track(event, metadata)` uses + `keepalive: true` fetch and swallows every failure so analytics can never + break the page. + +### Added — `@workspace/ui` + +- **`CopyButton` primitive.** Wraps the button with `navigator.clipboard` + plus an `execCommand("copy")` fallback for older Safari / non-secure + contexts. Exposes an `onCopy` callback and a `data-copy-state` attribute + (`idle` / `copied` / `error`) for consumers. + +### Changed — web / header + +- **Killed placeholder nav.** The shadcn template left behind ten fake links + (`Automation`, `Scalability`, `Marketplace`, `Guides`, `Partnerships`, etc.) + and a `Continue` button pointing nowhere. Replaced with real helpbase nav: + Docs, Pricing (anchor), GitHub. Right-side: `Sign in` + `Deploy now`. + +### Security + +- **`cf-connecting-ip` precedence in edge analytics.** The `track` function + now prefers Cloudflare's authoritative IP header over the client-spoofable + `x-forwarded-for`, closing a trivial session-hash forgery. +- **Scoped Cloudinary allowlist in `next.config.mjs`.** Added pathname + constraint `/dohqjvu9k/**` so arbitrary Cloudinary content cannot be + proxied through `/_next/image`. + +### For contributors + +- **Shadcn add, from `apps/web`.** Registry lives in + `apps/web/components.json`, not the repo root. Document in your workflow: + `cd apps/web && pnpm dlx shadcn@latest add @tailark-pro/...`. +- **Test coverage + 26.** Net-new tests: `apps/web/test/analytics.test.ts` + (7), `apps/web/test/copy-button.test.tsx` (8), and + `apps/web/test/track-edge-handler.test.ts` (12). Handler refactored out of + the Deno `Deno.serve` entry so vitest can exercise it directly. + ## [create-helpbase 0.5.0] — 2026-04-19 ### Changed diff --git a/apps/web/app/(main)/docs/page.tsx b/apps/web/app/(main)/docs/page.tsx index dbc3552..59ecc27 100644 --- a/apps/web/app/(main)/docs/page.tsx +++ b/apps/web/app/(main)/docs/page.tsx @@ -73,10 +73,10 @@ export default async function HomePage() {

- The AI-native knowledge layer, as code you own. + The docs your AI tools can read.

- Helpbase ships MCP, llms.txt, and codebase-grounded doc sync as primitives that run in your repo. Open source, self-hostable, built on shadcn/ui + Next.js. + Helpbase includes an MCP server, an llms.txt, and a doc-sync tool that reads your source code. Open source, self-hostable, built on shadcn/ui + Next.js.

{/* Search bar in hero */} diff --git a/apps/web/app/(marketing)/page.tsx b/apps/web/app/(marketing)/page.tsx index 0ad1c53..d8ee01e 100644 --- a/apps/web/app/(marketing)/page.tsx +++ b/apps/web/app/(marketing)/page.tsx @@ -1,5 +1,42 @@ -import HeroSection from "@/components/hero-section" +import dynamic from "next/dynamic" + +import { Header } from "@/components/header" +import { Hero } from "@/components/marketing/hero" +import Comparator from "@/components/comparator-7" +import FooterSection from "@/components/footer" + +// Below-fold sections load via next/dynamic so their JS code-splits out +// of the first-paint bundle. SSR stays on — the HTML ships pre-rendered; +// only client hydration defers. +const HowItWorks = dynamic(() => import("@/components/how-it-works-3")) +const FeaturesOwnIt = dynamic(() => import("@/components/features-1")) +const AiNativeBento = dynamic(() => import("@/components/bento-2")) +const DemoCrossLink = dynamic(() => + import("@/components/marketing/demo-cross-link").then((m) => ({ + default: m.DemoCrossLink, + })), +) +const Pricing = dynamic(() => import("@/components/pricing")) +const FAQs = dynamic(() => import("@/components/faqs-3")) export default function LandingPage() { - return + return ( + <> +
+
+ + + + + + + + +
+ + + ) } diff --git a/apps/web/components.json b/apps/web/components.json index ca9a735..5a5b823 100644 --- a/apps/web/components.json +++ b/apps/web/components.json @@ -26,6 +26,7 @@ "headers": { "Authorization": "Bearer ${TAILARK_API_KEY}" } - } + }, + "@magicui": "https://magicui.design/r/{name}.json" } } diff --git a/apps/web/components/bento-2.tsx b/apps/web/components/bento-2.tsx new file mode 100644 index 0000000..6479dbd --- /dev/null +++ b/apps/web/components/bento-2.tsx @@ -0,0 +1,109 @@ +import { AgentTasksIllustration } from "@/components/illustrations/agent-tasks" +import { ModelsIllustration } from "@/components/illustrations/models" +import { Card } from '@/components/ui/card' +import { UptimeIllustration } from "@/components/illustrations/uptime" +import { DocumentAnalysisIllustration } from "@/components/illustrations/document-analysis" + +export default function AiNativeBento() { + return ( +
+
+
+

+ Claude and Cursor can read your docs. Day one. +

+

+ Every helpbase site includes an MCP server and an llms.txt. Claude, Cursor, and ChatGPT can answer from your real docs the moment you deploy. You don't pay us for this later. +

+
+
+ +
+ + +
+
+

MCP server, built in

+

+ Every helpbase site runs a Model Context Protocol endpoint. Point Claude Code or Cursor at the URL and they answer from your real docs instead of guessing. +

+
+
+ +
+ + +
+
+

llms.txt, always fresh

+

+ A fresh /llms.txt on every build. It's the manifest AI agents already look for. You don't have to remember to write it. +

+
+
+ +
+ + +
+
+

Structured agent output

+

+ The MCP server exposes your content as typed tool calls: list articles, read a specific slug, search. No scraping, no guesswork. +

+
+
+ +
+ +
+
+

Docs that stay in sync with code

+

+ helpbase sync reads your source and proposes MDX edits based on the actual functions and types. You review, you merge. AI assists, it does not author. +

+
+
+ +
+ +
+
+

Hosted tier, if you want it

+

+ helpbase deploy pushes the same app to {'{'}slug{'}'}.helpbase.dev. No servers to manage. Hosted MCP at scale, custom domain, team roles, analytics when you upgrade. +

+
+
+
+
+
+ ) +} + +const Stripes = () => ( +
+) + +const LlmsTxtPreview = () => ( +
+
llms.txt
+
+
# Your Product docs
+
Docs: https://docs.yourco.com
+
MCP: https://docs.yourco.com/api/mcp
+
## Articles
+
- getting-started
+
- authentication
+
- webhooks
+
- rate-limits
+
+
+) diff --git a/apps/web/components/code-block.tsx b/apps/web/components/code-block.tsx new file mode 100644 index 0000000..1565d0e --- /dev/null +++ b/apps/web/components/code-block.tsx @@ -0,0 +1,92 @@ +'use client' + +import { cn } from '@workspace/ui/lib/utils' +import { toJsxRuntime } from 'hast-util-to-jsx-runtime' +import { JSX, useLayoutEffect, useState } from 'react' +import { Fragment, jsx, jsxs } from 'react/jsx-runtime' +import type { BundledLanguage } from 'shiki/bundle/web' + +const highlightCache = new Map() + +let shikiPromise: Promise | null = null + +function getShiki() { + if (!shikiPromise) { + shikiPromise = import('shiki/bundle/web') + } + return shikiPromise +} + +export async function highlight(code: string, lang: BundledLanguage) { + const cacheKey = `${lang}:${code.length}:${code.slice(0, 50)}:${code.slice(-50)}` + + const cached = highlightCache.get(cacheKey) + if (cached) return cached + + const { codeToHast } = await getShiki() + + const hast = await codeToHast(code, { + lang, + themes: { + light: 'github-light', + dark: 'vesper', + }, + }) + + const result = toJsxRuntime(hast, { + Fragment, + jsx, + jsxs, + }) as JSX.Element + + if (highlightCache.size > 100) { + const firstKey = highlightCache.keys().next().value + if (firstKey) highlightCache.delete(firstKey) + } + highlightCache.set(cacheKey, result) + + return result +} + +type Props = { + code: string | null + lang: BundledLanguage + initial?: JSX.Element + preHighlighted?: JSX.Element | null + maxHeight?: number + className?: string + theme?: string + lineNumbers?: boolean // ← added +} + +export default function CodeBlock({ code, lang, initial, maxHeight=940, preHighlighted, theme, className }: Props) { + const [content, setContent] = useState(preHighlighted || initial || null) + + useLayoutEffect(() => { + if (preHighlighted) { + return + } + + let isMounted = true + + if (code) { + highlight(code, lang).then((result) => { + if (isMounted) setContent(result) + }) + } + + return () => { + isMounted = false + } + }, [code, lang, theme, preHighlighted]) + + return content ? ( +
+ {content} +
+ ) : ( +
Loading...
+ ) +} \ No newline at end of file diff --git a/apps/web/components/comparator-7.tsx b/apps/web/components/comparator-7.tsx new file mode 100644 index 0000000..7c2cea6 --- /dev/null +++ b/apps/web/components/comparator-7.tsx @@ -0,0 +1,189 @@ +import { cn } from '@workspace/ui/lib/utils' +import { TooltipProvider, Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' + +const plans = ['rollYourOwn', 'hostedSaas', 'helpbase'] as const + +type Plan = (typeof plans)[number] + +type Cell = boolean | string + +type Feature = { + name: string + description?: string + plans: Record +} + +const planLabels: Record = { + rollYourOwn: 'Roll your own', + hostedSaas: 'Hosted SaaS', + helpbase: 'Helpbase', +} + +const features: Feature[] = [ + { + name: 'Time to first site', + description: 'From zero to a live docs site on your domain.', + plans: { rollYourOwn: '2-3 days', hostedSaas: '1 day', helpbase: '3 min' }, + }, + { + name: 'You own every file', + description: 'Code lives in your repo, commits land in your git history.', + plans: { rollYourOwn: true, hostedSaas: false, helpbase: true }, + }, + { + name: 'MCP server', + description: 'Model Context Protocol endpoint Claude / Cursor / ChatGPT can query.', + plans: { rollYourOwn: 'DIY', hostedSaas: 'Limited', helpbase: 'Built in' }, + }, + { + name: 'llms.txt + typed output', + description: 'Discoverability manifest + machine-readable content, by default.', + plans: { rollYourOwn: false, hostedSaas: false, helpbase: true }, + }, + { + name: 'Deploy anywhere', + description: 'No platform lock-in. Ship your docs on whatever infra you already use \u2014 Vercel, Fly, self-host, your call.', + plans: { rollYourOwn: true, hostedSaas: false, helpbase: true }, + }, + { + name: 'Cost', + description: 'Sticker price for a working help center at seed stage.', + plans: { rollYourOwn: 'Your weekend', hostedSaas: '$200-1k/mo', helpbase: 'Free or hosted' }, + }, + { + name: 'Maintained without you', + description: 'Hosted tier handles updates, caching, MCP scaling.', + plans: { rollYourOwn: false, hostedSaas: true, helpbase: 'Hosted tier' }, + }, + { + name: 'Agents can read it', + description: 'Your editor autocompletes from your real docs instead of guessing.', + plans: { rollYourOwn: 'DIY', hostedSaas: 'Limited', helpbase: true }, + }, +] + +const renderPlanColumn = (plan: Plan) => { + const isPrimary = plan === 'helpbase' + const header = ( +
+ {planLabels[plan]} +
+ ) + + return ( +
+ {header} + +
+ {features.map((feature, index) => { + const value = feature.plans[plan] + return ( +
+
+ {value === true ? ( + + ) : value === false ? ( + + ) : ( + {value} + )} +
+
+ ) + })} +
+
+
+ ) +} + +export default function Comparator() { + return ( +
+
+
+
+
+

+ Two options today. Both are compromises. +

+

+ Build your own in Next.js and burn a weekend. Pay a hosted docs SaaS and get locked in. Helpbase is the third option: free as open source, paid only when you want us to host it. +

+
+
+ +
+
+
+
Feature
+
+ + {features.map((feature, index) => ( +
+
{feature.name}
+ {feature.description && ( + + + + ? + + {feature.description} + + + )} +
+ ))} +
+ + {plans.map((plan) => ( +
+ {renderPlanColumn(plan)} +
+ ))} +
+
+
+
+ ) +} + +const Indicator = ({ checked = false }: { checked?: boolean }) => { + return ( + + {checked ? : '✗'} + + ) +} + +const CheckIcon = () => { + return ( + + + + ) +} diff --git a/apps/web/components/const.ts b/apps/web/components/const.ts new file mode 100644 index 0000000..3857414 --- /dev/null +++ b/apps/web/components/const.ts @@ -0,0 +1,6 @@ +const MESCHAC_AVATAR = 'https://avatars.githubusercontent.com/u/47919550?v=4' +const BERNARD_AVATAR = 'https://avatars.githubusercontent.com/u/31113941?v=4' +const THEO_AVATAR = 'https://avatars.githubusercontent.com/u/68236786?v=4' +const GLODIE_AVATAR = 'https://avatars.githubusercontent.com/u/99137927?v=4' + +export { BERNARD_AVATAR, GLODIE_AVATAR, THEO_AVATAR, MESCHAC_AVATAR } \ No newline at end of file diff --git a/apps/web/components/faqs-3.tsx b/apps/web/components/faqs-3.tsx new file mode 100644 index 0000000..94b375d --- /dev/null +++ b/apps/web/components/faqs-3.tsx @@ -0,0 +1,138 @@ +import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion' +import Link from 'next/link' + +const faqItems = [ + { + group: 'General', + items: [ + { + id: 'gen-1', + question: 'Is helpbase open source?', + answer: 'Yes, MIT-licensed on GitHub. The CLI writes a real Next.js app straight into your repo. Every file is yours to edit, commit, fork, and deploy anywhere.', + }, + { + id: 'gen-2', + question: 'How is this different from a hosted docs SaaS?', + answer: 'You own every file. Your docs are code in your git history, not rows in someone else\'s database. MCP and llms.txt are free and built in, not a Pro-tier add-on.', + }, + { + id: 'gen-3', + question: 'What framework does it use?', + answer: 'Next.js 14 App Router + shadcn/ui + MDX. A standard stack, nothing proprietary. If you already build with Next.js, you already know it.', + }, + ], + }, + { + group: 'Hosted tier', + items: [ + { + id: 'host-1', + question: 'Can I migrate off the hosted tier?', + answer: 'Yes. The hosted tier runs the exact same MDX + config that create-helpbase put in your repo. Clone your repo, deploy the Next.js app anywhere, done. We built it so you can leave.', + }, + { + id: 'host-2', + question: 'What is on the free hosted tier vs Pro?', + answer: 'Hosted free gets you one site at {slug}.helpbase.dev with a hosted MCP endpoint we run, edge caching, and SSL. Pro adds custom domain, team members and roles, analytics, higher MCP rate limits, and priority support.', + }, + { + id: 'host-3', + question: 'Where is data stored on the hosted tier?', + answer: 'Supabase (Postgres + edge functions), plus Vercel edge runtime for serving content and the MCP endpoint. Your MDX source is always in your own git repo, the hosted tier just serves a build of it.', + }, + ], + }, + { + group: 'MCP & AI', + items: [ + { + id: 'mcp-1', + question: 'Does the MCP server work with Claude Code and Cursor?', + answer: 'Yes, over standard Model Context Protocol. The CLI prints the URL. Point your editor at it. Autocomplete and chat answer from your real docs from that moment on.', + }, + { + id: 'mcp-2', + question: 'What do you mean by AI built in?', + answer: 'Every site includes an llms.txt and an MCP server by default. The MCP exposes typed tool calls for list, read, and search. No scraping, no lock-in, no Pro-tier upsell.', + }, + { + id: 'mcp-3', + question: 'Do I need an AI key to use it?', + answer: 'No. The install CLI does not call external AI APIs. Optional features like helpbase sync (which proposes doc edits from code changes) use your own provider keys.', + }, + ], + }, +] + +export default function FAQs() { + return ( +
+
+
+
+

+ FAQs +

+

The questions founders ask before running the install.

+

+ Still stuck? Open an issue on{' '} + + GitHub + + . +

+
+ +
+ {faqItems.map((group) => ( +
+

{group.group}

+ + {group.items.map((item) => ( + + + {item.question} + + +

{item.answer}

+
+
+ ))} +
+
+ ))} +
+
+ +

+ Still stuck? Open an issue on{' '} + + GitHub + + . +

+
+
+ ) +} diff --git a/apps/web/components/features-1.tsx b/apps/web/components/features-1.tsx new file mode 100644 index 0000000..96055e7 --- /dev/null +++ b/apps/web/components/features-1.tsx @@ -0,0 +1,47 @@ +import { FlowIllustration } from "@/components/illustrations/flow" +import { MdxSourcePreview } from "@/components/illustrations/mdx-source-preview" + +export default function FeaturesOwnIt() { + return ( +
+
+
+

+ Every file is yours, from the first commit. +

+

+ Helpbase doesn't store your content anywhere. The CLI writes a Next.js app straight into your git history. Every edit is a commit. Every file is yours. +

+
+
+
+
+

MDX all the way down

+

+ Every article is a plain .mdx file in help-center/content. Import React components, version control every change, diff in PR review. +

+
+
+ +
+
+
+
+

Zero vendor runtime

+

+ No hosted CMS, no editorial database, no cloud to migrate off if we disappear. Deploy the same repo anywhere that runs Next.js. +

+
+
+ +
+
+
+
+
+ ) +} diff --git a/apps/web/components/footer.tsx b/apps/web/components/footer.tsx index e64770e..5f3ad17 100644 --- a/apps/web/components/footer.tsx +++ b/apps/web/components/footer.tsx @@ -1,38 +1,159 @@ -import Link from "next/link" - -export function Footer() { - return ( - - ) +import { Button } from '@/components/ui/button' +import Link from 'next/link' + +const links = [ + { + group: 'Product', + items: [ + { title: 'Pricing', href: '/#pricing' }, + { title: 'Demo', href: 'https://demo.helpbase.dev' }, + { title: 'FAQ', href: '/#faq' }, + ], + }, + { + group: 'Resources', + items: [ + { title: 'Docs', href: '/docs' }, + { title: 'GitHub', href: 'https://github.com/Codehagen/helpbase' }, + { title: 'MCP', href: '/docs/mcp' }, + ], + }, + { + group: 'Company', + items: [ + { title: 'Changelog', href: 'https://github.com/Codehagen/helpbase/releases' }, + { title: 'License', href: 'https://github.com/Codehagen/helpbase/blob/main/LICENSE' }, + { title: 'Privacy', href: '/docs/privacy' }, + ], + }, +] + +export default function FooterSection() { + return ( +
+
+
+
+ + helpbase + + +

+ Open-source help centers with an MCP server and llms.txt built in. Self-host free, or host it with us. +

+
+ +
+ + + + + + + + + + +
+
+
+
+
+ {links.map((group) => ( +
+ {group.group} + +
+ {group.items.map((item) => ( + + {item.title} + + ))} +
+
+ ))} +
+ +
+
+
Updates, every release
+
+ +
+

+ Release notes and shipped features, straight from the repo. No newsletter inbox to opt out of. +

+
+
+
+ +
+ + © {new Date().getFullYear()} helpbase. Built with shadcn/ui, Next.js, Supabase, Vercel. + +
+
+ + +
+ Open source, live +
+
+
+
+ ) } + +export { FooterSection as Footer } diff --git a/apps/web/components/header.tsx b/apps/web/components/header.tsx index f18a898..d21a9e1 100644 --- a/apps/web/components/header.tsx +++ b/apps/web/components/header.tsx @@ -3,94 +3,20 @@ import Link from 'next/link' import { Button } from '@/components/ui/button' import React from 'react' import { useScroll, useMotionValueEvent } from 'motion/react' -import { NavigationMenu, NavigationMenuContent, NavigationMenuItem, NavigationMenuLink, NavigationMenuList, NavigationMenuTrigger, navigationMenuTriggerStyle } from '@/components/ui/navigation-menu' import { Menu, X, ArrowRight } from 'lucide-react' import { useMedia } from '@/hooks/use-media' -import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion' import { cn } from '@workspace/ui/lib/utils' -interface FeatureLink { - href: string +interface NavLink { name: string - description?: string -} - -interface MobileLink { - groupName?: string - links?: FeatureLink[] - name?: string - href?: string + href: string + external?: boolean } -const features: FeatureLink[] = [ - { - href: '#automation', - name: 'Automation', - description: 'Automate your workflow', - }, - { - href: '#scalability', - name: 'Scalability', - description: 'Scale your application effortlessly', - }, - { - href: '#backup', - name: 'Backup', - description: 'Keep your data backed up', - }, - { - href: '#analytics', - name: 'Analytics', - description: 'Track and measure your progress', - }, -] - -const useCases: FeatureLink[] = [ - { - href: '#ux', - name: 'Marketplace', - description: 'Find and buy AI tools', - }, - { - href: '#performance', - name: 'Guides', - description: 'Learn how to use AI tools', - }, - { - href: '#security', - name: 'API Integration', - description: 'Integrate AI tools into your app', - }, - { - href: '#support', - name: 'Partnerships', - description: 'Get help when you need it', - }, -] - -const contentLinks: FeatureLink[] = [ - { - name: 'Announcements', - href: '#link', - }, - { - name: 'Resources', - href: '#link', - }, - { name: 'Blog', href: '#link' }, -] - -const mobileLinks: MobileLink[] = [ - { - groupName: 'Product', - links: features, - }, - { - groupName: 'Solutions', - links: [...useCases, ...contentLinks], - }, - { name: 'Pricing', href: '#' }, - { name: 'Company', href: '#' }, +const NAV_LINKS: NavLink[] = [ + { name: 'Docs', href: '/docs' }, + { name: 'Pricing', href: '/#pricing' }, + { name: 'GitHub', href: 'https://github.com/Codehagen/helpbase', external: true }, ] export function Header() { @@ -109,7 +35,12 @@ export function Header() { role="banner" data-state={isMobileMenuOpen ? 'active' : 'inactive'} {...(isScrolled && { 'data-scrolled': true })}> -
+
@@ -121,35 +52,59 @@ export function Header() { {isLarge && ( -
- {' '} -
+ )} +
- {!isLarge && isMobileMenuOpen && setIsMobileMenuOpen(false)} />} - -
-
- -
+ {!isLarge && isMobileMenuOpen && ( + setIsMobileMenuOpen(false)} /> + )} + +
+ +
@@ -161,123 +116,31 @@ export function Header() { const MobileMenu = ({ closeMenu }: { closeMenu: () => void }) => { return ( ) } - -const NavMenu = () => { - return ( - - - - Product - -
- Features -
    - {features.map((feature, index) => ( - - ))} -
-
-
- Agents Workflow -
    - {useCases.map((useCase, index) => ( - - ))} -
-
-
-
- - - Pricing - - - - - Company - - -
-
- ) -} - -function ListItem({ title, description, href, ...props }: React.ComponentPropsWithoutRef<'li'> & { href: string; title: string; description?: string }) { - return ( -
  • - - -
    {title}
    -

    {description}

    - -
    -
  • - ) -} \ No newline at end of file diff --git a/apps/web/components/hero-section.tsx b/apps/web/components/hero-section.tsx deleted file mode 100644 index e56f88c..0000000 --- a/apps/web/components/hero-section.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import React from 'react' -import { Header } from "@/components/header" -import { Button } from '@/components/ui/button' -import Link from 'next/link' -import { ProductIllustration } from "@/components/ui/illustrations/product-illustration" -import { LogoCloud } from "@/components/logo-cloud" -import Image from 'next/image' -import { ChevronRight } from 'lucide-react' - -export default function HeroSection() { - return ( - <> -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    New
    - - Open source, live today - - -
    -
    -
    -
    -

    Docs your AI agent can actually use.

    - -

    Helpbase turns your repo into an AI-native knowledge layer. One command gives you an MCP server, an llms.txt, and a Next.js help center generated from your code. You own every file.

    - - -
    -
    -
    -
    - -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    - - -
    -
    -
    - -
    -
    - - ) -} \ No newline at end of file diff --git a/apps/web/components/how-it-works-3.tsx b/apps/web/components/how-it-works-3.tsx new file mode 100644 index 0000000..60be0fb --- /dev/null +++ b/apps/web/components/how-it-works-3.tsx @@ -0,0 +1,78 @@ +import { cn } from '@workspace/ui/lib/utils' +import { InstallCommandPreview } from "@/components/illustrations/install-command-preview" +import { PreviewUrlCard } from "@/components/illustrations/preview-url-card" +import { WorkflowIllustration } from "@/components/illustrations/workflow" + +const steps = [ + { + title: "Install", + body: "Run pnpm dlx create-helpbase in your repo. You get a Next.js app with shadcn/ui, MDX content, an MCP server, and an llms.txt. Every file is yours.", + visual: , + }, + { + title: "Preview", + body: "pnpm dev runs it locally. helpbase deploy --preview pushes a draft to a shareable URL without touching your production site.", + visual: , + }, + { + title: "Deploy", + body: "Push to helpbase.dev with one command, or deploy the same files to Vercel, Fly, or your own server. Same content, same MCP endpoint, your choice of host.", + visual: , + }, +] + +export default function HowItWorks() { + return ( +
    +
    +
    + + + + +
    +
    +

    + From zero to live in three commands. +

    +

    + No hosted CMS. No lock-in. Your docs are plain MDX files in your git history from day one. +

    +
    + +
    + {steps.map((step, index) => ( +
    +
    + + {index + 1} + +

    {step.title}

    +

    {step.body}

    +
    + {step.visual} +
    + ))} +
    +
    +
    +
    +
    + ) +} + +const PlusDecorator = ({ className }: { className?: string }) => ( +
    +) diff --git a/apps/web/components/illustrations/agent-tasks.tsx b/apps/web/components/illustrations/agent-tasks.tsx new file mode 100644 index 0000000..13e616a --- /dev/null +++ b/apps/web/components/illustrations/agent-tasks.tsx @@ -0,0 +1,43 @@ +import { CircleDashed } from 'lucide-react' + +export const AgentTasksIllustration = () => { + return ( +
    +
    create, agent-tasks illustration
    + +
    + Thought for 4s +
    + +
    +
    + 1 / 3 tasks done +
    +
    +
      +
    • + 1 + Fetch user data +
    • +
    • + + Analyze purchase history +
    • +
    • + + Generate recommendations +
    • +
    +
    +
    + +
    + Read streaming-response.tsx +
    +
    + ) +} + +export default AgentTasksIllustration \ No newline at end of file diff --git a/apps/web/components/illustrations/document-analysis.tsx b/apps/web/components/illustrations/document-analysis.tsx new file mode 100644 index 0000000..445bd1a --- /dev/null +++ b/apps/web/components/illustrations/document-analysis.tsx @@ -0,0 +1,68 @@ +import { cn } from '@workspace/ui/lib/utils' + +export const DocumentAnalysisIllustration = () => { + return ( +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    + ) +} + +export const CardDecorator = ({ className }: { className?: string }) => ( + <> + + + + + +) + +export default DocumentAnalysisIllustration \ No newline at end of file diff --git a/apps/web/components/illustrations/flow.tsx b/apps/web/components/illustrations/flow.tsx new file mode 100644 index 0000000..1008e7e --- /dev/null +++ b/apps/web/components/illustrations/flow.tsx @@ -0,0 +1,234 @@ +'use client' + +import { LogoIcon } from "@/components/logo" +import { Vercel } from '@/components/ui/svgs/vercel' +import { Supabase } from '@/components/ui/svgs/supabase' +import { Firebase } from '@/components/ui/svgs/firebase' +import { cn } from '@workspace/ui/lib/utils' + +export const FlowIllustration = () => { + return ( +
    + + + + + + + + {/* animated paths */} + + + + + + + + + + + + + + + + + + + + + + +
    + {[ + { name: 'Vercel', icon: Vercel }, + { name: 'Supabase', icon: Supabase }, + { name: 'Firebase', icon: Firebase }, + ].map((node, index) => ( +
    +
    +
    + {node.name} +
    + +
    + +
    +
    +
    +
    + {[1, 2].map((row) => ( +
    +
    +
    +
    + ))} + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + ))} +
    + +
    +
    + +
    +
    + +
    +
    MDX
    + +
    +
    help-center
    + +
    + {[1, 2].map((row) => ( +
    +
    +
    +
    + ))} + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    + ) +} + +export default FlowIllustration \ No newline at end of file diff --git a/apps/web/components/illustrations/install-command-preview.tsx b/apps/web/components/illustrations/install-command-preview.tsx new file mode 100644 index 0000000..deed81a --- /dev/null +++ b/apps/web/components/illustrations/install-command-preview.tsx @@ -0,0 +1,35 @@ +export const InstallCommandPreview = () => ( +
    +
    + + + +
    + +
    +
    + $ + pnpm dlx create-helpbase +
    +
    + Help center created at{" "} + ./help-center +
    +
    + MDX, shadcn/ui, MCP server, llms.txt +
    +
    + Installing dependencies… +
    +
    + Ready.{" "} + Next:{" "} + cd help-center && pnpm dev +
    +
    +
    +) + +export default InstallCommandPreview diff --git a/apps/web/components/illustrations/mdx-source-preview.tsx b/apps/web/components/illustrations/mdx-source-preview.tsx new file mode 100644 index 0000000..31dd286 --- /dev/null +++ b/apps/web/components/illustrations/mdx-source-preview.tsx @@ -0,0 +1,38 @@ +export const MdxSourcePreview = () => ( +
    +
    + content/getting-started.mdx +
    +
    +
    ---
    +
    + title:{" "} + Getting started +
    +
    + order:{" "} + 1 +
    +
    ---
    +
    +
    # Install helpbase
    +
    +
    + Run one command in your repo to +
    +
    + create a full Next.js help center. +
    +
    +
    {``}
    +
    + Edit this file to add your own content. +
    +
    {``}
    +
    +
    +) + +export default MdxSourcePreview diff --git a/apps/web/components/illustrations/models.tsx b/apps/web/components/illustrations/models.tsx new file mode 100644 index 0000000..f2700b9 --- /dev/null +++ b/apps/web/components/illustrations/models.tsx @@ -0,0 +1,63 @@ +import { Gemini } from '@/components/ui/svgs/gemini' +import { PerplexityAi } from '@/components/ui/svgs/perplexity-ai' +import { MistralAi } from '@/components/ui/svgs/mistral-ai' +import { Openai } from '@/components/ui/svgs/openai' +import { Deepseek } from '@/components/ui/svgs/deepseek' +import { QwenLight as Qwen } from '@/components/ui/svgs/qwen' +import { Cohere } from '@/components/ui/svgs/cohere' +import { Play } from 'lucide-react' + +type model = { + name: string + icon: React.ReactNode +} + +export const ModelsIllustration = () => { + const upModels: model[] = [ + { name: 'Gemini', icon: }, + { name: 'Perplexity', icon: }, + { name: 'Deepseek', icon: }, + ] + + const bottomModels: model[] = [ + { name: 'Cohere AI', icon: }, + { name: 'Open AI', icon: }, + { name: 'Qwen', icon: }, + ] + + return ( +
    +
    + +
    +
    + {upModels.map((model, index) => ( +
    + {model.icon} + {model.name} +
    + ))} +
    + + Mistral Chat +
    + {bottomModels.map((model, index) => ( +
    + {model.icon} + {model.name} +
    + ))} +
    +
    + ) +} + +export default ModelsIllustration \ No newline at end of file diff --git a/apps/web/components/illustrations/preview-url-card.tsx b/apps/web/components/illustrations/preview-url-card.tsx new file mode 100644 index 0000000..e16e2be --- /dev/null +++ b/apps/web/components/illustrations/preview-url-card.tsx @@ -0,0 +1,42 @@ +import { Check, Copy, Globe } from "lucide-react" + +export const PreviewUrlCard = () => ( +
    +
    + + + + + + Preview ready + + + 12s ago + +
    + +
    + + + docs-pr-42.helpbase.dev + +
    + +
    + +
    + + Deploy check passed +
    +
    +
    +) + +export default PreviewUrlCard diff --git a/apps/web/components/illustrations/uptime.tsx b/apps/web/components/illustrations/uptime.tsx new file mode 100644 index 0000000..d2daf89 --- /dev/null +++ b/apps/web/components/illustrations/uptime.tsx @@ -0,0 +1,19 @@ +export const UptimeIllustration = () => ( +
    +
    + Uptime + 99.9% +
    +
    + {Array.from({ length: 40 }).map((_, index) => ( +
    + ))} +
    +
    +) + +export default UptimeIllustration \ No newline at end of file diff --git a/apps/web/components/illustrations/workflow.tsx b/apps/web/components/illustrations/workflow.tsx new file mode 100644 index 0000000..1173e4f --- /dev/null +++ b/apps/web/components/illustrations/workflow.tsx @@ -0,0 +1,52 @@ +import { Linear } from '@/components/ui/svgs/linear' +import { Vercel } from '@/components/ui/svgs/vercel' +import { CheckCircle2, GitBranch } from 'lucide-react' + +export const WorkflowIllustration = () => { + return ( +
    +
    +
    + + Workflow completed +
    +
    +
    +
    +
    +
    + + + Issue created 12s ago + +
    +
    + +
    +
    +
    + + + Branch created 3s ago + +
    +
    + +
    +
    +
    + + + Preview deployed now + +
    +
    +
    +
    +
    + ) +} + +export default WorkflowIllustration \ No newline at end of file diff --git a/apps/web/components/logo-cloud.tsx b/apps/web/components/logo-cloud.tsx deleted file mode 100644 index 6bba9c7..0000000 --- a/apps/web/components/logo-cloud.tsx +++ /dev/null @@ -1,204 +0,0 @@ -'use client' -import { GeminiFull } from '@/components/ui/svgs/gemini' -import { Beacon } from '@/components/ui/svgs/beacon' -import { Bolt } from '@/components/ui/svgs/bolt' -import { Cisco } from '@/components/ui/svgs/cisco' -import { Hulu } from '@/components/ui/svgs/hulu' -import { OpenAIFull } from '@/components/ui/svgs/open-ai' -import { Primevideo } from '@/components/ui/svgs/prime' -import { Stripe } from '@/components/ui/svgs/stripe' -import { Supabase } from '@/components/ui/svgs/supabase' -import { Polars } from '@/components/ui/svgs/polars' -import { AnimatePresence, motion } from 'motion/react' -import React, { useEffect, useState } from 'react' -import { Cloudflare } from '@/components/ui/svgs/cloudflare' -import { VercelFull } from '@/components/ui/svgs/vercel' -import { Spotify } from '@/components/ui/svgs/spotify' -import { PayPal } from '@/components/ui/svgs/paypal' -import { LeapWallet } from '@/components/ui/svgs/leap-wallet' -import { Linear } from '@/components/ui/svgs/linear' -import { Slack } from '@/components/ui/svgs/slack' -import { Twilio } from '@/components/ui/svgs/twilio' -import { cn } from '@workspace/ui/lib/utils' - -const aiLogos: React.ReactNode[] = [ - , - , - , -] - -const hostingLogos: React.ReactNode[] = [ - , - , - , -] - -const paymentsLogos: React.ReactNode[] = [ - , - , - , -] - -const streamingLogos: React.ReactNode[] = [ - , - , - , -] - -const otherLogos: React.ReactNode[] = [ - , - , - , -] - -const toolsLogos: React.ReactNode[] = [ - , - , - , -] - -const logoGroups = [aiLogos, hostingLogos, paymentsLogos, streamingLogos, otherLogos, toolsLogos] - -export function LogoCloud() { - const [logoIndices, setLogoIndices] = useState([0, 0, 0, 0, 0, 0]) - - useEffect(() => { - let wrapperIndex = 0 - const interval = setInterval(() => { - setLogoIndices((prev) => { - const groupLogos = logoGroups[wrapperIndex] - if (!groupLogos) return prev - const newIndices = [...prev] - newIndices[wrapperIndex] = ((newIndices[wrapperIndex] ?? 0) + 1) % groupLogos.length - return newIndices - }) - wrapperIndex = (wrapperIndex + 1) % logoGroups.length - }, 2000) - - return () => clearInterval(interval) - }, []) - - return ( -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    - - - - - - -
    -
    -
    - ) -} - -const LogoWrapper = ({ logos, group, logoIndex = 0, className }: { logos: React.ReactNode[]; group?: string; logoIndex?: number; className?: string }) => { - return ( -
    - - - {logos[logoIndex]} - - -
    - ) -} \ No newline at end of file diff --git a/apps/web/components/logo.tsx b/apps/web/components/logo.tsx new file mode 100644 index 0000000..3d192b2 --- /dev/null +++ b/apps/web/components/logo.tsx @@ -0,0 +1,69 @@ +import { cn } from '@workspace/ui/lib/utils' + +export const Logo = ({ className, uniColor }: { className?: string; uniColor?: boolean }) => { + return ( + + + + + + + + ) +} + +export const LogoIcon = ({ className, uniColor }: { className?: string; uniColor?: boolean }) => { + return ( + + + + + + + ) +} \ No newline at end of file diff --git a/apps/web/components/map.tsx b/apps/web/components/map.tsx new file mode 100644 index 0000000..de2fbbd --- /dev/null +++ b/apps/web/components/map.tsx @@ -0,0 +1,31 @@ +'use client' +import DottedMap from 'dotted-map' + +const map = new DottedMap({ height: 55, grid: 'vertical' }) + +const points = map.getPoints() + +const svgOptions = { + backgroundColor: 'var(--color-background)', + color: 'currentColor', + radius: 0.15, +} + +export const Map = () => { + const viewBox = `0 0 120 60` + return ( + + {points.map((point, index) => ( + + ))} + + ) +} \ No newline at end of file diff --git a/apps/web/components/marketing/demo-cross-link.tsx b/apps/web/components/marketing/demo-cross-link.tsx new file mode 100644 index 0000000..5c982a7 --- /dev/null +++ b/apps/web/components/marketing/demo-cross-link.tsx @@ -0,0 +1,87 @@ +"use client" + +import Link from "next/link" + +import { Button } from "@/components/ui/button" +import { track } from "@/lib/analytics" + +export function DemoCrossLink() { + return ( +
    +
    +
    +
    +
    +

    + See a real helpbase site. Running right now. +

    +

    + demo.helpbase.dev is a live helpbase site with real MDX + content, a working MCP server, and an llms.txt you can curl. + Point Claude Code or Cursor at{" "} + + demo.helpbase.dev/api/mcp + {" "} + and watch it answer from the docs. +

    +
    + + +
    +
    +
    +
    +
    +
    + $ curl + demo.helpbase.dev/llms.txt +
    +
    # Helpbase demo docs
    +
    + Docs: https://demo.helpbase.dev/docs +
    +
    + MCP: https://demo.helpbase.dev/api/mcp +
    +
    ## Articles
    +
    - getting-started
    +
    - mcp-integration
    +
    - deploy-preview
    +
    +
    +
    +
    +
    +
    +
    + ) +} diff --git a/apps/web/components/marketing/hero.tsx b/apps/web/components/marketing/hero.tsx new file mode 100644 index 0000000..1a7a639 --- /dev/null +++ b/apps/web/components/marketing/hero.tsx @@ -0,0 +1,147 @@ +"use client" + +import Link from "next/link" +import { Check, Copy, Terminal as TerminalIcon } from "lucide-react" + +import { Button } from "@/components/ui/button" +import { + AnimatedSpan, + Terminal, + TypingAnimation, +} from "@/components/ui/terminal" +import { CopyButton } from "@workspace/ui/components/copy-button" +import { track } from "@/lib/analytics" + +const INSTALL_COMMAND = "pnpm dlx create-helpbase" + +export function Hero() { + return ( +
    +
    + {/* Announcement pill */} +
    +
    +
    + New +
    + + Open source, shipped today + + → + + +
    +
    + +
    +
    +

    + Half your docs readers are AI agents now. +
    {" "} + Helpbase is the help center built for them. +

    + +

    + Every helpbase site includes an MCP server and an{" "} + llms.txt, so Claude and Cursor answer from your real docs. One command drops a full Next.js site into your git history. Host it yourself, or deploy with us. +

    + + {/* Dual CTA: install command + demo */} +
    +
    + + + {INSTALL_COMMAND} + + + track("hero_install_copied", { command: INSTALL_COMMAND }) + } + copiedLabel={ + <> + + Copied + + }> + + Copy + +
    + +
    +
    + + {/* Terminal canvas */} +
    + +
    +
    + + {/* Bottom strip (logo cloud slot handled by ) */} +
    +
    + ) +} + +function HeroTerminal() { + return ( + + + {`$ ${INSTALL_COMMAND}`} + + + ◇ Help center created at ./help-center + + + ◇ MDX content, shadcn/ui, MCP server, llms.txt + + + ◇ Installing dependencies (parallel)… + + + ◇ Ready. Next steps: + + + cd help-center && pnpm dev + + + → Live preview: docs-a1b2c3.helpbase.dev + + + ) +} diff --git a/apps/web/components/particles.tsx b/apps/web/components/particles.tsx new file mode 100644 index 0000000..85b881a --- /dev/null +++ b/apps/web/components/particles.tsx @@ -0,0 +1,407 @@ +'use client' +import { useEffect, useMemo, useState } from 'react' +import Particles, { initParticlesEngine } from '@tsparticles/react' +import { type Container, type ISourceOptions, MoveDirection } from '@tsparticles/engine' +import { loadSlim } from '@tsparticles/slim' + +export const LightDarkParticles = ({ id }: { id: string }) => { + const [init, setInit] = useState(true) + + useEffect(() => { + initParticlesEngine(async (engine) => { + await loadSlim(engine) + }).then(() => { + setInit(true) + }) + }, []) + + const particlesLoaded = async (container?: Container): Promise => { + console.log(container) + } + + const options: ISourceOptions = useMemo( + () => ({ + background: { + color: { + value: 'transparent', + }, + }, + fullScreen: { + enable: false, + zIndex: 10, + }, + fpsLimit: 120, + interactivity: { + events: { + onClick: { + enable: true, + mode: 'push', + }, + onHover: { + enable: false, + mode: 'repulse', + }, + resize: { enable: true }, + }, + modes: { + push: { + quantity: 4, + }, + repulse: { + distance: 200, + duration: 0.4, + }, + }, + }, + particles: { + bounce: { + horizontal: { + value: 1, + }, + vertical: { + value: 1, + }, + }, + collisions: { + absorb: { + speed: 2, + }, + bounce: { + horizontal: { + value: 1, + }, + vertical: { + value: 1, + }, + }, + enable: false, + maxSpeed: 50, + mode: 'bounce', + overlap: { + enable: true, + retries: 0, + }, + }, + color: { + value: '#ffffff', + animation: { + h: { + count: 0, + enable: false, + speed: 1, + decay: 0, + delay: 0, + sync: true, + offset: 0, + }, + s: { + count: 0, + enable: false, + speed: 1, + decay: 0, + delay: 0, + sync: true, + offset: 0, + }, + l: { + count: 0, + enable: false, + speed: 1, + decay: 0, + delay: 0, + sync: true, + offset: 0, + }, + }, + }, + effect: { + close: true, + fill: true, + options: {}, + type: undefined, + }, + groups: {}, + move: { + angle: { + offset: 0, + value: 90, + }, + attract: { + distance: 200, + enable: false, + rotate: { + x: 3000, + y: 3000, + }, + }, + center: { + x: 50, + y: 50, + mode: 'percent', + radius: 0, + }, + decay: 0, + distance: {}, + direction: MoveDirection.none, // Set move direction to right + drift: 0, + enable: true, + gravity: { + acceleration: 9.81, + enable: false, + inverse: false, + maxSpeed: 50, + }, + path: { + clamp: true, + delay: { + value: 0, + }, + enable: false, + options: {}, + }, + outModes: { + default: 'out', + }, + random: false, + size: false, + speed: { + min: 0.1, + max: 1, + }, + spin: { + acceleration: 0, + enable: false, + }, + straight: false, + trail: { + enable: false, + length: 10, + fill: {}, + }, + vibrate: false, + warp: false, + }, + number: { + density: { + enable: true, + width: 400, + height: 400, + }, + limit: { + mode: 'delete', + value: 0, + }, + value: 120, + }, + opacity: { + value: { + min: 0.1, + max: 1, + }, + animation: { + count: 0, + enable: true, + speed: 4, + decay: 0, + delay: 0, + sync: false, + mode: 'auto', + startValue: 'random', + destroy: 'none', + }, + }, + reduceDuplicates: false, + shadow: { + blur: 0, + color: { + value: '#000', + }, + enable: false, + offset: { + x: 0, + y: 0, + }, + }, + shape: { + close: true, + fill: true, + options: {}, + type: 'circle', + }, + size: { + value: { + min: 0.25, + max: 1.5, + }, + animation: { + count: 0, + enable: false, + speed: 5, + decay: 0, + delay: 0, + sync: false, + mode: 'auto', + startValue: 'random', + destroy: 'none', + }, + }, + stroke: { + width: 0.25, + }, + zIndex: { + value: 0, + opacityRate: 1, + sizeRate: 1, + velocityRate: 1, + }, + destroy: { + bounds: {}, + mode: 'none', + split: { + count: 1, + factor: { + value: 3, + }, + rate: { + value: { + min: 4, + max: 9, + }, + }, + sizeOffset: true, + }, + }, + roll: { + darken: { + enable: false, + value: 0, + }, + enable: false, + enlighten: { + enable: false, + value: 0, + }, + mode: 'vertical', + speed: 25, + }, + tilt: { + value: 0, + animation: { + enable: false, + speed: 0, + decay: 0, + sync: false, + }, + direction: 'clockwise', + enable: false, + }, + twinkle: { + lines: { + enable: false, + frequency: 0.05, + opacity: 1, + }, + particles: { + enable: false, + frequency: 0.05, + opacity: 1, + }, + }, + wobble: { + distance: 5, + enable: false, + speed: { + angle: 50, + move: 10, + }, + }, + life: { + count: 0, + delay: { + value: 0, + sync: false, + }, + duration: { + value: 0, + sync: false, + }, + }, + rotate: { + value: 0, + animation: { + enable: false, + speed: 0, + decay: 0, + sync: false, + }, + direction: 'clockwise', + path: false, + }, + orbit: { + animation: { + count: 0, + enable: false, + speed: 1, + decay: 0, + delay: 0, + sync: false, + }, + enable: false, + opacity: 1, + rotation: { + value: 45, + }, + width: 1, + }, + links: { + blink: false, + color: { + value: '#ffffff', + }, + consent: false, + distance: 80, + enable: true, + frequency: 1, + opacity: 0.4, + shadow: { + blur: 5, + color: { + value: '#000', + }, + enable: false, + }, + triangles: { + enable: false, + frequency: 1, + }, + width: 0.5, + warp: false, + maxConnections: 2, + }, + repulse: { + value: 0, + enabled: false, + distance: 1, + duration: 1, + factor: 1, + speed: 1, + }, + }, + detectRetina: true, + }), + [] + ) + + if (init) { + return ( + + ) + } + + return <> +} \ No newline at end of file diff --git a/apps/web/components/pricing.tsx b/apps/web/components/pricing.tsx new file mode 100644 index 0000000..319cd93 --- /dev/null +++ b/apps/web/components/pricing.tsx @@ -0,0 +1,161 @@ +'use client' +import { Button } from '@/components/ui/button' +import { Check } from 'lucide-react' +import Link from 'next/link' +import { CardTitle, CardDescription } from '@/components/ui/card' +import { track } from '@/lib/analytics' + +type TierKey = 'self-host' | 'hosted-free' | 'hosted-pro' + +export default function Pricing() { + return ( +
    +
    +
    +

    + Free as open source. Paid when you want us to host it. +

    +

    + Self-host is free forever. Hosted saves you a server. Pick the one that matches how you want to spend your time. +

    +
    +
    +
    +
    +
    +
    + Self-host + Own every file. Run it on your own server. +
    +
    +
    Free
    +
    MIT license, no strings
    +
    + +
      + {[ + 'pnpm dlx create-helpbase', + 'MDX + shadcn/ui in your repo', + 'MCP server runs on your infra', + 'llms.txt auto-generated', + 'Deploy to Vercel / Fly / self-host', + 'MIT license, your code forever', + ].map((item) => ( +
    • + + {item} +
    • + ))} +
    +
    +
    +
    + Hosted free + Skip the server setup. One command to live. +
    +
    +
    + $0/mo +
    +
    Up to 1 site, fair-use limits
    +
    + +
      + {[ + 'Everything in Self-host, plus:', + 'helpbase deploy to {slug}.helpbase.dev', + 'Hosted MCP endpoint we run', + 'Edge-cached llms.txt + content', + 'Zero-config SSL + CDN', + 'Migrate to self-host any time', + ].map((item) => ( +
    • + + {item} +
    • + ))} +
    +
    +
    +
    + Pro + For teams writing real docs together. +
    +
    +
    + $29/mo per site +
    +
    Early access, locked in for the first year
    +
    + +
      + {[ + 'Everything in Hosted free, plus:', + 'Custom domain (docs.yourco.com)', + 'Team members and roles', + 'Analytics and AI usage dashboards', + 'Higher hosted MCP rate limits', + 'Priority support + SLA', + ].map((item) => ( +
    • + + {item} +
    • + ))} +
    +
    +
    +
    +
    +
    +
    + ) +} diff --git a/apps/web/components/ui/accordion.tsx b/apps/web/components/ui/accordion.tsx index bfb6ba1..4aa2a6d 100644 --- a/apps/web/components/ui/accordion.tsx +++ b/apps/web/components/ui/accordion.tsx @@ -4,7 +4,8 @@ import * as React from "react" import { Accordion as AccordionPrimitive } from "radix-ui" import { cn } from "@workspace/ui/lib/utils" -import { ChevronDown, ChevronUp } from "lucide-react" +import { HugeiconsIcon } from "@hugeicons/react" +import { ArrowDown01Icon, ArrowUp01Icon } from "@hugeicons/core-free-icons" function Accordion({ className, @@ -51,8 +52,8 @@ function AccordionTrigger({ {...props} > {children} - - + + ) diff --git a/apps/web/components/ui/card.tsx b/apps/web/components/ui/card.tsx new file mode 100644 index 0000000..583f57f --- /dev/null +++ b/apps/web/components/ui/card.tsx @@ -0,0 +1,70 @@ +import * as React from 'react' +import { cn } from '@workspace/ui/lib/utils' +import { Slot } from '@radix-ui/react-slot' + +export interface CardProps extends React.HTMLAttributes { + asChild?: boolean +} + +function Card({ className, asChild = false, ...props }: CardProps) { + const Comp = asChild ? Slot : 'div' + return ( + + ) +} + +function CardHeader({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
    + ) +} + +function CardTitle({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
    + ) +} + +function CardDescription({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
    + ) +} + +function CardContent({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
    + ) +} + +function CardFooter({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
    + ) +} + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } \ No newline at end of file diff --git a/apps/web/components/ui/chart.tsx b/apps/web/components/ui/chart.tsx new file mode 100644 index 0000000..d8704e0 --- /dev/null +++ b/apps/web/components/ui/chart.tsx @@ -0,0 +1,373 @@ +"use client" + +import * as React from "react" +import * as RechartsPrimitive from "recharts" +import type { TooltipValueType } from "recharts" + +import { cn } from "@workspace/ui/lib/utils" + +// Format: { THEME_NAME: CSS_SELECTOR } +const THEMES = { light: "", dark: ".dark" } as const + +const INITIAL_DIMENSION = { width: 320, height: 200 } as const +type TooltipNameType = number | string + +export type ChartConfig = Record< + string, + { + label?: React.ReactNode + icon?: React.ComponentType + } & ( + | { color?: string; theme?: never } + | { color?: never; theme: Record } + ) +> + +type ChartContextProps = { + config: ChartConfig +} + +const ChartContext = React.createContext(null) + +function useChart() { + const context = React.useContext(ChartContext) + + if (!context) { + throw new Error("useChart must be used within a ") + } + + return context +} + +function ChartContainer({ + id, + className, + children, + config, + initialDimension = INITIAL_DIMENSION, + ...props +}: React.ComponentProps<"div"> & { + config: ChartConfig + children: React.ComponentProps< + typeof RechartsPrimitive.ResponsiveContainer + >["children"] + initialDimension?: { + width: number + height: number + } +}) { + const uniqueId = React.useId() + const chartId = `chart-${id ?? uniqueId.replace(/:/g, "")}` + + return ( + +
    + + + {children} + +
    +
    + ) +} + +const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { + const colorConfig = Object.entries(config).filter( + ([, config]) => config.theme ?? config.color + ) + + if (!colorConfig.length) { + return null + } + + return ( +