Skip to content

Commit b7e5e89

Browse files
authored
Merge pull request #44 from itspavant/main
improved-ChallengeBoard.tsx
2 parents ec25956 + 01dc6ca commit b7e5e89

File tree

1 file changed

+109
-72
lines changed

1 file changed

+109
-72
lines changed

src/components/Challenges/ChallengeBoard.tsx

Lines changed: 109 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useState } from 'react';
1+
import React, { useState, useEffect, useMemo, useCallback } from 'react';
22
import { Search, Filter, Grid, List } from 'lucide-react';
33
import { ChallengeCard, Challenge } from './ChallengeCard';
44

@@ -10,69 +10,75 @@ const mockChallenges: Challenge[] = [
1010
{
1111
id: '1',
1212
title: 'Design a Mobile App Onboarding Flow',
13-
description: 'Create a 3-screen onboarding experience for a fitness tracking app. Focus on user engagement and clear value proposition.',
13+
description:
14+
'Create a 3-screen onboarding experience for a fitness tracking app. Focus on user engagement and clear value proposition.',
1415
domain: 'Design',
1516
difficulty: 'Medium',
1617
estimatedTime: '2-4 hours',
1718
participants: 234,
1819
rating: 4.7,
19-
tags: ['UI/UX', 'Mobile', 'Figma', 'Onboarding']
20+
tags: ['UI/UX', 'Mobile', 'Figma', 'Onboarding'],
2021
},
2122
{
2223
id: '2',
2324
title: 'Build a Real-Time Chat Application',
24-
description: 'Develop a chat app using WebSockets. Include user authentication, message history, and typing indicators.',
25+
description:
26+
'Develop a chat app using WebSockets. Include user authentication, message history, and typing indicators.',
2527
domain: 'Development',
2628
difficulty: 'Hard',
2729
estimatedTime: '6-8 hours',
2830
participants: 156,
2931
rating: 4.8,
30-
tags: ['React', 'Node.js', 'WebSocket', 'Real-time']
32+
tags: ['React', 'Node.js', 'WebSocket', 'Real-time'],
3133
},
3234
{
3335
id: '3',
3436
title: 'Write a Technical Blog Post',
35-
description: 'Explain a complex programming concept to beginners. Make it engaging with examples and practical applications.',
37+
description:
38+
'Explain a complex programming concept to beginners. Make it engaging with examples and practical applications.',
3639
domain: 'Writing',
3740
difficulty: 'Easy',
3841
estimatedTime: '1-2 hours',
3942
participants: 89,
4043
rating: 4.5,
41-
tags: ['Technical Writing', 'Blog', 'Tutorial']
44+
tags: ['Technical Writing', 'Blog', 'Tutorial'],
4245
},
4346
{
4447
id: '4',
4548
title: 'Analyze E-commerce Sales Data',
46-
description: 'Use Python to analyze a real e-commerce dataset. Create visualizations and provide actionable insights.',
49+
description:
50+
'Use Python to analyze a real e-commerce dataset. Create visualizations and provide actionable insights.',
4751
domain: 'Data',
4852
difficulty: 'Medium',
4953
estimatedTime: '3-5 hours',
5054
participants: 67,
5155
rating: 4.6,
52-
tags: ['Python', 'Pandas', 'Visualization', 'Analysis']
56+
tags: ['Python', 'Pandas', 'Visualization', 'Analysis'],
5357
},
5458
{
5559
id: '5',
5660
title: 'Create a Brand Identity System',
57-
description: 'Design a complete brand identity for a sustainable fashion startup, including logo, colors, and guidelines.',
61+
description:
62+
'Design a complete brand identity for a sustainable fashion startup, including logo, colors, and guidelines.',
5863
domain: 'Design',
5964
difficulty: 'Hard',
6065
estimatedTime: '8-12 hours',
6166
participants: 123,
6267
rating: 4.9,
63-
tags: ['Branding', 'Logo Design', 'Brand Guidelines']
68+
tags: ['Branding', 'Logo Design', 'Brand Guidelines'],
6469
},
6570
{
6671
id: '6',
6772
title: 'Build a REST API with Authentication',
68-
description: 'Create a secure REST API for a task management system. Include JWT authentication and proper error handling.',
73+
description:
74+
'Create a secure REST API for a task management system. Include JWT authentication and proper error handling.',
6975
domain: 'Development',
7076
difficulty: 'Medium',
7177
estimatedTime: '4-6 hours',
7278
participants: 198,
7379
rating: 4.4,
74-
tags: ['API', 'Node.js', 'JWT', 'Authentication']
75-
}
80+
tags: ['API', 'Node.js', 'JWT', 'Authentication'],
81+
},
7682
];
7783

