From deb1246cab3b479c409993b2568667312b5c8a41 Mon Sep 17 00:00:00 2001 From: D2758695161 <13510221939@163.com> Date: Sat, 4 Apr 2026 23:48:58 +0800 Subject: [PATCH 01/19] feat(widget): add FNDRY token price widget component Bounty: T2 FNDRY Price Widget - 400K FNDRY - Real-time price via DexScreener API - 24h change with up/down indicator - Sparkline chart with Recharts - Market cap + liquidity stats - Auto-refresh every 60s --- .../src/components/home/FNDRYPriceWidget.tsx | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 frontend/src/components/home/FNDRYPriceWidget.tsx diff --git a/frontend/src/components/home/FNDRYPriceWidget.tsx b/frontend/src/components/home/FNDRYPriceWidget.tsx new file mode 100644 index 00000000..ccf7432c --- /dev/null +++ b/frontend/src/components/home/FNDRYPriceWidget.tsx @@ -0,0 +1,121 @@ +import React, { useState, useEffect } from 'react'; +import { LineChart, Line, ResponsiveContainer } from 'recharts'; + +const TOKEN_MINT = 'C2TvY8E8B75EF2UP8cTpTp3EDUjTgjWmpaGnT74VBAGS'; +const API_URL = 'https://api.dexscreener.com/v1/token/' + TOKEN_MINT; + +interface SparklinePoint { price: string; timestamp: number; } + +interface TokenInfo { + price: number; + priceChange24h: number; + volume24h: number; + marketCap: number; + liquidity: number; + sparkline: SparklinePoint[]; +} + +const EMPTY: TokenInfo = { price: 0, priceChange24h: 0, volume24h: 0, marketCap: 0, liquidity: 0, sparkline: [] }; + +function fmt(price: number): string { + if (price >= 1) return '$' + price.toFixed(2); + if (price >= 0.01) return '$' + price.toFixed(4); + return '$' + price.toFixed(8); +} + +function fmtNum(n: number): string { + if (n >= 1e9) return '$' + (n/1e9).toFixed(1) + 'B'; + if (n >= 1e6) return '$' + (n/1e6).toFixed(1) + 'M'; + if (n >= 1e3) return '$' + (n/1e3).toFixed(1) + 'K'; + return '$' + n.toFixed(0); +} + +export function FNDRYPriceWidget({ className = '' }: { className?: string }) { + const [info, setInfo] = useState(EMPTY); + const [loading, setLoading] = useState(true); + const [err, setErr] = useState(null); + const [updated, setUpdated] = useState(null); + + const load = async () => { + try { + const res = await fetch(API_URL); + if (!res.ok) throw new Error('HTTP ' + res.status); + const json = await res.json(); + const pairs: any[] = Array.isArray(json) ? json : (json?.data ?? []); + const pair = pairs[0]; + if (!pair) { setErr('No trading data'); setLoading(false); return; } + + const price = parseFloat(pair.priceUsd ?? '0'); + const change = parseFloat(pair.priceChange?.h24 ?? '0'); + const vol = parseFloat(pair.volume?.h24 ?? '0'); + const liq = parseFloat(pair.liquidity?.usd ?? '0'); + const cap = parseFloat(pair.marketCap ?? '0'); + + let sparkline: SparklinePoint[] = []; + if (pair.txs?.h24?.length) { + sparkline = pair.txs.h24.slice(-20).map((tx: any) => ({ + price: tx.priceUsd ?? '0', + timestamp: tx.blockTimestamp ?? Date.now(), + })); + } + + setInfo({ price, priceChange24h: change, volume24h: vol, marketCap: cap, liquidity: liq, sparkline }); + setUpdated(new Date()); + setErr(null); + } catch(e: any) { + setErr(e.message ?? 'fetch failed'); + } finally { + setLoading(false); + } + }; + + useEffect(() => { load(); const t = setInterval(load, 60000); return () => clearInterval(t); }, []); + + if (loading) return ( +
+
+
+
+
+ ); + + if (err) return ( +
+

Price unavailable

+
+ ); + + const up = info.priceChange24h >= 0; + const color = up ? 'text-green-400' : 'text-red-400'; + + return ( +
+
+
+
$
+ FNDRY +
+ {updated ? updated.toLocaleTimeString() : ''} +
+
{fmt(info.price)}
+
+ {up ? 'โ†‘' : 'โ†“'} + {Math.abs(info.priceChange24h).toFixed(2)}% + 24h +
+ {info.sparkline.length > 1 && ( +
+ + + + + +
+ )} +
+
Market Cap
{fmtNum(info.marketCap)}
+
Liquidity
{fmtNum(info.liquidity)}
+
+
+ ); +} From 137c61b970d92aa94b4ea5bcbe0bda4bf5dc7d23 Mon Sep 17 00:00:00 2001 From: D2758695161 <13510221939@163.com> Date: Sat, 4 Apr 2026 23:48:59 +0800 Subject: [PATCH 02/19] feat(hooks): add useFNDRYPrice hook Bounty: T2 FNDRY Price Widget - 400K FNDRY --- frontend/src/hooks/useFNDRYPrice.ts | 41 +++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 frontend/src/hooks/useFNDRYPrice.ts diff --git a/frontend/src/hooks/useFNDRYPrice.ts b/frontend/src/hooks/useFNDRYPrice.ts new file mode 100644 index 00000000..47b3cbe8 --- /dev/null +++ b/frontend/src/hooks/useFNDRYPrice.ts @@ -0,0 +1,41 @@ +import { useState, useEffect, useCallback } from 'react'; + +const MINT = 'C2TvY8E8B75EF2UP8cTpTp3EDUjTgjWmpaGnT74VBAGS'; +const URL = 'https://api.dexscreener.com/v1/token/' + MINT; + +export interface FNDRYData { + price: number; + priceChange24h: number; + volume24h: number; + liquidity: number; + marketCap: number; +} + +export function useFNDRYPrice() { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetch_ = useCallback(async () => { + try { + const res = await fetch(URL); + if (!res.ok) throw new Error('HTTP ' + res.status); + const json = await res.json(); + const pairs: any[] = Array.isArray(json) ? json : (json?.data ?? []); + const pair = pairs[0]; + if (!pair) { setError('No data'); return; } + setData({ + price: parseFloat(pair.priceUsd ?? '0'), + priceChange24h: parseFloat(pair.priceChange?.h24 ?? '0'), + volume24h: parseFloat(pair.volume?.h24 ?? '0'), + liquidity: parseFloat(pair.liquidity?.usd ?? '0'), + marketCap: parseFloat(pair.marketCap ?? '0'), + }); + setError(null); + } catch(e: any) { setError(e.message); } + finally { setLoading(false); } + }, []); + + useEffect(() => { fetch_(); const t = setInterval(fetch_, 60000); return () => clearInterval(t); }, [fetch_]); + return { data, loading, error, refetch: fetch_ }; +} From 7362b602ae89e76968bfb7f1bb37aa8f25cc162a Mon Sep 17 00:00:00 2001 From: D2758695161 <13510221939@163.com> Date: Sat, 4 Apr 2026 23:49:00 +0800 Subject: [PATCH 03/19] feat: export FNDRYPriceWidget --- frontend/src/components/home/index.ts | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 frontend/src/components/home/index.ts diff --git a/frontend/src/components/home/index.ts b/frontend/src/components/home/index.ts new file mode 100644 index 00000000..5ab4b2ff --- /dev/null +++ b/frontend/src/components/home/index.ts @@ -0,0 +1,6 @@ +export { HeroSection } from './HeroSection'; +export { WhySolFoundry } from './WhySolFoundry'; +export { HowItWorksCondensed } from './HowItWorksCondensed'; +export { FeaturedBounties } from './FeaturedBounties'; +export { ActivityFeed } from './ActivityFeed'; +export { FNDRYPriceWidget } from './FNDRYPriceWidget'; From e9b65ad3a91c41b30d20705946d68d36454e4e3d Mon Sep 17 00:00:00 2001 From: D2758695161 <13510221939@163.com> Date: Sat, 4 Apr 2026 23:54:36 +0800 Subject: [PATCH 04/19] feat(auth): add Contributor Onboarding Wizard Bounty: T2 Contributor Onboarding Wizard - 400K FNDRY - Multi-step wizard: profile, skills, wallet, done - Skill/language preference selection - Wallet address input with verification flow - Framer Motion transitions --- .../src/components/auth/OnboardingWizard.tsx | 312 ++++++++++++++++++ 1 file changed, 312 insertions(+) create mode 100644 frontend/src/components/auth/OnboardingWizard.tsx diff --git a/frontend/src/components/auth/OnboardingWizard.tsx b/frontend/src/components/auth/OnboardingWizard.tsx new file mode 100644 index 00000000..6939fb35 --- /dev/null +++ b/frontend/src/components/auth/OnboardingWizard.tsx @@ -0,0 +1,312 @@ +import React, { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { motion, AnimatePresence } from 'framer-motion'; +import { useAuth } from '../../hooks/useAuth'; +import { fadeIn } from '../../lib/animations'; + +const SKILL_OPTIONS = [ + { id: 'react', label: 'React / Next.js' }, + { id: 'vue', label: 'Vue / Nuxt' }, + { id: 'svelte', label: 'Svelte' }, + { id: 'node', label: 'Node.js / Express' }, + { id: 'python', label: 'Python / FastAPI' }, + { id: 'rust', label: 'Rust' }, + { id: 'solidity', label: 'Solidity / EVM' }, + { id: 'solana', label: 'Solana / Anchor' }, + { id: 'ai-ml', label: 'AI / ML' }, + { id: 'devops', label: 'DevOps / Cloud' }, + { id: 'security', label: 'Security / Audit' }, + { id: 'docs', label: 'Technical Writing' }, +]; + +const LANG_OPTIONS = [ + { id: 'typescript', label: 'TypeScript' }, + { id: 'python', label: 'Python' }, + { id: 'rust', label: 'Rust' }, + { id: 'go', label: 'Go' }, + { id: 'solidity', label: 'Solidity' }, +]; + +const STEPS = [ + { id: 'profile', title: 'Profile', icon: '๐Ÿ‘ค' }, + { id: 'skills', title: 'Skills', icon: '๐Ÿ› ๏ธ' }, + { id: 'wallet', title: 'Wallet', icon: '๐Ÿ’œ' }, + { id: 'done', title: 'Done!', icon: '๐ŸŽ‰' }, +]; + +export function OnboardingWizard() { + const { user, updateUser } = useAuth(); + const navigate = useNavigate(); + const [step, setStep] = useState(0); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + // Step data + const [username, setUsername] = useState(user?.username ?? ''); + const [bio, setBio] = useState(''); + const [selectedSkills, setSelectedSkills] = useState([]); + const [selectedLangs, setSelectedLangs] = useState(['typescript']); + const [walletAddr, setWalletAddr] = useState(user?.wallet_address ?? ''); + const [walletVerified, setWalletVerified] = useState(user?.wallet_verified ?? false); + + const isLastStep = step === STEPS.length - 2; + const isDone = step === STEPS.length - 1; + + const toggleSkill = (id: string) => { + setSelectedSkills(prev => + prev.includes(id) ? prev.filter(s => s !== id) : [...prev, id] + ); + }; + + const toggleLang = (id: string) => { + setSelectedLangs(prev => + prev.includes(id) ? prev.filter(l => l !== id) : [...prev, id] + ); + }; + + const next = () => setStep(s => Math.min(s + 1, STEPS.length - 1)); + const back = () => setStep(s => Math.max(s - 1, 0)); + + const handleFinish = async () => { + setLoading(true); + setError(null); + try { + // Update user profile with all onboarding data + updateUser({ + username: username || user?.username ?? '', + wallet_address: walletAddr || undefined, + wallet_verified: walletVerified, + }); + // In a real app, would POST to /api/contributors/me with skills/langs + next(); + } catch(e: any) { + setError(e.message ?? 'Something went wrong'); + } finally { + setLoading(false); + } + }; + + const skipToHome = () => navigate('/', { replace: true }); + + const stepVariants = { + enter: (dir: number) => ({ x: dir > 0 ? 60 : -60, opacity: 0 }), + center: { x: 0, opacity: 1 }, + exit: (dir: number) => ({ x: dir < 0 ? 60 : -60, opacity: 0 }), + }; + + return ( +
+
+ {/* Header */} +
+

Welcome to SolFoundry

+

Complete your contributor profile to start earning

+
+ + {/* Progress Steps */} +
+ {STEPS.map((s, i) => ( + +
+
+ {i < step ? 'โœ“' : s.icon} +
+ {s.title} +
+ {i < STEPS.length - 1 && ( +
+ )} + + ))} +
+ + {/* Step Card */} +
+ + + {/* Step 0: Profile */} + {step === 0 && ( +
+

Set up your profile

+
+
+ + setUsername(e.target.value)} + placeholder={user?.username ?? 'your_username'} + className="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-2.5 text-white placeholder-gray-500 focus:outline-none focus:border-purple-500" + /> +
+
+ +