diff --git a/client-new/.gitignore b/client-new/.gitignore new file mode 100644 index 0000000..c207408 --- /dev/null +++ b/client-new/.gitignore @@ -0,0 +1,58 @@ +# Dependencies +node_modules/ +.pnp +.pnp.js + +# Build outputs +dist/ +.vocs/ +build/ +out/ + +# Environment variables +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +# OS files +.DS_Store +Thumbs.db + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +# TypeScript +*.tsbuildinfo + +# Testing +coverage/ +.nyc_output + +# Misc +*.log +.cache +.parcel-cache +.vercel +.turbo + +# Temporary files +*.tmp +*.temp +.tmp/ +.temp/ \ No newline at end of file diff --git a/client-new/components/CTA.tsx b/client-new/components/CTA.tsx new file mode 100644 index 0000000..cd6bdd8 --- /dev/null +++ b/client-new/components/CTA.tsx @@ -0,0 +1,83 @@ +import { ReactNode } from 'react' + +interface CTAProps { + title: string + description?: string + children?: ReactNode + variant?: 'default' | 'gradient' +} + +export function CTA({ title, description, children, variant = 'default' }: CTAProps) { + const bgClass = variant === 'gradient' + ? 'bg-gradient-to-r from-dojo-primary to-dojo-light' + : 'bg-surface border border-dojo-primary/20' + + const textClass = variant === 'gradient' + ? 'text-white' + : 'text-text-primary' + + const descClass = variant === 'gradient' + ? 'text-white/90' + : 'text-text-secondary' + + return ( +
+
+
+

+ {title} +

