ADD: feedback post exam

This commit is contained in:
Nishan
2025-05-03 22:36:52 +05:30
parent 601aab38bb
commit e4925f4775
6 changed files with 412 additions and 41 deletions

View File

@@ -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;
}

View File

@@ -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
};

View File

@@ -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 = '<i class="fas fa-spinner fa-spin me-2"></i> 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 = '<i class="fas fa-paper-plane me-2"></i> Submit Feedback';
// Create toast element
const toast = document.createElement('div');
toast.className = 'toast-notification';
toast.innerHTML = `
<div class="toast-content">
<i class="fas fa-comment-dots toast-icon"></i>
<div class="toast-message">
<p><strong>Your opinion matters!</strong></p>
<p>Please take a moment to share your feedback on CK-X</p>
if (success) {
toast.innerHTML = `
<div class="toast-content">
<i class="fas fa-check-circle toast-icon" style="color: #28a745;"></i>
<div class="toast-message">
<p><strong>Thank you!</strong></p>
<p>Your feedback has been submitted successfully.</p>
</div>
<button class="toast-close">&times;</button>
</div>
<a href="https://forms.gle/Dac9ALQnQb2dH1mw8" target="_blank" class="toast-button">Give Feedback</a>
<button class="toast-close">&times;</button>
</div>
`;
`;
} else {
toast.innerHTML = `
<div class="toast-content">
<i class="fas fa-exclamation-circle toast-icon" style="color: #dc3545;"></i>
<div class="toast-message">
<p><strong>Something went wrong</strong></p>
<p>We couldn't submit your feedback. Please try again.</p>
</div>
<button class="toast-close">&times;</button>
</div>
`;
}
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);
}

View File

@@ -111,6 +111,61 @@
</div>
</div>
</div>
<!-- Feedback Modal -->
<div id="feedbackModal" class="modal-overlay" style="display: none;">
<div class="modal-content">
<div class="modal-header">
<h4>Your Feedback Matters</h4>
</div>
<div class="modal-body">
<div class="feedback-form">
<div class="rating-container">
<p>How would you rate your overall experience with CK-X?</p>
<div class="star-rating">
<input type="radio" id="star5" name="rating" value="5" />
<label for="star5" title="5 stars"><i class="fas fa-star"></i></label>
<input type="radio" id="star4" name="rating" value="4" />
<label for="star4" title="4 stars"><i class="fas fa-star"></i></label>
<input type="radio" id="star3" name="rating" value="3" />
<label for="star3" title="3 stars"><i class="fas fa-star"></i></label>
<input type="radio" id="star2" name="rating" value="2" />
<label for="star2" title="2 stars"><i class="fas fa-star"></i></label>
<input type="radio" id="star1" name="rating" value="1" />
<label for="star1" title="1 star"><i class="fas fa-star"></i></label>
</div>
</div>
<div class="feedback-text">
<label for="feedbackComment">How was your overall experience?</label>
<textarea id="feedbackComment" rows="4" placeholder="Please share your thoughts..."></textarea>
</div>
<div class="testimonial-option">
<input type="checkbox" id="testimonialConsent" />
<label for="testimonialConsent">I'd like to be featured in testimonials</label>
</div>
<div id="testimonialFields" style="display: none;">
<div class="form-field">
<label for="testimonialName">Name <span class="required">*</span></label>
<input type="text" id="testimonialName" placeholder="Your name" required />
</div>
<div class="form-field">
<label for="testimonialSocial">Social Handle (optional)</label>
<input type="text" id="testimonialSocial" placeholder="@yoursocialhandle" />
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button id="skipFeedbackBtn" class="btn btn-secondary">Skip for now</button>
<button id="submitFeedbackBtn" class="btn btn-primary">
<i class="fas fa-paper-plane me-2"></i> Submit Feedback
</button>
</div>
</div>
</div>
<script data-name="BMC-Widget" data-cfasync="false" src="https://cdnjs.buymeacoffee.com/1.0.0/widget.prod.min.js" data-id="nishan.b" data-description="Support me on Buy me a coffee!" data-message="CK-X helped you prep? A coffee helps it grow !!" data-color="#5F7FFF" data-position="Right" data-x_margin="18" data-y_margin="18"></script>
</body>
</html>

View File

@@ -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
};

View File

@@ -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;