Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions frontend/src/components/bounty/BountyCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,10 @@ export function BountyCard({ bounty }: BountyCardProps) {
initial="rest"
whileHover="hover"
onClick={() => navigate(`/bounties/${bounty.id}`)}
className="relative rounded-xl border border-border bg-forge-900 p-5 cursor-pointer transition-colors duration-200 overflow-hidden group"
className="relative flex h-full flex-col overflow-hidden rounded-xl border border-border bg-forge-900 p-4 transition-colors duration-200 group cursor-pointer sm:p-5"
>
{/* Row 1: Repo + Tier */}
<div className="flex items-center justify-between text-sm">
<div className="flex items-start justify-between gap-3 text-sm">
<div className="flex items-center gap-2 min-w-0">
{bounty.org_avatar_url && (
<img src={bounty.org_avatar_url} className="w-5 h-5 rounded-full flex-shrink-0" alt="" />
Expand All @@ -84,7 +84,7 @@ export function BountyCard({ bounty }: BountyCardProps) {

{/* Row 3: Language dots */}
{skills.length > 0 && (
<div className="flex items-center gap-3 mt-3">
<div className="mt-3 flex flex-wrap items-center gap-x-3 gap-y-2">
{skills.map((lang) => (
<span key={lang} className="inline-flex items-center gap-1.5 text-xs text-text-muted">
<span
Expand All @@ -98,14 +98,14 @@ export function BountyCard({ bounty }: BountyCardProps) {
)}

{/* Separator */}
<div className="mt-4 border-t border-border/50" />
<div className="mt-auto border-t border-border/50 pt-4" />

{/* Row 4: Reward + Meta */}
<div className="flex items-center justify-between mt-3">
<div className="mt-3 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<span className="font-mono text-lg font-semibold text-emerald">
{formatCurrency(bounty.reward_amount, bounty.reward_token)}
</span>
<div className="flex items-center gap-3 text-xs text-text-muted">
<div className="flex flex-wrap items-center gap-x-3 gap-y-2 text-xs text-text-muted">
<span className="inline-flex items-center gap-1">
<GitPullRequest className="w-3.5 h-3.5" />
{bounty.submission_count} PRs
Expand All @@ -120,7 +120,7 @@ export function BountyCard({ bounty }: BountyCardProps) {
</div>

{/* Status badge */}
<span className={`absolute bottom-4 right-5 text-xs font-medium inline-flex items-center gap-1 ${statusColor}`}>
<span className={`mt-3 inline-flex self-start gap-1 text-xs font-medium sm:self-end ${statusColor}`}>
<span className={`w-1.5 h-1.5 rounded-full ${dotColor}`} />
{statusLabel}
</span>
Expand Down
87 changes: 42 additions & 45 deletions frontend/src/components/home/HeroSection.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { motion, useInView, animate, useMotionValue } from 'framer-motion';
import React, { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { animate, motion, useInView, useMotionValue } from 'framer-motion';
import { useStats } from '../../hooks/useStats';
import { getGitHubAuthorizeUrl } from '../../api/auth';
import { useAuth } from '../../hooks/useAuth';
Expand All @@ -26,7 +26,7 @@ function EmberParticles({ count = 5 }: { count?: number }) {
{particles.map((p) => (
<div
key={p.id}
className="absolute pointer-events-none rounded-full animate-ember opacity-60"
className="absolute pointer-events-none rounded-full opacity-60 animate-ember"
style={{
left: p.left,
bottom: '30%',
Expand Down Expand Up @@ -57,7 +57,10 @@ function CountUp({ target, prefix = '', suffix = '' }: { target: number; prefix?
ref.current.textContent = `${prefix}${Math.round(v).toLocaleString()}${suffix}`;
}
});
return () => { controls.stop(); unsubscribe(); };
return () => {
controls.stop();
unsubscribe();
};
}, [inView, target, motionValue, prefix, suffix]);

return <span ref={ref}>{prefix}0{suffix}</span>;
Expand All @@ -66,15 +69,16 @@ function CountUp({ target, prefix = '', suffix = '' }: { target: number; prefix?
export function HeroSection() {
const { data: stats } = useStats();
const { isAuthenticated } = useAuth();
const navigate = useNavigate();
const [typewriterDone, setTypewriterDone] = useState(false);
const [resultLinesVisible, setResultLinesVisible] = useState(false);

useEffect(() => {
// Typewriter takes ~3s (0.5s delay + 2.5s), then show result lines
const t1 = setTimeout(() => setTypewriterDone(true), 3100);
const t2 = setTimeout(() => setResultLinesVisible(true), 3400);
return () => { clearTimeout(t1); clearTimeout(t2); };
return () => {
clearTimeout(t1);
clearTimeout(t2);
};
}, []);

const handleSignIn = async () => {
Expand All @@ -87,38 +91,34 @@ export function HeroSection() {
};

return (
<section className="relative min-h-[90vh] flex flex-col items-center justify-center px-4 pt-24 pb-16 overflow-hidden">
{/* Background layers */}
<section className="relative flex min-h-[90vh] flex-col items-center justify-center overflow-hidden px-4 pt-24 pb-16">
<div className="absolute inset-0 bg-grid-forge bg-grid-forge pointer-events-none" style={{ backgroundSize: '40px 40px' }} />
<div className="absolute inset-0 bg-gradient-hero pointer-events-none" />
<EmberParticles count={5} />

{/* Terminal card */}
<motion.div
variants={fadeIn}
initial="initial"
animate="animate"
className="w-full max-w-xl rounded-xl border border-border bg-forge-900/90 backdrop-blur-sm overflow-hidden shadow-2xl shadow-black/50"
className="w-full max-w-xl overflow-hidden rounded-xl border border-border bg-forge-900/90 shadow-2xl shadow-black/50 backdrop-blur-sm"
>
{/* Title bar */}
<div className="flex items-center gap-2 px-4 py-2.5 bg-forge-800 border-b border-border">
<div className="flex items-center gap-2 border-b border-border bg-forge-800 px-4 py-2.5">
<div className="flex gap-1.5">
<span className="w-3 h-3 rounded-full bg-status-error/80" />
<span className="w-3 h-3 rounded-full bg-status-warning/80" />
<span className="w-3 h-3 rounded-full bg-status-success/80" />
</div>
<span className="font-mono text-xs text-text-muted ml-2">solfoundry terminal</span>
<span className="ml-2 font-mono text-xs text-text-muted">solfoundry terminal</span>
</div>

{/* Terminal body */}
<div className="p-5 font-mono text-sm leading-relaxed">
<div className="p-4 font-mono text-xs leading-relaxed sm:p-5 sm:text-sm">
<div className="overflow-hidden">
<span className="text-emerald">$ </span>
<span className="text-text-secondary overflow-hidden whitespace-nowrap inline-block animate-typewriter">
<span className="text-text-secondary inline-block max-w-[calc(100%-1.25rem)] overflow-hidden break-all whitespace-normal align-top sm:break-normal sm:whitespace-nowrap sm:animate-typewriter">
forge bounty --reward 100 --lang typescript --tier 2
</span>
{typewriterDone && (
<span className="inline-block w-2 h-5 bg-emerald animate-blink ml-0.5 align-middle" />
<span className="ml-0.5 inline-block h-5 w-2 animate-blink bg-emerald align-middle" />
)}
</div>

Expand All @@ -130,9 +130,9 @@ export function HeroSection() {
className="mt-3 space-y-1.5"
>
{[
{ text: 'Bounty created: #142', delay: 0 },
{ text: 'Escrow funded: 100 USDC', delay: 0.3 },
{ text: '3 contributors notified', delay: 0.6 },
{ text: 'Bounty created: #142', delay: 0 },
{ text: 'Escrow funded: 100 USDC', delay: 0.3 },
{ text: '3 contributors notified', delay: 0.6 },
].map((line, i) => (
<motion.div
key={i}
Expand All @@ -141,6 +141,7 @@ export function HeroSection() {
transition={{ delay: line.delay, duration: 0.3 }}
className="text-emerald"
>
<span className="mr-2">+</span>
{line.text}
</motion.div>
))}
Expand All @@ -149,37 +150,34 @@ export function HeroSection() {
</div>
</motion.div>

{/* Headline */}
<motion.h1
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3, duration: 0.5 }}
className="font-display text-4xl md:text-5xl font-bold text-text-primary tracking-wider text-center mt-10"
className="mt-10 text-center font-display text-3xl font-bold tracking-wider text-text-primary sm:text-4xl md:text-5xl"
>
THE AI-POWERED BOUNTY{' '}
<span className="text-emerald">FORGE</span>
THE AI-POWERED BOUNTY <span className="text-emerald">FORGE</span>
</motion.h1>

<motion.p
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.45, duration: 0.5 }}
className="font-sans text-lg text-text-secondary text-center mt-4 max-w-lg"
className="mt-4 max-w-lg text-center font-sans text-base text-text-secondary sm:text-lg"
>
Fund bounties. Ship code. Earn rewards.
</motion.p>

{/* CTAs */}
<motion.div
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.6, duration: 0.5 }}
className="flex flex-wrap items-center justify-center gap-4 mt-8"
className="mt-8 flex flex-wrap items-center justify-center gap-4"
>
<motion.div variants={buttonHover} initial="rest" whileHover="hover" whileTap="tap">
<Link
to="/bounties"
className="px-6 py-3 rounded-lg bg-emerald text-text-inverse font-semibold text-sm hover:bg-emerald-light transition-colors duration-200 shadow-lg shadow-emerald/20 inline-block"
className="inline-block rounded-lg bg-emerald px-6 py-3 text-sm font-semibold text-text-inverse shadow-lg shadow-emerald/20 transition-colors duration-200 hover:bg-emerald-light"
>
Browse Bounties
</Link>
Expand All @@ -188,7 +186,7 @@ export function HeroSection() {
<motion.div variants={buttonHover} initial="rest" whileHover="hover" whileTap="tap">
<Link
to="/bounties/create"
className="px-6 py-3 rounded-lg border border-emerald text-emerald font-semibold text-sm hover:bg-emerald-bg transition-colors duration-200 inline-block"
className="inline-block rounded-lg border border-emerald px-6 py-3 text-sm font-semibold text-emerald transition-colors duration-200 hover:bg-emerald-bg"
>
Post a Bounty
</Link>
Expand All @@ -198,40 +196,39 @@ export function HeroSection() {
<motion.div variants={buttonHover} initial="rest" whileHover="hover" whileTap="tap">
<button
onClick={handleSignIn}
className="px-6 py-3 rounded-lg border border-border text-text-secondary font-medium text-sm hover:border-border-hover hover:text-text-primary transition-all duration-200 inline-flex items-center gap-2"
className="inline-flex items-center gap-2 rounded-lg border border-border px-6 py-3 text-sm font-medium text-text-secondary transition-all duration-200 hover:border-border-hover hover:text-text-primary"
>
<GitHubIcon /> GitHub
</button>
</motion.div>
)}
</motion.div>

{/* Live stats strip */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.8, duration: 0.5 }}
className="flex items-center justify-center gap-6 mt-8 font-mono text-sm text-text-muted"
className="mt-8 flex flex-wrap items-center justify-center gap-x-6 gap-y-2 font-mono text-xs text-text-muted sm:text-sm"
>
<span>
<span className="text-text-primary font-semibold">
<span className="font-semibold text-text-primary">
<CountUp target={stats?.open_bounties ?? 142} />
</span>
{' '}open bounties
</span>{' '}
open bounties
</span>
<span className="text-text-muted">·</span>
<span className="hidden text-text-muted sm:inline">·</span>
<span>
<span className="text-text-primary font-semibold">
<span className="font-semibold text-text-primary">
$<CountUp target={stats?.total_paid_usdc ?? 24500} />
</span>
{' '}paid
</span>{' '}
paid
</span>
<span className="text-text-muted">·</span>
<span className="hidden text-text-muted sm:inline">·</span>
<span>
<span className="text-text-primary font-semibold">
<span className="font-semibold text-text-primary">
<CountUp target={stats?.total_contributors ?? 89} />
</span>
{' '}builders
</span>{' '}
builders
</span>
</motion.div>
</section>
Expand Down
65 changes: 56 additions & 9 deletions frontend/src/components/layout/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,22 @@ export function Navbar() {
return () => window.removeEventListener('scroll', onScroll);
}, []);

useEffect(() => {
setMenuOpen(false);
setDropdownOpen(false);
}, [location.pathname]);

useEffect(() => {
if (!menuOpen) return;

const previousOverflow = document.body.style.overflow;
document.body.style.overflow = 'hidden';

return () => {
document.body.style.overflow = previousOverflow;
};
}, [menuOpen]);

const handleGitHubSignIn = async () => {
try {
const url = await getGitHubAuthorizeUrl();
Expand Down Expand Up @@ -61,16 +77,16 @@ export function Navbar() {
}`}
/>

<div className="max-w-7xl mx-auto h-full px-4 flex items-center justify-between">
<div className="mx-auto flex h-full max-w-7xl items-center justify-between gap-3 px-4">
{/* Left: Logo + Nav */}
<div className="flex items-center gap-8">
<Link to="/" className="flex items-center gap-2.5 group">
<div className="flex min-w-0 items-center gap-4 md:gap-8">
<Link to="/" className="flex min-w-0 items-center gap-2.5 group">
<img
src="/logo-icon.png"
alt="SolFoundry"
className="w-7 h-7 group-hover:drop-shadow-[0_0_8px_rgba(0,230,118,0.4)] transition-all duration-200"
/>
<span className="font-display text-lg font-semibold text-text-primary tracking-wide">
<span className="truncate font-display text-base font-semibold tracking-wide text-text-primary sm:text-lg">
SolFoundry
</span>
</Link>
Expand Down Expand Up @@ -100,7 +116,7 @@ export function Navbar() {
</div>

{/* Right: Live count + Auth */}
<div className="flex items-center gap-3">
<div className="flex items-center gap-2 sm:gap-3">
{/* Live bounty count */}
{stats && (
<div className="hidden sm:inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-emerald-bg border border-emerald-border">
Expand Down Expand Up @@ -168,7 +184,9 @@ export function Navbar() {
{/* Mobile hamburger */}
<button
onClick={() => setMenuOpen(!menuOpen)}
className="md:hidden p-2 rounded-lg hover:bg-forge-800 transition-colors text-text-secondary"
aria-expanded={menuOpen}
aria-label={menuOpen ? 'Close navigation menu' : 'Open navigation menu'}
className="rounded-lg p-2 text-text-secondary transition-colors hover:bg-forge-800 md:hidden"
>
{menuOpen ? <X className="w-5 h-5" /> : <Menu className="w-5 h-5" />}
</button>
Expand All @@ -182,19 +200,48 @@ export function Navbar() {
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
className="md:hidden overflow-hidden bg-forge-900 border-b border-border"
className="overflow-hidden border-b border-border bg-forge-900 md:hidden"
>
<div className="px-4 py-4 flex flex-col gap-1">
<div className="flex flex-col gap-1 px-4 py-4">
{stats && (
<div className="mb-3 inline-flex items-center gap-1.5 self-start rounded-full border border-emerald-border bg-emerald-bg px-3 py-1">
<span className="h-2 w-2 rounded-full bg-emerald animate-pulse-glow" />
<span className="font-mono text-xs text-emerald">{stats.open_bounties} open</span>
</div>
)}
{NAV_LINKS.map((link) => (
<Link
key={link.to}
to={link.to}
onClick={() => setMenuOpen(false)}
className="px-4 py-2.5 rounded-lg text-sm font-medium text-text-secondary hover:text-text-primary hover:bg-forge-850 transition-colors duration-150"
className={`rounded-lg px-4 py-2.5 text-sm font-medium transition-colors duration-150 ${
isActive(link.to)
? 'bg-forge-850 text-text-primary'
: 'text-text-secondary hover:bg-forge-850 hover:text-text-primary'
}`}
>
{link.label}
</Link>
))}

{isAuthenticated && user && (
<>
<div className="my-3 border-t border-border/60" />
<Link
to="/profile"
onClick={() => setMenuOpen(false)}
className="rounded-lg px-4 py-2.5 text-sm font-medium text-text-secondary transition-colors duration-150 hover:bg-forge-850 hover:text-text-primary"
>
Profile
</Link>
<button
onClick={() => { logout(); setMenuOpen(false); navigate('/'); }}
className="rounded-lg px-4 py-2.5 text-left text-sm font-medium text-text-secondary transition-colors duration-150 hover:bg-forge-850 hover:text-status-error"
>
Sign out
</button>
</>
)}
</div>
</motion.div>
)}
Expand Down
Loading