From e4925f47758aa7527eb93a80149cda2631dd8d00 Mon Sep 17 00:00:00 2001 From: Nishan Date: Sat, 3 May 2025 22:36:52 +0530 Subject: [PATCH] ADD: feedback post exam --- app/public/css/feedback.css | 93 +++++++ app/public/js/components/exam-api.js | 24 +- app/public/js/feedback.js | 241 +++++++++++++++--- app/public/results.html | 55 ++++ facilitator/src/controllers/examController.js | 31 ++- facilitator/src/routes/examRoutes.js | 9 +- 6 files changed, 412 insertions(+), 41 deletions(-) diff --git a/app/public/css/feedback.css b/app/public/css/feedback.css index a853c37..4fd960b 100644 --- a/app/public/css/feedback.css +++ b/app/public/css/feedback.css @@ -98,4 +98,97 @@ font-size: 18px; cursor: pointer; color: #666; +} + +/* Feedback modal styles */ +.feedback-form { + padding: 10px 0; +} + +.rating-container { + margin-bottom: 20px; + text-align: center; +} + +.star-rating { + display: inline-flex; + flex-direction: row-reverse; + justify-content: center; +} + +.star-rating input { + display: none; +} + +.star-rating label { + cursor: pointer; + font-size: 30px; + color: #ddd; + margin: 0 5px; + transition: color 0.2s ease; +} + +.star-rating label:hover, +.star-rating label:hover ~ label, +.star-rating input:checked ~ label { + color: #ffb33e; +} + +.feedback-text { + margin-bottom: 20px; +} + +.feedback-text label { + display: block; + margin-bottom: 5px; + font-weight: 500; +} + +.feedback-text textarea { + width: 100%; + padding: 5px; + border: 1px solid #ddd; + border-radius: 4px; + resize: vertical; + font-family: inherit; +} + +.testimonial-option { + margin-bottom: 15px; + display: flex; + align-items: center; +} + +.testimonial-option input[type="checkbox"] { + margin-right: 10px; +} + +.form-field { + margin-bottom: 5px; +} + +.form-field label { + display: block; + margin-bottom: 5px; + font-weight: 500; +} + +.form-field input { + width: 100%; + padding: 8px 10px; + border: 1px solid #ddd; + border-radius: 4px; + font-family: inherit; +} + +#testimonialFields { + padding: 10px; + background-color: #f9f9f9; + border-radius: 4px; + margin-top: 10px; +} + +.form-field label .required { + color: #e74c3c; + margin-left: 3px; } \ No newline at end of file diff --git a/app/public/js/components/exam-api.js b/app/public/js/components/exam-api.js index e96b1c5..3429d71 100644 --- a/app/public/js/components/exam-api.js +++ b/app/public/js/components/exam-api.js @@ -131,6 +131,27 @@ function trackExamEvent(examId, events) { }); } +// Function to submit user feedback +function submitFeedback(examId, feedbackData) { + return fetch(`/facilitator/api/v1/exams/metrics/${examId}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(feedbackData) + }) + .then(response => { + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + return response.json(); + }) + .catch(error => { + console.error('Error submitting feedback:', error); + throw error; // Re-throw to be handled by the calling function + }); +} + // Export the API functions export { getExamId, @@ -140,5 +161,6 @@ export { evaluateExam, terminateSession, getVncInfo, - trackExamEvent + trackExamEvent, + submitFeedback }; \ No newline at end of file diff --git a/app/public/js/feedback.js b/app/public/js/feedback.js index b05afbf..1412a40 100644 --- a/app/public/js/feedback.js +++ b/app/public/js/feedback.js @@ -3,53 +3,218 @@ * Handles displaying feedback prompts and notifications */ +// Feedback state management +const feedbackState = { + rating: null, + comment: '', + isTestimonial: false, + name: '', + socialHandle: '', + submitted: false +}; + // Wait for DOM to be loaded document.addEventListener('DOMContentLoaded', function() { - // Show feedback reminder after a delay - setTimeout(function() { - // Check if results have loaded - const resultsContent = document.getElementById('resultsContent'); - if (resultsContent && resultsContent.style.display !== 'none') { - showFeedbackReminder(); - } else { - // If results haven't loaded yet, wait for them - const observer = new MutationObserver(function(mutations) { - mutations.forEach(function(mutation) { - if (mutation.target.style.display !== 'none') { - showFeedbackReminder(); - observer.disconnect(); - } - }); - }); - - if (resultsContent) { - observer.observe(resultsContent, { - attributes: true, - attributeFilter: ['style'] - }); - } + // DOM elements + const feedbackModal = document.getElementById('feedbackModal'); + const skipFeedbackBtn = document.getElementById('skipFeedbackBtn'); + const submitFeedbackBtn = document.getElementById('submitFeedbackBtn'); + const testimonialConsent = document.getElementById('testimonialConsent'); + const testimonialFields = document.getElementById('testimonialFields'); + const feedbackComment = document.getElementById('feedbackComment'); + const starRatingInputs = document.querySelectorAll('input[name="rating"]'); + + // Check if user has already submitted feedback + const hasSubmittedFeedback = localStorage.getItem('ckx_feedback_submitted'); + + // Check if we should show the reminder now + if (!hasSubmittedFeedback) { + // Check if we're skipping and when to ask again + const skipTimestamp = localStorage.getItem('ckx_feedback_skip_until'); + const currentTime = new Date().getTime(); + + if (!skipTimestamp || currentTime > parseInt(skipTimestamp)) { + // Safe to show after a delay + setTimeout(showFeedbackModal, 5000); } - }, 10 * 1000); // Show after 10 seconds + } + + // Toggle testimonial fields visibility based on checkbox + testimonialConsent.addEventListener('change', function() { + testimonialFields.style.display = this.checked ? 'block' : 'none'; + feedbackState.isTestimonial = this.checked; + }); + + // Handle rating selection + starRatingInputs.forEach(input => { + input.addEventListener('change', function() { + feedbackState.rating = parseInt(this.value); + }); + }); + + // Handle comment input + feedbackComment.addEventListener('input', function() { + feedbackState.comment = this.value.trim(); + }); + + // Handle name and social handle inputs + document.getElementById('testimonialName').addEventListener('input', function() { + feedbackState.name = this.value.trim(); + }); + + document.getElementById('testimonialSocial').addEventListener('input', function() { + feedbackState.socialHandle = this.value.trim(); + }); + + // Skip feedback handler + skipFeedbackBtn.addEventListener('click', function() { + // Set a timestamp for when to ask again (30 minutes from now) + const thirtyMinutesFromNow = new Date().getTime() + (30 * 60 * 1000); + localStorage.setItem('ckx_feedback_skip_until', thirtyMinutesFromNow); + + // Hide the modal + feedbackModal.style.display = 'none'; + }); + + // Submit feedback handler + submitFeedbackBtn.addEventListener('click', function() { + // Validate that we have at least a rating + if (!feedbackState.rating) { + alert('Please select a rating before submitting your feedback.'); + return; + } + + // Validate name is provided if user opts for testimonial + if (feedbackState.isTestimonial && !feedbackState.name) { + alert('Please provide your name to be featured in testimonials.'); + return; + } + + // Disable the button to prevent multiple submissions + submitFeedbackBtn.disabled = true; + submitFeedbackBtn.innerHTML = ' Submitting...'; + + // Send feedback data + sendFeedbackData(); + }); }); /** - * Display a toast notification prompting for feedback + * Display the feedback modal */ -function showFeedbackReminder() { +function showFeedbackModal() { + // Only show if results have loaded + const resultsContent = document.getElementById('resultsContent'); + if (resultsContent && resultsContent.style.display !== 'none') { + document.getElementById('feedbackModal').style.display = 'flex'; + } else { + // If results haven't loaded yet, wait for them + const observer = new MutationObserver(function(mutations) { + mutations.forEach(function(mutation) { + if (mutation.target.style.display !== 'none') { + document.getElementById('feedbackModal').style.display = 'flex'; + observer.disconnect(); + } + }); + }); + + if (resultsContent) { + observer.observe(resultsContent, { + attributes: true, + attributeFilter: ['style'] + }); + } + } +} + +/** + * Send feedback data to the server + */ +function sendFeedbackData() { + // Import API functions if needed + import('./components/exam-api.js').then(api => { + // Get the exam ID from the URL or DOM + const urlParams = new URLSearchParams(window.location.search); + const examId = urlParams.get('id') || document.getElementById('examId')?.textContent.replace('Exam ID: ', '').trim(); + + if (!examId) { + console.error('No exam ID found for feedback submission'); + showFeedbackSubmissionResult(false); + return; + } + + // Construct feedback data + const feedbackData = { + type: 'feedback', + rating: feedbackState.rating, + comment: feedbackState.comment, + isTestimonial: feedbackState.isTestimonial, + name: feedbackState.name, + socialHandle: feedbackState.socialHandle + }; + + // Use the API function to submit feedback + api.submitFeedback(examId, feedbackData) + .then(data => { + console.log('Feedback submitted successfully:', data); + // Set local storage to remember that feedback was submitted + localStorage.setItem('ckx_feedback_submitted', 'true'); + // Hide the modal + document.getElementById('feedbackModal').style.display = 'none'; + // Show success notification + showFeedbackSubmissionResult(true); + }) + .catch(error => { + console.error('Error submitting feedback:', error); + // Hide the modal despite the error + document.getElementById('feedbackModal').style.display = 'none'; + showFeedbackSubmissionResult(false); + }); + }).catch(error => { + console.error('Error importing API module:', error); + document.getElementById('feedbackModal').style.display = 'none'; + showFeedbackSubmissionResult(false); + }); +} + +/** + * Show toast notification for feedback submission result + * @param {boolean} success - Whether the submission was successful + */ +function showFeedbackSubmissionResult(success) { + const submitFeedbackBtn = document.getElementById('submitFeedbackBtn'); + + // Reset button state + submitFeedbackBtn.disabled = false; + submitFeedbackBtn.innerHTML = ' Submit Feedback'; + // Create toast element const toast = document.createElement('div'); toast.className = 'toast-notification'; - toast.innerHTML = ` -
- -
-

