From 5fe3fd7f763664f4200720111eae6f7727df0399 Mon Sep 17 00:00:00 2001 From: zipben Date: Wed, 4 Jun 2025 14:05:35 +0000 Subject: [PATCH] added star rating to comments --- client/pages/item/_id/index.vue | 243 +++++++++++++----- client/strings/en-us.json | 6 +- server/controllers/LibraryItemController.js | 6 +- .../migrations/v2.20.2-add-comment-ratings.js | 18 ++ server/models/Comment.js | 9 + 5 files changed, 210 insertions(+), 72 deletions(-) create mode 100644 server/migrations/v2.20.2-add-comment-ratings.js diff --git a/client/pages/item/_id/index.vue b/client/pages/item/_id/index.vue index 70b1d39d4..27694536a 100644 --- a/client/pages/item/_id/index.vue +++ b/client/pages/item/_id/index.vue @@ -148,37 +148,104 @@
-

{{ $strings.LabelComments }}

+
+

{{ $strings.LabelComments }}

- -
-
- -
- -
+ +
+

+ {{ $strings.LabelAverageRating.replace('{0}', averageRating.toFixed(1)) }} + + + +

-
- -
-
-
-
- {{ comment.user.username }} - {{ formatDate(comment.createdAt) }} + +
+
+ + + +
+ {{ $strings.LabelRating }}: +
+ +
+ +
+ +
+
-
-

{{ comment.text }}

