diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 3a96bde2c..51f98f1e9 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -8,6 +8,7 @@
"dependencies": {
"@tailwindcss/postcss": "^4.2.2",
"@tanstack/react-query": "^5.91.3",
+ "@types/three": "^0.184.1",
"autoprefixer": "^10.4.27",
"framer-motion": "^12.38.0",
"lucide-react": "^1.7.0",
@@ -20,7 +21,8 @@
"recharts": "^3.8.0",
"remark-gfm": "^4.0.1",
"tailwind-merge": "^3.5.0",
- "tailwindcss": "^4.2.2"
+ "tailwindcss": "^4.2.2",
+ "three": "^0.184.0"
},
"devDependencies": {
"@testing-library/jest-dom": "^6.4.5",
@@ -502,6 +504,12 @@
"node": ">=18"
}
},
+ "node_modules/@dimforge/rapier3d-compat": {
+ "version": "0.12.0",
+ "resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz",
+ "integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==",
+ "license": "Apache-2.0"
+ },
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
@@ -1829,6 +1837,12 @@
"@testing-library/dom": ">=7.21.4"
}
},
+ "node_modules/@tweenjs/tween.js": {
+ "version": "23.1.3",
+ "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
+ "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==",
+ "license": "MIT"
+ },
"node_modules/@types/aria-query": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
@@ -2063,6 +2077,26 @@
"@types/react": "*"
}
},
+ "node_modules/@types/stats.js": {
+ "version": "0.17.4",
+ "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz",
+ "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/three": {
+ "version": "0.184.1",
+ "resolved": "https://registry.npmjs.org/@types/three/-/three-0.184.1.tgz",
+ "integrity": "sha512-6q4VdiqVsrTRqmk62/BnlcAvIrnDM0zf2ZDVKI5kZiniWrSaOHaQzmbp+BNzoggc/8tgW412pL//wZIxu2PPTA==",
+ "license": "MIT",
+ "dependencies": {
+ "@dimforge/rapier3d-compat": "~0.12.0",
+ "@tweenjs/tween.js": "~23.1.3",
+ "@types/stats.js": "*",
+ "@types/webxr": ">=0.5.17",
+ "fflate": "~0.8.2",
+ "meshoptimizer": "~1.1.1"
+ }
+ },
"node_modules/@types/unist": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
@@ -2075,6 +2109,12 @@
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
"license": "MIT"
},
+ "node_modules/@types/webxr": {
+ "version": "0.5.24",
+ "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz",
+ "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==",
+ "license": "MIT"
+ },
"node_modules/@ungap/structured-clone": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
@@ -3025,6 +3065,12 @@
}
}
},
+ "node_modules/fflate": {
+ "version": "0.8.2",
+ "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
+ "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
+ "license": "MIT"
+ },
"node_modules/form-data": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
@@ -4219,6 +4265,12 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/meshoptimizer": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.1.1.tgz",
+ "integrity": "sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g==",
+ "license": "MIT"
+ },
"node_modules/micromark": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz",
@@ -5656,6 +5708,12 @@
"url": "https://opencollective.com/webpack"
}
},
+ "node_modules/three": {
+ "version": "0.184.0",
+ "resolved": "https://registry.npmjs.org/three/-/three-0.184.0.tgz",
+ "integrity": "sha512-wtTRjG92pM5eUg/KuUnHsqSAlPM296brTOcLgMRqEeylYTh/CdtvKUvCyyCQTzFuStieWxvZb8mVTMvdPyUpxg==",
+ "license": "MIT"
+ },
"node_modules/tiny-invariant": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index f3f83792a..c01e0c681 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -10,6 +10,7 @@
"dependencies": {
"@tailwindcss/postcss": "^4.2.2",
"@tanstack/react-query": "^5.91.3",
+ "@types/three": "^0.184.1",
"autoprefixer": "^10.4.27",
"framer-motion": "^12.38.0",
"lucide-react": "^1.7.0",
@@ -22,7 +23,8 @@
"recharts": "^3.8.0",
"remark-gfm": "^4.0.1",
"tailwind-merge": "^3.5.0",
- "tailwindcss": "^4.2.2"
+ "tailwindcss": "^4.2.2",
+ "three": "^0.184.0"
},
"devDependencies": {
"@testing-library/jest-dom": "^6.4.5",
diff --git a/frontend/src/components/bounty/BountyCard.tsx b/frontend/src/components/bounty/BountyCard.tsx
index aa974a474..967c7ba4e 100644
--- a/frontend/src/components/bounty/BountyCard.tsx
+++ b/frontend/src/components/bounty/BountyCard.tsx
@@ -84,7 +84,7 @@ export function BountyCard({ bounty }: BountyCardProps) {
{/* Row 3: Language dots */}
{skills.length > 0 && (
-
+
{skills.map((lang) => (
{/* Row 4: Reward + Meta */}
-
+
{formatCurrency(bounty.reward_amount, bounty.reward_token)}
-
+
{bounty.submission_count} PRs
diff --git a/frontend/src/components/home/ForgeVisualization.tsx b/frontend/src/components/home/ForgeVisualization.tsx
new file mode 100644
index 000000000..5454e3f35
--- /dev/null
+++ b/frontend/src/components/home/ForgeVisualization.tsx
@@ -0,0 +1,206 @@
+import { useEffect, useRef } from 'react';
+import * as THREE from 'three';
+
+export function ForgeVisualization() {
+ const mountRef = useRef(null);
+ const pointerRef = useRef({ x: 0, y: 0 });
+
+ useEffect(() => {
+ const mount = mountRef.current;
+ if (!mount) return;
+
+ const scene = new THREE.Scene();
+ scene.fog = new THREE.Fog(0x02030a, 4, 18);
+
+ const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
+ renderer.setSize(mount.clientWidth, mount.clientHeight);
+ renderer.outputColorSpace = THREE.SRGBColorSpace;
+ mount.appendChild(renderer.domElement);
+
+ const camera = new THREE.PerspectiveCamera(55, mount.clientWidth / mount.clientHeight, 0.1, 100);
+ camera.position.set(0, 1.6, 5.8);
+
+ const ambient = new THREE.AmbientLight(0x4a6077, 0.55);
+ scene.add(ambient);
+
+ const key = new THREE.DirectionalLight(0x80f5ff, 0.7);
+ key.position.set(2, 5, 3);
+ scene.add(key);
+
+ const fireLight = new THREE.PointLight(0xff6a00, 2.2, 12, 2);
+ fireLight.position.set(0, 0.45, 0);
+ scene.add(fireLight);
+
+ const forgeBase = new THREE.Mesh(
+ new THREE.CylinderGeometry(1.7, 2.1, 1.25, 32),
+ new THREE.MeshStandardMaterial({ color: 0x111723, roughness: 0.86, metalness: 0.15 }),
+ );
+ forgeBase.position.y = -0.5;
+ scene.add(forgeBase);
+
+ const core = new THREE.Mesh(
+ new THREE.TorusGeometry(0.75, 0.24, 24, 64),
+ new THREE.MeshStandardMaterial({
+ color: 0xff7d1a,
+ emissive: 0xff4f00,
+ emissiveIntensity: 1.1,
+ roughness: 0.3,
+ metalness: 0.1,
+ }),
+ );
+ core.rotation.x = Math.PI / 2;
+ scene.add(core);
+
+ const anvil = new THREE.Mesh(
+ new THREE.BoxGeometry(1.05, 0.26, 0.5),
+ new THREE.MeshStandardMaterial({ color: 0x8c97a8, roughness: 0.44, metalness: 0.75 }),
+ );
+ anvil.position.set(0, 0.42, 0);
+ scene.add(anvil);
+
+ const hammerHead = new THREE.Mesh(
+ new THREE.BoxGeometry(0.44, 0.2, 0.25),
+ new THREE.MeshStandardMaterial({ color: 0xaab4c7, roughness: 0.35, metalness: 0.9 }),
+ );
+ const hammerHandle = new THREE.Mesh(
+ new THREE.CylinderGeometry(0.05, 0.05, 1, 12),
+ new THREE.MeshStandardMaterial({ color: 0x5a3626, roughness: 0.85, metalness: 0.15 }),
+ );
+ hammerHead.position.set(0.95, 1.05, 0.1);
+ hammerHandle.position.set(0.95, 0.6, 0.1);
+ hammerHandle.rotation.z = 0.18;
+ scene.add(hammerHead, hammerHandle);
+
+ const sparkCount = 520;
+ const sparkPositions = new Float32Array(sparkCount * 3);
+ const sparkVelocities = new Float32Array(sparkCount * 3);
+ for (let i = 0; i < sparkCount; i += 1) {
+ const j = i * 3;
+ sparkPositions[j] = 0;
+ sparkPositions[j + 1] = 0.35;
+ sparkPositions[j + 2] = 0;
+ }
+ const sparkGeo = new THREE.BufferGeometry();
+ sparkGeo.setAttribute('position', new THREE.BufferAttribute(sparkPositions, 3));
+ const sparks = new THREE.Points(
+ sparkGeo,
+ new THREE.PointsMaterial({ color: 0xffb347, size: 0.04, transparent: true, opacity: 0.85 }),
+ );
+ scene.add(sparks);
+
+ let lastForge = 0;
+ const forgeIntervalMs = 2200;
+ const clock = new THREE.Clock();
+ let userPulse = 0;
+
+ const triggerForge = () => {
+ fireLight.intensity = 3.7;
+ for (let i = 0; i < sparkCount; i += 1) {
+ const j = i * 3;
+ const angle = Math.random() * Math.PI * 2;
+ const speed = 0.9 + Math.random() * 2.4;
+ sparkPositions[j] = (Math.random() - 0.5) * 0.25;
+ sparkPositions[j + 1] = 0.4 + Math.random() * 0.16;
+ sparkPositions[j + 2] = (Math.random() - 0.5) * 0.25;
+ sparkVelocities[j] = Math.cos(angle) * speed * 0.4;
+ sparkVelocities[j + 1] = 1.5 + Math.random() * 2.2;
+ sparkVelocities[j + 2] = Math.sin(angle) * speed * 0.4;
+ }
+ sparkGeo.attributes.position.needsUpdate = true;
+ };
+
+ const onPointerMove = (event: PointerEvent) => {
+ if (!mount) return;
+ const rect = mount.getBoundingClientRect();
+ const x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
+ const y = -(((event.clientY - rect.top) / rect.height) * 2 - 1);
+ pointerRef.current.x = x;
+ pointerRef.current.y = y;
+ userPulse = 0.7;
+ };
+
+ const onPointerDown = () => {
+ triggerForge();
+ userPulse = 1;
+ };
+
+ const animate = () => {
+ const elapsed = clock.getElapsedTime();
+ const delta = Math.min(clock.getDelta(), 0.03);
+
+ if (performance.now() - lastForge > forgeIntervalMs) {
+ lastForge = performance.now();
+ triggerForge();
+ }
+
+ core.rotation.z += delta * 0.7;
+ core.scale.setScalar(1 + Math.sin(elapsed * 3.4) * 0.03);
+
+ const hammerPhase = (elapsed * 2.1) % 1;
+ const strikeCurve = Math.sin(hammerPhase * Math.PI);
+ hammerHead.position.y = 1.05 - strikeCurve * 0.54;
+ hammerHandle.position.y = 0.6 - strikeCurve * 0.5;
+ hammerHead.rotation.z = -strikeCurve * 0.18;
+ hammerHandle.rotation.z = 0.18 - strikeCurve * 0.2;
+
+ fireLight.intensity = 2.2 + Math.sin(elapsed * 6) * 0.35 + Math.max(0, 0.7 - hammerPhase * 0.7);
+
+ for (let i = 0; i < sparkCount; i += 1) {
+ const j = i * 3;
+ sparkPositions[j] += sparkVelocities[j] * delta;
+ sparkPositions[j + 1] += sparkVelocities[j + 1] * delta;
+ sparkPositions[j + 2] += sparkVelocities[j + 2] * delta;
+ sparkVelocities[j + 1] -= 4.2 * delta;
+ sparkVelocities[j] *= 0.986;
+ sparkVelocities[j + 2] *= 0.986;
+ if (sparkPositions[j + 1] < -0.2) {
+ sparkPositions[j] = 0;
+ sparkPositions[j + 1] = 0.25;
+ sparkPositions[j + 2] = 0;
+ sparkVelocities[j] = 0;
+ sparkVelocities[j + 1] = 0;
+ sparkVelocities[j + 2] = 0;
+ }
+ }
+ sparkGeo.attributes.position.needsUpdate = true;
+
+ const targetX = Math.sin(elapsed * 0.24) * 0.85 + pointerRef.current.x * 0.45;
+ const targetY = 1.6 + pointerRef.current.y * 0.28;
+ camera.position.x += (targetX - camera.position.x) * 0.06;
+ camera.position.y += (targetY - camera.position.y) * 0.06;
+ camera.lookAt(pointerRef.current.x * 0.2, 0.35 + pointerRef.current.y * 0.1, 0);
+
+ userPulse = Math.max(0, userPulse - delta * 1.8);
+ sparks.material.opacity = 0.72 + userPulse * 0.2;
+ fireLight.color.setHSL(0.06 + userPulse * 0.04, 1, 0.5);
+
+ renderer.render(scene, camera);
+ frame = requestAnimationFrame(animate);
+ };
+
+ let frame = requestAnimationFrame(animate);
+
+ const onResize = () => {
+ if (!mount) return;
+ camera.aspect = mount.clientWidth / mount.clientHeight;
+ camera.updateProjectionMatrix();
+ renderer.setSize(mount.clientWidth, mount.clientHeight);
+ };
+ window.addEventListener('resize', onResize);
+ mount.addEventListener('pointermove', onPointerMove);
+ mount.addEventListener('pointerdown', onPointerDown);
+
+ return () => {
+ cancelAnimationFrame(frame);
+ window.removeEventListener('resize', onResize);
+ mount.removeEventListener('pointermove', onPointerMove);
+ mount.removeEventListener('pointerdown', onPointerDown);
+ renderer.dispose();
+ sparkGeo.dispose();
+ mount.removeChild(renderer.domElement);
+ };
+ }, []);
+
+ return ;
+}
diff --git a/frontend/src/components/home/HeroSection.tsx b/frontend/src/components/home/HeroSection.tsx
index e37307166..7df31fa67 100644
--- a/frontend/src/components/home/HeroSection.tsx
+++ b/frontend/src/components/home/HeroSection.tsx
@@ -5,6 +5,7 @@ import { useStats } from '../../hooks/useStats';
import { getGitHubAuthorizeUrl } from '../../api/auth';
import { useAuth } from '../../hooks/useAuth';
import { buttonHover, fadeIn } from '../../lib/animations';
+import { ForgeVisualization } from './ForgeVisualization';
const GitHubIcon = () => (
{/* Terminal body */}
-
+
$
- forge bounty --reward 100 --lang typescript --tier 2
+ forge bounty --reward 100 --lang typescript --tier 2
+ forge bounty --reward 100 --lang ts
{typewriterDone && (
-
+
)}
@@ -211,7 +214,7 @@ export function HeroSection() {
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="flex flex-wrap items-center justify-center gap-3 sm:gap-6 mt-8 font-mono text-xs sm:text-sm text-text-muted"
>
diff --git a/frontend/src/components/layout/Navbar.tsx b/frontend/src/components/layout/Navbar.tsx
index e4ec31b03..80628c3b8 100644
--- a/frontend/src/components/layout/Navbar.tsx
+++ b/frontend/src/components/layout/Navbar.tsx
@@ -182,7 +182,7 @@ 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="absolute top-full left-0 right-0 md:hidden overflow-hidden bg-forge-900 border-b border-border shadow-2xl"
>
{NAV_LINKS.map((link) => (