@@ -99,6 +99,86 @@ const sampleProjects = [
9999 let currentProjects = [ ...sampleProjects ] ;
100100 let selectedTag = null ;
101101
102+ // Voting system
103+ class VotingSystem {
104+ constructor ( ) {
105+ this . userFingerprint = this . generateUserFingerprint ( ) ;
106+ this . votes = this . loadVotes ( ) ;
107+ this . initializeProjectVotes ( ) ;
108+ }
109+
110+ generateUserFingerprint ( ) {
111+ let fingerprint = localStorage . getItem ( 'userFingerprint' ) ;
112+ if ( ! fingerprint ) {
113+ fingerprint = 'user_' + Date . now ( ) + '_' + Math . random ( ) . toString ( 36 ) . substr ( 2 , 9 ) ;
114+ localStorage . setItem ( 'userFingerprint' , fingerprint ) ;
115+ }
116+ return fingerprint ;
117+ }
118+
119+ loadVotes ( ) {
120+ const savedVotes = localStorage . getItem ( 'projectVotes' ) ;
121+ return savedVotes ? JSON . parse ( savedVotes ) : { } ;
122+ }
123+
124+ saveVotes ( ) {
125+ localStorage . setItem ( 'projectVotes' , JSON . stringify ( this . votes ) ) ;
126+ }
127+
128+ initializeProjectVotes ( ) {
129+ sampleProjects . forEach ( project => {
130+ if ( ! this . votes [ project . id ] ) {
131+ this . votes [ project . id ] = {
132+ count : project . upvotes || 0 ,
133+ voters : [ ]
134+ } ;
135+ }
136+ } ) ;
137+ this . saveVotes ( ) ;
138+ }
139+
140+ canUserVote ( projectId ) {
141+ const projectVotes = this . votes [ projectId ] ;
142+ return projectVotes && ! projectVotes . voters . includes ( this . userFingerprint ) ;
143+ }
144+
145+ upvoteProject ( projectId ) {
146+ if ( ! this . canUserVote ( projectId ) ) {
147+ return { success : false , message : 'You have already voted for this project!' } ;
148+ }
149+
150+ this . votes [ projectId ] . count ++ ;
151+ this . votes [ projectId ] . voters . push ( this . userFingerprint ) ;
152+ this . saveVotes ( ) ;
153+
154+ // Update the project in currentProjects array
155+ const project = currentProjects . find ( p => p . id === projectId ) ;
156+ if ( project ) {
157+ project . upvotes = this . votes [ projectId ] . count ;
158+ }
159+
160+ // Update the project in sampleProjects array
161+ const sampleProject = sampleProjects . find ( p => p . id === projectId ) ;
162+ if ( sampleProject ) {
163+ sampleProject . upvotes = this . votes [ projectId ] . count ;
164+ }
165+
166+ return { success : true , newCount : this . votes [ projectId ] . count } ;
167+ }
168+
169+ getProjectVotes ( projectId ) {
170+ return this . votes [ projectId ] ? this . votes [ projectId ] . count : 0 ;
171+ }
172+
173+ hasUserVoted ( projectId ) {
174+ const projectVotes = this . votes [ projectId ] ;
175+ return projectVotes && projectVotes . voters . includes ( this . userFingerprint ) ;
176+ }
177+ }
178+
179+ // Initialize voting system
180+ const votingSystem = new VotingSystem ( ) ;
181+
102182 //Store all the unique tags
103183 const allTagSet = new Set ( ) ;
104184 sampleProjects . forEach ( project => {
@@ -111,6 +191,7 @@ const sampleProjects = [
111191 const projectsContainer = document . getElementById ( 'projects-container' ) ;
112192 const loadingElement = document . getElementById ( 'loading' ) ;
113193 const emptyStateElement = document . getElementById ( 'empty-state' ) ;
194+ const sortByFilter = document . getElementById ( 'sort-by' ) ;
114195 const difficultyFilter = document . getElementById ( 'difficulty' ) ;
115196 const hasDemoFilter = document . getElementById ( 'has-demo' ) ;
116197 const applyFiltersBtn = document . getElementById ( 'apply-filters' ) ;
@@ -147,6 +228,7 @@ const sampleProjects = [
147228 function setupEventListeners ( ) {
148229 applyFiltersBtn . addEventListener ( 'click' , applyFilters ) ;
149230 resetFiltersBtn . addEventListener ( 'click' , resetFilters ) ;
231+ sortByFilter . addEventListener ( 'change' , applyFilters ) ;
150232
151233 // Search functionality
152234 searchInput . addEventListener ( 'input' , handleSearch ) ;
@@ -191,11 +273,43 @@ const sampleProjects = [
191273 return ;
192274 }
193275
276+ // Sort projects based on selected option
277+ const sortBy = sortByFilter . value ;
278+ const sortedProjects = [ ...projects ] . sort ( ( a , b ) => {
279+ switch ( sortBy ) {
280+ case 'popularity' :
281+ const aVotes = votingSystem . getProjectVotes ( a . id ) ;
282+ const bVotes = votingSystem . getProjectVotes ( b . id ) ;
283+ return bVotes - aVotes ;
284+
285+ case 'newest' :
286+ // Since we don't have dates, sort by ID (assuming higher ID = newer)
287+ return b . id - a . id ;
288+
289+ case 'difficulty' :
290+ const difficultyOrder = { 'beginner' : 1 , 'intermediate' : 2 , 'advanced' : 3 } ;
291+ return difficultyOrder [ a . difficulty ] - difficultyOrder [ b . difficulty ] ;
292+
293+ case 'alphabetical' :
294+ return a . title . localeCompare ( b . title ) ;
295+
296+ default :
297+ return 0 ;
298+ }
299+ } ) ;
300+
194301 emptyStateElement . style . display = 'none' ;
195302 projectsContainer . style . display = 'grid' ;
196303
197- projectsContainer . innerHTML = projects . map ( project => `
198- <div class="project-card">
304+ projectsContainer . innerHTML = sortedProjects . map ( ( project , index ) => {
305+ const hasVoted = votingSystem . hasUserVoted ( project . id ) ;
306+ const canVote = votingSystem . canUserVote ( project . id ) ;
307+ const voteCount = votingSystem . getProjectVotes ( project . id ) ;
308+ const isTopRanked = sortBy === 'popularity' && index < 3 && voteCount > 0 ;
309+
310+ return `
311+ <div class="project-card ${ isTopRanked ? 'top-ranked' : '' } ">
312+ ${ isTopRanked ? `<div class="rank-badge">#${ index + 1 } </div>` : '' }
199313 ${ project . previewImage
200314 ? `<img src="${ project . previewImage } " alt="${ project . title } " class="project-image"
201315 onerror="this.outerHTML='<div class=\\'project-placeholder\\'>No Preview Available</div>'">`
@@ -239,32 +353,80 @@ const sampleProjects = [
239353 </a>`
240354 : '<span></span>'
241355 }
242- <button class="upvote-btn" onclick="handleUpvote(${ project . id } )">
356+ <button class="upvote-btn ${ hasVoted ? 'voted' : '' } "
357+ onclick="handleUpvote(${ project . id } )"
358+ ${ ! canVote ? 'disabled' : '' }
359+ title="${ hasVoted ? 'You have already voted for this project' : 'Click to upvote this project' } ">
243360 <i class="fas fa-arrow-up"></i>
244- <span>${ project . upvotes } </span>
361+ <span>${ voteCount } </span>
245362 </button>
246363 </div>
247364 </div>
248- ` ) . join ( '' ) ;
365+ ` ;
366+ } ) . join ( '' ) ;
249367 }
250368
251369 // Handle upvote
252370 function handleUpvote ( projectId ) {
253- const project = currentProjects . find ( p => p . id === projectId ) ;
254- if ( project ) {
255- project . upvotes ++ ;
256- // Re-render projects to update the upvote count
257- renderProjects ( applyCurrentFilters ( ) ) ;
258-
259- // Add visual feedback
260- const button = event . target . closest ( '.upvote-btn' ) ;
261- button . style . transform = 'scale(1.1)' ;
371+ const result = votingSystem . upvoteProject ( projectId ) ;
372+
373+ if ( ! result . success ) {
374+ // Show error message
375+ showNotification ( result . message , 'error' ) ;
376+ return ;
377+ }
378+
379+ // Show success message
380+ showNotification ( 'Vote added successfully!' , 'success' ) ;
381+
382+ // Re-render projects to update the upvote count and sorting
383+ renderProjects ( applyCurrentFilters ( ) ) ;
384+
385+ // Add visual feedback
386+ const button = event . target . closest ( '.upvote-btn' ) ;
387+ if ( button ) {
388+ button . style . transform = 'scale(1.2)' ;
262389 setTimeout ( ( ) => {
263390 button . style . transform = 'scale(1)' ;
264- } , 150 ) ;
391+ } , 200 ) ;
265392 }
266393 }
267394
395+ // Show notification function
396+ function showNotification ( message , type = 'info' ) {
397+ // Remove existing notifications
398+ const existingNotification = document . querySelector ( '.vote-notification' ) ;
399+ if ( existingNotification ) {
400+ existingNotification . remove ( ) ;
401+ }
402+
403+ // Create notification element
404+ const notification = document . createElement ( 'div' ) ;
405+ notification . className = `vote-notification ${ type } ` ;
406+ notification . innerHTML = `
407+ <i class="fas ${ type === 'success' ? 'fa-check-circle' : 'fa-exclamation-circle' } "></i>
408+ <span>${ message } </span>
409+ ` ;
410+
411+ // Add to body
412+ document . body . appendChild ( notification ) ;
413+
414+ // Show notification
415+ setTimeout ( ( ) => {
416+ notification . classList . add ( 'show' ) ;
417+ } , 10 ) ;
418+
419+ // Hide notification after 3 seconds
420+ setTimeout ( ( ) => {
421+ notification . classList . remove ( 'show' ) ;
422+ setTimeout ( ( ) => {
423+ if ( notification . parentNode ) {
424+ notification . remove ( ) ;
425+ }
426+ } , 300 ) ;
427+ } , 3000 ) ;
428+ }
429+
268430 // Apply filters
269431 function applyFilters ( ) {
270432 const filteredProjects = applyCurrentFilters ( ) ;
@@ -336,6 +498,7 @@ const sampleProjects = [
336498
337499 // Reset filters
338500 function resetFilters ( ) {
501+ sortByFilter . value = 'popularity' ;
339502 difficultyFilter . value = 'all' ;
340503 hasDemoFilter . checked = false ;
341504 searchInput . value = '' ;
0 commit comments