+ {description && ( +

+ {description} +

+ )} + {children && ( +
+ {children} +
+ )} +
+
+
+ ) +} + +interface CTAButtonProps { + href: string + children: ReactNode + variant?: 'primary' | 'white' + external?: boolean +} + +export function CTAButton({ href, children, variant = 'primary', external = false }: CTAButtonProps) { + const className = variant === 'white' + ? 'inline-flex items-center px-6 py-3 rounded-lg bg-white text-dojo-primary font-semibold hover:bg-gray-100 transition-colors shadow-lg' + : 'inline-flex items-center px-6 py-3 rounded-lg bg-dojo-primary text-white font-semibold hover:bg-dojo-hover transition-colors shadow-lg' + + const content = ( + <> + {children} + + + ) + + if (external) { + return ( + + {content} + + ) + } + + return ( + + {content} + + ) +} \ No newline at end of file diff --git a/client-new/components/ErrorBoundary.tsx b/client-new/components/ErrorBoundary.tsx new file mode 100644 index 0000000..800a237 --- /dev/null +++ b/client-new/components/ErrorBoundary.tsx @@ -0,0 +1,90 @@ +import React from 'react' + +interface ErrorBoundaryState { + hasError: boolean + error?: Error +} + +interface ErrorBoundaryProps { + children: React.ReactNode + fallback?: React.ComponentType<{ error?: Error; resetError?: () => void }> +} + +class ErrorBoundary extends React.Component { + constructor(props: ErrorBoundaryProps) { + super(props) + this.state = { hasError: false } + } + + static getDerivedStateFromError(error: Error): ErrorBoundaryState { + return { hasError: true, error } + } + + componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { + console.error('ErrorBoundary caught an error:', error, errorInfo) + } + + resetError = () => { + this.setState({ hasError: false, error: undefined }) + } + + render() { + if (this.state.hasError) { + const FallbackComponent = this.props.fallback || DefaultErrorFallback + return + } + + return this.props.children + } +} + +interface ErrorFallbackProps { + error?: Error + resetError?: () => void +} + +function DefaultErrorFallback({ error, resetError }: ErrorFallbackProps) { + return ( +
+
+
⚠️
+

+ Something went wrong +

+

+ We encountered an unexpected error. This has been logged and we'll look into it. +

+ + {error && ( +
+ + Error details + +
+              {error.message}
+            
+
+ )} + +
+ {resetError && ( + + )} + +
+
+
+ ) +} + +export { ErrorBoundary, type ErrorBoundaryProps, type ErrorFallbackProps } \ No newline at end of file diff --git a/client-new/components/Features.tsx b/client-new/components/Features.tsx new file mode 100644 index 0000000..2bad845 --- /dev/null +++ b/client-new/components/Features.tsx @@ -0,0 +1,133 @@ +import { ReactNode } from 'react' + +interface Feature { + icon: string | ReactNode + title: string + description: string + href?: string +} + +interface FeaturesProps { + title?: string + subtitle?: string + features: Feature[] + columns?: 2 | 3 | 4 +} + +export function Features({ title, subtitle, features, columns = 3 }: FeaturesProps) { + const gridCols = { + 2: 'grid-cols-1 md:grid-cols-2', + 3: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3', + 4: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-4', + } + + return ( +
+
+ {(title || subtitle) && ( +
+ {subtitle && ( +

+ {subtitle} +

+ )} + {title && ( +

+ {title} +

+ )} +
+ )} + +
+ {features.map((feature, index) => ( + + ))} +
+
+
+ ) +} + +interface FeatureCardProps extends Feature {} + +export function FeatureCard({ icon, title, description, href }: FeatureCardProps) { + const content = ( + <> +
+ {icon} +
+

+ {title} +

+

+ {description} +

+ {href && ( +
+ Learn more → +
+ )} + + ) + + if (href) { + return ( + + {content} + + ) + } + + return ( +
+ {content} +
+ ) +} + +interface FeatureHighlightProps { + icon: string | ReactNode + title: string + description: string + image?: string + reverse?: boolean + children?: ReactNode +} + +export function FeatureHighlight({ icon, title, description, image, reverse = false, children }: FeatureHighlightProps) { + return ( +
+
+
+
+
+ {icon} +
+

+ {title} +

+

+ {description} +

+ {children} +
+ +
+ {image ? ( + {title} + ) : ( +
+
{icon}
+
+ )} +
+
+
+
+ ) +} \ No newline at end of file diff --git a/client-new/components/Hero.tsx b/client-new/components/Hero.tsx new file mode 100644 index 0000000..b9369a4 --- /dev/null +++ b/client-new/components/Hero.tsx @@ -0,0 +1,103 @@ +import { ReactNode } from 'react' + +interface HeroProps { + title: string | ReactNode + subtitle?: string + description?: string + children?: ReactNode +} + +export function Hero({ title, subtitle, description, children }: HeroProps) { + return ( +
+
+
+ {subtitle && ( +

+ {subtitle} +

+ )} + +

+ {title} +

+ + {description && ( +

+ {description} +

+ )} + + {children && ( +
+ {children} +
+ )} +
+
+ + {/* Gradient background effect */} +
+
+
+
+ ) +} + +interface HeroButtonProps { + href: string + children: ReactNode + variant?: 'primary' | 'secondary' + external?: boolean +} + +export function HeroButton({ href, children, variant = 'primary', external = false }: HeroButtonProps) { + const className = variant === 'primary' + ? 'inline-flex items-center px-6 py-3 rounded-lg bg-dojo-primary text-white font-semibold hover:bg-dojo-hover transition-colors shadow-lg hover:shadow-xl' + : 'inline-flex items-center px-6 py-3 rounded-lg bg-surface border border-border text-text-primary font-semibold hover:bg-surface-hover transition-colors' + + const content = ( + <> + {children} + + + ) + + if (external) { + return ( + + {content} + + ) + } + + return ( + + {content} + + ) +} + +interface HeroBadgeProps { + text: string + href?: string +} + +export function HeroBadge({ text, href }: HeroBadgeProps) { + const className = "inline-flex items-center px-3 py-1 rounded-full bg-dojo-primary/10 text-dojo-primary text-sm font-medium" + + if (href) { + return ( + + {text} + + ) + } + + return {text} +} \ No newline at end of file diff --git a/client-new/components/LoadingStates.tsx b/client-new/components/LoadingStates.tsx new file mode 100644 index 0000000..1d27bfa --- /dev/null +++ b/client-new/components/LoadingStates.tsx @@ -0,0 +1,117 @@ +import React from 'react' + +interface LoadingSpinnerProps { + size?: 'sm' | 'md' | 'lg' + className?: string +} + +export function LoadingSpinner({ size = 'md', className = '' }: LoadingSpinnerProps) { + const sizeClasses = { + sm: 'w-4 h-4', + md: 'w-6 h-6', + lg: 'w-8 h-8' + } + + return ( +
+ + + + +
+ ) +} + +interface LoadingDotsProps { + className?: string +} + +export function LoadingDots({ className = '' }: LoadingDotsProps) { + return ( +
+ {[0, 1, 2].map((i) => ( +
+ ))} +
+ ) +} + +interface LoadingSkeletonProps { + className?: string + lines?: number +} + +export function LoadingSkeleton({ className = '', lines = 3 }: LoadingSkeletonProps) { + return ( +
+ {Array.from({ length: lines }).map((_, i) => ( +
+ ))} +
+ ) +} + +interface FullPageLoadingProps { + message?: string +} + +export function FullPageLoading({ message = 'Loading...' }: FullPageLoadingProps) { + return ( +
+ +

{message}

+
+ ) +} + +interface ContentLoadingProps { + children: React.ReactNode + loading: boolean + error?: Error | null + fallback?: React.ReactNode +} + +export function ContentLoading({ children, loading, error, fallback }: ContentLoadingProps) { + if (error) { + return ( +
+

+ {error.message || 'An error occurred while loading content'} +

+
+ ) + } + + if (loading) { + return ( +
+ {fallback || } +
+ ) + } + + return <>{children} +} \ No newline at end of file diff --git a/client-new/components/Navigation.tsx b/client-new/components/Navigation.tsx new file mode 100644 index 0000000..6dea73a --- /dev/null +++ b/client-new/components/Navigation.tsx @@ -0,0 +1,137 @@ +import { ReactNode } from 'react' + +interface NavigationCardProps { + title: string + description: string + href: string + icon?: string + children?: ReactNode +} + +export function NavigationCard({ title, description, href, icon, children }: NavigationCardProps) { + return ( + +
+ {icon && ( +
+ {icon} +
+ )} +
+

+ {title} +

+

+ {description} +

+ {children && ( +
+ {children} +
+ )} +
+
+ → +
+
+
+ ) +} + +interface NavigationGridProps { + children: ReactNode + columns?: 1 | 2 | 3 +} + +export function NavigationGrid({ children, columns = 2 }: NavigationGridProps) { + const gridCols = { + 1: 'grid-cols-1', + 2: 'grid-cols-1 md:grid-cols-2', + 3: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3', + } + + return ( +
+ {children} +
+ ) +} + +interface BreadcrumbProps { + items: Array<{ + label: string + href?: string + }> +} + +export function Breadcrumb({ items }: BreadcrumbProps) { + return ( + + ) +} + +interface NextPrevProps { + prev?: { + label: string + href: string + } + next?: { + label: string + href: string + } +} + +export function NextPrev({ prev, next }: NextPrevProps) { + return ( +
+ + + +
+ ) +} \ No newline at end of file diff --git a/client-new/components/Stats.tsx b/client-new/components/Stats.tsx new file mode 100644 index 0000000..dd5c706 --- /dev/null +++ b/client-new/components/Stats.tsx @@ -0,0 +1,108 @@ +interface Stat { + label: string + value: string | number + suffix?: string +} + +interface StatsProps { + title?: string + subtitle?: string + stats: Stat[] +} + +export function Stats({ title, subtitle, stats }: StatsProps) { + return ( +
+
+ {(title || subtitle) && ( +
+ {subtitle && ( +

+ {subtitle} +

+ )} + {title && ( +

+ {title} +

+ )} +
+ )} + +
+ {stats.map((stat, index) => ( +
+
+ {stat.value}{stat.suffix} +
+
+ {stat.label} +
+
+ ))} +
+
+
+ ) +} + +interface TimelineItem { + date: string + title: string + description: string + icon?: string +} + +interface TimelineProps { + title?: string + subtitle?: string + items: TimelineItem[] +} + +export function Timeline({ title, subtitle, items }: TimelineProps) { + return ( +
+
+ {(title || subtitle) && ( +
+ {subtitle && ( +

+ {subtitle} +

+ )} + {title && ( +

+ {title} +

+ )} +
+ )} + +
+ {/* Vertical line */} +
+ + {items.map((item, index) => ( +
+ {/* Dot */} +
+ {item.icon && ( +
+ {item.icon} +
+ )} +
+ + {/* Content */} +
+
{item.date}
+

{item.title}

+

{item.description}

+
+
+ ))} +
+
+
+ ) +} \ No newline at end of file diff --git a/client-new/components/hooks/useAsyncState.tsx b/client-new/components/hooks/useAsyncState.tsx new file mode 100644 index 0000000..56c10d7 --- /dev/null +++ b/client-new/components/hooks/useAsyncState.tsx @@ -0,0 +1,88 @@ +import { useState, useCallback, useEffect } from 'react' + +interface AsyncState { + data: T | null + loading: boolean + error: Error | null +} + +interface UseAsyncStateOptions { + initialData?: any + executeOnMount?: boolean +} + +export function useAsyncState( + asyncFunction?: () => Promise, + options: UseAsyncStateOptions = {} +): [ + AsyncState, + (newAsyncFunction?: () => Promise) => Promise, + () => void +] { + const { initialData = null, executeOnMount = false } = options + + const [state, setState] = useState>({ + data: initialData, + loading: false, + error: null, + }) + + const execute = useCallback(async (newAsyncFunction?: () => Promise) => { + const functionToExecute = newAsyncFunction || asyncFunction + + if (!functionToExecute) { + console.warn('No async function provided to execute') + return + } + + setState(prev => ({ ...prev, loading: true, error: null })) + + try { + const result = await functionToExecute() + setState({ + data: result, + loading: false, + error: null, + }) + } catch (error) { + setState({ + data: null, + loading: false, + error: error instanceof Error ? error : new Error(String(error)), + }) + } + }, [asyncFunction]) + + const reset = useCallback(() => { + setState({ + data: initialData, + loading: false, + error: null, + }) + }, [initialData]) + + useEffect(() => { + if (executeOnMount && asyncFunction) { + execute() + } + }, [execute, executeOnMount, asyncFunction]) + + return [state, execute, reset] +} + +// Helper hook for common loading patterns +export function useLoading(initialState = false) { + const [loading, setLoading] = useState(initialState) + + const withLoading = useCallback(async (asyncFn: () => Promise): Promise => { + setLoading(true) + try { + const result = await asyncFn() + return result + } finally { + setLoading(false) + } + }, []) + + return { loading, setLoading, withLoading } +} \ No newline at end of file diff --git a/client-new/components/mdx/Callout.tsx b/client-new/components/mdx/Callout.tsx new file mode 100644 index 0000000..8fd7ae8 --- /dev/null +++ b/client-new/components/mdx/Callout.tsx @@ -0,0 +1,96 @@ +import { ReactNode } from 'react' + +interface CalloutProps { + type?: 'info' | 'warning' | 'error' | 'success' | 'tip' + title?: string + children: ReactNode + emoji?: string +} + +export function Callout({ type = 'info', title, children, emoji }: CalloutProps) { + const styles = { + info: { + bg: 'bg-gradient-to-r from-blue-50 to-blue-100/50 dark:from-blue-950/30 dark:to-blue-900/20', + border: 'border-blue-200 dark:border-blue-800', + text: 'text-blue-700 dark:text-blue-300', + iconBg: 'bg-blue-100 dark:bg-blue-900/50', + iconText: 'text-blue-600 dark:text-blue-400', + icon: emoji || 'ℹ️' + }, + warning: { + bg: 'bg-gradient-to-r from-amber-50 to-orange-100/50 dark:from-amber-950/30 dark:to-orange-900/20', + border: 'border-amber-200 dark:border-amber-800', + text: 'text-amber-700 dark:text-amber-300', + iconBg: 'bg-amber-100 dark:bg-amber-900/50', + iconText: 'text-amber-600 dark:text-amber-400', + icon: emoji || '⚠️' + }, + error: { + bg: 'bg-gradient-to-r from-red-50 to-red-100/50 dark:from-red-950/30 dark:to-red-900/20', + border: 'border-red-200 dark:border-red-800', + text: 'text-red-700 dark:text-red-300', + iconBg: 'bg-red-100 dark:bg-red-900/50', + iconText: 'text-red-600 dark:text-red-400', + icon: emoji || '❌' + }, + success: { + bg: 'bg-gradient-to-r from-emerald-50 to-green-100/50 dark:from-emerald-950/30 dark:to-green-900/20', + border: 'border-emerald-200 dark:border-emerald-800', + text: 'text-emerald-700 dark:text-emerald-300', + iconBg: 'bg-emerald-100 dark:bg-emerald-900/50', + iconText: 'text-emerald-600 dark:text-emerald-400', + icon: emoji || '✅' + }, + tip: { + bg: 'bg-gradient-to-r from-violet-50 to-purple-100/50 dark:from-violet-950/30 dark:to-purple-900/20', + border: 'border-violet-200 dark:border-violet-800', + text: 'text-violet-700 dark:text-violet-300', + iconBg: 'bg-violet-100 dark:bg-violet-900/50', + iconText: 'text-violet-600 dark:text-violet-400', + icon: emoji || '💡' + } + } + + const style = styles[type] + + return ( +
+
+
+ {style.icon} +
+
+ {title && ( +

+ {title} +

+ )} +
+ {children} +
+
+
+
+ ) +} + +// Alias components for convenience +export function Info({ children, title }: Omit) { + return {children} +} + +export function Warning({ children, title }: Omit) { + return {children} +} + +export function Error({ children, title }: Omit) { + return {children} +} + +export function Success({ children, title }: Omit) { + return {children} +} + +export function Tip({ children, title }: Omit) { + return {children} +} \ No newline at end of file diff --git a/client-new/components/mdx/CodeBlock.tsx b/client-new/components/mdx/CodeBlock.tsx new file mode 100644 index 0000000..b0a887a --- /dev/null +++ b/client-new/components/mdx/CodeBlock.tsx @@ -0,0 +1,99 @@ +import { useState } from 'react' + +interface CodeBlockProps { + children: string + language?: string + filename?: string + highlight?: string // e.g., "1,3-5,10" + showLineNumbers?: boolean + title?: string +} + +export function CodeBlock({ + children, + language = 'text', + filename, + highlight, + showLineNumbers = false, + title +}: CodeBlockProps) { + const [copied, setCopied] = useState(false) + + const copyToClipboard = async () => { + await navigator.clipboard.writeText(children) + setCopied(true) + setTimeout(() => setCopied(false), 2000) + } + + // Parse highlighted lines + const highlightedLines = new Set() + if (highlight) { + highlight.split(',').forEach(part => { + if (part.includes('-')) { + const [start, end] = part.split('-').map(Number) + for (let i = start; i <= end; i++) { + highlightedLines.add(i) + } + } else { + highlightedLines.add(Number(part)) + } + }) + } + + const lines = children.split('\n') + + return ( +
+ {(filename || title) && ( +
+ + {filename || title} + + {language} +
+ )} + +
+ + +
+          
+            {showLineNumbers ? (
+              
+
+ {lines.map((_, i) => ( + + {i + 1} + + ))} +
+
+ {lines.map((line, i) => ( +
+ {line} +
+ ))} +
+
+ ) : ( + children + )} +
+
+
+
+ ) +} \ No newline at end of file diff --git a/client-new/components/mdx/Demo.tsx b/client-new/components/mdx/Demo.tsx new file mode 100644 index 0000000..3f86ecb --- /dev/null +++ b/client-new/components/mdx/Demo.tsx @@ -0,0 +1,158 @@ +import { useState, ReactNode } from 'react' + +interface DemoProps { + children: ReactNode + title?: string + description?: string + showCode?: boolean + code?: string +} + +export function Demo({ children, title, description, showCode = true, code }: DemoProps) { + const [isCodeVisible, setIsCodeVisible] = useState(false) + + return ( +
+ {(title || description) && ( +
+ {title && ( +

+ + {title} +

+ )} + {description && ( +

+ {description} +

+ )} +
+ )} + +
+ {children} +
+ + {showCode && code && ( + <> +
+ +
+ + {isCodeVisible && ( +
+
+                {code}
+              
+
+ )} + + )} +
+ ) +} + +interface PlaygroundProps { + children: ReactNode + title?: string + controls?: ReactNode +} + +export function Playground({ children, title, controls }: PlaygroundProps) { + return ( +
+ {title && ( +
+

+ + {title} + Interactive +

+
+ )} + + {controls && ( +
+
+ {controls} +
+
+ )} + +
+ {children} +
+
+ ) +} + +interface InteractiveExampleProps { + initialCode: string + onRun: (code: string) => void + output?: string + language?: string +} + +export function InteractiveExample({ + initialCode, + onRun, + output +}: InteractiveExampleProps) { + const [code, setCode] = useState(initialCode) + const [isRunning, setIsRunning] = useState(false) + + const handleRun = () => { + setIsRunning(true) + onRun(code) + setIsRunning(false) + } + + return ( +
+
+
+ Code Editor + +
+