Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
222 changes: 175 additions & 47 deletions website/templates/includes/page_stats.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{% load custom_tags %}
{% get_current_template as current_template %}
{% get_page_views current_template 30 as page_views %}
{% get_current_url_path as current_url_path %}
{% get_page_views current_url_path 30 as page_views %}
{% get_page_votes current_template as upvotes %}
{% get_page_votes current_template "downvote" as downvotes %}
<div id="pageStatsContainer"
Expand All @@ -16,11 +17,11 @@
</svg>
</div>
<!-- Stats Content -->
<div class="bg-white shadow-lg rounded-lg p-3 w-64">
<div class="text-sm font-medium text-gray-700 mb-2">Page Statistics (Last 30 Days)</div>
<div class="bg-white shadow-lg rounded-lg p-3 w-72">
<div class="text-sm font-medium text-gray-700 mb-2">Page Views (Last 30 Days)</div>
<!-- Bar Chart -->
<div class="mb-3">
<canvas id="pageViewsChart" height="100"></canvas>
<canvas id="pageViewsChart" height="120"></canvas>
</div>
<!-- Vote Buttons -->
<div class="flex justify-between items-center">
Expand All @@ -36,6 +37,9 @@
</button>
<span id="upvoteCount" class="text-sm">{{ upvotes }}</span>
</div>
<div class="text-xs text-center text-gray-500">
Total: <span id="totalViews">0</span> views
</div>
<div class="flex items-center">
<button id="downvoteBtn"
class="text-gray-600 hover:text-[#e74c3c] focus:outline-none mr-2">
Expand Down Expand Up @@ -63,46 +67,102 @@

const ctx = document.getElementById('pageViewsChart').getContext('2d');

// Initialize default empty data structure
let viewsByDate = {};

// Safely parse the page views data from Django template
// Safely parse the page views data from Django template
let pageViewsData;
try {


// Try to parse the data, ensuring it's an array of numbers
pageViewsData = JSON.parse('{{ page_views|safe|escapejs }}');
console.log(pageViewsData);

// Ensure the data consists of numbers
pageViewsData = pageViewsData.map(count => Number(count) || 0);

} catch (e) {
console.error('Failed to parse page views data:', e);
pageViewsData = Array(30).fill(0);
}
// Generate labels for the last 30 days (day numbers only)
const labels = [];
const today = new Date();
for (let i = 29; i >= 0; i--) {
const date = new Date(today);
date.setDate(date.getDate() - i);
labels.push(date.getDate());
try {
// Get the raw JSON string and ensure it's properly escaped/formatted
const rawData = '{{ page_views|escapejs }}';

// Parse the JSON data only if it's not empty
if (rawData && rawData.trim() !== '') {
viewsByDate = JSON.parse(rawData);
console.log('Successfully parsed view data:', viewsByDate);
} else {
console.warn('Empty page views data received');
}
} catch (e) {
console.error('Failed to parse page views data:', e);
}

// Ensure viewsByDate is an object, not null or undefined
if (!viewsByDate || typeof viewsByDate !== 'object') {
console.warn('Invalid view data structure, creating empty object');
viewsByDate = {};
}

// Sort the dates chronologically
const sortedDates = Object.keys(viewsByDate).sort();

// Extract data in chronological order
const dateLabels = [];
const pageViewsData = [];

// Format dates for display
const shortMonthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];