-
-
- {{ $strings.MessageNoComments }} + + +
+
+
+
+ {{ comment.user.username }} + + {{ formatDate(comment.createdAt) }} + +
+
+ +
+ +
+ +
+ + +
+
+
+ + +
+ + + +
+ {{ $strings.LabelRating }}: +
+ +
+ +
+ +
+ + +
+
+ + +
+ {{ comment.text }} +
+
+
+
+ {{ $strings.MessageNoComments }} +
@@ -231,10 +298,13 @@ export default { showBookmarksModal: false, isDescriptionClamped: false, showFullDescription: false, - newCommentText: '', - editingComment: null, - isAddingComment: false, - isUpdatingComment: false + newComment: '', + newRating: 0, + hoverRating: 0, + editingCommentId: null, + editCommentText: '', + editRating: 0, + comments: [] } }, computed: { @@ -486,6 +556,12 @@ export default { }, currentUser() { return this.$store.state.user.user + }, + averageRating() { + const ratedComments = this.comments.filter((c) => c.rating) + if (!ratedComments.length) return 0 + const sum = ratedComments.reduce((acc, comment) => acc + comment.rating, 0) + return sum / ratedComments.length } }, methods: { @@ -833,68 +909,90 @@ export default { this.$store.commit('globals/setShareModal', this.mediaItemShare) } }, - async logNewComment() { - if (!this.newCommentText.trim()) return + async postComment() { + if (!this.newComment.trim()) return try { - // Save the comment first - const savedComment = await this.$axios.$post(`/api/items/${this.libraryItemId}/comments`, { - text: this.newCommentText + console.log('Posting comment:', { + text: this.newComment.trim(), + rating: this.newRating || null }) - // Initialize comments array if it doesn't exist - if (!this.libraryItem.comments) { - this.libraryItem.comments = [] + const response = await this.$axios.$post(`/api/items/${this.libraryItemId}/comments`, { + text: this.newComment.trim(), + rating: this.newRating || null + }) + + // Load comments if they haven't been loaded yet + if (!this.comments) { + this.comments = [] } - // Add the saved comment to the beginning of the array - this.libraryItem.comments.unshift(savedComment) - - // Clear the input after successful save - this.newCommentText = '' + this.comments.unshift(response) + this.newComment = '' + this.newRating = 0 this.$toast.success(this.$strings.MessageCommentAdded) } catch (error) { - console.error('Error saving comment:', error) + console.error('Error posting comment:', error) this.$toast.error(this.$strings.ErrorAddingComment) } }, - formatDate(dateString) { - return new Date(dateString).toLocaleDateString(undefined, { - year: 'numeric', - month: 'short', - day: 'numeric', - hour: '2-digit', - minute: '2-digit' - }) + startEditing(comment) { + this.editingCommentId = comment.id + this.editCommentText = comment.text + this.editRating = comment.rating || 0 }, - canDeleteComment(comment) { - return this.currentUser.id === comment.userId || this.currentUser.isAdmin + async saveEdit(comment) { + try { + const response = await this.$axios.put(`/api/items/${this.libraryItem.id}/comments/${comment.id}`, { + text: this.editCommentText.trim(), + rating: this.editRating || null + }) + + const index = this.comments.findIndex((c) => c.id === comment.id) + this.comments.splice(index, 1, response.data) + this.cancelEdit() + this.$toast.success(this.$strings.MessageCommentUpdated) + } catch (error) { + this.$toast.error(this.$strings.ErrorUpdatingComment) + } }, - async deleteComment(commentId) { + cancelEdit() { + this.editingCommentId = null + this.editCommentText = '' + this.editRating = 0 + }, + async deleteComment(comment) { if (!confirm(this.$strings.ConfirmDeleteComment)) return try { - // Remove comment from the array - const index = this.libraryItem.comments.findIndex((c) => c.id === commentId) - if (index !== -1) { - this.libraryItem.comments.splice(index, 1) - - // Delete the comment - await this.$axios.$delete(`/api/items/${this.libraryItemId}/comments/${commentId}`) - this.$toast.success(this.$strings.MessageCommentDeleted) - } + await this.$axios.delete(`/api/items/${this.libraryItem.id}/comments/${comment.id}`) + const index = this.comments.findIndex((c) => c.id === comment.id) + this.comments.splice(index, 1) + this.$toast.success(this.$strings.MessageCommentDeleted) } catch (error) { - console.error('Error deleting comment:', error) this.$toast.error(this.$strings.ErrorDeletingComment) - // Restore the comment if the delete failed - if (this.editingComment) { - this.libraryItem.comments.splice(index, 0, this.editingComment) - } + } + }, + canEditComment(comment) { + return this.$store.state.user.user.id === comment.userId || this.$store.state.user.user.type === 'admin' + }, + formatDate(date) { + return new Date(date).toLocaleDateString() + }, + async loadComments() { + try { + const response = await this.$axios.$get(`/api/items/${this.libraryItemId}/comments`) + this.comments = response || [] + } catch (error) { + console.error('Error loading comments:', error) + this.$toast.error(this.$strings.ErrorLoadingComments) } } }, - mounted() { + async mounted() { this.checkDescriptionClamped() + await this.loadComments() this.episodeDownloadsQueued = this.libraryItem.episodeDownloadsQueued || [] this.episodesDownloading = this.libraryItem.episodesDownloading || [] @@ -948,4 +1046,9 @@ export default { -webkit-line-clamp: unset; max-height: 999rem; } - + +.comments-section { + max-width: 800px; + margin: 0 auto; +} + \ No newline at end of file diff --git a/client/strings/en-us.json b/client/strings/en-us.json index c9e938173..1d1570d76 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -1115,5 +1115,9 @@ "ErrorAddingComment": "Error adding comment", "ErrorUpdatingComment": "Error updating comment", "ErrorDeletingComment": "Error deleting comment", - "ConfirmDeleteComment": "Are you sure you want to delete this comment?" + "ConfirmDeleteComment": "Are you sure you want to delete this comment?", + "LabelRating": "Rating", + "LabelStarRating": "{0} stars", + "LabelNoRating": "No rating", + "LabelAverageRating": "Average rating: {0}" } diff --git a/server/controllers/LibraryItemController.js b/server/controllers/LibraryItemController.js index ac011c984..f6dac4696 100644 --- a/server/controllers/LibraryItemController.js +++ b/server/controllers/LibraryItemController.js @@ -1242,6 +1242,7 @@ class LibraryItemController { try { const comment = await Database.commentModel.create({ text: req.body.text, + rating: req.body.rating, libraryItemId: req.params.id, userId: req.user.id }) @@ -1287,7 +1288,10 @@ class LibraryItemController { return res.status(403).json({ error: 'Not authorized to update this comment' }) } - await comment.update({ text: req.body.text }) + await comment.update({ + text: req.body.text, + rating: req.body.rating + }) res.json(comment) } catch (error) { Logger.error('[LibraryItemController] updateComment error:', error) diff --git a/server/migrations/v2.20.2-add-comment-ratings.js b/server/migrations/v2.20.2-add-comment-ratings.js new file mode 100644 index 000000000..87497d414 --- /dev/null +++ b/server/migrations/v2.20.2-add-comment-ratings.js @@ -0,0 +1,18 @@ +const { DataTypes } = require('sequelize') + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.addColumn('comments', 'rating', { + type: DataTypes.INTEGER, + allowNull: true, + validate: { + min: 1, + max: 5 + } + }) + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.removeColumn('comments', 'rating') + } +} \ No newline at end of file diff --git a/server/models/Comment.js b/server/models/Comment.js index 94f8ab4e7..827e93793 100644 --- a/server/models/Comment.js +++ b/server/models/Comment.js @@ -13,6 +13,14 @@ class Comment extends Model { type: DataTypes.TEXT, allowNull: false }, + rating: { + type: DataTypes.INTEGER, + allowNull: true, + validate: { + min: 1, + max: 5 + } + }, libraryItemId: { type: DataTypes.UUID, allowNull: false, @@ -68,6 +76,7 @@ class Comment extends Model { return { id: this.id, text: this.text, + rating: this.rating, userId: this.userId, libraryItemId: this.libraryItemId, createdAt: this.createdAt,