diff --git a/apps/docs/app/layout.tsx b/apps/docs/app/layout.tsx index 91eeb72f972..ea06926c72a 100644 --- a/apps/docs/app/layout.tsx +++ b/apps/docs/app/layout.tsx @@ -18,7 +18,7 @@ export const metadata = { metadataBase: new URL('https://docs.sim.ai'), title: { default: 'Sim Documentation β€” Build AI Agents & Run Your Agentic Workforce', - template: '%s', + template: '%s | Sim Docs', }, description: 'Documentation for Sim β€” the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows.', diff --git a/apps/sim/app/(home)/components/footer/footer.tsx b/apps/sim/app/(home)/components/footer/footer.tsx index 35a12d31b5c..33517a74135 100644 --- a/apps/sim/app/(home)/components/footer/footer.tsx +++ b/apps/sim/app/(home)/components/footer/footer.tsx @@ -26,6 +26,8 @@ const RESOURCES_LINKS: FooterItem[] = [ { label: 'Blog', href: '/blog' }, // { label: 'Templates', href: '/templates' }, { label: 'Docs', href: 'https://docs.sim.ai', external: true }, + { label: 'Academy', href: '/academy' }, + { label: 'Partners', href: '/partners' }, { label: 'Careers', href: 'https://jobs.ashbyhq.com/sim', external: true }, { label: 'Changelog', href: '/changelog' }, ] diff --git a/apps/sim/app/(landing)/blog/components/blog-image.tsx b/apps/sim/app/(landing)/blog/components/blog-image.tsx new file mode 100644 index 00000000000..84be2e6b2f5 --- /dev/null +++ b/apps/sim/app/(landing)/blog/components/blog-image.tsx @@ -0,0 +1,43 @@ +'use client' + +import { useState } from 'react' +import NextImage from 'next/image' +import { cn } from '@/lib/core/utils/cn' +import { Lightbox } from '@/app/(landing)/blog/components/lightbox' + +interface BlogImageProps { + src: string + alt?: string + width?: number + height?: number + className?: string +} + +export function BlogImage({ src, alt = '', width = 800, height = 450, className }: BlogImageProps) { + const [isLightboxOpen, setIsLightboxOpen] = useState(false) + + return ( + <> + setIsLightboxOpen(true)} + /> + setIsLightboxOpen(false)} + src={src} + alt={alt} + /> + + ) +} diff --git a/apps/sim/app/(landing)/blog/components/lightbox.tsx b/apps/sim/app/(landing)/blog/components/lightbox.tsx new file mode 100644 index 00000000000..edc83015f98 --- /dev/null +++ b/apps/sim/app/(landing)/blog/components/lightbox.tsx @@ -0,0 +1,62 @@ +'use client' + +import { useEffect, useRef } from 'react' + +interface LightboxProps { + isOpen: boolean + onClose: () => void + src: string + alt: string +} + +export function Lightbox({ isOpen, onClose, src, alt }: LightboxProps) { + const overlayRef = useRef(null) + + useEffect(() => { + if (!isOpen) return + + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + onClose() + } + } + + const handleClickOutside = (event: MouseEvent) => { + if (overlayRef.current && event.target === overlayRef.current) { + onClose() + } + } + + document.addEventListener('keydown', handleKeyDown) + document.addEventListener('click', handleClickOutside) + document.body.style.overflow = 'hidden' + + return () => { + document.removeEventListener('keydown', handleKeyDown) + document.removeEventListener('click', handleClickOutside) + document.body.style.overflow = 'unset' + } + }, [isOpen, onClose]) + + if (!isOpen) return null + + return ( +
+
+ {alt} +
+
+ ) +} diff --git a/apps/sim/app/(landing)/partners/page.tsx b/apps/sim/app/(landing)/partners/page.tsx new file mode 100644 index 00000000000..253a5283838 --- /dev/null +++ b/apps/sim/app/(landing)/partners/page.tsx @@ -0,0 +1,291 @@ +import type { Metadata } from 'next' +import Link from 'next/link' +import { getNavBlogPosts } from '@/lib/blog/registry' +import { martianMono } from '@/app/_styles/fonts/martian-mono/martian-mono' +import { season } from '@/app/_styles/fonts/season/season' +import Footer from '@/app/(home)/components/footer/footer' +import Navbar from '@/app/(home)/components/navbar/navbar' + +export const metadata: Metadata = { + title: 'Partner Program', + description: + 'Join the Sim partner program. Build, deploy, and sell AI workflow solutions. Earn your certification through Sim Academy.', + metadataBase: new URL('https://sim.ai'), + openGraph: { + title: 'Partner Program | Sim', + description: 'Join the Sim partner program.', + type: 'website', + }, +} + +const PARTNER_TIERS = [ + { + name: 'Certified Partner', + badge: 'Entry', + color: '#3A3A3A', + requirements: ['Complete Sim Academy certification', 'Deploy at least 1 live workflow'], + perks: [ + 'Official partner badge', + 'Listed in partner directory', + 'Early access to new features', + ], + }, + { + name: 'Silver Partner', + badge: 'Growth', + color: '#5A5A5A', + requirements: [ + 'All Certified requirements', + '3+ active client deployments', + 'Sim Academy advanced certification', + ], + perks: [ + 'All Certified perks', + 'Dedicated partner Slack channel', + 'Co-marketing opportunities', + 'Priority support', + ], + }, + { + name: 'Gold Partner', + badge: 'Premier', + color: '#8B7355', + requirements: [ + 'All Silver requirements', + '10+ active client deployments', + 'Sim solutions architect certification', + ], + perks: [ + 'All Silver perks', + 'Revenue share program', + 'Joint case studies', + 'Dedicated partner success manager', + 'Influence product roadmap', + ], + }, +] + +const HOW_IT_WORKS = [ + { + step: '01', + title: 'Sign up & complete Sim Academy', + description: + 'Create an account and work through the Sim Academy certification program. Learn to build, integrate, and deploy AI workflows through hands-on canvas exercises.', + }, + { + step: '02', + title: 'Build & deploy real solutions', + description: + 'Put your skills to work. Build workflow automations for clients, integrate Sim into existing products, or create your own Sim-powered applications.', + }, + { + step: '03', + title: 'Get certified & grow', + description: + 'Earn your partner certification and unlock perks, co-marketing opportunities, and revenue share as you scale your practice.', + }, +] + +const BENEFITS = [ + { + icon: 'πŸŽ“', + title: 'Interactive Learning', + description: + 'Learn on the real Sim canvas with drag-and-drop exercises, instant feedback, and guided exercises β€” not just videos.', + }, + { + icon: '🀝', + title: 'Co-Marketing', + description: + 'Get listed in the Sim partner directory, featured in case studies, and promoted to the Sim user base.', + }, + { + icon: 'πŸ’°', + title: 'Revenue Share', + description: 'Gold partners earn revenue share on referred customers and managed deployments.', + }, + { + icon: 'πŸš€', + title: 'Early Access', + description: + 'Partners get early access to new Sim features, APIs, and integrations before they launch publicly.', + }, + { + icon: 'πŸ› οΈ', + title: 'Technical Support', + description: + 'Priority technical support, private Slack access, and a dedicated partner success manager for Gold partners.', + }, + { + icon: 'πŸ“£', + title: 'Community', + description: + 'Join a growing community of Sim builders. Share workflows, collaborate on solutions, and shape the product roadmap.', + }, +] + +export default async function PartnersPage() { + const blogPosts = await getNavBlogPosts() + + return ( +
+
+ +
+ +
+ {/* Hero */} +
+
+
+ Partner Program +
+

+ Build the future +
+ of AI automation +

+

+ Become a certified Sim partner. Complete Sim Academy, deploy real solutions, and earn + recognition in the growing ecosystem of AI workflow builders. +

+
+ + Start Sim Academy β†’ + + + Learn more + +
+
+
+ + {/* Benefits grid */} +
+
+
+ Why partner with Sim +
+
+ {BENEFITS.map((b) => ( +
+
{b.icon}
+

{b.title}

+

{b.description}

+
+ ))} +
+
+
+ + {/* How it works */} +
+
+
+ How it works +
+
+ {HOW_IT_WORKS.map((step) => ( +
+
+ {step.step} +
+
+

{step.title}

+

{step.description}

+
+
+ ))} +
+
+
+ + {/* Partner tiers */} +
+
+
+ Partner tiers +
+
+ {PARTNER_TIERS.map((tier) => ( +
+
+

{tier.name}

+ + {tier.badge} + +
+ +
+

+ Requirements +

+
    + {tier.requirements.map((r) => ( +
  • + + {r} +
  • + ))} +
+
+ +
+

Perks

+
    + {tier.perks.map((p) => ( +
  • + + {p} +
  • + ))} +
+
+
+ ))} +
+
+
+ + {/* CTA */} +
+
+

+ Ready to get started? +

+

+ Complete Sim Academy to earn your first certification and unlock partner benefits. + It's free to start β€” no credit card required. +

+ + Start Sim Academy β†’ + +
+
+
+ +
+
+ ) +} diff --git a/apps/sim/app/_shell/providers/theme-provider.tsx b/apps/sim/app/_shell/providers/theme-provider.tsx index 43a6f0af2b5..e64ec9232c0 100644 --- a/apps/sim/app/_shell/providers/theme-provider.tsx +++ b/apps/sim/app/_shell/providers/theme-provider.tsx @@ -25,6 +25,10 @@ export function ThemeProvider({ children, ...props }: ThemeProviderProps) { pathname.startsWith('/form') || pathname.startsWith('/oauth') + const isDarkModePage = pathname.startsWith('/academy') + + const forcedTheme = isLightModePage ? 'light' : isDarkModePage ? 'dark' : undefined + return ( {children} diff --git a/apps/sim/app/academy/(catalog)/[courseSlug]/components/course-progress.tsx b/apps/sim/app/academy/(catalog)/[courseSlug]/components/course-progress.tsx new file mode 100644 index 00000000000..ff0b9edd49e --- /dev/null +++ b/apps/sim/app/academy/(catalog)/[courseSlug]/components/course-progress.tsx @@ -0,0 +1,156 @@ +'use client' + +import { useEffect, useState } from 'react' +import { CheckCircle2, Circle, ExternalLink, GraduationCap, Loader2 } from 'lucide-react' +import Link from 'next/link' +import { getCompletedLessons } from '@/lib/academy/local-progress' +import type { Course } from '@/lib/academy/types' +import { useSession } from '@/lib/auth/auth-client' +import { useCourseCertificate, useIssueCertificate } from '@/hooks/queries/academy' + +interface CourseProgressProps { + course: Course + courseSlug: string +} + +export function CourseProgress({ course, courseSlug }: CourseProgressProps) { + // Start with an empty set so SSR and initial client render match, then hydrate from localStorage. + const [completedIds, setCompletedIds] = useState>(() => new Set()) + useEffect(() => { + setCompletedIds(getCompletedLessons()) + }, []) + const { data: session } = useSession() + const { data: fetchedCert } = useCourseCertificate(session ? course.id : undefined) + const { mutate: issueCertificate, isPending, data: issuedCert, error } = useIssueCertificate() + const certificate = fetchedCert ?? issuedCert + + const allLessons = course.modules.flatMap((m) => m.lessons) + const totalLessons = allLessons.length + const completedCount = allLessons.filter((l) => completedIds.has(l.id)).length + const percentComplete = totalLessons > 0 ? Math.round((completedCount / totalLessons) * 100) : 0 + + return ( + <> + {completedCount > 0 && ( +
+
+
+ Your progress + + {completedCount}/{totalLessons} lessons + +
+
+
+
+
+
+ )} + +
+
+ {course.modules.map((mod, modIndex) => ( +
+
+ Module {modIndex + 1} +
+
+

{mod.title}

+
+ {mod.lessons.map((lesson) => ( + + {completedIds.has(lesson.id) ? ( + + ) : ( + + )} + {lesson.title} + {lesson.lessonType} + {lesson.videoDurationSeconds && ( + + {Math.round(lesson.videoDurationSeconds / 60)} min + + )} + + ))} +
+
+ ))} +
+
+ + {totalLessons > 0 && completedCount === totalLessons && ( +
+
+ {certificate ? ( +
+
+ +
+

Certificate issued!

+

+ {certificate.certificateNumber} +

+
+
+ + View certificate + + +
+ ) : ( +
+
+ +
+

Course Complete!

+

+ {session + ? error + ? 'Something went wrong. Try again.' + : 'Claim your certificate of completion.' + : 'Sign in to claim your certificate.'} +

+
+
+ {session ? ( + + ) : ( + + Sign in + + )} +
+ )} +
+
+ )} + + ) +} diff --git a/apps/sim/app/academy/(catalog)/[courseSlug]/page.tsx b/apps/sim/app/academy/(catalog)/[courseSlug]/page.tsx new file mode 100644 index 00000000000..63da8de68d2 --- /dev/null +++ b/apps/sim/app/academy/(catalog)/[courseSlug]/page.tsx @@ -0,0 +1,68 @@ +import { Clock, GraduationCap } from 'lucide-react' +import type { Metadata } from 'next' +import Link from 'next/link' +import { notFound } from 'next/navigation' +import { COURSES, getCourse } from '@/lib/academy/content' +import { CourseProgress } from './components/course-progress' + +interface CourseDetailPageProps { + params: Promise<{ courseSlug: string }> +} + +export function generateStaticParams() { + return COURSES.map((course) => ({ courseSlug: course.slug })) +} + +export async function generateMetadata({ params }: CourseDetailPageProps): Promise { + const { courseSlug } = await params + const course = getCourse(courseSlug) + if (!course) return { title: 'Course Not Found' } + return { + title: course.title, + description: course.description, + } +} + +export default async function CourseDetailPage({ params }: CourseDetailPageProps) { + const { courseSlug } = await params + const course = getCourse(courseSlug) + + if (!course) notFound() + + return ( +
+
+
+ + ← All courses + +

+ {course.title} +

+ {course.description && ( +

+ {course.description} +

+ )} +
+ {course.estimatedMinutes && ( + + + {course.estimatedMinutes} min total + + )} + + + Certificate upon completion + +
+
+
+ + +
+ ) +} diff --git a/apps/sim/app/academy/(catalog)/certificate/[certificateNumber]/page.tsx b/apps/sim/app/academy/(catalog)/certificate/[certificateNumber]/page.tsx new file mode 100644 index 00000000000..3f70f454973 --- /dev/null +++ b/apps/sim/app/academy/(catalog)/certificate/[certificateNumber]/page.tsx @@ -0,0 +1,127 @@ +import { cache } from 'react' +import { db } from '@sim/db' +import { academyCertificate } from '@sim/db/schema' +import { eq } from 'drizzle-orm' +import { CheckCircle2, GraduationCap, XCircle } from 'lucide-react' +import type { Metadata } from 'next' +import { notFound } from 'next/navigation' +import type { AcademyCertificate } from '@/lib/academy/types' + +interface CertificatePageProps { + params: Promise<{ certificateNumber: string }> +} + +export async function generateMetadata({ params }: CertificatePageProps): Promise { + const { certificateNumber } = await params + const certificate = await fetchCertificate(certificateNumber) + if (!certificate) return { title: 'Certificate Not Found' } + return { + title: `${certificate.metadata?.courseTitle ?? 'Certificate'} β€” Certificate`, + description: `Verified certificate of completion awarded to ${certificate.metadata?.recipientName ?? 'a recipient'}.`, + } +} + +const fetchCertificate = cache( + async (certificateNumber: string): Promise => { + const [row] = await db + .select() + .from(academyCertificate) + .where(eq(academyCertificate.certificateNumber, certificateNumber)) + .limit(1) + return (row as unknown as AcademyCertificate) ?? null + } +) + +const DATE_FORMAT: Intl.DateTimeFormatOptions = { year: 'numeric', month: 'long', day: 'numeric' } +function formatDate(date: string | Date) { + return new Date(date).toLocaleDateString('en-US', DATE_FORMAT) +} + +function MetaRow({ label, children }: { label: string; children: React.ReactNode }) { + return ( +
+ {label} + {children} +
+ ) +} + +export default async function CertificatePage({ params }: CertificatePageProps) { + const { certificateNumber } = await params + const certificate = await fetchCertificate(certificateNumber) + + if (!certificate) notFound() + + return ( +
+
+
+
+
+ +
+
+ +
+ Certificate of Completion +
+ +

+ {certificate.metadata?.courseTitle} +

+ + {certificate.metadata?.recipientName && ( +

+ Awarded to{' '} + {certificate.metadata.recipientName} +

+ )} + + {certificate.status === 'active' ? ( +
+ + Verified +
+ ) : ( +
+ + {certificate.status} +
+ )} +
+ +
+ + + {certificate.certificateNumber} + + + + {formatDate(certificate.issuedAt)} + + + + {certificate.status} + + + {certificate.expiresAt && ( + + + {formatDate(certificate.expiresAt)} + + + )} +
+ +

+ This certificate was issued by Sim AI, Inc. and verifies the holder has completed the{' '} + {certificate.metadata?.courseTitle} program. +

+
+
+ ) +} diff --git a/apps/sim/app/academy/(catalog)/layout.tsx b/apps/sim/app/academy/(catalog)/layout.tsx new file mode 100644 index 00000000000..fb400ef2834 --- /dev/null +++ b/apps/sim/app/academy/(catalog)/layout.tsx @@ -0,0 +1,16 @@ +import type React from 'react' +import { getNavBlogPosts } from '@/lib/blog/registry' +import Footer from '@/app/(home)/components/footer/footer' +import Navbar from '@/app/(home)/components/navbar/navbar' + +export default async function AcademyCatalogLayout({ children }: { children: React.ReactNode }) { + const blogPosts = await getNavBlogPosts() + + return ( + <> + + {children} +