// Process each date if we have any
if (sortedDates.length > 0) {
for (const dateStr of sortedDates) {
try {
// Parse the date from the YYYY-MM-DD format
const dateParts = dateStr.split('-');
if (dateParts.length !== 3) {
console.warn(`Invalid date format: ${dateStr}`);
continue;
}

const year = parseInt(dateParts[0], 10);
const month = parseInt(dateParts[1], 10) - 1; // 0-based month
const day = parseInt(dateParts[2], 10);

// Validate date parts
if (isNaN(year) || isNaN(month) || isNaN(day) || month < 0 || month > 11 || day < 1 || day > 31) {
console.warn(`Invalid date parts: ${year}-${month+1}-${day}`);
continue;
}

// Format for display
dateLabels.push(`${shortMonthNames[month]} ${day}`);

// Add view count for this date (ensure it's a number)
const viewCount = parseInt(viewsByDate[dateStr], 10);
pageViewsData.push(isNaN(viewCount) ? 0 : viewCount);
} catch (e) {
console.warn(`Error processing date ${dateStr}:`, e);
// Skip this date if there's an error
}
}
} else {
// If no data, generate default empty data for the last 30 days
const today = new Date();
for (let i = 29; i >= 0; i--) {
const date = new Date(today);
date.setDate(date.getDate() - i);
dateLabels.push(`${shortMonthNames[date.getMonth()]} ${date.getDate()}`);
pageViewsData.push(0);
}
}

// Calculate total views - ensure we're only summing numbers
const totalViews = pageViewsData.reduce((sum, count) => sum + (isNaN(count) ? 0 : count), 0);
document.getElementById('totalViews').textContent = totalViews;

// Create the chart
new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
labels: dateLabels,
datasets: [{
label: 'Unique Views',
label: 'Views',
data: pageViewsData,
backgroundColor: 'rgba(231, 76, 60, 0.7)',
borderColor: '#e74c3c',
borderWidth: 1,
borderRadius: 4,
barPercentage: 0.7,
categoryPercentage: 0.8
barPercentage: 0.8,
categoryPercentage: 0.9
}]
},
options: {
Expand All @@ -116,12 +176,28 @@
enabled: true,
callbacks: {
title: function(tooltipItems) {
const date = new Date(today);
date.setDate(date.getDate() - (29 - tooltipItems[0].dataIndex));
return date.toLocaleDateString();
const index = tooltipItems[0].dataIndex;
if (sortedDates.length > 0 && index < sortedDates.length) {
try {
const dateStr = sortedDates[index];
const date = new Date(dateStr);
if (!isNaN(date.getTime())) {
return date.toLocaleDateString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
});
}
} catch (e) {
console.warn('Error formatting tooltip date:', e);
}
}
// Fallback to just showing the label if we can't format the date
return dateLabels[index] || 'Unknown Date';
},
label: function(context) {
const count = context.raw || 0;
const count = isNaN(context.raw) ? 0 : context.raw;
return count + ' view' + (count !== 1 ? 's' : '');
}
}
Expand All @@ -133,9 +209,13 @@
display: false
},
ticks: {
maxRotation: 0,
maxRotation: 45,
minRotation: 45,
autoSkip: true,
maxTicksLimit: 10
maxTicksLimit: 15,
font: {
size: 9
}
}
},
y: {
Expand All @@ -144,34 +224,62 @@
color: 'rgba(200, 200, 200, 0.2)'
},
ticks: {
precision: 0
precision: 0,
stepSize: 1,
// Ensure we only show integer values
callback: function(value) {
if (value % 1 === 0) {
return value;
}
}
}
}
}
}
});
};

// Initialize the chart
// Initialize the chart
initializeChart();

// Set up vote functionality
const pageStatsContainer = document.getElementById('pageStatsContainer');
// Set up vote functionality
const pageStatsContainer = document.getElementById('pageStatsContainer');
const upvoteBtn = document.getElementById('upvoteBtn');
const downvoteBtn = document.getElementById('downvoteBtn');
const upvoteCount = document.getElementById('upvoteCount');
const downvoteCount = document.getElementById('downvoteCount');
const currentTemplate = '{{ current_template }}';
const currentUrlPath = '{{ current_url_path }}';
Comment on lines +247 to +254
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Duplicate DOM Element Declaration
The pageStatsContainer element is queried twice (lines 247 and 249). This duplication can lead to confusion and potential future issues. Please remove the redundant declaration.


// Toggle container on click of the handle
const handleElement = pageStatsContainer.querySelector('.flex.justify-center');
if (handleElement) {
handleElement.addEventListener('click', function(e) {
e.preventDefault();
if (pageStatsContainer.classList.contains('translate-y-0')) {
pageStatsContainer.classList.remove('translate-y-0');
pageStatsContainer.classList.add('translate-y-[calc(100%-40px)]');
} else {
pageStatsContainer.classList.remove('translate-y-[calc(100%-40px)]');
pageStatsContainer.classList.add('translate-y-0');
}
});
}

