From a95d1639ed5bc25f8138b0642618ce81c16588df Mon Sep 17 00:00:00 2001 From: liqiuniu <1165448306@qq.com> Date: Sun, 5 Apr 2026 15:29:56 +0800 Subject: [PATCH] feat: add advanced bounty search with filters (Bounty #842) --- src/components/AdvancedBountySearch.tsx | 416 ++++++++++++++++++++++++ 1 file changed, 416 insertions(+) create mode 100644 src/components/AdvancedBountySearch.tsx diff --git a/src/components/AdvancedBountySearch.tsx b/src/components/AdvancedBountySearch.tsx new file mode 100644 index 000000000..f301f3fb4 --- /dev/null +++ b/src/components/AdvancedBountySearch.tsx @@ -0,0 +1,416 @@ +import { useState, useEffect } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { Search, X, Save, Trash2, Filter, ChevronDown } from 'lucide-react'; + +interface SearchFilters { + query: string; + languages: string[]; + tiers: string[]; + domains: string[]; + rewardMin: number; + rewardMax: number; + sortBy: 'reward' | 'deadline' | 'created'; + sortOrder: 'asc' | 'desc'; +} + +interface SavedFilter { + id: string; + name: string; + filters: SearchFilters; +} + +const LANGUAGES = [ + 'TypeScript', + 'JavaScript', + 'Python', + 'Rust', + 'Go', + 'Solidity', + 'C++', + 'Java', +]; + +const TIERS = ['T1', 'T2', 'T3']; + +const DOMAINS = [ + 'Frontend', + 'Backend', + 'Smart Contracts', + 'Documentation', + 'Design', + 'DevOps', + 'AI/ML', +]; + +const REWARD_PRESETS = [ + { label: 'All', min: 0, max: 1000000 }, + { label: '< 100K', min: 0, max: 100000 }, + { label: '100K - 500K', min: 100000, max: 500000 }, + { label: '> 500K', min: 500000, max: 1000000 }, +]; + +const defaultFilters: SearchFilters = { + query: '', + languages: [], + tiers: [], + domains: [], + rewardMin: 0, + rewardMax: 1000000, + sortBy: 'reward', + sortOrder: 'desc', +}; + +export function AdvancedBountySearch({ + onFiltersChange, +}: { + onFiltersChange: (filters: SearchFilters) => void; +}) { + const [filters, setFilters] = useState(defaultFilters); + const [savedFilters, setSavedFilters] = useState([]); + const [showFilters, setShowFilters] = useState(false); + const [showSaveModal, setShowSaveModal] = useState(false); + const [saveName, setSaveName] = useState(''); + const [activePreset, setActivePreset] = useState(0); + + // Load saved filters from localStorage + useEffect(() => { + const saved = localStorage.getItem('bounty-saved-filters'); + if (saved) { + setSavedFilters(JSON.parse(saved)); + } + }, []); + + // Notify parent of filter changes + useEffect(() => { + onFiltersChange(filters); + }, [filters, onFiltersChange]); + + const toggleArrayFilter = ( + key: 'languages' | 'tiers' | 'domains', + value: string + ) => { + setFilters((prev) => ({ + ...prev, + [key]: prev[key].includes(value) + ? prev[key].filter((v) => v !== value) + : [...prev[key], value], + })); + }; + + const applyPreset = (index: number) => { + const preset = REWARD_PRESETS[index]; + setFilters((prev) => ({ + ...prev, + rewardMin: preset.min, + rewardMax: preset.max, + })); + setActivePreset(index); + }; + + const saveFilterSet = () => { + if (!saveName.trim()) return; + + const newSaved: SavedFilter = { + id: Date.now().toString(), + name: saveName, + filters, + }; + + const updated = [...savedFilters, newSaved]; + setSavedFilters(updated); + localStorage.setItem('bounty-saved-filters', JSON.stringify(updated)); + setShowSaveModal(false); + setSaveName(''); + }; + + const loadFilterSet = (saved: SavedFilter) => { + setFilters(saved.filters); + setShowFilters(false); + }; + + const deleteFilterSet = (id: string) => { + const updated = savedFilters.filter((f) => f.id !== id); + setSavedFilters(updated); + localStorage.setItem('bounty-saved-filters', JSON.stringify(updated)); + }; + + const clearAllFilters = () => { + setFilters(defaultFilters); + setActivePreset(0); + }; + + const hasActiveFilters = + filters.languages.length > 0 || + filters.tiers.length > 0 || + filters.domains.length > 0 || + filters.query !== ''; + + return ( +
+ {/* Search Bar */} +
+
+ + + setFilters((prev) => ({ ...prev, query: e.target.value })) + } + placeholder="Search bounties..." + className="w-full pl-10 pr-4 py-3 bg-forge-800 border border-forge-700 rounded-lg text-primary placeholder:text-muted focus:outline-none focus:border-emerald transition-colors" + /> + {filters.query && ( + + )} +
+ + + + +
+ + {/* Filter Panel */} + + {showFilters && ( + + {/* Reward Range */} +
+ +
+ {REWARD_PRESETS.map((preset, i) => ( + + ))} +
+
+ + {/* Tiers */} +
+ +
+ {TIERS.map((tier) => ( + + ))} +
+
+ + {/* Languages */} +
+ +
+ {LANGUAGES.map((lang) => ( + + ))} +
+
+ + {/* Domains */} +
+ +
+ {DOMAINS.map((domain) => ( + + ))} +
+
+ + {/* Sort & Clear */} +
+
+ + + +
+ + {hasActiveFilters && ( + + )} +
+
+ )} +
+ + {/* Saved Filters */} + {savedFilters.length > 0 && ( +
+ Saved: + {savedFilters.map((saved) => ( +
+ + +
+ ))} +
+ )} + + {/* Save Modal */} + + {showSaveModal && ( + setShowSaveModal(false)} + > + e.stopPropagation()} + className="bg-forge-900 border border-forge-700 rounded-lg p-6 w-full max-w-md" + > +

+ Save Filter Set +

+ setSaveName(e.target.value)} + placeholder="Filter name..." + className="w-full px-4 py-2 bg-forge-800 border border-forge-700 rounded text-primary placeholder:text-muted focus:outline-none focus:border-emerald mb-4" + /> +
+ + +
+
+
+ )} +
+
+ ); +} + +export default AdvancedBountySearch; \ No newline at end of file