feat: Implement advanced rating system with half-star precision

- Add 5-star display with half-star support
- Implement dialog-based rating input with 0.5 increments
- Add rating conversion functions (10-point ↔ 5-point)
- Update visual styling for half-star display
- Maintain default 0 rating and "Not rated" state
- Add proper error handling and user feedback
This commit is contained in:
2026-01-26 13:51:50 -05:00
parent e4d53e0d0a
commit 1140c4215e
+80 -4
View File
@@ -41,6 +41,33 @@ templ Dashboard(user User) {
.btn-primary:hover {
opacity: 0.8;
}
.rating-star {
display: inline-block;
transition: all 0.2s ease;
cursor: pointer;
position: relative;
}
.rating-star:hover {
transform: scale(1.1);
}
.half-star {
position: relative;
display: inline-block;
}
.half-star::before {
content: '★';
position: absolute;
left: 0;
top: 0;
width: 50%;
overflow: hidden;
color: #facc15; /* yellow-400 */
}
.rating-text {
font-size: 0.875rem;
margin-left: 0.5rem;
opacity: 0.8;
}
</style>
</head>
<body class="theme-tokyo-night">
@@ -170,16 +197,62 @@ templ Dashboard(user User) {
}
}
// Convert 10-point rating to 5-point with half-star precision
function convertToFivePoint(rating) {
return rating / 2;
}
// Convert 5-point with half-stars back to 10-point
function convertToTenPoint(fivePointRating) {
return Math.round(fivePointRating * 2);
}
function renderRatingStars(ebookId, currentRating) {
const fivePointRating = convertToFivePoint(currentRating);
let stars = '';
for (let i = 1; i <= 5; i++) {
const filled = i <= currentRating;
const starClass = filled ? 'text-yellow-400' : 'text-gray-400';
stars += `<span onclick="setRating('${ebookId}', ${i})" class="cursor-pointer text-xl ${starClass} hover:text-yellow-300 transition-colors">${filled ? '★' : '☆'}</span>`;
const filled = i <= Math.floor(fivePointRating);
const halfFilled = !filled && (i - 0.5) <= fivePointRating;
let starHtml = '';
if (filled) {
starHtml = '★';
} else if (halfFilled) {
starHtml = '⭐'; // Using different symbol for half-star
} else {
starHtml = '☆';
}
const starClass = filled ? 'text-yellow-400' :
halfFilled ? 'text-yellow-200' : 'text-gray-400';
stars += `<span onclick="openRatingDialog('${ebookId}', ${currentRating})" class="cursor-pointer text-xl ${starClass} hover:text-yellow-300 transition-colors" title="Click to rate (supports half-stars)">${starHtml}</span>`;
}
// Add rating text
stars += `<span class="text-sm ml-2" style="color: var(--text-secondary)">${currentRating > 0 ? fivePointRating.toFixed(1) + '/5' : 'Not rated'}</span>`;
return stars;
}
function openRatingDialog(ebookId, currentRating) {
const fivePointRating = convertToFivePoint(currentRating);
const newRating = prompt(`Rate this ebook (1-5, supports 0.5 increments):`, currentRating > 0 ? fivePointRating.toFixed(1) : '');
if (newRating === null) return; // User cancelled
const rating = parseFloat(newRating);
if (isNaN(rating) || rating < 0.5 || rating > 5) {
alert('Please enter a valid rating between 0.5 and 5');
return;
}
// Round to nearest 0.5
const roundedRating = Math.round(rating * 2) / 2;
setRating(ebookId, convertToTenPoint(roundedRating));
}
async function setRating(ebookId, rating) {
try {
const response = await fetch(`/api/ebooks/${ebookId}/rating`, {
@@ -198,10 +271,13 @@ templ Dashboard(user User) {
ratingContainer.innerHTML = renderRatingStars(ebookId, data.rating);
}
} else {
console.error('Error setting rating:', await response.text());
const errorText = await response.text();
console.error('Error setting rating:', errorText);
alert('Failed to set rating: ' + errorText);
}
} catch (error) {
console.error('Error setting rating:', error);
alert('Failed to set rating due to network error');
}
}