mirror of
https://github.com/sailor-sh/CK-X.git
synced 2026-07-11 00:29:20 +00:00
ADD: feedback post exam
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
@@ -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">×</button>
|
||||
</div>
|
||||
<a href="https://forms.gle/Dac9ALQnQb2dH1mw8" target="_blank" class="toast-button">Give Feedback</a>
|
||||
<button class="toast-close">×</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">×</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);
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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
|
||||
};
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user