diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 43ca19e..95b32c9 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,5 +1,7 @@ import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; import { AuthProvider, useAuth } from './contexts/AuthContext'; +import { ThemeProvider } from './components/providers/ThemeProvider'; +import { TooltipProvider } from './components/ui/tooltip'; import { LoginPage } from './pages/LoginPage'; import { SignupPage } from './pages/SignupPage'; import { LandingPage } from './pages/LandingPage'; @@ -78,12 +80,29 @@ function AppRoutes() { ); } +/** + * Root application component that sets up global providers and routing. + * + * Wraps the app with theme and tooltip contexts, initializes the router, and + * provides authentication state to the route tree. + * + * @returns The root React element with ThemeProvider, TooltipProvider, BrowserRouter, and AuthProvider applied. + */ export function App() { return ( - - - - - + + + + + + + + + ); -} +} \ No newline at end of file diff --git a/frontend/src/components/landing/FAQ.tsx b/frontend/src/components/landing/FAQ.tsx new file mode 100644 index 0000000..7c58381 --- /dev/null +++ b/frontend/src/components/landing/FAQ.tsx @@ -0,0 +1,102 @@ +import { motion } from 'framer-motion' +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from '@/components/ui/accordion' + +const FAQS = [ + { + q: 'How is this different from GitHub search or grep?', + a: `Think about the last time you joined a new codebase. You knew what you needed ("the function that retries API calls") but had no idea what it was called. GitHub search and grep need exact keywords. If the function is named make_request_with_backoff, you'd never find it by searching "retry logic." + +CodeIntel actually understands what code does, not just what it's named. We use AI embeddings trained specifically on code semantics. So you can search "retry logic with exponential backoff" and find it, even if those words appear nowhere in the file.`, + }, + { + q: 'What languages do you support?', + a: `Right now, Python is our focus. We have deep support for Flask, FastAPI, and Django, meaning we understand routes, middleware, decorators, and framework-specific patterns. + +TypeScript and JavaScript are next on our roadmap, followed by Go and Rust. The good news: CodeIntel is completely open source. If you need a language we don't support yet, you can contribute a parser or sponsor its development. Our architecture is built to be language-agnostic from day one.`, + }, + { + q: 'How does MCP integration work?', + a: `MCP (Model Context Protocol) is how AI assistants like Claude and Cursor talk to external tools. CodeIntel runs as an MCP server that these tools can connect to. + +Once connected, your AI assistant gains the ability to semantically search your entire codebase. When you ask Claude "how does authentication work in this project?", it can actually find and read the relevant code instead of guessing. It's like giving your AI assistant a senior developer's knowledge of your codebase.`, + }, + { + q: 'Is my code stored on your servers?', + a: `We never store your raw source code. What we store are vector embeddings, which are mathematical representations of what your code does. Think of it like storing a summary, not the actual document. You can delete these anytime from your dashboard. + +But if you want complete control, you can self-host CodeIntel on your own infrastructure. It's fully open source under MIT license. Your code never leaves your servers, and you get the same search quality. Many teams with sensitive codebases go this route.`, + }, + { + q: 'How fast is indexing?', + a: `For most repositories under 100,000 lines of code, initial indexing takes about 1-2 minutes. We parse your code, generate embeddings, and build the search index in parallel. + +After the first index, updates are incremental. Change a file, and only that file gets re-indexed. This usually takes seconds. Search itself returns results in under 100ms regardless of how large your codebase is, because we're searching vectors, not scanning text.`, + }, +] + +/** + * Renders the FAQ section with an animated header, an accordion of frequently asked questions, and a contact link. + * + * @returns The section element containing the FAQ accordion and contact paragraph + */ +export function FAQ() { + return ( +
+
+ +

+ Questions? Answers. +

+
+ + + + {FAQS.map((faq, i) => ( + + + {faq.q} + + + {faq.a} + + + ))} + + + + + More questions?{' '} + + Get in touch + + +
+
+ ) +} \ No newline at end of file diff --git a/frontend/src/components/landing/Features.tsx b/frontend/src/components/landing/Features.tsx new file mode 100644 index 0000000..068e4f7 --- /dev/null +++ b/frontend/src/components/landing/Features.tsx @@ -0,0 +1,149 @@ +import { motion } from 'framer-motion' +import { Search, Zap, GitBranch, Brain, Lock, Terminal } from 'lucide-react' + +const FEATURES = [ + { + icon: Search, + title: 'Semantic Search', + description: 'Find code by what it does, not what it\'s named. Ask "retry logic with exponential backoff" and get exactly that.', + color: 'text-blue-400', + bg: 'bg-blue-500/10', + glow: 'group-hover:shadow-blue-500/20', + }, + { + icon: Brain, + title: 'Understands Context', + description: 'Knows the difference between a Flask route handler and a FastAPI dependency. Context-aware results every time.', + color: 'text-violet-400', + bg: 'bg-violet-500/10', + glow: 'group-hover:shadow-violet-500/20', + }, + { + icon: Zap, + title: 'Instant Results', + description: 'Sub-100ms search across your entire codebase. No waiting, no spinning, just answers.', + color: 'text-amber-400', + bg: 'bg-amber-500/10', + glow: 'group-hover:shadow-amber-500/20', + }, + { + icon: GitBranch, + title: 'Dependency Graph', + description: 'See how code connects. Trace imports, find usages, understand impact before you change anything.', + color: 'text-emerald-400', + bg: 'bg-emerald-500/10', + glow: 'group-hover:shadow-emerald-500/20', + }, + { + icon: Terminal, + title: 'MCP Integration', + description: 'Works with Claude, Cursor, and any MCP-compatible AI. Your AI assistant finally understands your code.', + color: 'text-cyan-400', + bg: 'bg-cyan-500/10', + glow: 'group-hover:shadow-cyan-500/20', + }, + { + icon: Lock, + title: 'Self-Host Option', + description: 'Keep your code private. Run CodeIntel on your own infrastructure with full control.', + color: 'text-rose-400', + bg: 'bg-rose-500/10', + glow: 'group-hover:shadow-rose-500/20', + }, +] + +const containerVariants = { + hidden: { opacity: 0 }, + visible: { + opacity: 1, + transition: { staggerChildren: 0.08, delayChildren: 0.1 } + } +} + +const itemVariants = { + hidden: { opacity: 0, y: 30, scale: 0.95 }, + visible: { + opacity: 1, + y: 0, + scale: 1, + transition: { type: 'spring', stiffness: 200, damping: 20 } + } +} + +/** + * Renders the "Features" section with an animated, responsive grid of feature cards. + * + * Each card displays an icon, title, and description and includes hover interactions and reveal animations. + * + * @returns A section element containing the animated, responsive grid of feature cards. + */ +export function Features() { + return ( +
+ {/* Subtle gradient background */} +
+ +
+ {/* Section header */} + +

+ Built for how developers actually work +

+

+ Not another code search tool. CodeIntel understands your codebase the way you do. +

+
+ + {/* Features grid */} + + {FEATURES.map((feature, index) => ( + + {/* Icon */} + + + + +

+ {feature.title} +

+

+ {feature.description} +

+ + {/* Subtle corner accent on hover */} +
+
+
+ + ))} + +
+
+ ) +} \ No newline at end of file diff --git a/frontend/src/components/landing/Footer.tsx b/frontend/src/components/landing/Footer.tsx new file mode 100644 index 0000000..8487564 --- /dev/null +++ b/frontend/src/components/landing/Footer.tsx @@ -0,0 +1,119 @@ +import { Github, Twitter } from 'lucide-react' + +const LINKS = { + product: [ + { label: 'Features', href: '#features' }, + { label: 'Pricing', href: '#pricing' }, + { label: 'Docs', href: '/docs' }, + { label: 'Changelog', href: '/changelog' }, + ], + company: [ + { label: 'About', href: '/about' }, + { label: 'Blog', href: '/blog' }, + { label: 'Careers', href: '/careers' }, + ], + legal: [ + { label: 'Privacy', href: '/privacy' }, + { label: 'Terms', href: '/terms' }, + ], +} + +/** + * Renders the site footer containing the brand, social icons, grouped link lists, and a bottom credit bar. + * + * The copyright year is computed dynamically from the current date. + * + * @returns A JSX element containing the footer layout. + */ +export function Footer() { + return ( + + ) +} \ No newline at end of file diff --git a/frontend/src/components/landing/GitHubStars.tsx b/frontend/src/components/landing/GitHubStars.tsx new file mode 100644 index 0000000..73ae020 --- /dev/null +++ b/frontend/src/components/landing/GitHubStars.tsx @@ -0,0 +1,40 @@ +import { Star } from 'lucide-react' +import { useGitHubStars } from '@/hooks/useGitHubStars' + +const REPO_URL = 'https://github.com/OpenCodeIntel/opencodeintel' + +/** + * Render a clickable GitHub repository badge that displays the repository's star count. + * + * The badge links to the repository, shows a GitHub icon, a star icon, and the star count; + * while the count is loading it displays "—", and counts >= 1000 are formatted as `X.Xk`. + * + * @returns A JSX element representing the clickable GitHub stars badge + */ +export function GitHubStars() { + const { stars, loading } = useGitHubStars() + + const formatStars = (count: number) => { + if (count >= 1000) return `${(count / 1000).toFixed(1)}k` + return count.toString() + } + + return ( + + +
+ + + {loading ? '—' : formatStars(stars || 0)} + +
+
+ ) +} \ No newline at end of file diff --git a/frontend/src/components/landing/Hero.tsx b/frontend/src/components/landing/Hero.tsx index d776570..2cb0bfa 100644 --- a/frontend/src/components/landing/Hero.tsx +++ b/frontend/src/components/landing/Hero.tsx @@ -1,6 +1,6 @@ -import { useRef, useEffect, useState } from 'react' +import { useRef, useEffect, useState, useCallback } from 'react' import { motion, AnimatePresence } from 'framer-motion' -import { Search, Loader2 } from 'lucide-react' +import { Loader2, Sparkles } from 'lucide-react' import { HeroSearch, type HeroSearchHandle } from './HeroSearch' import { useDemoSearch, DEMO_REPOS, type DemoRepo } from '@/hooks/useDemoSearch' import type { SearchResult } from '@/types' @@ -11,21 +11,89 @@ interface Props { const PYTHON_REPOS = DEMO_REPOS.filter(r => ['flask', 'fastapi'].includes(r.id)) +const TYPING_QUERIES = [ + 'authentication middleware', + 'database connection pool', + 'error handling patterns', + 'caching implementation', + 'request validation', +] + +// staggered text animation variants +const headlineVariants = { + hidden: { opacity: 0 }, + visible: { + opacity: 1, + transition: { staggerChildren: 0.08, delayChildren: 0.2 } + } +} + +const wordVariants = { + hidden: { opacity: 0, y: 20 }, + visible: { + opacity: 1, + y: 0, + transition: { duration: 0.4, ease: [0.25, 0.4, 0.25, 1] } + } +} + +/** + * Render the page hero with an animated headline, demo search input, repo switcher, and a preview result card. + * + * The component runs a simulated typing/demo search workflow until the user interacts, exposes a '/' keyboard + * shortcut to focus the search input, and invokes the optional `onResultsReady` callback whenever search results become available. + * + * @param onResultsReady - Optional callback invoked when results are ready with `(results, query, repoId, time)` + * @returns The hero section UI as a React element + */ export function Hero({ onResultsReady }: Props) { const searchRef = useRef(null) const cardRef = useRef(null) const { query, repo, results, loading, searchTime, setQuery, setRepo, search } = useDemoSearch(false) const [mousePos, setMousePos] = useState({ x: 0, y: 0 }) + const [isTyping, setIsTyping] = useState(true) + const [typingIndex, setTypingIndex] = useState(0) + const typingTimeoutRef = useRef>() useEffect(() => { if (results.length) onResultsReady?.(results, query, repo.id, searchTime) }, [results, query, repo.id, searchTime, onResultsReady]) + // Typing animation + useEffect(() => { + if (!isTyping) return + const currentQuery = TYPING_QUERIES[typingIndex] + let charIndex = 0 + const typeChar = () => { + if (charIndex <= currentQuery.length) { + setQuery(currentQuery.slice(0, charIndex)) + charIndex++ + typingTimeoutRef.current = setTimeout(typeChar, 60 + Math.random() * 40) + } else { + typingTimeoutRef.current = setTimeout(() => { + search() + typingTimeoutRef.current = setTimeout(() => { + setTypingIndex((i) => (i + 1) % TYPING_QUERIES.length) + }, 4000) + }, 500) + } + } + typingTimeoutRef.current = setTimeout(typeChar, 1500) + return () => { if (typingTimeoutRef.current) clearTimeout(typingTimeoutRef.current) } + }, [isTyping, typingIndex, setQuery, search]) + + const handleUserInput = useCallback((val: string) => { + setIsTyping(false) + if (typingTimeoutRef.current) clearTimeout(typingTimeoutRef.current) + setQuery(val) + }, [setQuery]) + useEffect(() => { const onKey = (e: KeyboardEvent) => { const tag = (e.target as HTMLElement).tagName if (e.key === '/' && tag !== 'INPUT' && tag !== 'TEXTAREA') { e.preventDefault() + setIsTyping(false) searchRef.current?.focus() } } @@ -39,72 +107,106 @@ export function Hero({ onResultsReady }: Props) { setMousePos({ x: e.clientX - rect.left, y: e.clientY - rect.top }) } - const switchRepo = (r: DemoRepo) => { - setRepo(r) - } - const topResult = results[0] + const line1Words = ['Stop', 'feeling', 'lost', 'in'] + const line2Words = ['unfamiliar', 'codebases.'] return ( -
- {/* Animated gradient orbs - Linear style */} +
+ {/* Grid pattern background */} +
+ + {/* Floating gradient orbs */}
- {/* Headline */} + {/* Badge */} -

- Find code by meaning, -
- - not by keywords. - -

-

- Stop grep-ing through thousands of files. -
- Describe what you need and get the exact function. -

+
+ + Now in beta + • Free for open source +
- {/* Search */} + {/* Staggered Headline */} +
+ + + {line1Words.map((word, i) => ( + + {word} + + ))} + + + {line2Words.map((word, i) => ( + + {word} + + ))} + + + + Describe what you're looking for in plain English. +
+ Get the exact function, class, or pattern. Instantly. +
+
+ + {/* Search with glow */} - search()} - searching={loading} - repoName={repo.name} - /> + {/* Animated glow behind search */} +
+
+ { setIsTyping(false); search() }} + searching={loading} + repoName={repo.name} + /> +
{/* Repo switcher */} @@ -112,119 +214,88 @@ export function Hero({ onResultsReady }: Props) { className="mt-4 flex items-center justify-center gap-2" initial={{ opacity: 0 }} animate={{ opacity: 1 }} - transition={{ duration: 0.4, delay: 0.2 }} + transition={{ duration: 0.5, delay: 0.7 }} > - Try on: + Try on: {PYTHON_REPOS.map(r => ( - + ))} - {/* Result card - only shows when loading or has results */} + {/* Result card */} {(loading || topResult) && ( -
- {/* Mouse glow effect */} +
- - {/* Card */} -
+
{loading ? ( - +
- - Searching {repo.name}... + + Searching {repo.name}...
-
-
-
-
+
+ + +
) : topResult ? ( - - {/* Header */} +
- Found in {searchTime}ms + Found in {searchTime}ms
- - {Math.round(topResult.score * 100)}% match - + {Math.round(topResult.score * 100)}% match
- {repo.name} + {repo.name}
- - {/* Content */}
- {topResult.name} - - {topResult.type} - + {topResult.name} + {topResult.type}
-
{topResult.file_path}
+
{topResult.file_path}
- - {/* Code preview */}
-
+
                               {topResult.content?.slice(0, 250)}...
                             
- - {/* Footer */} {results.length > 1 && (
- +{results.length - 1} more results + +{results.length - 1} more results
)} @@ -238,22 +309,38 @@ export function Hero({ onResultsReady }: Props) { {/* CTA */} - - Index your first repo free → - -

- Works with any Python repository • Now in beta -

+
+ + Index your first repo free + + + + + View on GitHub + +
+

Works with any Python repository • Self-host or cloud

) -} +} \ No newline at end of file diff --git a/frontend/src/components/landing/MobileMenu.tsx b/frontend/src/components/landing/MobileMenu.tsx new file mode 100644 index 0000000..3102761 --- /dev/null +++ b/frontend/src/components/landing/MobileMenu.tsx @@ -0,0 +1,76 @@ +import { Menu, X } from 'lucide-react' +import { Sheet, SheetContent, SheetTrigger, SheetClose } from '@/components/ui/sheet' +import { ThemeToggle } from './ThemeToggle' +import { GitHubStars } from './GitHubStars' + +interface MobileMenuProps { + onNavigate: (path: string) => void +} + +const NAV_LINKS = [ + { label: 'Features', href: '#features' }, + { label: 'Pricing', href: '#pricing' }, + { label: 'Docs', href: '/docs' }, +] + +/** + * Render a right-anchored slide-out mobile navigation menu with navigation links, authentication actions, and bottom utilities. + * + * @param onNavigate - Callback invoked with a path string when a navigation action is requested (called with '/login' for Sign in and '/signup' for Get Started). + * @returns A JSX element that displays the mobile navigation sheet. + */ +export function MobileMenu({ onNavigate }: MobileMenuProps) { + return ( + + + + + +
+ + +
+ +
+ + + + + + +
+ +
+
+ + +
+
+
+ + + ) +} \ No newline at end of file diff --git a/frontend/src/components/landing/Navbar.tsx b/frontend/src/components/landing/Navbar.tsx index 3f8d373..d4ff148 100644 --- a/frontend/src/components/landing/Navbar.tsx +++ b/frontend/src/components/landing/Navbar.tsx @@ -1,10 +1,27 @@ import { useState, useEffect } from 'react' import { useNavigate } from 'react-router-dom' +import { ThemeToggle } from './ThemeToggle' +import { GitHubStars } from './GitHubStars' +import { MobileMenu } from './MobileMenu' interface NavbarProps { minimal?: boolean } +const NAV_LINKS = [ + { label: 'Features', href: '#features' }, + { label: 'Pricing', href: '#pricing' }, + { label: 'Docs', href: '/docs' }, +] + +/** + * Renders the top navigation bar with logo, responsive navigation links, and action controls. + * + * The bar updates its background and border when the page is scrolled beyond 20px, and it cleans up the scroll listener on unmount. When `minimal` is true, desktop navigation links and right-side controls are hidden. Sign in and Get Started buttons navigate to '/login' and '/signup' respectively; the mobile menu receives a navigation handler. + * + * @param minimal - When true, hide desktop navigation links and right-side controls + * @returns The rendered navbar element + */ export function Navbar({ minimal }: NavbarProps) { const navigate = useNavigate() const [scrolled, setScrolled] = useState(false) @@ -19,37 +36,60 @@ export function Navbar({ minimal }: NavbarProps) { ) -} +} \ No newline at end of file diff --git a/frontend/src/components/landing/Pricing.tsx b/frontend/src/components/landing/Pricing.tsx new file mode 100644 index 0000000..9382976 --- /dev/null +++ b/frontend/src/components/landing/Pricing.tsx @@ -0,0 +1,158 @@ +import { motion } from 'framer-motion' +import { Check, Sparkles } from 'lucide-react' + +const PLANS = [ + { + name: 'Free', + price: '$0', + period: 'forever', + description: 'For open source and hobby projects', + features: ['Up to 3 repositories', 'Semantic code search', 'Dependency visualization', 'Community support', 'Public repos only'], + cta: 'Get Started', + ctaHref: '/signup', + popular: false, + }, + { + name: 'Pro', + price: '$19', + period: '/month', + description: 'For professional developers', + features: ['Unlimited repositories', 'Private repo support', 'API access', 'MCP server integration', 'Priority indexing', 'Email support'], + cta: 'Join Waitlist', + ctaHref: '/waitlist', + popular: true, + badge: 'Coming Soon', + }, + { + name: 'Enterprise', + price: 'Custom', + period: '', + description: 'For teams that need control', + features: ['Self-hosted deployment', 'SSO / SAML authentication', 'Dedicated support', 'Custom integrations', 'SLA guarantee', 'On-premise option'], + cta: 'Contact Sales', + ctaHref: 'mailto:devanshurajesh@gmail.com?subject=CodeIntel%20Enterprise', + popular: false, + }, +] + +const containerVariants = { + hidden: { opacity: 0 }, + visible: { opacity: 1, transition: { staggerChildren: 0.12, delayChildren: 0.1 } } +} + +const cardVariants = { + hidden: { opacity: 0, y: 40, scale: 0.95 }, + visible: { opacity: 1, y: 0, scale: 1, transition: { type: 'spring', stiffness: 200, damping: 20 } } +} + +/** + * Render the pricing section with three animated plan cards, per-feature lists, optional badges, and CTAs. + * + * The component includes a centered header, a responsive grid of plan cards (one highlighted as popular), animated entrance and hover interactions via Framer Motion, and a footer note about updates and security patches. + * + * @returns The JSX element for the complete pricing section containing plan cards, feature lists, badges, and call-to-action buttons. + */ +export function Pricing() { + return ( +
+
+ +

Simple, transparent pricing

+

Start free, upgrade when you need more. No hidden fees.

+
+ + + {PLANS.map((plan) => ( + + {plan.badge && ( + +
+ + {plan.badge} +
+
+ )} + +
+

{plan.name}

+
+ {plan.price} + {plan.period && {plan.period}} +
+

{plan.description}

+
+ +
    + {plan.features.map((feature, i) => ( + + + {feature} + + ))} +
+ + + {plan.cta} + +
+ ))} +
+ + + All plans include automatic updates and security patches + +
+
+ ) +} \ No newline at end of file diff --git a/frontend/src/components/landing/ThemeToggle.tsx b/frontend/src/components/landing/ThemeToggle.tsx new file mode 100644 index 0000000..90eb846 --- /dev/null +++ b/frontend/src/components/landing/ThemeToggle.tsx @@ -0,0 +1,31 @@ +import { useTheme } from 'next-themes' +import { Sun, Moon } from 'lucide-react' +import { useEffect, useState } from 'react' + +/** + * Render a hydration-safe theme toggle button for switching between light and dark modes. + * + * Renders a placeholder element until mounted to avoid hydration mismatch. When mounted, displays a button that toggles the application's theme and updates its icon and ARIA label: Sun icon when the current theme is dark, Moon icon when the current theme is light. + * + * @returns The theme toggle button element. + */ +export function ThemeToggle() { + const { theme, setTheme } = useTheme() + const [mounted, setMounted] = useState(false) + + // avoid hydration mismatch + useEffect(() => setMounted(true), []) + if (!mounted) return
+ + const isDark = theme === 'dark' + + return ( + + ) +} \ No newline at end of file diff --git a/frontend/src/components/providers/ThemeProvider.tsx b/frontend/src/components/providers/ThemeProvider.tsx new file mode 100644 index 0000000..c8856d4 --- /dev/null +++ b/frontend/src/components/providers/ThemeProvider.tsx @@ -0,0 +1,14 @@ +"use client" + +import { ThemeProvider as NextThemesProvider } from "next-themes" +import { type ThemeProviderProps } from "next-themes" + +/** + * Provides theme context to descendant components using next-themes. + * + * @param children - React nodes rendered within the provider + * @param props - Remaining ThemeProviderProps forwarded to the underlying theme provider + */ +export function ThemeProvider({ children, ...props }: ThemeProviderProps) { + return {children} +} \ No newline at end of file diff --git a/frontend/src/hooks/useGitHubStars.ts b/frontend/src/hooks/useGitHubStars.ts new file mode 100644 index 0000000..e0f6070 --- /dev/null +++ b/frontend/src/hooks/useGitHubStars.ts @@ -0,0 +1,63 @@ +import { useState, useEffect } from 'react' + +const REPO = 'OpenCodeIntel/opencodeintel' +const CACHE_KEY = 'github-stars-cache' +const CACHE_DURATION = 5 * 60 * 1000 // 5 minutes + +interface CacheData { + stars: number + timestamp: number +} + +/** + * Provides the GitHub star count for the configured repository with client-side caching. + * + * Uses a short-lived localStorage cache and falls back to any cached value if a network request fails. + * + * @returns An object with `stars` set to the repository star count (or `null` if unknown) and `loading` set to `true` while the value is being resolved, `false` otherwise. + */ +export function useGitHubStars() { + const [stars, setStars] = useState(null) + const [loading, setLoading] = useState(true) + + useEffect(() => { + const fetchStars = async () => { + // check cache first + const cached = localStorage.getItem(CACHE_KEY) + if (cached) { + const data: CacheData = JSON.parse(cached) + if (Date.now() - data.timestamp < CACHE_DURATION) { + setStars(data.stars) + setLoading(false) + return + } + } + + try { + const res = await fetch(`https://api.github.com/repos/${REPO}`) + if (!res.ok) throw new Error('Failed to fetch') + const data = await res.json() + const starCount = data.stargazers_count || 0 + + // cache it + localStorage.setItem(CACHE_KEY, JSON.stringify({ + stars: starCount, + timestamp: Date.now() + })) + + setStars(starCount) + } catch { + // fallback to cached even if expired + if (cached) { + setStars(JSON.parse(cached).stars) + } + } finally { + setLoading(false) + } + } + + fetchStars() + }, []) + + return { stars, loading } +} \ No newline at end of file diff --git a/frontend/src/pages/LandingPage.tsx b/frontend/src/pages/LandingPage.tsx index 8cf2999..243de7b 100644 --- a/frontend/src/pages/LandingPage.tsx +++ b/frontend/src/pages/LandingPage.tsx @@ -1,10 +1,21 @@ import { useState, useEffect } from 'react' import { useNavigate } from 'react-router-dom' -import { Navbar, Hero, ResultsView } from '@/components/landing' +import { Navbar, Hero, ResultsView, Features, Pricing, FAQ, Footer } from '@/components/landing' import { API_URL } from '@/config/api' import { playgroundAPI } from '@/services/playground-api' import type { SearchResult } from '@/types' +/** + * Render the landing page and manage its search state and flows. + * + * Manages local state for search results, loading, timing, query inputs, repository selection, + * and rate-limiting. On mount it fetches playground limits and updates remaining/limit. + * Exposes behavior to perform searches (against a demo repo or a custom playground), handle + * hero-initiated results, re-search the current query, and reset to the initial landing view. + * + * @returns The landing page React element containing a Navbar and either a ResultsView (after a search) + * or the public landing sections (Hero, Features, Pricing, FAQ, Footer). + */ export function LandingPage() { const navigate = useNavigate() @@ -93,7 +104,7 @@ export function LandingPage() { } return ( -
+
{hasSearched ? ( @@ -113,8 +124,14 @@ export function LandingPage() { onSignUp={() => navigate('/signup')} /> ) : ( - + <> + + + + +
) -} +} \ No newline at end of file