7884
export const ChallengeBoard: React.FC<ChallengeBoardProps> = ({ onChallengeSelect }) => {
@@ -82,101 +88,141 @@ export const ChallengeBoard: React.FC<ChallengeBoardProps> = ({ onChallengeSelec
8288
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
8389
const [bookmarkedChallenges, setBookmarkedChallenges] = useState<Set<string>>(new Set());
8490

91+
// Load bookmarks safely
92+
useEffect(() => {
93+
try {
94+
const saved = localStorage.getItem('bookmarkedChallenges');
95+
if (saved) {
96+
const parsed: unknown = JSON.parse(saved);
97+
if (Array.isArray(parsed)) {
98+
setBookmarkedChallenges(new Set(parsed as string[]));
99+
}
100+
}
101+
} catch (err) {
102+
// corrupted localStorage; ignore and reset
103+
console.warn('Failed to parse bookmarkedChallenges from localStorage:', err);
104+
localStorage.removeItem('bookmarkedChallenges');
105+
setBookmarkedChallenges(new Set());
106+
}
107+
}, []);
108+
109+
// Persist bookmarks
110+
useEffect(() => {
111+
try {
112+
localStorage.setItem('bookmarkedChallenges', JSON.stringify(Array.from(bookmarkedChallenges)));
113+
} catch (err) {
114+
console.warn('Failed to save bookmarkedChallenges to localStorage:', err);
115+
}
116+
}, [bookmarkedChallenges]);
117+
85118
const domains = ['All', 'Design', 'Development', 'Writing', 'Data', 'Creative'];
86119
const difficulties = ['All', 'Easy', 'Medium', 'Hard'];
87120

88-
const filteredChallenges = mockChallenges.filter(challenge => {
89-
const matchesSearch = challenge.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
90-
challenge.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
91-
challenge.tags.some(tag => tag.toLowerCase().includes(searchTerm.toLowerCase()));
92-
93-
const matchesDomain = selectedDomain === 'All' || challenge.domain === selectedDomain;
94-
const matchesDifficulty = selectedDifficulty === 'All' || challenge.difficulty === selectedDifficulty;
95-
96-
return matchesSearch && matchesDomain && matchesDifficulty;
97-
});
98-
99-
const handleBookmark = (challengeId: string) => {
100-
const newBookmarked = new Set(bookmarkedChallenges);
101-
if (newBookmarked.has(challengeId)) {
102-
newBookmarked.delete(challengeId);
103-
} else {
104-
newBookmarked.add(challengeId);
105-
}
106-
setBookmarkedChallenges(newBookmarked);
107-
};
121+
// Filter (memoized)
122+
const filteredChallenges = useMemo(() => {
123+
const q = searchTerm.trim().toLowerCase();
124+
return mockChallenges.filter((challenge) => {
125+
const matchesSearch =
126+
!q ||
127+
challenge.title.toLowerCase().includes(q) ||
128+
challenge.description.toLowerCase().includes(q) ||
129+
challenge.tags.some((tag) => tag.toLowerCase().includes(q));
108130

109-
const challengesWithBookmarks = filteredChallenges.map(challenge => ({
110-
...challenge,
111-
isBookmarked: bookmarkedChallenges.has(challenge.id)
112-
}));
131+
const matchesDomain = selectedDomain === 'All' || challenge.domain === selectedDomain;
132+
const matchesDifficulty = selectedDifficulty === 'All' || challenge.difficulty === selectedDifficulty;
133+
134+
return matchesSearch && matchesDomain && matchesDifficulty;
135+
});
136+
}, [searchTerm, selectedDomain, selectedDifficulty]);
137+
138+
// Bookmark handler (stable identity)
139+
const handleBookmark = useCallback((challengeId: string) => {
140+
setBookmarkedChallenges((prev) => {
141+
const next = new Set(prev);
142+
if (next.has(challengeId)) next.delete(challengeId);
143+
else next.add(challengeId);
144+
return next;
145+
});
146+
}, []);
147+
148+
const challengesWithBookmarks = useMemo(
149+
() =>
150+
filteredChallenges.map((challenge) => ({
151+
...challenge,
152+
isBookmarked: bookmarkedChallenges.has(challenge.id),
153+
})),
154+
[filteredChallenges, bookmarkedChallenges]
155+
);
113156

114157
return (
115-
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 pt-8 transition-colors duration-300">
158+
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-8 transition-colors duration-300">
116159
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
117160
{/* Header */}
118161
<div className="mb-8">
119-
<h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-2">Challenge Board</h1>
120-
<p className="text-gray-600 dark:text-gray-400">Discover real-world challenges to level up your skills</p>
162+
<h1 className="text-4xl font-extrabold text-gray-900 dark:text-white mb-2">Challenge Board</h1>
163+
<p className="text-gray-600 dark:text-gray-400 text-lg">Explore real-world challenges and level up your skills</p>
121164
</div>
122165

123166
{/* Filters */}
124-
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 p-6 mb-8 transition-colors duration-300">
167+
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-lg border border-gray-200 dark:border-gray-700 p-6 mb-8 transition-colors duration-300">
125168
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4">
126169
{/* Search */}
127170
<div className="relative flex-1 max-w-md">
128-
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400 dark:text-gray-500" />
171+
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-gray-400 dark:text-gray-500" />
129172
<input
130173
type="text"
174+
aria-label="Search challenges"
131175
placeholder="Search challenges..."
132176
value={searchTerm}
133177
onChange={(e) => setSearchTerm(e.target.value)}
134-
className="w-full pl-10 pr-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
178+
className="w-full pl-10 pr-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white transition-shadow duration-200"
135179
/>
136180
</div>
137181

138182
{/* Filters */}
139183
<div className="flex items-center space-x-4">
140184
<select
185+
aria-label="Filter by domain"
141186
value={selectedDomain}
142187
onChange={(e) => setSelectedDomain(e.target.value)}
143-
className="px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
188+
className="px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white transition-colors duration-200"
144189
>
145-
{domains.map(domain => (
146-
<option key={domain} value={domain}>{domain} Domain</option>
190+
{domains.map((domain) => (
191+
<option key={domain} value={domain}>
192+
{domain} Domain
193+
</option>
147194
))}
148195
</select>
149196

150197
<select
198+
aria-label="Filter by difficulty"
151199
value={selectedDifficulty}
152200
onChange={(e) => setSelectedDifficulty(e.target.value)}
153-
className="px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
201+
className="px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white transition-colors duration-200"
154202
>
155-
{difficulties.map(difficulty => (
203+
{difficulties.map((difficulty) => (
156204
<option key={difficulty} value={difficulty}>
157205
{difficulty === 'All' ? 'All Levels' : difficulty}
158206
</option>
159207
))}
160208
</select>
161209

162-
<div className="flex border border-gray-300 dark:border-gray-600 rounded-lg">
210+
<div className="flex border border-gray-300 dark:border-gray-600 rounded-lg overflow-hidden" role="tablist" aria-label="View mode">
163211
<button
212+
type="button"
213+
aria-pressed={viewMode === 'grid'}
164214
onClick={() => setViewMode('grid')}
165-
className={`p-2 transition-colors duration-200 ${
166-
viewMode === 'grid'
167-
? 'bg-purple-100 text-purple-600 dark:bg-purple-900 dark:text-purple-400'
168-
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700'
169-
}`}
215+
className={`p-2 transition-colors duration-200 ${viewMode === 'grid' ? 'bg-purple-100 text-purple-600 dark:bg-purple-900 dark:text-purple-400' : 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700'}`}
216+
title="Grid view"
170217
>
171218
<Grid className="h-4 w-4" />
172219
</button>
173220
<button
221+
type="button"
222+
aria-pressed={viewMode === 'list'}
174223
onClick={() => setViewMode('list')}
175-
className={`p-2 transition-colors duration-200 ${
176-
viewMode === 'list'
177-
? 'bg-purple-100 text-purple-600 dark:bg-purple-900 dark:text-purple-400'
178-
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700'
179-
}`}
224+
className={`p-2 transition-colors duration-200 ${viewMode === 'list' ? 'bg-purple-100 text-purple-600 dark:bg-purple-900 dark:text-purple-400' : 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700'}`}
225+
title="List view"
180226
>
181227
<List className="h-4 w-4" />
182228
</button>
@@ -193,22 +239,13 @@ export const ChallengeBoard: React.FC<ChallengeBoardProps> = ({ onChallengeSelec
193239
</div>
194240

195241
{/* Challenge Grid */}
196-
<div className={`grid gap-6 ${
197-
viewMode === 'grid'
198-
? 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3'
199-
: 'grid-cols-1'
200-
}`}>
242+
<div className={`grid gap-6 transition-all duration-300 ${viewMode === 'grid' ? 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3' : 'grid-cols-1'}`}>
201243
{challengesWithBookmarks.length > 0 ? (
202244
challengesWithBookmarks.map((challenge) => (
203-
<ChallengeCard
204-
key={challenge.id}
205-
challenge={challenge}
206-
onSelect={onChallengeSelect}
207-
onBookmark={handleBookmark}
208-
/>
245+
<ChallengeCard key={challenge.id} challenge={challenge} onSelect={onChallengeSelect} onBookmark={handleBookmark} />
209246
))
210247
) : (
211-
<div className="text-center py-12 col-span-full">
248+
<div className="text-center py-16 col-span-full">
212249
<Filter className="h-12 w-12 text-gray-400 dark:text-gray-600 mx-auto mb-4" />
213250
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2">No challenges found</h3>
214251
<p className="text-gray-600 dark:text-gray-400">Try adjusting your search or filters</p>

0 commit comments

Comments
 (0)