Files
libredesk/static/public/web-templates/csat.html
T
Abhinav Raut 88a58c0f3b Custom static directory support, self hosted installations can pass static dir path to override csat and other web-templates, js and css files. Allowing instances to be fully customizable.
- New meta JSONB field in CSAT responses to capture random adhoc data as that might be needed for custom CSAT pages.
- Add support prefilled rating for csat pages picked from query param
2026-04-09 00:04:00 +05:30

354 lines
10 KiB
HTML

{{ define "csat" }}
{{ template "header" . }}
<div class="csat-container">
<div class="csat-header">
<p class="csat-subtitle">{{ L.T "csat.rateYourInteraction" }}</p>
</div>
<form action="/csat/{{ .Data.CSAT.UUID }}" method="POST" class="csat-form" novalidate>
<div class="rating-container">
<div class="rating-options">
<input type="radio" id="rating-1" name="rating" value="1" required>
<label for="rating-1" class="rating-option" tabindex="0">
<span class="emoji-circle"><span class="emoji">😢</span></span>
<span class="rating-label">{{ L.T "globals.terms.poor" }}</span>
</label>
<input type="radio" id="rating-2" name="rating" value="2">
<label for="rating-2" class="rating-option" tabindex="0">
<span class="emoji-circle"><span class="emoji">😕</span></span>
<span class="rating-label">{{ L.T "globals.terms.fair" }}</span>
</label>
<input type="radio" id="rating-3" name="rating" value="3">
<label for="rating-3" class="rating-option" tabindex="0">
<span class="emoji-circle"><span class="emoji">😊</span></span>
<span class="rating-label">{{ L.T "globals.terms.good" }}</span>
</label>
<input type="radio" id="rating-4" name="rating" value="4">
<label for="rating-4" class="rating-option" tabindex="0">
<span class="emoji-circle"><span class="emoji">😃</span></span>
<span class="rating-label">{{ L.T "globals.terms.great" }}</span>
</label>
<input type="radio" id="rating-5" name="rating" value="5">
<label for="rating-5" class="rating-option" tabindex="0">
<span class="emoji-circle"><span class="emoji">🤩</span></span>
<span class="rating-label">{{ L.T "globals.terms.excellent" }}</span>
</label>
</div>
<div class="validation-msg" id="ratingValidationMessage">
{{ L.Ts "globals.messages.pleaseSelect" "name" "rating" }}
</div>
</div>
<div class="feedback-group">
<label for="feedback">{{ L.T "globals.messages.additionalFeedback" }}</label>
<textarea id="feedback" name="feedback" rows="3" maxlength="1000"
oninput="updateCharCount(this)"></textarea>
<div class="char-count"><span id="charCount">0</span> / 1000</div>
</div>
<button type="submit" class="button submit-button" id="submitBtn">
<span class="btn-text">{{ L.T "globals.messages.submit" }}</span>
<span class="btn-loading" style="display:none">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M12 2v4m0 12v4m-7.07-3.93l2.83-2.83m8.48-8.48l2.83-2.83M2 12h4m12 0h4M4.93 4.93l2.83 2.83m8.48 8.48l2.83 2.83"/></svg>
</span>
</button>
</form>
</div>
<script>
function updateCharCount(el) {
var c = document.getElementById('charCount');
c.textContent = el.value.length;
c.parentElement.classList.toggle('warn', el.value.length > 900);
}
document.querySelector('.csat-form').addEventListener('submit', function(e) {
var rating = document.querySelector('input[name="rating"]:checked');
var msg = document.getElementById('ratingValidationMessage');
var btn = document.getElementById('submitBtn');
if (!rating) {
e.preventDefault();
msg.classList.add('show');
setTimeout(function() { msg.classList.remove('show'); }, 4000);
return;
}
msg.classList.remove('show');
btn.disabled = true;
btn.querySelector('.btn-text').style.display = 'none';
btn.querySelector('.btn-loading').style.display = 'inline-flex';
});
document.querySelectorAll('input[name="rating"]').forEach(function(r) {
r.addEventListener('change', function() {
document.getElementById('ratingValidationMessage').classList.remove('show');
});
});
// Pre-select from ?rating= query param (e.g. from email rating links).
var params = new URLSearchParams(window.location.search);
var initial = params.get('rating');
if (initial) {
var radio = document.getElementById('rating-' + initial);
if (radio) radio.checked = true;
}
// Stagger entrance animation
document.querySelectorAll('.rating-option').forEach(function(el, i) {
el.style.animationDelay = (i * 0.06) + 's';
});
</script>
<style>
.csat-container {
max-width: 540px;
margin: 0 auto;
}
.csat-header {
text-align: center;
margin-bottom: 2rem;
}
.csat-subtitle {
font-size: 1.15em;
font-weight: 600;
color: var(--primary-color);
letter-spacing: -0.01em;
margin: 0;
}
/* Rating options */
.rating-container {
margin-bottom: 2rem;
}
.rating-options {
display: flex;
justify-content: center;
gap: 8px;
}
.rating-options input[type="radio"] {
position: absolute;
opacity: 0;
pointer-events: none;
}
.rating-option {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
cursor: pointer;
padding: 12px 10px;
border-radius: 12px;
transition: transform 0.18s ease, background-color 0.18s ease;
min-width: 72px;
animation: fadeUp 0.35s ease both;
}
.rating-option:hover {
background: var(--secondary-color);
transform: translateY(-3px);
}
.rating-option:focus-visible {
outline: 2px solid var(--primary-color);
outline-offset: 2px;
}
.rating-options input[type="radio"]:checked + .rating-option {
background: var(--secondary-color);
transform: translateY(-3px);
}
/* Emoji circle */
.emoji-circle {
width: 52px;
height: 52px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
border: 2px solid var(--border-color);
background: var(--background-color);
transition: border-color 0.18s ease, box-shadow 0.18s ease, transform 0.18s ease;
overflow: hidden;
}
.rating-option:hover .emoji-circle {
border-color: var(--primary-color);
}
.rating-options input[type="radio"]:checked + .rating-option .emoji-circle {
border-color: var(--primary-color);
box-shadow: 0 0 0 3px var(--shadow-color);
transform: scale(1.08);
}
.emoji {
font-size: 1.6em;
line-height: 1;
display: block;
}
.rating-label {
font-size: 0.72em;
font-weight: 500;
color: var(--text-color);
opacity: 0.5;
transition: opacity 0.18s ease;
text-align: center;
}
.rating-option:hover .rating-label {
opacity: 0.8;
}
.rating-options input[type="radio"]:checked + .rating-option .rating-label {
opacity: 1;
font-weight: 600;
}
/* Validation */
.validation-msg {
text-align: center;
margin-top: 0.5rem;
font-size: 0.82em;
color: #dc2626;
max-height: 0;
opacity: 0;
overflow: hidden;
transition: max-height 0.25s ease, opacity 0.25s ease;
}
.validation-msg.show {
max-height: 2rem;
opacity: 1;
}
/* Feedback */
.feedback-group {
margin-bottom: 1.25rem;
}
.feedback-group label {
display: block;
margin-bottom: 0.4rem;
font-size: 0.88em;
font-weight: 500;
color: var(--text-color);
}
.feedback-group textarea {
width: 100%;
padding: 0.65rem 0.85rem;
border: 1px solid var(--border-color);
border-radius: 0.375rem;
font-size: 0.92em;
font-family: inherit;
line-height: 1.55;
resize: vertical;
color: var(--text-color);
background: var(--background-color);
transition: border-color 0.18s ease, box-shadow 0.18s ease;
}
.feedback-group textarea:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px var(--shadow-color);
}
.char-count {
margin-top: 0.25rem;
text-align: right;
font-size: 0.72em;
color: var(--text-color);
opacity: 0.35;
transition: color 0.18s ease, opacity 0.18s ease;
}
.char-count.warn {
color: #dc2626;
opacity: 0.9;
}
/* Submit */
.submit-button {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
.submit-button:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none !important;
}
.btn-loading {
display: inline-flex;
animation: spin 0.8s linear infinite;
}
/* Animations */
@keyframes fadeUp {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Responsive */
@media screen and (max-width: 480px) {
.rating-options {
gap: 4px;
}
.rating-option {
min-width: 58px;
padding: 10px 6px;
}
.emoji-circle {
width: 44px;
height: 44px;
}
.emoji {
font-size: 1.3em;
}
.rating-label {
font-size: 0.65em;
}
}
@media (prefers-reduced-motion: reduce) {
.rating-option {
animation: none;
}
.btn-loading {
animation-duration: 2s;
}
}
</style>
{{ template "footer" }}
{{ end }}