Your opinion matters!

-

Please take a moment to share your feedback on CK-X

+ + if (success) { + toast.innerHTML = ` +
+ +
+

Thank you!

+

Your feedback has been submitted successfully.

+
+
- Give Feedback - -
- `; + `; + } else { + toast.innerHTML = ` +
+ +
+

Something went wrong

+

We couldn't submit your feedback. Please try again.

+
+ +
+ `; + } document.body.appendChild(toast); @@ -63,7 +228,7 @@ function showFeedbackReminder() { }, 500); }); - // Auto-close after 15 seconds + // Auto-close after 5 seconds setTimeout(function() { if (document.body.contains(toast)) { toast.style.animation = 'slideOut 0.5s ease forwards'; @@ -73,5 +238,5 @@ function showFeedbackReminder() { } }, 500); } - }, 15000); + }, 5000); } \ No newline at end of file diff --git a/app/public/results.html b/app/public/results.html index a35fafc..492febf 100644 --- a/app/public/results.html +++ b/app/public/results.html @@ -111,6 +111,61 @@
+ + + \ No newline at end of file diff --git a/facilitator/src/controllers/examController.js b/facilitator/src/controllers/examController.js index 9b731ad..c9b101c 100644 --- a/facilitator/src/controllers/examController.js +++ b/facilitator/src/controllers/examController.js @@ -373,6 +373,34 @@ async function updateExamEvents(req, res) { } } +/** + * Submit feedback metrics for an exam + * @param {Object} req - Express request object + * @param {Object} res - Express response object + */ +async function submitMetrics(req, res) { + const examId = req.params.examId; + const feedbackData = req.body; + + logger.info('Received feedback metrics submission', { examId, type: feedbackData.type }); + + try { + // Send the feedback data to the metric service + const result = await MetricService.sendMetrics(examId, { event: { ...feedbackData } }); + + return res.status(200).json({ + success: true, + message: 'Feedback submitted successfully' + }); + } catch (error) { + logger.error('Error submitting feedback metrics', { error: error.message }); + return res.status(500).json({ + error: 'Failed to submit feedback', + message: error.message + }); + } +} + module.exports = { createExam, getCurrentExam, @@ -383,5 +411,6 @@ module.exports = { getExamAnswers, getExamStatus, getExamResult, - updateExamEvents + updateExamEvents, + submitMetrics }; \ No newline at end of file diff --git a/facilitator/src/routes/examRoutes.js b/facilitator/src/routes/examRoutes.js index bbdb6a9..3656016 100644 --- a/facilitator/src/routes/examRoutes.js +++ b/facilitator/src/routes/examRoutes.js @@ -40,7 +40,7 @@ router.get('/:examId/questions', examController.getExamQuestions); router.post('/:examId/evaluate', validateEvaluateExam, examController.evaluateExam); /** - * @route POST /api/v1/exams/:examId/end + * @route POST /api/v1/exams/:examId/terminate * @desc End an exam * @access Public */ @@ -74,4 +74,11 @@ router.get('/:examId/result', examController.getExamResult); */ router.post('/:examId/events', validateExamEvents, examController.updateExamEvents); +/** + * @route POST /api/v1/exams/metrics/:examId + * @desc Submit feedback metrics for an exam + * @access Public + */ +router.post('/metrics/:examId', examController.submitMetrics); + module.exports = router; \ No newline at end of file