// Add touch support for mobile devices
pageStatsContainer.addEventListener('touchstart', function(e) {
e.preventDefault();
if (pageStatsContainer.classList.contains('translate-y-0')) {
pageStatsContainer.classList.remove('translate-y-0');
pageStatsContainer.classList.add('translate-y-[calc(100%-40px)]');
} else {
pageStatsContainer.classList.remove('translate-y-[calc(100%-40px)]');
pageStatsContainer.classList.add('translate-y-0');
// Only handle touch events on the handle element
if (e.target.closest('.flex.justify-center')) {
e.preventDefault();
if (pageStatsContainer.classList.contains('translate-y-0')) {
pageStatsContainer.classList.remove('translate-y-0');
pageStatsContainer.classList.add('translate-y-[calc(100%-40px)]');
} else {
pageStatsContainer.classList.remove('translate-y-[calc(100%-40px)]');
pageStatsContainer.classList.add('translate-y-0');
}
}
});

Expand All @@ -189,14 +297,28 @@
}
}
return cookieValue;
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}


const csrftoken = getCookie('csrftoken');

// Vote submission handler
// Vote submission handler
function submitVote(voteType) {
const formData = new FormData();
formData.append('template_name', currentTemplate);
formData.append('url_path', currentUrlPath);
formData.append('vote_type', voteType);

fetch('{% url "page_vote" %}', {
Expand Down Expand Up @@ -226,15 +348,21 @@
});
}

// Event listeners for vote buttons
// Event listeners for vote buttons
upvoteBtn.addEventListener('click', function(e) {
e.stopPropagation();
submitVote('upvote');
e.stopPropagation();
submitVote('upvote');
});

downvoteBtn.addEventListener('click', function(e) {
e.stopPropagation();
submitVote('downvote');
e.stopPropagation();
submitVote('downvote');
});
Comment on lines +351 to 365
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

Duplicate Vote Submission Calls
Both the upvote (lines 353–358) and downvote (lines 360–364) event listeners call e.stopPropagation() and submitVote() twice, which will result in duplicate vote submissions. Please remove the redundant calls so that each button click only triggers a single vote submission.

Proposed Diff for Upvote Button:

-upvoteBtn.addEventListener('click', function(e) {
-    e.stopPropagation();
-    submitVote('upvote');
-    e.stopPropagation();
-    submitVote('upvote');
-});
+upvoteBtn.addEventListener('click', function(e) {
+    e.stopPropagation();
+    submitVote('upvote');
+});

Proposed Diff for Downvote Button:

-downvoteBtn.addEventListener('click', function(e) {
-    e.stopPropagation();
-    submitVote('downvote');
-    e.stopPropagation();
-    submitVote('downvote');
-});
+downvoteBtn.addEventListener('click', function(e) {
+    e.stopPropagation();
+    submitVote('downvote');
+});
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Event listeners for vote buttons
// Event listeners for vote buttons
upvoteBtn.addEventListener('click', function(e) {
e.stopPropagation();
submitVote('upvote');
e.stopPropagation();
submitVote('upvote');
});
downvoteBtn.addEventListener('click', function(e) {
e.stopPropagation();
submitVote('downvote');
e.stopPropagation();
submitVote('downvote');
});
// Event listeners for vote buttons
// Event listeners for vote buttons
upvoteBtn.addEventListener('click', function(e) {
e.stopPropagation();
submitVote('upvote');
});
downvoteBtn.addEventListener('click', function(e) {
e.stopPropagation();
submitVote('downvote');
});

});
});
Comment on lines 366 to +367
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Extraneous Closing Bracket
An extra closing bracket (});) appears at line 367 that could cause a JavaScript syntax error. Please remove the redundant closing bracket to ensure the script runs correctly.

</script>
Loading