Skip to content

Commit be3ed74

Browse files
committed
freebuff landing page: another round of improvements
1 parent c34a61e commit be3ed74

File tree

2 files changed

+239
-74
lines changed

2 files changed

+239
-74
lines changed

freebuff/web/src/app/home-client.tsx

Lines changed: 192 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22

33
import { AnimatePresence, motion } from 'framer-motion'
44
import {
5+
Check,
56
ChevronDown,
7+
Copy,
68
} from 'lucide-react'
79
import Image from 'next/image'
810
import Link from 'next/link'
9-
import { useState } from 'react'
11+
import { useMemo, useState } from 'react'
1012

1113
import { BackgroundBeams } from '@/components/background-beams'
1214
import { CopyButton } from '@/components/copy-button'
@@ -120,21 +122,107 @@ function SetupGuide() {
120122
)
121123
}
122124

125+
const PARTICLE_COUNT = 14
126+
123127
function InstallCommand({ className }: { className?: string }) {
128+
const [copied, setCopied] = useState(false)
129+
const [copyCount, setCopyCount] = useState(0)
130+
131+
const particles = useMemo(() =>
132+
Array.from({ length: PARTICLE_COUNT }).map((_, i) => ({
133+
angle: (i / PARTICLE_COUNT) * 360 + (Math.random() - 0.5) * 25,
134+
distance: 35 + Math.random() * 35,
135+
size: 3 + Math.random() * 4,
136+
durationExtra: Math.random() * 0.3,
137+
})),
138+
[copyCount],
139+
)
140+
141+
const handleCopy = () => {
142+
navigator.clipboard.writeText(INSTALL_COMMAND)
143+
setCopied(true)
144+
setCopyCount(c => c + 1)
145+
setTimeout(() => setCopied(false), 1800)
146+
}
147+
124148
return (
125-
<div
126-
className={cn(
127-
'flex items-center gap-2 bg-zinc-900/80 border border-zinc-700/50 rounded-lg px-4 py-3 font-mono text-sm',
128-
'hover:border-acid-matrix/50 hover:shadow-[0_0_20px_rgba(124,255,63,0.12)] transition-all duration-300',
129-
'gradient-border-shine',
130-
className,
131-
)}
132-
>
133-
<span className="text-acid-matrix select-none">$</span>
134-
<code className="text-white/90 select-all flex-1">
135-
{INSTALL_COMMAND}
136-
</code>
137-
<CopyButton value={INSTALL_COMMAND} />
149+
<div className="relative">
150+
<div
151+
className={cn(
152+
'flex items-center gap-2 bg-zinc-900/80 border rounded-lg px-4 py-3 font-mono text-sm',
153+
'gradient-border-shine',
154+
copied
155+
? 'border-acid-matrix shadow-[0_0_30px_rgba(124,255,63,0.45),0_0_60px_rgba(124,255,63,0.2)]'
156+
: 'border-acid-matrix/60 install-box-glow hover:border-acid-matrix hover:shadow-[0_0_30px_rgba(124,255,63,0.35),0_0_60px_rgba(124,255,63,0.15)]',
157+
'transition-all duration-300',
158+
className,
159+
)}
160+
>
161+
<span className="text-acid-matrix select-none">$</span>
162+
<code className="text-white/90 select-all flex-1">
163+
{INSTALL_COMMAND}
164+
</code>
165+
<button
166+
onClick={handleCopy}
167+
className="p-1.5 rounded-md transition-colors hover:bg-white/10 cursor-pointer"
168+
aria-label={`Copy: ${INSTALL_COMMAND}`}
169+
>
170+
<AnimatePresence mode="wait" initial={false}>
171+
{copied ? (
172+
<motion.span
173+
key="check"
174+
initial={{ scale: 0, rotate: -90 }}
175+
animate={{ scale: 1, rotate: 0 }}
176+
exit={{ scale: 0, rotate: 90 }}
177+
transition={{ duration: 0.2 }}
178+
className="block"
179+
>
180+
<Check className="h-4 w-4 text-acid-matrix" />
181+
</motion.span>
182+
) : (
183+
<motion.span
184+
key="copy"
185+
initial={{ scale: 0 }}
186+
animate={{ scale: 1 }}
187+
exit={{ scale: 0 }}
188+
transition={{ duration: 0.15 }}
189+
className="block"
190+
>
191+
<Copy className="h-4 w-4 text-white/60" />
192+
</motion.span>
193+
)}
194+
</AnimatePresence>
195+
</button>
196+
</div>
197+
198+
{/* Celebration particles */}
199+
<AnimatePresence>
200+
{copied &&
201+
particles.map((p, i) => {
202+
const rad = (p.angle * Math.PI) / 180
203+
return (
204+
<motion.span
205+
key={i}
206+
initial={{ opacity: 1, scale: 1, x: 0, y: 0 }}
207+
animate={{
208+
opacity: 0,
209+
scale: 0,
210+
x: Math.cos(rad) * p.distance,
211+
y: Math.sin(rad) * p.distance,
212+
}}
213+
exit={{ opacity: 0 }}
214+
transition={{ duration: 0.5 + p.durationExtra, ease: 'easeOut' }}
215+
className="absolute right-5 top-1/2 rounded-full pointer-events-none"
216+
style={{
217+
width: p.size,
218+
height: p.size,
219+
backgroundColor:
220+
i % 3 === 0 ? '#7CFF3F' : i % 3 === 1 ? '#a8ff7a' : '#ffffff',
221+
}}
222+
/>
223+
)
224+
})}
225+
</AnimatePresence>
138226
</div>
139227
)
140228
}
@@ -143,28 +231,50 @@ function FAQList() {
143231
const [openIndex, setOpenIndex] = useState<number | null>(null)
144232

145233
return (
146-
<div className="space-y-3">
234+
<div className="divide-y divide-zinc-800/60">
147235
{faqs.map((faq, i) => {
148236
const isOpen = openIndex === i
149237
return (
150238
<motion.div
151239
key={i}
152-
initial={{ opacity: 0, y: 15 }}
153-
whileInView={{ opacity: 1, y: 0 }}
154-
viewport={{ once: true }}
155-
transition={{ duration: 0.4, delay: i * 0.08 }}
240+
initial={{ opacity: 0, filter: 'blur(8px)', x: 20 }}
241+
whileInView={{ opacity: 1, filter: 'blur(0px)', x: 0 }}
242+
viewport={{ once: true, amount: 0.5 }}
243+
transition={{ duration: 0.5, delay: i * 0.1 }}
244+
className={cn(
245+
'transition-all duration-300',
246+
isOpen && 'bg-acid-matrix/[0.03]',
247+
)}
156248
>
157249
<button
158250
onClick={() => setOpenIndex(isOpen ? null : i)}
159-
className="w-full flex items-center justify-between gap-4 bg-zinc-900/50 border border-zinc-800 rounded-xl px-6 py-4 text-left hover:border-acid-matrix/30 hover:bg-zinc-900/80 transition-all duration-300 cursor-pointer"
251+
className="w-full flex items-center gap-4 px-4 py-5 text-left transition-all duration-300 cursor-pointer group"
160252
>
161-
<span className="font-semibold text-white">{faq.question}</span>
253+
<span
254+
className={cn(
255+
'flex-shrink-0 font-mono text-xs transition-colors duration-300',
256+
isOpen ? 'text-acid-matrix' : 'text-zinc-600 group-hover:text-zinc-400',
257+
)}
258+
>
259+
{String(i + 1).padStart(2, '0')}
260+
</span>
261+
<span
262+
className={cn(
263+
'font-semibold flex-1 transition-colors duration-300',
264+
isOpen ? 'text-white' : 'text-zinc-300 group-hover:text-white',
265+
)}
266+
>
267+
{faq.question}
268+
</span>
162269
<motion.span
163270
animate={{ rotate: isOpen ? 180 : 0 }}
164271
transition={{ duration: 0.25 }}
165-
className="flex-shrink-0 text-zinc-400"
272+
className={cn(
273+
'flex-shrink-0 transition-colors duration-300',
274+
isOpen ? 'text-acid-matrix' : 'text-zinc-600',
275+
)}
166276
>
167-
<ChevronDown className="h-5 w-5" />
277+
<ChevronDown className="h-4 w-4" />
168278
</motion.span>
169279
</button>
170280
<AnimatePresence initial={false}>
@@ -176,9 +286,14 @@ function FAQList() {
176286
transition={{ duration: 0.25, ease: 'easeInOut' }}
177287
className="overflow-hidden"
178288
>
179-
<p className="px-6 pt-3 pb-1 text-zinc-400 leading-relaxed">
180-
{faq.answer}
181-
</p>
289+
<div className="flex gap-4 px-4 pb-5">
290+
<span className="flex-shrink-0 w-[1.5ch]"></span>
291+
<div className="border-l-2 border-acid-matrix/40 pl-4">
292+
<p className="text-zinc-300 leading-relaxed text-sm">
293+
{faq.answer}
294+
</p>
295+
</div>
296+
</div>
182297
</motion.div>
183298
)}
184299
</AnimatePresence>
@@ -190,9 +305,9 @@ function FAQList() {
190305
}
191306

192307
const PHILOSOPHY_WORDS = [
193-
{ word: 'SIMPLE', description: 'No modes. No config. Just code.' },
308+
{ word: 'SIMPLE', description: 'No modes. No config. Just works.' },
194309
{ word: 'FAST', description: 'Up to 3× the speed of Claude Code' },
195-
{ word: 'LOADED', description: 'Built in web research, browser use, and more' },
310+
{ word: 'LOADED', description: 'Built-in web research, browser use, and more' },
196311
]
197312

198313
function PhilosophySection() {
@@ -215,34 +330,32 @@ function PhilosophySection() {
215330
}
216331

217332
return (
218-
<div className="relative z-10 container mx-auto max-w-5xl px-4 pt-16 md:pt-24 pb-24 md:pb-32">
219-
<div className="flex flex-col gap-12 md:gap-16">
220-
{PHILOSOPHY_WORDS.map((item, i) => (
333+
<div className="flex flex-col gap-12 md:gap-16">
334+
{PHILOSOPHY_WORDS.map((item, i) => (
335+
<motion.div
336+
key={item.word}
337+
initial={{ opacity: 0, filter: 'blur(12px)' }}
338+
whileInView={{ opacity: 1, filter: 'blur(0px)' }}
339+
viewport={{ once: true, amount: 0.5 }}
340+
transition={{ duration: 0.7, delay: i * 0.1 }}
341+
className="group"
342+
>
221343
<motion.div
222-
key={item.word}
223-
initial={{ opacity: 0, filter: 'blur(12px)' }}
224-
whileInView={{ opacity: 1, filter: 'blur(0px)' }}
225-
viewport={{ once: true, amount: 0.5 }}
226-
transition={{ duration: 0.7, delay: i * 0.1 }}
227-
className="group"
344+
onViewportEnter={() => lightUp(i)}
345+
onViewportLeave={() => dimDown(i)}
346+
viewport={{ margin: '0px 0px -50% 0px' }}
347+
className={cn(
348+
'font-dm-mono text-7xl md:text-[8rem] lg:text-[6rem] xl:text-[8rem] font-medium leading-[0.85] tracking-tighter select-none transition-all duration-500',
349+
litWords.has(i) ? 'keyword-filled' : 'keyword-hollow',
350+
)}
228351
>
229-
<motion.div
230-
onViewportEnter={() => lightUp(i)}
231-
onViewportLeave={() => dimDown(i)}
232-
viewport={{ margin: '0px 0px -55% 0px' }}
233-
className={cn(
234-
'font-dm-mono text-7xl md:text-[8rem] lg:text-[10rem] font-medium leading-[0.85] tracking-tighter select-none transition-all duration-500',
235-
litWords.has(i) ? 'keyword-filled' : 'keyword-hollow',
236-
)}
237-
>
238-
{item.word}
239-
</motion.div>
240-
<p className="mt-3 md:mt-4 text-zinc-500 text-sm md:text-base font-mono tracking-wide">
241-
{item.description}
242-
</p>
352+
{item.word}
243353
</motion.div>
244-
))}
245-
</div>
354+
<p className="mt-3 md:mt-4 text-zinc-500 text-sm md:text-base font-mono tracking-wide">
355+
{item.description}
356+
</p>
357+
</motion.div>
358+
))}
246359
</div>
247360
)
248361
}
@@ -282,7 +395,7 @@ export default function HomeClient() {
282395
>
283396
<Link
284397
href="/"
285-
className="flex items-center space-x-2 group transition-all duration-300 hover:scale-105"
398+
className="flex items-center space-x-2 group transition-all duration-300 hover:translate-x-0.5"
286399
>
287400
<Image
288401
src="/logo-icon.png"
@@ -301,7 +414,7 @@ export default function HomeClient() {
301414
href="https://github.com/CodebuffAI/codebuff"
302415
target="_blank"
303416
rel="noopener noreferrer"
304-
className="relative font-medium px-3 py-2 rounded-md transition-all duration-200 hover:bg-white/10 text-zinc-400 hover:text-white flex items-center gap-2 text-sm"
417+
className="relative font-medium px-3 py-2 rounded-md transition-all duration-200 text-zinc-400 hover:text-white flex items-center gap-2 text-sm"
305418
>
306419
<Icons.github className="h-4 w-4" />
307420
<span className="hidden sm:inline">GitHub</span>
@@ -327,7 +440,7 @@ export default function HomeClient() {
327440
<motion.span
328441
key={i}
329442
variants={wordVariant}
330-
className={word === 'free' ? 'inline-block mr-[0.3em] text-acid-matrix neon-text animate-glow-pulse' : 'inline-block mr-[0.3em] text-white'}
443+
className={word === 'free' ? 'inline-block mr-[0.3em] text-acid-matrix neon-text animate-glow-pulse cursor-default hover-glow-flare' : 'inline-block mr-[0.3em] text-white'}
331444
>
332445
{word}
333446
</motion.span>
@@ -365,25 +478,30 @@ export default function HomeClient() {
365478
</motion.div>
366479
</div>
367480

368-
{/* Philosophy content — same background, continuous flow */}
369-
<PhilosophySection />
370-
371-
{/* ─── FAQ Section ─── */}
372-
<div className="relative z-10 py-24 px-4">
373-
<div className="container mx-auto max-w-2xl">
374-
<motion.div
375-
initial={{ opacity: 0, y: 20 }}
376-
whileInView={{ opacity: 1, y: 0 }}
377-
viewport={{ once: true, amount: 0.3 }}
378-
transition={{ duration: 0.6 }}
379-
className="text-center mb-12"
380-
>
381-
<h2 className="text-3xl md:text-4xl font-bold mb-4">
382-
Frequently asked questions
383-
</h2>
384-
</motion.div>
481+
{/* ─── Philosophy + FAQ: side-by-side on large screens ─── */}
482+
<div className="relative z-10 container mx-auto max-w-7xl px-4 pt-16 md:pt-24 pb-24 md:pb-32 lg:pb-[25vh]">
483+
<div className="flex flex-col lg:flex-row lg:gap-16 xl:gap-24">
484+
{/* Philosophy — left side */}
485+
<div className="lg:flex-1 min-w-0">
486+
<PhilosophySection />
487+
</div>
488+
489+
{/* FAQ — right side (sticky on lg) */}
490+
<div className="lg:flex-1 min-w-0 mt-20 lg:mt-0 lg:sticky lg:top-24 lg:self-start lg:max-h-[calc(100vh-6rem)] lg:overflow-y-auto">
491+
<motion.div
492+
initial={{ opacity: 0, y: 20 }}
493+
whileInView={{ opacity: 1, y: 0 }}
494+
viewport={{ once: true, amount: 0.3 }}
495+
transition={{ duration: 0.6 }}
496+
className="text-center lg:text-left mb-12"
497+
>
498+
<h2 className="text-3xl md:text-4xl font-bold mb-4">
499+
FAQ
500+
</h2>
501+
</motion.div>
385502

386-
<FAQList />
503+
<FAQList />
504+
</div>
387505
</div>
388506
</div>
389507
</div>

0 commit comments

Comments
 (0)