Skip to content

Commit 3b8d8f1

Browse files
committed
feat(landing): redesign navbar with theme toggle and mobile menu
- add GitHub stars badge with live API count + caching - add ThemeToggle component (sun/moon icon) - add MobileMenu with slide-out drawer (Sheet) - add nav links: Features, Pricing, Docs - sticky navbar with blur on scroll - responsive desktop/mobile layouts - useGitHubStars hook with 5min cache
1 parent ba78f56 commit 3b8d8f1

6 files changed

Lines changed: 238 additions & 21 deletions

File tree

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { Star } from 'lucide-react'
2+
import { useGitHubStars } from '@/hooks/useGitHubStars'
3+
4+
const REPO_URL = 'https://github.com/OpenCodeIntel/opencodeintel'
5+
6+
export function GitHubStars() {
7+
const { stars, loading } = useGitHubStars()
8+
9+
const formatStars = (count: number) => {
10+
if (count >= 1000) return `${(count / 1000).toFixed(1)}k`
11+
return count.toString()
12+
}
13+
14+
return (
15+
<a
16+
href={REPO_URL}
17+
target="_blank"
18+
rel="noopener noreferrer"
19+
className="flex items-center gap-2 px-3 py-1.5 rounded-lg border border-white/10 dark:border-white/10 light:border-black/10 hover:bg-white/5 dark:hover:bg-white/5 light:hover:bg-black/5 transition-colors group"
20+
>
21+
<svg viewBox="0 0 24 24" className="w-4 h-4 fill-current" aria-hidden="true">
22+
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z" />
23+
</svg>
24+
<div className="flex items-center gap-1 text-sm">
25+
<Star className="w-3.5 h-3.5 text-yellow-500 fill-yellow-500" />
26+
<span className="text-zinc-400 group-hover:text-foreground transition-colors">
27+
{loading ? '—' : formatStars(stars || 0)}
28+
</span>
29+
</div>
30+
</a>
31+
)
32+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { Menu, X } from 'lucide-react'
2+
import { Sheet, SheetContent, SheetTrigger, SheetClose } from '@/components/ui/sheet'
3+
import { ThemeToggle } from './ThemeToggle'
4+
import { GitHubStars } from './GitHubStars'
5+
6+
interface MobileMenuProps {
7+
onNavigate: (path: string) => void
8+
}
9+
10+
const NAV_LINKS = [
11+
{ label: 'Features', href: '#features' },
12+
{ label: 'Pricing', href: '#pricing' },
13+
{ label: 'Docs', href: '/docs' },
14+
]
15+
16+
export function MobileMenu({ onNavigate }: MobileMenuProps) {
17+
return (
18+
<Sheet>
19+
<SheetTrigger asChild>
20+
<button className="p-2 rounded-lg text-zinc-400 hover:text-foreground hover:bg-white/5 transition-colors md:hidden">
21+
<Menu className="w-5 h-5" />
22+
</button>
23+
</SheetTrigger>
24+
<SheetContent side="right" className="w-[300px] bg-background border-l border-white/10">
25+
<div className="flex flex-col h-full pt-8">
26+
<nav className="flex flex-col gap-1">
27+
{NAV_LINKS.map(link => (
28+
<SheetClose asChild key={link.href}>
29+
<a
30+
href={link.href}
31+
className="px-4 py-3 text-zinc-300 hover:text-foreground hover:bg-white/5 rounded-lg transition-colors"
32+
>
33+
{link.label}
34+
</a>
35+
</SheetClose>
36+
))}
37+
</nav>
38+
39+
<div className="border-t border-white/10 my-6" />
40+
41+
<div className="flex flex-col gap-3 px-4">
42+
<SheetClose asChild>
43+
<button
44+
onClick={() => onNavigate('/login')}
45+
className="w-full py-2.5 text-sm text-zinc-300 hover:text-foreground transition-colors"
46+
>
47+
Sign in
48+
</button>
49+
</SheetClose>
50+
<SheetClose asChild>
51+
<button
52+
onClick={() => onNavigate('/signup')}
53+
className="w-full py-2.5 text-sm font-medium text-white rounded-lg bg-accent hover:bg-accent/90 transition-colors"
54+
>
55+
Get Started
56+
</button>
57+
</SheetClose>
58+
</div>
59+
60+
<div className="mt-auto pb-8 px-4">
61+
<div className="flex items-center justify-between">
62+
<GitHubStars />
63+
<ThemeToggle />
64+
</div>
65+
</div>
66+
</div>
67+
</SheetContent>
68+
</Sheet>
69+
)
70+
}

frontend/src/components/landing/Navbar.tsx

Lines changed: 52 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
11
import { useState, useEffect } from 'react'
22
import { useNavigate } from 'react-router-dom'
3+
import { ThemeToggle } from './ThemeToggle'
4+
import { GitHubStars } from './GitHubStars'
5+
import { MobileMenu } from './MobileMenu'
36

47
interface NavbarProps {
58
minimal?: boolean
69
}
710

11+
const NAV_LINKS = [
12+
{ label: 'Features', href: '#features' },
13+
{ label: 'Pricing', href: '#pricing' },
14+
{ label: 'Docs', href: '/docs' },
15+
]
16+
817
export function Navbar({ minimal }: NavbarProps) {
918
const navigate = useNavigate()
1019
const [scrolled, setScrolled] = useState(false)
@@ -19,35 +28,58 @@ export function Navbar({ minimal }: NavbarProps) {
1928
<nav className={`
2029
fixed top-0 left-0 right-0 z-50 transition-all duration-300
2130
${scrolled
22-
? 'bg-black/80 backdrop-blur-xl border-b border-white/[0.06]'
31+
? 'bg-background/80 backdrop-blur-xl border-b border-white/[0.06] dark:border-white/[0.06] light:border-black/[0.06]'
2332
: 'bg-transparent'
2433
}
2534
`}>
26-
<div className="max-w-5xl mx-auto px-6 py-4 flex items-center justify-between">
35+
<div className="max-w-6xl mx-auto px-4 sm:px-6 py-4 flex items-center justify-between">
2736
{/* Logo */}
28-
<a href="/" className="flex items-center gap-3 group">
29-
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-blue-400 to-blue-600 flex items-center justify-center">
37+
<a href="/" className="flex items-center gap-2.5 group">
38+
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-accent to-blue-600 flex items-center justify-center shadow-lg shadow-accent/20">
3039
<span className="text-white font-bold text-sm">CI</span>
3140
</div>
32-
<span className="font-semibold text-white">CodeIntel</span>
41+
<span className="font-semibold text-foreground">CodeIntel</span>
3342
</a>
3443

35-
{/* Right side */}
44+
{/* Desktop nav */}
3645
{!minimal && (
37-
<div className="flex items-center gap-2">
38-
<button
39-
onClick={() => navigate('/login')}
40-
className="px-4 py-2 text-sm text-zinc-400 hover:text-white transition-colors"
41-
>
42-
Sign in
43-
</button>
44-
<button
45-
onClick={() => navigate('/signup')}
46-
className="px-4 py-2 text-sm font-medium text-white rounded-lg bg-white/10 hover:bg-white/15 border border-white/10 transition-all"
47-
>
48-
Get started
49-
</button>
50-
</div>
46+
<>
47+
<div className="hidden md:flex items-center gap-1">
48+
{NAV_LINKS.map(link => (
49+
<a
50+
key={link.href}
51+
href={link.href}
52+
className="px-3 py-2 text-sm text-muted-foreground hover:text-foreground transition-colors rounded-lg hover:bg-white/5 dark:hover:bg-white/5 light:hover:bg-black/5"
53+
>
54+
{link.label}
55+
</a>
56+
))}
57+
</div>
58+
59+
<div className="hidden md:flex items-center gap-3">
60+
<GitHubStars />
61+
<ThemeToggle />
62+
<div className="w-px h-6 bg-white/10 dark:bg-white/10 light:bg-black/10" />
63+
<button
64+
onClick={() => navigate('/login')}
65+
className="px-3 py-2 text-sm text-muted-foreground hover:text-foreground transition-colors"
66+
>
67+
Sign in
68+
</button>
69+
<button
70+
onClick={() => navigate('/signup')}
71+
className="px-4 py-2 text-sm font-medium text-white rounded-lg bg-accent hover:bg-accent/90 transition-colors shadow-lg shadow-accent/20"
72+
>
73+
Get Started
74+
</button>
75+
</div>
76+
77+
{/* Mobile */}
78+
<div className="flex md:hidden items-center gap-2">
79+
<ThemeToggle />
80+
<MobileMenu onNavigate={navigate} />
81+
</div>
82+
</>
5183
)}
5284
</div>
5385
</nav>
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { useTheme } from 'next-themes'
2+
import { Sun, Moon } from 'lucide-react'
3+
import { useEffect, useState } from 'react'
4+
5+
export function ThemeToggle() {
6+
const { theme, setTheme } = useTheme()
7+
const [mounted, setMounted] = useState(false)
8+
9+
// avoid hydration mismatch
10+
useEffect(() => setMounted(true), [])
11+
if (!mounted) return <div className="w-9 h-9" />
12+
13+
const isDark = theme === 'dark'
14+
15+
return (
16+
<button
17+
onClick={() => setTheme(isDark ? 'light' : 'dark')}
18+
className="p-2 rounded-lg text-zinc-400 hover:text-foreground hover:bg-white/5 dark:hover:bg-white/5 light:hover:bg-black/5 transition-colors"
19+
aria-label={`Switch to ${isDark ? 'light' : 'dark'} mode`}
20+
>
21+
{isDark ? <Sun className="w-5 h-5" /> : <Moon className="w-5 h-5" />}
22+
</button>
23+
)
24+
}
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Landing page components barrel export
1+
// Landing page components
22
export { Navbar } from './Navbar'
33
export { Hero } from './Hero'
44
export { HeroSearch } from './HeroSearch'
@@ -7,3 +7,6 @@ export { ResultCard } from './ResultCard'
77
export { SkeletonCard } from './SkeletonCard'
88
export { CompactSearchBar } from './CompactSearchBar'
99
export { RepoSwitcher } from './RepoSwitcher'
10+
export { ThemeToggle } from './ThemeToggle'
11+
export { GitHubStars } from './GitHubStars'
12+
export { MobileMenu } from './MobileMenu'
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { useState, useEffect } from 'react'
2+
3+
const REPO = 'OpenCodeIntel/opencodeintel'
4+
const CACHE_KEY = 'github-stars-cache'
5+
const CACHE_DURATION = 5 * 60 * 1000 // 5 minutes
6+
7+
interface CacheData {
8+
stars: number
9+
timestamp: number
10+
}
11+
12+
export function useGitHubStars() {
13+
const [stars, setStars] = useState<number | null>(null)
14+
const [loading, setLoading] = useState(true)
15+
16+
useEffect(() => {
17+
const fetchStars = async () => {
18+
// check cache first
19+
const cached = localStorage.getItem(CACHE_KEY)
20+
if (cached) {
21+
const data: CacheData = JSON.parse(cached)
22+
if (Date.now() - data.timestamp < CACHE_DURATION) {
23+
setStars(data.stars)
24+
setLoading(false)
25+
return
26+
}
27+
}
28+
29+
try {
30+
const res = await fetch(`https://api.github.com/repos/${REPO}`)
31+
if (!res.ok) throw new Error('Failed to fetch')
32+
const data = await res.json()
33+
const starCount = data.stargazers_count || 0
34+
35+
// cache it
36+
localStorage.setItem(CACHE_KEY, JSON.stringify({
37+
stars: starCount,
38+
timestamp: Date.now()
39+
}))
40+
41+
setStars(starCount)
42+
} catch {
43+
// fallback to cached even if expired
44+
if (cached) {
45+
setStars(JSON.parse(cached).stars)
46+
}
47+
} finally {
48+
setLoading(false)
49+
}
50+
}
51+
52+
fetchStars()
53+
}, [])
54+
55+
return { stars, loading }
56+
}

0 commit comments

Comments
 (0)