diff --git a/app/(landing)/_components/hero-section.tsx b/app/(landing)/_components/hero-section.tsx new file mode 100644 index 0000000..e1ce1bc --- /dev/null +++ b/app/(landing)/_components/hero-section.tsx @@ -0,0 +1,116 @@ +'use client'; + +import { MdCheckCircle,MdDns, MdRocketLaunch } from 'react-icons/md'; + +import { Button } from '@/components/ui/button'; + +interface HeroSectionProps { + onGetStarted: () => void; + isLoading: boolean; + authError: string | null; + buttonText: string; // Dynamic button text based on auth/environment +} + +export function HeroSection({ + onGetStarted, + isLoading, + authError, + buttonText, +}: HeroSectionProps) { + return ( +
+ {/* Background glow effect */} +
+
+
+ +
+ {/* Version Badge */} +
+ + + + + v2.0 is now live +
+ + {/* Main Heading */} +

+ Ship at the
+ + Speed of Thought + +

+ + {/* Subtitle */} +

+ Agentic Full-Stack Development Platform +

+ + {/* Description */} +

+ Powered by Agents in isolated sandbox environments +

+ + {/* Error message */} + {authError && ( +
+ {authError} +
+ )} + + {/* CTA Buttons */} +
+ + + +
+ + {/* Features */} + {FEATURES_JSX} +
+
+ ); +} + +// Static JSX hoisted outside component to avoid recreation on every render +const FEATURES_JSX = ( +
+
+ + No config required +
+ +); diff --git a/app/(landing)/_components/landing-client.tsx b/app/(landing)/_components/landing-client.tsx new file mode 100644 index 0000000..3480da8 --- /dev/null +++ b/app/(landing)/_components/landing-client.tsx @@ -0,0 +1,144 @@ +'use client'; + +import { useCallback, useEffect, useRef, useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { useSession } from 'next-auth/react'; + +import { authenticateWithSealos } from '@/lib/actions/sealos-auth'; +import { useSealos } from '@/provider/sealos'; + +import { HeroSection } from './hero-section'; +import { LandingHeader } from './landing-header'; +import { TerminalDemo } from './terminal-demo'; + +interface LandingClientProps { + starCount: number | null; +} + +/** + * Client-side landing page shell. + * + * Handles all interactive logic (auth, navigation) while receiving + * server-fetched data (starCount) as props. + * + * Authentication Flow (v2.0.0-alpha-3): + * - Sealos environment: Auto-trigger auth on page load if unauthenticated + * - Non-Sealos + Authenticated: Show "Go to Projects" button + * - Non-Sealos + Unauthenticated: Show "Start Building Now" → /login + * - Authentication success: Update button text, user clicks to navigate + */ +export function LandingClient({ starCount }: LandingClientProps) { + const router = useRouter(); + const { status } = useSession(); + const { isInitialized, isLoading, isSealos, sealosToken, sealosKubeconfig } = useSealos(); + + const [isAuthenticating, setIsAuthenticating] = useState(false); + const [authError, setAuthError] = useState(null); + const hasAttemptedAuth = useRef(false); // Prevent duplicate auth attempts + + // Auto-trigger authentication in Sealos environment + useEffect(() => { + // Wait for Sealos initialization + if (!isInitialized || isLoading) return; + + // Already authenticated, no need to auth again + if (status === 'authenticated') return; + + // Not in Sealos, don't auto-authenticate + if (!isSealos) return; + + // Prevent duplicate attempts + if (hasAttemptedAuth.current) return; + hasAttemptedAuth.current = true; + + // Check credentials + if (!sealosToken || !sealosKubeconfig) { + queueMicrotask(() => { + setAuthError('Missing Sealos credentials'); + }); + return; + } + + // Trigger authentication + queueMicrotask(() => { + setIsAuthenticating(true); + }); + + authenticateWithSealos(sealosToken, sealosKubeconfig) + .then((result) => { + if (result.success) { + // Authentication successful - don't auto-redirect, let user click + setIsAuthenticating(false); + router.refresh(); // Refresh session + } else { + setAuthError(result.error || 'Authentication failed'); + setIsAuthenticating(false); + hasAttemptedAuth.current = false; // Allow retry + } + }) + .catch((error) => { + setAuthError(error instanceof Error ? error.message : 'Unknown error'); + setIsAuthenticating(false); + hasAttemptedAuth.current = false; // Allow retry + }); + }, [isInitialized, isLoading, status, isSealos, sealosToken, sealosKubeconfig, router]); + + // Handle Get Started button click + const handleGetStarted = useCallback(() => { + setAuthError(null); + + // Authenticated users go to projects + if (status === 'authenticated') { + router.push('/projects'); + return; + } + + // Non-Sealos environment - go to login + if (!isSealos) { + router.push('/login'); + return; + } + + // Sealos environment - retry authentication + hasAttemptedAuth.current = false; + }, [status, isSealos, router]); + + // Handle Sign In button click + const handleSignIn = useCallback(() => { + if (status === 'authenticated') { + router.push('/projects'); + } else if (isSealos) { + // Sealos environment - retry auth + hasAttemptedAuth.current = false; + setAuthError(null); + } else { + router.push('/login'); + } + }, [status, isSealos, router]); + + // Button state and text logic + const isInitializing = !isInitialized || isLoading; + const isButtonLoading = isInitializing || isAuthenticating; + const shouldShowGoToProjects = isSealos || status === 'authenticated'; + + return ( +
+ +
+ + +
+
+ ); +} diff --git a/app/(landing)/_components/landing-header.tsx b/app/(landing)/_components/landing-header.tsx new file mode 100644 index 0000000..dd8a02a --- /dev/null +++ b/app/(landing)/_components/landing-header.tsx @@ -0,0 +1,87 @@ +'use client'; + +import Image from 'next/image'; +import Link from 'next/link'; + +import { Button } from '@/components/ui/button'; + +interface LandingHeaderProps { + isAuthenticated: boolean; + isSealos: boolean; // Environment detection + onSignIn?: () => void; + starCount: number | null; + isLoading: boolean; // Loading state for button +} + +function formatStarCount(count: number): string { + if (count >= 1000) { + return new Intl.NumberFormat('en', { + notation: 'compact', + maximumFractionDigits: 1, + }).format(count); + } + return count.toString(); +} + +export function LandingHeader({ isAuthenticated, isSealos, onSignIn, starCount, isLoading }: LandingHeaderProps) { + return ( +
+
+ {/* Logo */} + + Fulling Logo + + Fulling + + + + {/* Navigation */} + +
+
+ ); +} + diff --git a/app/(landing)/_components/terminal-demo.tsx b/app/(landing)/_components/terminal-demo.tsx new file mode 100644 index 0000000..18c15ff --- /dev/null +++ b/app/(landing)/_components/terminal-demo.tsx @@ -0,0 +1,130 @@ +import { MdArrowOutward, MdAutoAwesome,MdCheck, MdTerminal } from 'react-icons/md'; + +export function TerminalDemo() { + return ( +
+ {/* Background gradient */} +
+
+ + {/* Terminal Window */} +
+ {/* Title Bar */} +
+
+
+
+
+
+
+ + Fulling — zsh +
+
+
+ + {/* Terminal Content */} +
+ {/* Command Input */} +
+
+ +
+
+ ~/projects + claude "Build a personal blog + with Next.js 16 and Payload CMS" +
+
+ + {/* Progress Steps */} +
+
+
+ Initializing project structure... +
+
+ Creating payload.config.ts... +
+
+ Setting up Next.js 16 App Router... +
+
+
+ + {/* File Tree */} +
+
+
# Generated file tree
+
+ + + + + + +
+
+ ... and 14 other files created. +
+
+ + {/* Check Items */} +
+ + + + +
+ + {/* Success Card */} +
+
+
+
+
+ Success! Your Blog is live +
+ + Live at your-blog-on.sealos.app + + +
+
+
+
+ + {/* Waiting Input */} +
+
+ +
+
+ Add a dark mode toggle to the navbar + +
+
+
+
+
+ ); +} + +function FileEntry({ name }: { name: string }) { + return ( +
+ + + {name} +
+ ); +} + +function CheckItem({ text }: { text: string }) { + return ( +
+ + {text} +
+ ); +} + diff --git a/app/(landing)/page.tsx b/app/(landing)/page.tsx new file mode 100644 index 0000000..fb1e3e5 --- /dev/null +++ b/app/(landing)/page.tsx @@ -0,0 +1,25 @@ +import { LandingClient } from './_components/landing-client'; + +/** + * Landing page — Server Component. + * + * Fetches the GitHub star count on the server with ISR caching (1 hour), + * then passes it down to the client-side shell. + */ +export default async function LandingPage() { + const starCount = await getStarCount(); + return ; +} + +async function getStarCount(): Promise { + try { + const res = await fetch('https://api.github.com/repos/FullAgent/fulling', { + next: { revalidate: 3600 }, // ISR: revalidate every hour + }); + if (!res.ok) return null; + const data = await res.json(); + return data.stargazers_count; + } catch { + return null; + } +} diff --git a/app/globals.css b/app/globals.css index ff6b425..4a22e0a 100644 --- a/app/globals.css +++ b/app/globals.css @@ -374,7 +374,12 @@ font-family: var(--font-body), sans-serif; } - h1, h2, h3, h4, h5, h6 { + h1, + h2, + h3, + h4, + h5, + h6 { font-family: var(--font-heading), sans-serif; } @@ -390,11 +395,9 @@ @apply leading-7 text-muted-foreground; } - span { - @apply text-muted-foreground; - } - - code, pre, .font-mono { + code, + pre, + .font-mono { font-family: var(--font-mono), monospace; } } @@ -414,4 +417,34 @@ .animate-fade-in { animation: fade-in 0.2s ease-out; +} + +/* Cursor blink animation for terminal demo */ +@keyframes blink { + + 0%, + 100% { + opacity: 1; + } + + 50% { + opacity: 0; + } +} + +.cursor-blink { + animation: blink 1s step-end infinite; +} + +/* Respect user motion preferences globally */ +@media (prefers-reduced-motion: reduce) { + + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } } \ No newline at end of file diff --git a/app/page.tsx b/app/page.tsx deleted file mode 100644 index 5ad9ced..0000000 --- a/app/page.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { HomePage } from '@/components/home-page'; - -export default async function Page() { - // Always render HomePage - it handles all states internally: - // - Authenticated users: Show marketing page with "Go to Projects" button - // - Unauthenticated users: Show marketing page with "Login" button - // - Sealos environment: Show marketing page with auto-auth overlay - return ; -} diff --git a/package.json b/package.json index 95d142c..f8a70eb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fulling", - "version": "2.0.0-alpha.1", + "version": "2.0.0-alpha.2", "description": "AI-Powered Full-Stack Development Platform", "author": "fanux", "license": "MIT", diff --git a/provider/sealos.tsx b/provider/sealos.tsx index ad31394..21c2b2a 100644 --- a/provider/sealos.tsx +++ b/provider/sealos.tsx @@ -13,6 +13,24 @@ interface SealosUserInfo { let sealosInitPromise: Promise | null = null; +/** + * Detect if running inside Sealos iframe environment + * Uses ancestorOrigins to check parent frame domain + */ +function isSealosIframe(): boolean { + if (typeof window === 'undefined') return false; + + try { + const ancestorOrigin = window.location.ancestorOrigins?.[0]; + if (!ancestorOrigin) return false; + + // Check if ancestor domain contains Sealos domains + return ancestorOrigin.includes('sealos.io') || ancestorOrigin.includes('sealos.run'); + } catch { + return false; + } +} + interface SealosContextType { isInitialized: boolean; isLoading: boolean; @@ -50,6 +68,27 @@ export function SealosProvider({ children }: { children: React.ReactNode }) { try { setState((prev) => ({ ...prev, isLoading: true, error: null })); + // First, check if we're in Sealos iframe environment + const isInSealosIframe = isSealosIframe(); + + if (!isInSealosIframe) { + // Not in Sealos environment, skip SDK initialization + console.info('Not in Sealos iframe environment'); + setState({ + isInitialized: true, + isLoading: false, + isSealos: false, + error: null, + sealosToken: null, + sealosKubeconfig: null, + sealosUser: null, + sealosNs: null, + }); + return; + } + + // In Sealos iframe, initialize SDK and get credentials + console.info('Detected Sealos iframe environment, initializing SDK...'); const cleanupApp = createSealosApp(); // get session info @@ -75,7 +114,7 @@ export function SealosProvider({ children }: { children: React.ReactNode }) { }; } catch (error) { console.info( - 'Maybe not in Sealos environment, Sealos initialization failed, error info:', + 'Sealos SDK initialization failed, falling back to non-Sealos mode:', error ); setState((prev) => ({