diff --git a/apps/framework-docs-v2/.eslintrc.json b/apps/framework-docs-v2/.eslintrc.json new file mode 100644 index 0000000000..883bd0636b --- /dev/null +++ b/apps/framework-docs-v2/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": ["@repo/eslint-config-custom/next"] +} diff --git a/apps/framework-docs-v2/.gitignore b/apps/framework-docs-v2/.gitignore new file mode 100644 index 0000000000..df4d98ef07 --- /dev/null +++ b/apps/framework-docs-v2/.gitignore @@ -0,0 +1,38 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +# pagefind +/public/pagefind diff --git a/apps/framework-docs-v2/IMPLEMENTATION.md b/apps/framework-docs-v2/IMPLEMENTATION.md new file mode 100644 index 0000000000..c0b3b0bb6f --- /dev/null +++ b/apps/framework-docs-v2/IMPLEMENTATION.md @@ -0,0 +1,297 @@ +# Framework Docs v2 - Implementation Summary + +## Overview + +A custom-built documentation site for MooseStack using Next.js 15, featuring language-specific documentation (TypeScript & Python), full-text search with Pagefind, and comprehensive analytics. + +## Completed Implementation + +### ✅ Phase 1: Project Setup and Configuration + +- **Next.js 15 App** with TypeScript configuration +- **Dependencies configured**: + - Next.js 15, React 19 + - Pagefind for search + - shadcn/ui components (@radix-ui/*) + - posthog-js for analytics + - MDX/markdown processing (gray-matter, remark, rehype) + - Testing utilities (vitest) +- **Monorepo integration**: Already configured in pnpm-workspace.yaml and turbo.json +- **Build system**: Standard Next.js SSG with `generateStaticParams` + +### ✅ Phase 2: Core Architecture + +**Directory Structure Created:** +``` +apps/framework-docs-v2/ +├── src/ +│ ├── app/ +│ │ ├── layout.tsx (root layout with Pagefind loader) +│ │ ├── page.tsx (redirects to /typescript) +│ │ ├── typescript/ +│ │ │ ├── layout.tsx (with nav + analytics) +│ │ │ ├── page.tsx (index page) +│ │ │ └── [...slug]/page.tsx (dynamic doc pages) +│ │ ├── python/ +│ │ │ ├── layout.tsx (with nav + analytics) +│ │ │ ├── page.tsx (index page) +│ │ │ └── [...slug]/page.tsx (dynamic doc pages) +│ │ └── api/ +│ │ └── llms.txt/route.ts (LLM text generation) +│ ├── components/ +│ │ ├── ui/ (shadcn components) +│ │ │ ├── button.tsx +│ │ │ ├── separator.tsx +│ │ │ ├── collapsible.tsx +│ │ │ ├── navigation-menu.tsx +│ │ │ ├── accordion.tsx +│ │ │ └── dialog.tsx +│ │ ├── navigation/ +│ │ │ ├── top-nav.tsx (MooseStack, Hosting, AI + language switcher) +│ │ │ ├── side-nav.tsx (auto-generated from content) +│ │ │ └── toc-nav.tsx (table of contents + helpful links) +│ │ ├── search/ +│ │ │ ├── search-bar.tsx (Pagefind integration with Cmd+K) +│ │ │ └── pagefind-loader.tsx (script loader) +│ │ └── analytics-provider.tsx (PostHog + custom tracking) +│ ├── lib/ +│ │ ├── content.ts (markdown parsing, nav generation, TOC) +│ │ ├── analytics.ts (PostHog + custom wrapper SDK) +│ │ ├── llms-generator.ts (auto-generate llms.txt) +│ │ ├── snippet-tester.ts (validate code snippets) +│ │ └── cn.ts (utility for className merging) +│ └── styles/ +│ └── globals.css (with prose styles) +├── content/ +│ ├── typescript/ +│ │ └── quickstart.md (sample content) +│ └── python/ +│ └── quickstart.md (sample content) +├── scripts/ +│ ├── migrate-content.ts (migration from framework-docs) +│ └── test-snippets.ts (extract & test code blocks) +└── tests/ + └── snippets/ (automated snippet validation) +``` + +**Content Management System:** +- Markdown parser with frontmatter support (title, description, order, category, helpfulLinks) +- Navigation tree generator from file structure and frontmatter +- TOC generator from markdown headings (h2, h3) +- Support for code block language tagging and testing annotations + +### ✅ Phase 3: UI Components + +**Top-Level Navigation:** +- Three main items: MooseStack, Hosting (external), AI +- Language switcher (TypeScript/Python) that changes URL base +- Search bar integration with keyboard shortcut (Cmd/Ctrl+K) +- Responsive mobile menu + +**Side Navigation:** +- Auto-generated from content directory structure +- Nested categories with collapsible sections +- Active page highlighting +- Scroll position persistence + +**Right-Side Navigation (TOC):** +- Generated from h2/h3 headings in current page +- "On this page" section with scroll spy +- "Helpful links" section (configurable via frontmatter) +- External link indicators + +**shadcn Components:** +- Button, Separator, Collapsible, Navigation Menu, Accordion, Dialog +- All configured with proper theming and accessibility + +### ✅ Phase 4: Search with Pagefind + +**Integration:** +- Pagefind indexes after build via post-build script +- Search UI component using shadcn Dialog +- Keyboard shortcuts (Cmd+K / Ctrl+K) +- Both TypeScript and Python content indexed separately +- Language indicator in search results + +**Build Process:** +- Post-build script: `pagefind --site .next/server/app --output-path public/pagefind` +- Lazy-loaded via Script component in root layout + +### ✅ Phase 5: Analytics and Instrumentation + +**PostHog Integration:** +- Provider set up in language-specific layouts +- Tracks: page views, navigation, code copies, search queries +- Debug mode in development + +**Custom Metrics SDK Wrapper:** +- Wraps PostHog with custom event types +- Sends to internal endpoint: `https://moosefood.514.dev/ingest/DocsEvent` +- Tracking events: + - Page views with language context + - Documentation snippet copies + - Search queries and result clicks + - Navigation patterns + - Session management + +**Event Types:** +- `DocsEvent` interface with eventType, language, path, metadata +- Auto-tracks code copying from code blocks +- Non-blocking analytics (won't disrupt UX on failure) + +### ✅ Phase 6: Content Migration + +**Migration Script (`scripts/migrate-content.ts`):** +- Extracts content from `apps/framework-docs/src/pages/moose/` +- Separates language-specific content (strips `` and `` tags) +- Splits into `/content/typescript/` and `/content/python/` directories +- Preserves frontmatter, converts relative links +- Maintains folder structure for navigation consistency +- Cleans MDX components and imports + +**Usage:** +```bash +tsx scripts/migrate-content.ts +``` + +### ✅ Phase 7: llms.txt Generation + +**Auto-Generation:** +- API route at `/api/llms.txt?lang=typescript|python` +- Language-specific versions available +- Extracts content, strips MDX components, formats for LLM consumption +- Includes frontmatter metadata (title, description) +- Adds source path references +- Generates table of contents + +**Access:** +- `/api/llms.txt?lang=typescript` - TypeScript docs only +- `/api/llms.txt?lang=python` - Python docs only +- Default serves TypeScript + +### ✅ Phase 8: Code Snippet Testing + +**Test Infrastructure:** +- Snippet extraction utility parses code blocks with language annotations +- Looks for test directive comments (e.g., `@test`) +- Generates validation results + +**Automated Validation:** +- Run via: `pnpm test:snippets` +- TypeScript snippets: syntax validation (brace matching, etc.) +- Python snippets: indentation validation (tabs vs spaces) +- Reports errors with file/line references +- Can be integrated into build process + +**Integration:** +- Added to `turbo.json` as `test:snippets` task +- Can run before or during static generation + +### ✅ Phase 9: Build Configuration + +**Next.js Configuration:** +- Standard SSG with `generateStaticParams` for all doc pages +- Rewrites for PostHog proxy (/ingest/*) +- Image optimization enabled +- Environment variables for PostHog configured + +**Build Scripts (package.json):** +```json +{ + "dev": "next dev", + "build": "next build && pnpm run index:search", + "index:search": "pagefind --site .next/server/app --output-path public/pagefind", + "test:snippets": "tsx scripts/test-snippets.ts" +} +``` + +## Environment Variables + +Create `.env.local`: +```bash +NEXT_PUBLIC_POSTHOG_KEY=your_posthog_key +NEXT_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com +SITE_URL=https://docs.moosestack.com +``` + +## Next Steps + +1. **Run Content Migration:** + ```bash + cd apps/framework-docs-v2 + pnpm install + tsx scripts/migrate-content.ts + ``` + +2. **Review and Fix Content:** + - Check migrated content in `content/typescript/` and `content/python/` + - Fix any broken links or formatting issues + - Update image paths if necessary + +3. **Build and Test:** + ```bash + pnpm build + pnpm start + ``` + +4. **Test Snippets:** + ```bash + pnpm test:snippets + ``` + +5. **Deploy:** + - Configure environment variables on hosting platform + - Push to repository + - Vercel will auto-deploy on push to main + +## Key Features Implemented + +✅ Language-specific URLs (`/typescript/*` and `/python/*`) +✅ Auto-generated navigation from content structure +✅ Auto-generated table of contents from headings +✅ Full-text search with Pagefind +✅ Keyboard shortcuts (Cmd+K) +✅ PostHog analytics integration +✅ Custom metrics to internal Moose endpoint +✅ Code copy tracking +✅ Search query tracking +✅ Migration script for existing content +✅ llms.txt auto-generation +✅ Code snippet testing framework +✅ Static site generation +✅ Responsive design (mobile, tablet, desktop) +✅ shadcn/ui components +✅ TypeScript throughout +✅ Monorepo integration + +## Architecture Decisions + +1. **No `output: 'export'`**: Using standard Next.js SSG to keep API routes available for dynamic llms.txt generation +2. **Separate URLs for languages**: `/typescript/*` and `/python/*` for better SEO and navigation +3. **File-based content**: Markdown files in `/content` directory for easy editing +4. **Auto-generated nav**: Navigation structure derived from file system and frontmatter +5. **Client-side search**: Pagefind provides fast, static search without server +6. **Dual analytics**: PostHog for general analytics + custom endpoint for Moose-specific tracking + +## Testing + +Sample content created in: +- `content/typescript/quickstart.md` +- `content/python/quickstart.md` + +To test the site: +```bash +cd apps/framework-docs-v2 +pnpm install +pnpm dev +``` + +Visit: http://localhost:3000 + +## Maintenance + +- **Adding new docs**: Create markdown files in `content/typescript/` or `content/python/` +- **Updating navigation**: Modify frontmatter `order` and `category` fields +- **Adding helpful links**: Use frontmatter `helpfulLinks` array +- **Testing snippets**: Run `pnpm test:snippets` before deploying +- **Updating analytics**: Modify `src/lib/analytics.ts` diff --git a/apps/framework-docs-v2/README.md b/apps/framework-docs-v2/README.md new file mode 100644 index 0000000000..51fe19b55a --- /dev/null +++ b/apps/framework-docs-v2/README.md @@ -0,0 +1,37 @@ +# Framework Docs v2 + +Custom-built documentation site for MooseStack using Next.js 15, Pagefind search, and shadcn components. + +## Features + +- 📚 Language-specific documentation (TypeScript & Python) +- 🔍 Fast static search with Pagefind +- 🎨 Modern UI with shadcn components +- 📊 Analytics with PostHog and custom instrumentation +- 🧪 Automated code snippet testing +- 🤖 Auto-generated llms.txt for AI assistants +- 🗺️ Auto-generated navigation and TOC + +## Development + +```bash +# Install dependencies +pnpm install + +# Run development server +pnpm dev + +# Build for production +pnpm build + +# Test code snippets +pnpm test:snippets +``` + +## Structure + +- `/src/app/typescript` - TypeScript documentation +- `/src/app/python` - Python documentation +- `/content` - Markdown content files +- `/src/components` - React components +- `/src/lib` - Utility functions and content processing diff --git a/apps/framework-docs-v2/components.json b/apps/framework-docs-v2/components.json new file mode 100644 index 0000000000..2b47a24a6c --- /dev/null +++ b/apps/framework-docs-v2/components.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "src/styles/globals.css", + "baseColor": "slate", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/cn", + "ui": "@/components/ui" + } +} diff --git a/apps/framework-docs-v2/content/python/index.mdx b/apps/framework-docs-v2/content/python/index.mdx new file mode 100644 index 0000000000..86dd16d27d --- /dev/null +++ b/apps/framework-docs-v2/content/python/index.mdx @@ -0,0 +1,37 @@ +--- +title: "Welcome to MooseStack" +description: "Complete documentation for building data-intensive applications with MooseStack" +order: 0 +--- + +# Welcome to MooseStack Documentation + +MooseStack is a modern framework for building data-intensive applications. This documentation will help you get started and master all aspects of the framework. + +## Quick Start + +Get up and running with MooseStack in minutes: + +```python +from moose_lib import Moose + +app = Moose(name="my-app") + +await app.start() +``` + +## What is MooseStack? + +MooseStack is a code-first framework that combines: + +- **OLAP**: ClickHouse integration for analytics +- **Streaming**: Redpanda/Kafka for real-time data processing +- **Workflows**: Temporal for orchestration +- **APIs**: Built-in REST and GraphQL support + +## Next Steps + +- [Getting Started](/python/getting-started) - Create your first MooseStack app +- [Data Modeling](/python/data-modeling) - Learn how to model your data +- [Local Development](/python/local-dev) - Set up your development environment + diff --git a/apps/framework-docs-v2/content/python/quickstart.md b/apps/framework-docs-v2/content/python/quickstart.md new file mode 100644 index 0000000000..570cd4a399 --- /dev/null +++ b/apps/framework-docs-v2/content/python/quickstart.md @@ -0,0 +1,58 @@ +--- +title: "Quickstart Guide" +description: "Get started with MooseStack in minutes" +order: 1 +category: "getting-started" +--- + +# Quickstart Guide + +Welcome to MooseStack! This guide will help you get started with building data-intensive applications using Python. + +## Installation + +First, install the Moose CLI: + +```bash +pip install moose-cli +``` + +## Create a New Project + +Create a new MooseStack project: + +```python +# Initialize a new project +moose init my-app + +# Navigate to your project +cd my-app + +# Start the development server +moose dev +``` + +## Your First Data Model + +Define your first data model in `datamodels/user.py`: + +```python +from dataclasses import dataclass +from datetime import datetime + +@dataclass +class User: + id: str + email: str + name: str + created_at: datetime +``` + +## Next Steps + +- Learn about [Data Modeling](/python/data-modeling) +- Explore [OLAP capabilities](/python/olap) +- Set up [Streaming Functions](/python/streaming) + +This is a sample page to test the documentation site structure. + diff --git a/apps/framework-docs-v2/content/typescript/index.mdx b/apps/framework-docs-v2/content/typescript/index.mdx new file mode 100644 index 0000000000..137e1b9f11 --- /dev/null +++ b/apps/framework-docs-v2/content/typescript/index.mdx @@ -0,0 +1,39 @@ +--- +title: "Welcome to MooseStack" +description: "Complete documentation for building data-intensive applications with MooseStack" +order: 0 +--- + +# Welcome to MooseStack Documentation + +MooseStack is a modern framework for building data-intensive applications. This documentation will help you get started and master all aspects of the framework. + +## Quick Start + +Get up and running with MooseStack in minutes: + +```typescript +import { Moose } from "@514labs/moose-lib"; + +const app = new Moose({ + name: "my-app", +}); + +await app.start(); +``` + +## What is MooseStack? + +MooseStack is a code-first framework that combines: + +- **OLAP**: ClickHouse integration for analytics +- **Streaming**: Redpanda/Kafka for real-time data processing +- **Workflows**: Temporal for orchestration +- **APIs**: Built-in REST and GraphQL support + +## Next Steps + +- [Getting Started](/typescript/getting-started) - Create your first MooseStack app +- [Data Modeling](/typescript/data-modeling) - Learn how to model your data +- [Local Development](/typescript/local-dev) - Set up your development environment + diff --git a/apps/framework-docs-v2/content/typescript/quickstart.md b/apps/framework-docs-v2/content/typescript/quickstart.md new file mode 100644 index 0000000000..d962823b55 --- /dev/null +++ b/apps/framework-docs-v2/content/typescript/quickstart.md @@ -0,0 +1,55 @@ +--- +title: "Quickstart Guide" +description: "Get started with MooseStack in minutes" +order: 1 +category: "getting-started" +--- + +# Quickstart Guide + +Welcome to MooseStack! This guide will help you get started with building data-intensive applications. + +## Installation + +First, install the Moose CLI: + +```bash +npm install -g @514labs/moose-cli +``` + +## Create a New Project + +Create a new MooseStack project: + +```typescript +// Initialize a new project +npx create-moose-app my-app + +// Navigate to your project +cd my-app + +// Start the development server +npm run dev +``` + +## Your First Data Model + +Define your first data model in `datamodels/User.ts`: + +```typescript +export interface User { + id: string; + email: string; + name: string; + createdAt: Date; +} +``` + +## Next Steps + +- Learn about [Data Modeling](/typescript/data-modeling) +- Explore [OLAP capabilities](/typescript/olap) +- Set up [Streaming Functions](/typescript/streaming) + +This is a sample page to test the documentation site structure. + diff --git a/apps/framework-docs-v2/next-sitemap.config.js b/apps/framework-docs-v2/next-sitemap.config.js new file mode 100644 index 0000000000..58d178c667 --- /dev/null +++ b/apps/framework-docs-v2/next-sitemap.config.js @@ -0,0 +1,7 @@ +/** @type {import('next-sitemap').IConfig} */ +module.exports = { + siteUrl: process.env.SITE_URL || "https://docs.moosestack.com", + generateRobotsTxt: true, + exclude: ["/api/*"], +}; + diff --git a/apps/framework-docs-v2/next.config.js b/apps/framework-docs-v2/next.config.js new file mode 100644 index 0000000000..47a2a42b24 --- /dev/null +++ b/apps/framework-docs-v2/next.config.js @@ -0,0 +1,30 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + reactStrictMode: true, + images: { + unoptimized: true, + }, + env: { + NEXT_PUBLIC_POSTHOG_KEY: process.env.NEXT_PUBLIC_POSTHOG_KEY, + NEXT_PUBLIC_POSTHOG_HOST: process.env.NEXT_PUBLIC_POSTHOG_HOST, + }, + async rewrites() { + return [ + { + source: "/ingest/static/:path*", + destination: "https://us-assets.i.posthog.com/static/:path*", + }, + { + source: "/ingest/:path*", + destination: "https://us.i.posthog.com/:path*", + }, + { + source: "/ingest/decide", + destination: "https://us.i.posthog.com/decide", + }, + ]; + }, +}; + +module.exports = nextConfig; + diff --git a/apps/framework-docs-v2/next.config.ts b/apps/framework-docs-v2/next.config.ts new file mode 100644 index 0000000000..eb93d82f8a --- /dev/null +++ b/apps/framework-docs-v2/next.config.ts @@ -0,0 +1,32 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + reactStrictMode: true, + images: { + unoptimized: true, + }, + env: { + NEXT_PUBLIC_POSTHOG_KEY: process.env.NEXT_PUBLIC_POSTHOG_KEY, + NEXT_PUBLIC_POSTHOG_HOST: process.env.NEXT_PUBLIC_POSTHOG_HOST, + }, + async rewrites() { + return [ + // PostHog proxy rewrites + { + source: "/ingest/static/:path*", + destination: "https://us-assets.i.posthog.com/static/:path*", + }, + { + source: "/ingest/:path*", + destination: "https://us.i.posthog.com/:path*", + }, + { + source: "/ingest/decide", + destination: "https://us.i.posthog.com/decide", + }, + ]; + }, +}; + +export default nextConfig; + diff --git a/apps/framework-docs-v2/package.json b/apps/framework-docs-v2/package.json new file mode 100644 index 0000000000..567293e2f3 --- /dev/null +++ b/apps/framework-docs-v2/package.json @@ -0,0 +1,58 @@ +{ + "name": "framework-docs-v2", + "version": "1.0.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build && pnpm run index:search", + "postbuild": "next-sitemap", + "index:search": "pagefind --site .next/server/app --output-path public/pagefind", + "start": "next start", + "lint": "next lint", + "test:snippets": "tsx scripts/test-snippets.ts" + }, + "dependencies": { + "@radix-ui/react-accordion": "^1.2.3", + "@radix-ui/react-collapsible": "^1.1.0", + "@radix-ui/react-dialog": "^1.1.1", + "@radix-ui/react-dropdown-menu": "^2.0.6", + "@radix-ui/react-navigation-menu": "^1.2.0", + "@radix-ui/react-scroll-area": "^1.2.2", + "@radix-ui/react-separator": "^1.0.3", + "@radix-ui/react-slot": "^1.1.0", + "@radix-ui/react-tabs": "^1.0.4", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.1", + "cmdk": "^1.0.0", + "gray-matter": "^4.0.3", + "lucide-react": "^0.427.0", + "next": "^15.1.4", + "next-sitemap": "^4.2.3", + "posthog-js": "^1.233.1", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "rehype-autolink-headings": "^7.1.0", + "rehype-pretty-code": "^0.14.0", + "rehype-slug": "^6.0.0", + "remark": "^15.0.1", + "remark-gfm": "^4.0.0", + "remark-html": "^16.0.1", + "shiki": "^1.22.2", + "tailwind-merge": "^2.4.0", + "tailwindcss-animate": "^1.0.7" + }, + "devDependencies": { + "@repo/eslint-config-custom": "workspace:*", + "@repo/ts-config": "workspace:*", + "@types/node": "^22.10.2", + "@types/react": "^19.0.1", + "@types/react-dom": "^19.0.2", + "autoprefixer": "^10.4.16", + "pagefind": "^1.2.0", + "postcss": "^8.4.32", + "tailwindcss": "^3.4.0", + "tsx": "^4.19.2", + "typescript": "^5.7.0", + "vitest": "^2.1.8" + } +} diff --git a/apps/framework-docs-v2/pagefind.yml b/apps/framework-docs-v2/pagefind.yml new file mode 100644 index 0000000000..b017d87233 --- /dev/null +++ b/apps/framework-docs-v2/pagefind.yml @@ -0,0 +1,19 @@ +source: .next +bundle_dir: public/pagefind + +glob: "**/*.html" + +exclude_selectors: + - "nav" + - "header" + - ".toc" + - "footer" + +custom_records: + - url: /typescript + content: TypeScript documentation + language: typescript + - url: /python + content: Python documentation + language: python + diff --git a/apps/framework-docs-v2/postcss.config.js b/apps/framework-docs-v2/postcss.config.js new file mode 100644 index 0000000000..c21c076356 --- /dev/null +++ b/apps/framework-docs-v2/postcss.config.js @@ -0,0 +1,7 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; + diff --git a/apps/framework-docs-v2/postcss.config.mjs b/apps/framework-docs-v2/postcss.config.mjs new file mode 100644 index 0000000000..1d926516e7 --- /dev/null +++ b/apps/framework-docs-v2/postcss.config.mjs @@ -0,0 +1,7 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; + diff --git a/apps/framework-docs-v2/scripts/migrate-content.ts b/apps/framework-docs-v2/scripts/migrate-content.ts new file mode 100755 index 0000000000..f62480ba31 --- /dev/null +++ b/apps/framework-docs-v2/scripts/migrate-content.ts @@ -0,0 +1,216 @@ +#!/usr/bin/env tsx + +import fs from "fs"; +import path from "path"; +import matter from "gray-matter"; + +const SOURCE_DIR = path.join(__dirname, "../../framework-docs/src/pages/moose"); +const TARGET_TS_DIR = path.join(__dirname, "../content/typescript"); +const TARGET_PY_DIR = path.join(__dirname, "../content/python"); + +interface MigrationStats { + filesProcessed: number; + filesCreated: number; + errors: string[]; +} + +const stats: MigrationStats = { + filesProcessed: 0, + filesCreated: 0, + errors: [], +}; + +/** + * Extract content for a specific language from MDX + */ +function extractLanguageContent(content: string, language: "typescript" | "python"): string { + const languageTag = language === "typescript" ? "TypeScript" : "Python"; + const otherTag = language === "typescript" ? "Python" : "TypeScript"; + + // Remove other language content + const otherTagRegex = new RegExp(`<${otherTag}[^>]*>([\\s\\S]*?)`, "gi"); + let processed = content.replace(otherTagRegex, ""); + + // Extract this language's content (remove wrapper tags) + const thisTagRegex = new RegExp(`<${languageTag}[^>]*>([\\s\\S]*?)`, "gi"); + processed = processed.replace(thisTagRegex, (_match, inner) => inner || ""); + + // Clean up MDX-specific imports and exports + processed = processed.replace(/^import .*$/gm, ""); + processed = processed.replace(/^export default .*$/gm, ""); + processed = processed.replace(/^export const .*$/gm, ""); + + // Clean up React components + processed = processed.replace(/<>|<\/>/g, ""); + processed = processed.replace(/<[A-Z][A-Za-z0-9]*(?:\s[^<>]*)?>/g, ""); + processed = processed.replace(/<\/[A-Z][A-Za-z0-9]*>/g, ""); + + // Clean up excessive whitespace + processed = processed.replace(/\n{3,}/g, "\n\n"); + processed = processed.trim(); + + return processed; +} + +/** + * Process a single MDX file + */ +function processFile(sourcePath: string, relativePath: string) { + try { + stats.filesProcessed++; + + const content = fs.readFileSync(sourcePath, "utf-8"); + const { data: frontMatter, content: body } = matter(content); + + // Skip files that shouldn't be migrated + if (relativePath.startsWith("_") || relativePath.includes("/_")) { + console.log(` Skipping: ${relativePath}`); + return; + } + + // Extract content for both languages + const tsContent = extractLanguageContent(body, "typescript"); + const pyContent = extractLanguageContent(body, "python"); + + // Only create files if there's actual content + if (tsContent.trim()) { + const tsPath = path.join(TARGET_TS_DIR, relativePath); + const tsDir = path.dirname(tsPath); + + if (!fs.existsSync(tsDir)) { + fs.mkdirSync(tsDir, { recursive: true }); + } + + const tsFrontMatter = { + ...frontMatter, + // Add default order if not present + order: frontMatter.order || 999, + }; + + const tsFile = matter.stringify(tsContent, tsFrontMatter); + fs.writeFileSync(tsPath, tsFile); + stats.filesCreated++; + console.log(` ✓ Created TypeScript: ${relativePath}`); + } + + if (pyContent.trim()) { + const pyPath = path.join(TARGET_PY_DIR, relativePath); + const pyDir = path.dirname(pyPath); + + if (!fs.existsSync(pyDir)) { + fs.mkdirSync(pyDir, { recursive: true }); + } + + const pyFrontMatter = { + ...frontMatter, + order: frontMatter.order || 999, + }; + + const pyFile = matter.stringify(pyContent, pyFrontMatter); + fs.writeFileSync(pyPath, pyFile); + stats.filesCreated++; + console.log(` ✓ Created Python: ${relativePath}`); + } + } catch (error) { + const errorMsg = `Error processing ${relativePath}: ${error}`; + stats.errors.push(errorMsg); + console.error(` ✗ ${errorMsg}`); + } +} + +/** + * Recursively process directory + */ +function processDirectory(dir: string, baseDir: string) { + const entries = fs.readdirSync(dir, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + const relativePath = path.relative(baseDir, fullPath); + + if (entry.isDirectory()) { + processDirectory(fullPath, baseDir); + } else if (entry.isFile() && entry.name.endsWith(".mdx")) { + processFile(fullPath, relativePath); + } + } +} + +/** + * Copy assets (images, etc.) + */ +function copyAssets() { + const publicSource = path.join(__dirname, "../../framework-docs/public"); + const publicTarget = path.join(__dirname, "../public"); + + if (fs.existsSync(publicSource)) { + console.log("\nCopying assets..."); + + // Copy relevant asset directories + const assetDirs = ["images", "img", "assets"]; + + for (const dir of assetDirs) { + const sourceDir = path.join(publicSource, dir); + const targetDir = path.join(publicTarget, dir); + + if (fs.existsSync(sourceDir)) { + if (!fs.existsSync(targetDir)) { + fs.mkdirSync(targetDir, { recursive: true }); + } + // Note: For a production migration, use a proper recursive copy + console.log(` Would copy: ${dir}/`); + } + } + } +} + +/** + * Main migration function + */ +function main() { + console.log("Starting content migration...\n"); + + if (!fs.existsSync(SOURCE_DIR)) { + console.error(`Source directory not found: ${SOURCE_DIR}`); + console.error("Make sure framework-docs exists in the apps directory."); + process.exit(1); + } + + // Create target directories + if (!fs.existsSync(TARGET_TS_DIR)) { + fs.mkdirSync(TARGET_TS_DIR, { recursive: true }); + } + if (!fs.existsSync(TARGET_PY_DIR)) { + fs.mkdirSync(TARGET_PY_DIR, { recursive: true }); + } + + // Process all MDX files + console.log("Processing MDX files...\n"); + processDirectory(SOURCE_DIR, SOURCE_DIR); + + // Copy assets + copyAssets(); + + // Print summary + console.log("\n" + "=".repeat(50)); + console.log("Migration Summary"); + console.log("=".repeat(50)); + console.log(`Files processed: ${stats.filesProcessed}`); + console.log(`Files created: ${stats.filesCreated}`); + console.log(`Errors: ${stats.errors.length}`); + + if (stats.errors.length > 0) { + console.log("\nErrors:"); + stats.errors.forEach((error) => console.log(` - ${error}`)); + } + + console.log("\n✓ Migration complete!"); + console.log("\nNext steps:"); + console.log("1. Review migrated content in content/typescript and content/python"); + console.log("2. Fix any broken links or formatting issues"); + console.log("3. Update image paths if necessary"); + console.log("4. Run 'pnpm build' to test the site"); +} + +// Run migration +main(); diff --git a/apps/framework-docs-v2/scripts/test-snippets.ts b/apps/framework-docs-v2/scripts/test-snippets.ts new file mode 100755 index 0000000000..1f0d530ea2 --- /dev/null +++ b/apps/framework-docs-v2/scripts/test-snippets.ts @@ -0,0 +1,55 @@ +#!/usr/bin/env tsx + +import path from "path"; +import { extractAllSnippets, testSnippets } from "../src/lib/snippet-tester"; + +async function main() { + console.log("Testing code snippets...\n"); + + const tsContentDir = path.join(__dirname, "../content/typescript"); + const pyContentDir = path.join(__dirname, "../content/python"); + + // Extract snippets from both languages + console.log("Extracting TypeScript snippets..."); + const tsSnippets = extractAllSnippets(tsContentDir); + console.log(`Found ${tsSnippets.length} TypeScript snippets\n`); + + console.log("Extracting Python snippets..."); + const pySnippets = extractAllSnippets(pyContentDir); + console.log(`Found ${pySnippets.length} Python snippets\n`); + + // Test all snippets + const allSnippets = [...tsSnippets, ...pySnippets]; + console.log(`Testing ${allSnippets.length} total snippets...\n`); + + const results = await testSnippets(allSnippets); + + // Report results + const passed = results.filter((r) => r.passed); + const failed = results.filter((r) => !r.passed); + + console.log("=".repeat(50)); + console.log("Test Results"); + console.log("=".repeat(50)); + console.log(`Total: ${results.length}`); + console.log(`Passed: ${passed.length}`); + console.log(`Failed: ${failed.length}`); + + if (failed.length > 0) { + console.log("\nFailed snippets:"); + failed.forEach((result) => { + console.log(`\n ✗ ${result.snippet.file}:${result.snippet.lineNumber}`); + console.log(` Language: ${result.snippet.language}`); + console.log(` Error: ${result.error}`); + }); + + process.exit(1); + } else { + console.log("\n✓ All snippets passed!"); + } +} + +main().catch((error) => { + console.error("Error testing snippets:", error); + process.exit(1); +}); diff --git a/apps/framework-docs-v2/src/app/api/llms.txt/route.ts b/apps/framework-docs-v2/src/app/api/llms.txt/route.ts new file mode 100644 index 0000000000..c61abc60f5 --- /dev/null +++ b/apps/framework-docs-v2/src/app/api/llms.txt/route.ts @@ -0,0 +1,21 @@ +import { NextRequest, NextResponse } from "next/server"; +import { generateLLMsTxt } from "@/lib/llms-generator"; + +export async function GET(request: NextRequest) { + const { searchParams } = new URL(request.url); + const language = searchParams.get("lang") || "typescript"; + + try { + const content = generateLLMsTxt(language as "typescript" | "python"); + + return new NextResponse(content, { + headers: { + "Content-Type": "text/plain; charset=utf-8", + "Cache-Control": "s-maxage=1800, stale-while-revalidate=300", + }, + }); + } catch (error) { + console.error("Failed to generate llms.txt:", error); + return new NextResponse("Internal Server Error", { status: 500 }); + } +} diff --git a/apps/framework-docs-v2/src/app/layout.tsx b/apps/framework-docs-v2/src/app/layout.tsx new file mode 100644 index 0000000000..bbc240faee --- /dev/null +++ b/apps/framework-docs-v2/src/app/layout.tsx @@ -0,0 +1,23 @@ +import type { Metadata } from "next"; +import "@/styles/globals.css"; +import { PagefindLoader } from "@/components/search/pagefind-loader"; + +export const metadata: Metadata = { + title: "MooseStack Documentation", + description: "Build data-intensive applications with MooseStack", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + {children} + + + + ); +} diff --git a/apps/framework-docs-v2/src/app/page.tsx b/apps/framework-docs-v2/src/app/page.tsx new file mode 100644 index 0000000000..3d3fa397f6 --- /dev/null +++ b/apps/framework-docs-v2/src/app/page.tsx @@ -0,0 +1,6 @@ +import { redirect } from "next/navigation"; + +export default function HomePage() { + // Redirect to TypeScript docs by default + redirect("/typescript"); +} diff --git a/apps/framework-docs-v2/src/app/python/[...slug]/page.tsx b/apps/framework-docs-v2/src/app/python/[...slug]/page.tsx new file mode 100644 index 0000000000..372244cd87 --- /dev/null +++ b/apps/framework-docs-v2/src/app/python/[...slug]/page.tsx @@ -0,0 +1,47 @@ +import { notFound } from "next/navigation"; +import { getAllSlugs, parseMarkdownContent } from "@/lib/content"; +import { TOCNav } from "@/components/navigation/toc-nav"; + +interface PageProps { + params: Promise<{ + slug: string[]; + }>; +} + +export async function generateStaticParams() { + const slugs = getAllSlugs("python"); + return slugs.map((slug) => ({ + slug: slug.split("/"), + })); +} + +export default async function PythonDocPage({ params }: PageProps) { + const resolvedParams = await params; + const slug = resolvedParams.slug.join("/"); + + let content; + try { + content = await parseMarkdownContent("python", slug); + } catch (error) { + notFound(); + } + + return ( + <> +
+ {content.frontMatter.title && ( +

{content.frontMatter.title}

+ )} + {content.frontMatter.description && ( +

{content.frontMatter.description}

+ )} +
+
+ + + ); +} + diff --git a/apps/framework-docs-v2/src/app/python/[[...slug]]/page.tsx b/apps/framework-docs-v2/src/app/python/[[...slug]]/page.tsx new file mode 100644 index 0000000000..cddc799a7f --- /dev/null +++ b/apps/framework-docs-v2/src/app/python/[[...slug]]/page.tsx @@ -0,0 +1,96 @@ +import { notFound } from "next/navigation"; +import { getAllDocSlugs, getDocBySlug, getBreadcrumbs } from "@/lib/content"; +import { TOCNav } from "@/components/navigation/TOCNav"; +import { CodeBlock } from "@/components/CodeBlock"; +import Link from "next/link"; +import { ChevronRight } from "lucide-react"; + +export async function generateStaticParams() { + const slugs = await getAllDocSlugs("python"); + return slugs.map((slug) => ({ + slug: slug.length === 0 ? undefined : slug, + })); +} + +export async function generateMetadata({ + params, +}: { + params: Promise<{ slug?: string[] }>; +}) { + const { slug = [] } = await params; + const doc = await getDocBySlug("python", slug); + + if (!doc) { + return { + title: "Not Found", + }; + } + + return { + title: `${doc.frontmatter.title || "Documentation"} | MooseStack`, + description: doc.frontmatter.description, + }; +} + +export default async function PythonDocPage({ + params, +}: { + params: Promise<{ slug?: string[] }>; +}) { + const { slug = [] } = await params; + const doc = await getDocBySlug("python", slug); + + if (!doc) { + notFound(); + } + + const breadcrumbs = await getBreadcrumbs("python", slug); + + return ( +
+
+ {/* Breadcrumbs */} + + + {/* Page Title */} + {doc.frontmatter.title && ( +

+ {doc.frontmatter.title} +

+ )} + + {/* Description */} + {doc.frontmatter.description && ( +

+ {doc.frontmatter.description} +

+ )} + + {/* Content */} +
+
+ + {/* TOC Nav */} + +
+ ); +} + diff --git a/apps/framework-docs-v2/src/app/python/layout.tsx b/apps/framework-docs-v2/src/app/python/layout.tsx new file mode 100644 index 0000000000..61f8366120 --- /dev/null +++ b/apps/framework-docs-v2/src/app/python/layout.tsx @@ -0,0 +1,24 @@ +import { TopNav } from "@/components/navigation/top-nav"; +import { SideNav } from "@/components/navigation/side-nav"; +import { buildNavigationTree } from "@/lib/content"; +import { AnalyticsProvider } from "@/components/analytics-provider"; + +export default function PythonLayout({ + children, +}: { + children: React.ReactNode; +}) { + const navItems = buildNavigationTree("python"); + + return ( + + +
+ +
+ {children} +
+
+
+ ); +} diff --git a/apps/framework-docs-v2/src/app/python/llms.txt/route.ts b/apps/framework-docs-v2/src/app/python/llms.txt/route.ts new file mode 100644 index 0000000000..b293ee0458 --- /dev/null +++ b/apps/framework-docs-v2/src/app/python/llms.txt/route.ts @@ -0,0 +1,6 @@ +import { redirect } from "next/navigation"; + +export async function GET() { + redirect("/api/llms.txt?language=python"); +} + diff --git a/apps/framework-docs-v2/src/app/python/page.tsx b/apps/framework-docs-v2/src/app/python/page.tsx new file mode 100644 index 0000000000..35774b900c --- /dev/null +++ b/apps/framework-docs-v2/src/app/python/page.tsx @@ -0,0 +1,20 @@ +import { redirect } from "next/navigation"; +import { getAllSlugs } from "@/lib/content"; + +export default function PythonIndexPage() { + // Redirect to first available page + const slugs = getAllSlugs("python"); + if (slugs.length > 0) { + redirect(`/python/${slugs[0]}`); + } + + return ( +
+

Python Documentation

+

+ Welcome to the MooseStack Python documentation. +

+
+ ); +} + diff --git a/apps/framework-docs-v2/src/app/typescript/[...slug]/page.tsx b/apps/framework-docs-v2/src/app/typescript/[...slug]/page.tsx new file mode 100644 index 0000000000..c8a5a4e714 --- /dev/null +++ b/apps/framework-docs-v2/src/app/typescript/[...slug]/page.tsx @@ -0,0 +1,47 @@ +import { notFound } from "next/navigation"; +import { getAllSlugs, parseMarkdownContent } from "@/lib/content"; +import { TOCNav } from "@/components/navigation/toc-nav"; + +interface PageProps { + params: Promise<{ + slug: string[]; + }>; +} + +export async function generateStaticParams() { + const slugs = getAllSlugs("typescript"); + return slugs.map((slug) => ({ + slug: slug.split("/"), + })); +} + +export default async function TypeScriptDocPage({ params }: PageProps) { + const resolvedParams = await params; + const slug = resolvedParams.slug.join("/"); + + let content; + try { + content = await parseMarkdownContent("typescript", slug); + } catch (error) { + notFound(); + } + + return ( + <> +
+ {content.frontMatter.title && ( +

{content.frontMatter.title}

+ )} + {content.frontMatter.description && ( +

{content.frontMatter.description}

+ )} +
+
+ + + ); +} + diff --git a/apps/framework-docs-v2/src/app/typescript/[[...slug]]/page.tsx b/apps/framework-docs-v2/src/app/typescript/[[...slug]]/page.tsx new file mode 100644 index 0000000000..ba9b667758 --- /dev/null +++ b/apps/framework-docs-v2/src/app/typescript/[[...slug]]/page.tsx @@ -0,0 +1,96 @@ +import { notFound } from "next/navigation"; +import { getAllDocSlugs, getDocBySlug, getBreadcrumbs } from "@/lib/content"; +import { TOCNav } from "@/components/navigation/TOCNav"; +import { CodeBlock } from "@/components/CodeBlock"; +import Link from "next/link"; +import { ChevronRight } from "lucide-react"; + +export async function generateStaticParams() { + const slugs = await getAllDocSlugs("typescript"); + return slugs.map((slug) => ({ + slug: slug.length === 0 ? undefined : slug, + })); +} + +export async function generateMetadata({ + params, +}: { + params: Promise<{ slug?: string[] }>; +}) { + const { slug = [] } = await params; + const doc = await getDocBySlug("typescript", slug); + + if (!doc) { + return { + title: "Not Found", + }; + } + + return { + title: `${doc.frontmatter.title || "Documentation"} | MooseStack`, + description: doc.frontmatter.description, + }; +} + +export default async function TypeScriptDocPage({ + params, +}: { + params: Promise<{ slug?: string[] }>; +}) { + const { slug = [] } = await params; + const doc = await getDocBySlug("typescript", slug); + + if (!doc) { + notFound(); + } + + const breadcrumbs = await getBreadcrumbs("typescript", slug); + + return ( +
+
+ {/* Breadcrumbs */} + + + {/* Page Title */} + {doc.frontmatter.title && ( +

+ {doc.frontmatter.title} +

+ )} + + {/* Description */} + {doc.frontmatter.description && ( +

+ {doc.frontmatter.description} +

+ )} + + {/* Content */} +
+
+ + {/* TOC Nav */} + +
+ ); +} + diff --git a/apps/framework-docs-v2/src/app/typescript/layout.tsx b/apps/framework-docs-v2/src/app/typescript/layout.tsx new file mode 100644 index 0000000000..26c6c54829 --- /dev/null +++ b/apps/framework-docs-v2/src/app/typescript/layout.tsx @@ -0,0 +1,24 @@ +import { TopNav } from "@/components/navigation/top-nav"; +import { SideNav } from "@/components/navigation/side-nav"; +import { buildNavigationTree } from "@/lib/content"; +import { AnalyticsProvider } from "@/components/analytics-provider"; + +export default function TypeScriptLayout({ + children, +}: { + children: React.ReactNode; +}) { + const navItems = buildNavigationTree("typescript"); + + return ( + + +
+ +
+ {children} +
+
+
+ ); +} diff --git a/apps/framework-docs-v2/src/app/typescript/llms.txt/route.ts b/apps/framework-docs-v2/src/app/typescript/llms.txt/route.ts new file mode 100644 index 0000000000..1390fd7975 --- /dev/null +++ b/apps/framework-docs-v2/src/app/typescript/llms.txt/route.ts @@ -0,0 +1,6 @@ +import { redirect } from "next/navigation"; + +export async function GET() { + redirect("/api/llms.txt?language=typescript"); +} + diff --git a/apps/framework-docs-v2/src/app/typescript/page.tsx b/apps/framework-docs-v2/src/app/typescript/page.tsx new file mode 100644 index 0000000000..5554010874 --- /dev/null +++ b/apps/framework-docs-v2/src/app/typescript/page.tsx @@ -0,0 +1,20 @@ +import { redirect } from "next/navigation"; +import { getAllSlugs } from "@/lib/content"; + +export default function TypeScriptIndexPage() { + // Redirect to first available page + const slugs = getAllSlugs("typescript"); + if (slugs.length > 0) { + redirect(`/typescript/${slugs[0]}`); + } + + return ( +
+

TypeScript Documentation

+

+ Welcome to the MooseStack TypeScript documentation. +

+
+ ); +} + diff --git a/apps/framework-docs-v2/src/components/CodeBlock.tsx b/apps/framework-docs-v2/src/components/CodeBlock.tsx new file mode 100644 index 0000000000..4893b6dd9d --- /dev/null +++ b/apps/framework-docs-v2/src/components/CodeBlock.tsx @@ -0,0 +1,40 @@ +"use client"; + +import { useState } from "react"; +import { Check, Copy } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { useAnalytics } from "@/lib/analytics"; + +interface CodeBlockProps { + code: string; + language?: string; +} + +export function CodeBlock({ code, language }: CodeBlockProps) { + const [copied, setCopied] = useState(false); + const { trackCodeCopy } = useAnalytics(); + + const handleCopy = async () => { + await navigator.clipboard.writeText(code); + setCopied(true); + trackCodeCopy(code); + setTimeout(() => setCopied(false), 2000); + }; + + return ( +
+ +
+        {code}
+      
+
+ ); +} + diff --git a/apps/framework-docs-v2/src/components/analytics-provider.tsx b/apps/framework-docs-v2/src/components/analytics-provider.tsx new file mode 100644 index 0000000000..98b1fe705d --- /dev/null +++ b/apps/framework-docs-v2/src/components/analytics-provider.tsx @@ -0,0 +1,58 @@ +"use client"; + +import { useEffect } from "react"; +import { usePathname } from "next/navigation"; +import { analytics } from "@/lib/analytics"; + +interface AnalyticsProviderProps { + language: "typescript" | "python"; + children: React.ReactNode; +} + +export function AnalyticsProvider({ language, children }: AnalyticsProviderProps) { + const pathname = usePathname(); + + useEffect(() => { + // Initialize analytics on mount + analytics.init(); + }, []); + + useEffect(() => { + // Track page views on route change + analytics.pageView(pathname, language); + }, [pathname, language]); + + useEffect(() => { + // Set up code copy tracking + const handleCopy = (event: ClipboardEvent) => { + const selection = document.getSelection(); + const selectedText = selection?.toString(); + + if (!selectedText) return; + + // Check if the copied text is from within a code block + const range = selection?.getRangeAt(0); + if (!range) return; + + const container = range.commonAncestorContainer; + const codeBlock = + container.nodeType === Node.TEXT_NODE + ? container.parentElement?.closest("pre code, code") + : (container as Element)?.closest("pre code, code"); + + if (codeBlock && selectedText.trim()) { + analytics.codeCopy({ + code: selectedText, + language, + page: pathname, + }); + } + }; + + document.addEventListener("copy", handleCopy); + return () => document.removeEventListener("copy", handleCopy); + }, [pathname, language]); + + return <>{children}; +} + diff --git a/apps/framework-docs-v2/src/components/navigation/SideNav.tsx b/apps/framework-docs-v2/src/components/navigation/SideNav.tsx new file mode 100644 index 0000000000..1aeebf2022 --- /dev/null +++ b/apps/framework-docs-v2/src/components/navigation/SideNav.tsx @@ -0,0 +1,91 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { ChevronDown, ChevronRight } from "lucide-react"; +import { useState } from "react"; +import { cn } from "@/lib/utils"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import type { NavItem } from "@/lib/content"; + +interface SideNavProps { + items: NavItem[]; + language: "typescript" | "python"; +} + +export function SideNav({ items, language }: SideNavProps) { + return ( + + ); +} + +function NavItemComponent({ + item, + language, + level = 0, +}: { + item: NavItem; + language: string; + level?: number; +}) { + const pathname = usePathname(); + const [isOpen, setIsOpen] = useState(true); + + const href = `/${language}/${item.slug.join("/")}`; + const isActive = pathname === href; + const hasChildren = item.children && item.children.length > 0; + + return ( +
+
0 && "ml-4", + isActive + ? "bg-secondary text-secondary-foreground font-medium" + : "text-muted-foreground hover:bg-secondary/50 hover:text-foreground" + )} + > + {hasChildren && ( + + )} + {!hasChildren && } + + {item.title} + +
+ + {hasChildren && isOpen && ( +
+ {item.children?.map((child) => ( + + ))} +
+ )} +
+ ); +} + diff --git a/apps/framework-docs-v2/src/components/navigation/TOCNav.tsx b/apps/framework-docs-v2/src/components/navigation/TOCNav.tsx new file mode 100644 index 0000000000..778b643e51 --- /dev/null +++ b/apps/framework-docs-v2/src/components/navigation/TOCNav.tsx @@ -0,0 +1,96 @@ +"use client"; + +import { useEffect, useState } from "react"; +import Link from "next/link"; +import { cn } from "@/lib/utils"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import type { Heading } from "@/lib/content"; + +interface TOCNavProps { + headings: Heading[]; + helpfulLinks?: Array<{ title: string; url: string }>; +} + +export function TOCNav({ headings, helpfulLinks }: TOCNavProps) { + const [activeId, setActiveId] = useState(""); + + useEffect(() => { + const observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + setActiveId(entry.target.id); + } + }); + }, + { rootMargin: "0% 0% -80% 0%" }, + ); + + // Observe all headings + headings.forEach((heading) => { + const element = document.getElementById(heading.id); + if (element) { + observer.observe(element); + } + }); + + return () => { + observer.disconnect(); + }; + }, [headings]); + + if (headings.length === 0 && (!helpfulLinks || helpfulLinks.length === 0)) { + return null; + } + + return ( + + ); +} + diff --git a/apps/framework-docs-v2/src/components/navigation/TopNav.tsx b/apps/framework-docs-v2/src/components/navigation/TopNav.tsx new file mode 100644 index 0000000000..bfb45b6b66 --- /dev/null +++ b/apps/framework-docs-v2/src/components/navigation/TopNav.tsx @@ -0,0 +1,134 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { Menu } from "lucide-react"; +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; +import { SearchBar } from "@/components/search/SearchBar"; + +export function TopNav() { + const pathname = usePathname(); + const [mobileMenuOpen, setMobileMenuOpen] = useState(false); + + const currentLanguage = + pathname?.startsWith("/typescript") ? "typescript" + : pathname?.startsWith("/python") ? "python" + : "typescript"; + + const navItems = [ + { label: "MooseStack", href: `/${currentLanguage}` }, + { label: "Hosting", href: "https://www.fiveonefour.com/boreal", external: true }, + { label: "AI", href: `/${currentLanguage}/ai` }, + ]; + + return ( + + ); +} + +function LanguageSwitcher({ currentLanguage }: { currentLanguage: string }) { + const pathname = usePathname(); + + const getOtherLanguageUrl = () => { + if (!pathname) return "/python"; + + if (currentLanguage === "typescript") { + return pathname.replace("/typescript", "/python"); + } else { + return pathname.replace("/python", "/typescript"); + } + }; + + return ( +
+ + + + + + +
+ ); +} + diff --git a/apps/framework-docs-v2/src/components/navigation/side-nav.tsx b/apps/framework-docs-v2/src/components/navigation/side-nav.tsx new file mode 100644 index 0000000000..1dec613599 --- /dev/null +++ b/apps/framework-docs-v2/src/components/navigation/side-nav.tsx @@ -0,0 +1,94 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { ChevronRight } from "lucide-react"; +import { cn } from "@/lib/cn"; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; +import type { NavItem } from "@/lib/content"; +import { useState } from "react"; + +interface SideNavProps { + items: NavItem[]; + language: "typescript" | "python"; +} + +export function SideNav({ items, language }: SideNavProps) { + return ( + + ); +} + +function NavItemComponent({ + item, + language, + level = 0, +}: { + item: NavItem; + language: string; + level?: number; +}) { + const pathname = usePathname(); + const href = `/${language}/${item.slug}`; + const isActive = pathname === href; + const hasChildren = item.children && item.children.length > 0; + const [isOpen, setIsOpen] = useState( + isActive || (hasChildren && item.children?.some((child) => pathname.includes(child.slug))), + ); + + if (hasChildren) { + return ( + + + {item.title} + + + +
+ {item.children?.map((child) => ( + + ))} +
+
+
+ ); + } + + return ( + + {item.title} + + ); +} + diff --git a/apps/framework-docs-v2/src/components/navigation/toc-nav.tsx b/apps/framework-docs-v2/src/components/navigation/toc-nav.tsx new file mode 100644 index 0000000000..bff7478e19 --- /dev/null +++ b/apps/framework-docs-v2/src/components/navigation/toc-nav.tsx @@ -0,0 +1,94 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { cn } from "@/lib/cn"; +import type { Heading } from "@/lib/content"; +import { ExternalLink } from "lucide-react"; + +interface TOCNavProps { + headings: Heading[]; + helpfulLinks?: Array<{ + title: string; + url: string; + }>; +} + +export function TOCNav({ headings, helpfulLinks }: TOCNavProps) { + const [activeId, setActiveId] = useState(""); + + useEffect(() => { + const observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + setActiveId(entry.target.id); + } + }); + }, + { rootMargin: "0% 0% -80% 0%" }, + ); + + headings.forEach(({ id }) => { + const element = document.getElementById(id); + if (element) { + observer.observe(element); + } + }); + + return () => observer.disconnect(); + }, [headings]); + + if (headings.length === 0 && (!helpfulLinks || helpfulLinks.length === 0)) { + return null; + } + + return ( + + ); +} + diff --git a/apps/framework-docs-v2/src/components/navigation/top-nav.tsx b/apps/framework-docs-v2/src/components/navigation/top-nav.tsx new file mode 100644 index 0000000000..35f54cb646 --- /dev/null +++ b/apps/framework-docs-v2/src/components/navigation/top-nav.tsx @@ -0,0 +1,151 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { Menu, Search } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/cn"; +import { useState } from "react"; +import { SearchBar } from "@/components/search/search-bar"; + +interface TopNavProps { + language: "typescript" | "python"; +} + +export function TopNav({ language }: TopNavProps) { + const pathname = usePathname(); + const [mobileMenuOpen, setMobileMenuOpen] = useState(false); + + const navItems = [ + { label: "MooseStack", href: `/${language}` }, + { label: "Hosting", href: "https://www.fiveonefour.com/boreal", external: true }, + { label: "AI", href: `/${language}/ai` }, + ]; + + return ( + <> + + + + {/* Mobile Menu */} + {mobileMenuOpen && ( +
+
+ {navItems.map((item) => ( + setMobileMenuOpen(false)} + > + {item.label} + + ))} +
+ + TypeScript + + + Python + +
+
+ )} +
+ + ); +} + diff --git a/apps/framework-docs-v2/src/components/search/SearchBar.tsx b/apps/framework-docs-v2/src/components/search/SearchBar.tsx new file mode 100644 index 0000000000..a69d7f5c82 --- /dev/null +++ b/apps/framework-docs-v2/src/components/search/SearchBar.tsx @@ -0,0 +1,154 @@ +"use client"; + +import { useCallback, useEffect, useState } from "react"; +import { Search } from "lucide-react"; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { useAnalytics } from "@/lib/analytics"; + +declare global { + interface Window { + pagefind?: { + search: (query: string) => Promise<{ results: Array<{ data: () => Promise }> }>; + }; + } +} + +export function SearchBar() { + const [open, setOpen] = useState(false); + const [query, setQuery] = useState(""); + const [results, setResults] = useState([]); + const [loading, setLoading] = useState(false); + const { trackSearch } = useAnalytics(); + + // Load Pagefind on mount + useEffect(() => { + if (open && !window.pagefind) { + const script = document.createElement("script"); + script.src = "/pagefind/pagefind.js"; + script.async = true; + document.body.appendChild(script); + } + }, [open]); + + // Handle keyboard shortcut + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if ((e.metaKey || e.ctrlKey) && e.key === "k") { + e.preventDefault(); + setOpen(true); + } + }; + + document.addEventListener("keydown", handleKeyDown); + return () => document.removeEventListener("keydown", handleKeyDown); + }, []); + + const handleSearch = useCallback( + async (searchQuery: string) => { + if (!searchQuery.trim() || !window.pagefind) { + setResults([]); + return; + } + + setLoading(true); + try { + const search = await window.pagefind.search(searchQuery); + const resultData = await Promise.all( + search.results.map((r) => r.data()) + ); + setResults(resultData); + trackSearch(searchQuery, resultData.length); + } catch (error) { + console.error("Search error:", error); + setResults([]); + } finally { + setLoading(false); + } + }, + [trackSearch] + ); + + useEffect(() => { + const debounce = setTimeout(() => { + if (query) { + handleSearch(query); + } else { + setResults([]); + } + }, 300); + + return () => clearTimeout(debounce); + }, [query, handleSearch]); + + return ( + <> + + + + + + Search Documentation +
+ + setQuery(e.target.value)} + autoFocus + /> +
+
+ +
+ {loading && ( +
+ Searching... +
+ )} + + {!loading && query && results.length === 0 && ( +
+ No results found for "{query}" +
+ )} + + {!loading && results.length > 0 && ( + + +
+ + ); +} + diff --git a/apps/framework-docs-v2/src/components/search/pagefind-loader.tsx b/apps/framework-docs-v2/src/components/search/pagefind-loader.tsx new file mode 100644 index 0000000000..e749db6e55 --- /dev/null +++ b/apps/framework-docs-v2/src/components/search/pagefind-loader.tsx @@ -0,0 +1,17 @@ +"use client"; + +import { useEffect } from "react"; +import Script from "next/script"; + +export function PagefindLoader() { + return ( +