diff --git a/src/landing/Footer.jsx b/src/landing/Footer.jsx index e373948ad..f50cc0637 100644 --- a/src/landing/Footer.jsx +++ b/src/landing/Footer.jsx @@ -29,7 +29,8 @@ const navigation = [ { name: 'Blog', href: '/blog' }, { name: 'Documentation', href: '/docs/intro' }, { name: 'Changelog', href: '/changelog' }, - { name: 'Roadmap', href: '/roadmap' } +{ name: 'Roadmap', href: '/roadmap' }, + { name: 'Examples', href: '/examples' } ], title: 'Resources' }, diff --git a/src/landing/LandingHeader.jsx b/src/landing/LandingHeader.jsx index 1f5df77e4..2397a384b 100644 --- a/src/landing/LandingHeader.jsx +++ b/src/landing/LandingHeader.jsx @@ -342,7 +342,7 @@ export default function LandingHeader() { Case studies diff --git a/src/landing/PublicAppCard.tsx b/src/landing/PublicAppCard.tsx new file mode 100644 index 000000000..56e3fa986 --- /dev/null +++ b/src/landing/PublicAppCard.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import Link from '@docusaurus/Link'; +import { ArrowRight } from 'lucide-react'; + +interface PublicAppCardProps { + title: string; + description: string; + href: string; + thumbnail?: string; +} + +export default function PublicAppCard({ + title, + description, + href, + thumbnail +}: PublicAppCardProps) { + return ( + + {/* Top section with thumbnail */} +
+ {thumbnail && ( + {title} + )} +
+ + {/* Content section */} +
+

+ {title} +

+

+ {description} +

+ {/* Bottom section with link */} +
+ + View app + + +
+
+ + ); +} diff --git a/src/landing/PublicAppLayout.tsx b/src/landing/PublicAppLayout.tsx new file mode 100644 index 000000000..d74cdd802 --- /dev/null +++ b/src/landing/PublicAppLayout.tsx @@ -0,0 +1,513 @@ +import React, { useState } from 'react'; +import Head from '@docusaurus/Head'; +import Link from '@docusaurus/Link'; +import LayoutProvider from '@theme/Layout/Provider'; +import LandingHeader from './LandingHeader'; +import Footer from './Footer'; +import RadialBlur from './RadialBlur'; +import { motion } from 'framer-motion'; +import { ArrowLeft, ExternalLink, Play, Code, ChevronRight, ChevronDown, FileCode, Folder, Github, Copy, Check } from 'lucide-react'; +import { PublicApp } from './publicAppsData'; +import { FileTreeItem, FlowStep } from './public-apps/types'; + +interface PublicAppLayoutProps { + publicApp: PublicApp; +} + +function GitHubDropdown({ repoUrl }: { repoUrl: string }) { + const [isOpen, setIsOpen] = useState(false); + const [copied, setCopied] = useState(false); + const cloneCommand = `git clone ${repoUrl}.git`; + + const handleCopy = () => { + navigator.clipboard.writeText(cloneCommand); + setCopied(true); + setTimeout(() => { + setCopied(false); + setIsOpen(false); + }, 1500); + }; + + return ( +
+ + + {isOpen && ( + <> +
setIsOpen(false)} /> +
+
+

+ Repository +

+
+
+ + setIsOpen(false)} + > + + Open in GitHub + +
+
+ + )} +
+ ); +} + + +function FileTreeNode({ + item, + depth = 0, + selectedFile, + onSelectFile +}: { + item: FileTreeItem; + depth?: number; + selectedFile: string; + onSelectFile: (name: string) => void; +}) { + const [isOpen, setIsOpen] = useState(true); + const isFolder = item.type === 'folder'; + const isSelected = item.name === selectedFile; + + const handleClick = () => { + if (isFolder) { + setIsOpen(!isOpen); + } else { + onSelectFile(item.name); + } + }; + + return ( +
+
+ {isFolder ? ( + <> + {isOpen ? ( + + ) : ( + + )} + + + ) : ( + <> + + + + )} + {item.name} +
+ {isFolder && isOpen && item.children && ( +
+ {item.children.map((child, index) => ( + + ))} +
+ )} +
+ ); +} + +function FlowStepBox({ step }: { step: FlowStep }) { + if (step.type === 'input' || step.type === 'output') { + return ( +
+ {step.label} +
+ ); + } + + const iconClass = "w-6 h-6 flex-shrink-0"; + const Icon = step.type === 'ai' ? ( + + + + ) : step.type === 'return' ? ( + + + + + ) : ( + + + + ); + + const isParallelScript = step.type === 'script' && !step.tag; + + return ( +
+ {Icon} + {step.label} + {step.tag && ( + + {step.tag} + + )} +
+ ); +} + +function FlowViewer({ steps, fileName, flowDescriptions }: { steps: FlowStep[]; fileName: string; flowDescriptions?: Record }) { + const description = flowDescriptions?.[fileName]; + + return ( +
+ {/* Description */} + {description && ( +
+ {description.map((line, i) => ( +

{line}

+ ))} +
+ )} + {/* Flow diagram */} +
+
+ {steps.map((step, index) => ( + + {/* Parallel steps */} + {step.parallel ? ( +
+ {step.parallel.map((pStep) => ( + + ))} +
+ ) : ( + + )} + {/* Connecting line */} + {index < steps.length - 1 && ( +
+ )} + + ))} +
+
+
+ ); +} + +interface MockCodeEditorProps { + fileTree: FileTreeItem[]; + fileContents: Record; + flowData?: Record; + flowDescriptions?: Record; +} + +function MockCodeEditor({ fileTree, fileContents, flowData, flowDescriptions }: MockCodeEditorProps) { + const [selectedFile, setSelectedFile] = useState('App.tsx'); + const code = fileContents[selectedFile] || ''; + const isFlow = flowData && selectedFile in flowData; + + return ( +
+ {/* File tree sidebar */} +
+
+ + Files + +
+
+ {fileTree.map((item, index) => ( + + ))} +
+
+ + {/* Code editor / Flow viewer */} +
+ {/* Content - either flow or code */} + {isFlow && flowData ? ( + + ) : ( +
+
+							
+								{code.split('\n').map((line, index) => (
+									
+ + {index + 1} + + +
+ ))} +
+
+
+ )} +
+
+ ); +} + +function highlightCode(line: string): string { + // Escape HTML first + let result = line + .replace(/&/g, '&') + .replace(//g, '>'); + + // Use placeholders to avoid regex conflicts with class names + const replacements: string[] = []; + const placeholder = (content: string, className: string) => { + const idx = replacements.length; + replacements.push(`${content}`); + return `__HIGHLIGHT_${idx}__`; + }; + + // Process in order: comments, strings, keywords, booleans, numbers + result = result.replace(/(\/\/.*$)/g, (match) => placeholder(match, 'text-gray-500')); + result = result.replace(/('.*?'|".*?")/g, (match) => placeholder(match, 'text-green-600 dark:text-green-400')); + result = result.replace(/\b(import|from|export|function|const|let|var|return|try|catch|finally|async|await|interface|type)\b/g, (match) => placeholder(match, 'text-purple-600 dark:text-purple-400')); + result = result.replace(/\b(true|false|null|undefined|new)\b/g, (match) => placeholder(match, 'text-orange-600 dark:text-orange-400')); + result = result.replace(/\b(\d+)\b/g, (match) => placeholder(match, 'text-blue-600 dark:text-blue-400')); + + // Replace placeholders with actual spans + replacements.forEach((span, idx) => { + result = result.replace(`__HIGHLIGHT_${idx}__`, span); + }); + + return result; +} + + +export default function PublicAppLayout({ publicApp }: PublicAppLayoutProps) { + const { title, description, iframeUrl, repoUrl, builtWith, features, codeData } = publicApp; + const [activeView, setActiveView] = useState<'app' | 'code'>('app'); + + return ( + +
+ + {title} | Apps | Windmill + + + + + + + +
+
+ {/* Back link */} + + + Back to apps + + + {/* Header section */} + +

+ {title} +

+

+ {description} +

+ + {features && features.length > 0 && ( +
    + {features.map((feature) => ( +
  • + + + + {feature} +
  • + ))} +
+ )} + +
+
+ {builtWith.map((tag) => ( + + {tag} + + ))} +
+ + Add to my workspace + +
+
+ + {/* Iframe container */} + +
+ {/* App header bar */} +
+
+
+
+
+
+ + {/* View toggle */} +
+ + +
+ +
+ {title} +
+ {activeView === 'app' ? ( + !iframeUrl && e.preventDefault()} + > + Open in new tab + + + ) : repoUrl ? ( + + ) : null} +
+ + {/* Content */} +
+ {/* App view */} +
+ {iframeUrl ? ( +