Skip to content

Commit b7c8623

Browse files
authored
Merge pull request #112 from Channpreetk/Canva
Implement user upvoting system with project rankings and duplicate vote prevention
2 parents 96618ad + d9557b5 commit b7c8623

File tree

3 files changed

+284
-15
lines changed

3 files changed

+284
-15
lines changed

index.html

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,15 @@ <h2><i class="fas fa-filter"></i> Filter Projects</h2>
5555
<button class="clear-search" id="clear-search" style="display: none;"><i class="fas fa-times"></i></button>
5656
</div>
5757
</div>
58+
<div class="filter-group">
59+
<label for="sort-by">Sort by:</label>
60+
<select id="sort-by">
61+
<option value="popularity">Most Popular</option>
62+
<option value="newest">Newest First</option>
63+
<option value="difficulty">Difficulty</option>
64+
<option value="alphabetical">A-Z</option>
65+
</select>
66+
</div>
5867
<div class="filter-group">
5968
<label for="difficulty">Difficulty:</label>
6069
<select id="difficulty">

script.js

Lines changed: 178 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -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 = '';

style.css

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,13 +385,38 @@
385385
transition: all 0.3s ease;
386386
display: flex;
387387
flex-direction: column;
388+
position: relative;
388389
}
389390

390391
.project-card:hover {
391392
transform: translateY(-5px);
392393
box-shadow: var(--card-hover-shadow);
393394
}
394395

396+
.project-card.top-ranked {
397+
border: 2px solid #ffd700;
398+
box-shadow: 0 8px 32px rgba(255, 215, 0, 0.2);
399+
}
400+
401+
.rank-badge {
402+
position: absolute;
403+
top: -10px;
404+
right: -10px;
405+
background: linear-gradient(135deg, #ffd700, #ffed4e);
406+
color: #333;
407+
width: 40px;
408+
height: 40px;
409+
border-radius: 50%;
410+
display: flex;
411+
align-items: center;
412+
justify-content: center;
413+
font-weight: bold;
414+
font-size: 0.9rem;
415+
z-index: 10;
416+
box-shadow: 0 4px 12px rgba(255, 215, 0, 0.3);
417+
border: 2px solid white;
418+
}
419+
395420

396421
.project-image {
397422
width: 100%;
@@ -523,13 +548,85 @@
523548
align-items: center;
524549
gap: 0.5rem;
525550
transition: all 0.3s ease;
551+
position: relative;
526552
}
527553

528554
.upvote-btn:hover {
529555
transform: translateY(-1px);
530556
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
531557
}
532558

559+
.upvote-btn.voted {
560+
background: linear-gradient(135deg, #28a745, #20c997);
561+
cursor: default;
562+
}
563+
564+
.upvote-btn.voted:hover {
565+
transform: none;
566+
box-shadow: 0 2px 8px rgba(40, 167, 69, 0.3);
567+
}
568+
569+
.upvote-btn:disabled {
570+
opacity: 0.7;
571+
cursor: not-allowed;
572+
}
573+
574+
.upvote-btn:disabled:hover {
575+
transform: none;
576+
box-shadow: none;
577+
}
578+
579+
/* Notification styles */
580+
.vote-notification {
581+
position: fixed;
582+
top: 20px;
583+
right: 20px;
584+
background: white;
585+
color: #333;
586+
padding: 1rem 1.5rem;
587+
border-radius: 8px;
588+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
589+
display: flex;
590+
align-items: center;
591+
gap: 0.5rem;
592+
z-index: 1000;
593+
transform: translateX(400px);
594+
opacity: 0;
595+
transition: all 0.3s ease;
596+
font-weight: 500;
597+
border-left: 4px solid #007bff;
598+
}
599+
600+
.vote-notification.success {
601+
border-left-color: #28a745;
602+
color: #155724;
603+
}
604+
605+
.vote-notification.success i {
606+
color: #28a745;
607+
}
608+
609+
.vote-notification.error {
610+
border-left-color: #dc3545;
611+
color: #721c24;
612+
}
613+
614+
.vote-notification.error i {
615+
color: #dc3545;
616+
}
617+
618+
.vote-notification.show {
619+
transform: translateX(0);
620+
opacity: 1;
621+
}
622+
623+
/* Dark theme notification styles */
624+
.dark-theme .vote-notification {
625+
background: var(--card-bg);
626+
color: var(--text-color);
627+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
628+
}
629+
533630

534631

535632
/* REVIEW SECTION */

0 commit comments

Comments
 (0)