mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-07-09 10:51:37 +00:00
limited ratings so that only one per user and restyled the comment frame for a user
This commit is contained in:
parent
c5ddede56e
commit
111742cf99
3 changed files with 58 additions and 33 deletions
|
|
@ -149,7 +149,7 @@
|
||||||
<div class="grow px-2 md:px-10">
|
<div class="grow px-2 md:px-10">
|
||||||
<div class="mt-12">
|
<div class="mt-12">
|
||||||
<div class="comments-section mt-4 border-t border-gray-700 pt-4">
|
<div class="comments-section mt-4 border-t border-gray-700 pt-4">
|
||||||
<h3 class="text-xl font-semibold mb-4">{{ $strings.LabelComments }}</h3>
|
<h3 class="text-xl font-semibold mb-4">{{ $strings.LabelRatings }}</h3>
|
||||||
|
|
||||||
<!-- Average Rating Display -->
|
<!-- Average Rating Display -->
|
||||||
<div v-if="comments.length" class="mb-4">
|
<div v-if="comments.length" class="mb-4">
|
||||||
|
|
@ -162,7 +162,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Add Comment Form -->
|
<!-- Add Comment Form -->
|
||||||
<div class="mb-6">
|
<div v-if="!hasUserComment" class="mb-6">
|
||||||
<div class="bg-bg border border-gray-700 rounded-lg p-4">
|
<div class="bg-bg border border-gray-700 rounded-lg p-4">
|
||||||
<textarea v-model="newComment" :placeholder="$strings.PlaceholderAddComment" class="w-full p-2 bg-transparent border border-gray-600 rounded resize-none focus:outline-none mb-3" rows="3"></textarea>
|
<textarea v-model="newComment" :placeholder="$strings.PlaceholderAddComment" class="w-full p-2 bg-transparent border border-gray-600 rounded resize-none focus:outline-none mb-3" rows="3"></textarea>
|
||||||
|
|
||||||
|
|
@ -174,11 +174,10 @@
|
||||||
<span class="abs-icons icon-star"></span>
|
<span class="abs-icons icon-star"></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button v-if="newRating" class="ml-2 text-sm text-gray-400 hover:text-white" @click="newRating = 0">({{ $strings.ButtonClear }})</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-end">
|
<div class="flex justify-end">
|
||||||
<button class="px-4 py-2 bg-primary text-white rounded hover:bg-opacity-75" @click="postComment">
|
<button class="px-4 py-2 bg-primary text-white rounded hover:bg-opacity-75 disabled:opacity-50 disabled:cursor-not-allowed" :disabled="!newRating" @click="postComment">
|
||||||
{{ $strings.ButtonPost }}
|
{{ $strings.ButtonPost }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -187,10 +186,15 @@
|
||||||
|
|
||||||
<!-- Comments List -->
|
<!-- Comments List -->
|
||||||
<div v-if="comments.length" class="space-y-4">
|
<div v-if="comments.length" class="space-y-4">
|
||||||
<div v-for="comment in comments" :key="comment.id" class="bg-bg border border-gray-700 rounded-lg p-4">
|
<div v-for="comment in sortedComments" :key="comment.id" class="bg-bg border rounded-lg p-4" :class="comment.userId === currentUser.id ? 'border-white border-4' : 'border-gray-700'">
|
||||||
<div class="flex justify-between items-start mb-2">
|
<div class="flex justify-between items-start mb-2">
|
||||||
<div>
|
<div>
|
||||||
<span class="font-semibold">{{ comment.user.username }}</span>
|
<template v-if="comment.userId === currentUser.id">
|
||||||
|
<nuxt-link to="/account" class="font-semibold hover:text-white hover:underline">{{ comment.user.username }}</nuxt-link>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<span class="font-semibold">{{ comment.user.username }}</span>
|
||||||
|
</template>
|
||||||
<span class="text-sm text-gray-400 ml-2">
|
<span class="text-sm text-gray-400 ml-2">
|
||||||
{{ formatDate(comment.createdAt) }}
|
{{ formatDate(comment.createdAt) }}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -224,11 +228,10 @@
|
||||||
<span class="abs-icons icon-star"></span>
|
<span class="abs-icons icon-star"></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button v-if="editRating" class="ml-2 text-sm text-gray-400 hover:text-white" @click="editRating = 0">({{ $strings.ButtonClear }})</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex space-x-2">
|
<div class="flex space-x-2">
|
||||||
<button class="px-4 py-2 bg-primary text-white rounded hover:bg-opacity-75" @click="saveEdit(comment)">
|
<button class="px-4 py-2 bg-primary text-white rounded hover:bg-opacity-75 disabled:opacity-50 disabled:cursor-not-allowed" :disabled="!editRating" @click="saveEdit(comment)">
|
||||||
{{ $strings.ButtonSave }}
|
{{ $strings.ButtonSave }}
|
||||||
</button>
|
</button>
|
||||||
<button class="px-4 py-2 bg-gray-700 text-white rounded hover:bg-opacity-75" @click="cancelEdit">
|
<button class="px-4 py-2 bg-gray-700 text-white rounded hover:bg-opacity-75" @click="cancelEdit">
|
||||||
|
|
@ -562,6 +565,18 @@ export default {
|
||||||
if (!ratedComments.length) return 0
|
if (!ratedComments.length) return 0
|
||||||
const sum = ratedComments.reduce((acc, comment) => acc + comment.rating, 0)
|
const sum = ratedComments.reduce((acc, comment) => acc + comment.rating, 0)
|
||||||
return sum / ratedComments.length
|
return sum / ratedComments.length
|
||||||
|
},
|
||||||
|
hasUserComment() {
|
||||||
|
return this.comments.some((comment) => comment.userId === this.$store.state.user.user.id)
|
||||||
|
},
|
||||||
|
sortedComments() {
|
||||||
|
return [...this.comments].sort((a, b) => {
|
||||||
|
// Put current user's comment at the top
|
||||||
|
if (a.userId === this.currentUser.id) return -1
|
||||||
|
if (b.userId === this.currentUser.id) return 1
|
||||||
|
// Sort remaining comments by date, newest first
|
||||||
|
return new Date(b.createdAt) - new Date(a.createdAt)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
@ -593,7 +608,7 @@ export default {
|
||||||
.$get(`/api/podcasts/${this.libraryItemId}/clear-queue`)
|
.$get(`/api/podcasts/${this.libraryItemId}/clear-queue`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.$toast.success(this.$strings.ToastEpisodeDownloadQueueClearSuccess)
|
this.$toast.success(this.$strings.ToastEpisodeDownloadQueueClearSuccess)
|
||||||
this.episodeDownloadQueued = []
|
this.episodeDownloadsQueued = []
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to clear queue', error)
|
console.error('Failed to clear queue', error)
|
||||||
|
|
@ -913,28 +928,22 @@ export default {
|
||||||
if (!this.newComment.trim()) return
|
if (!this.newComment.trim()) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('Posting comment:', {
|
|
||||||
text: this.newComment.trim(),
|
|
||||||
rating: this.newRating || null
|
|
||||||
})
|
|
||||||
|
|
||||||
const response = await this.$axios.$post(`/api/items/${this.libraryItemId}/comments`, {
|
const response = await this.$axios.$post(`/api/items/${this.libraryItemId}/comments`, {
|
||||||
text: this.newComment.trim(),
|
text: this.newComment.trim(),
|
||||||
rating: this.newRating || null
|
rating: this.newRating
|
||||||
})
|
})
|
||||||
|
|
||||||
// Load comments if they haven't been loaded yet
|
|
||||||
if (!this.comments) {
|
|
||||||
this.comments = []
|
|
||||||
}
|
|
||||||
|
|
||||||
this.comments.unshift(response)
|
this.comments.unshift(response)
|
||||||
this.newComment = ''
|
this.newComment = ''
|
||||||
this.newRating = 0
|
this.newRating = 0
|
||||||
this.$toast.success(this.$strings.MessageCommentAdded)
|
this.$toast.success(this.$strings.MessageCommentAdded)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error posting comment:', error)
|
console.error('Error posting comment:', error)
|
||||||
this.$toast.error(this.$strings.ErrorAddingComment)
|
if (error.response?.data?.error) {
|
||||||
|
this.$toast.error(error.response.data.error)
|
||||||
|
} else {
|
||||||
|
this.$toast.error(this.$strings.ErrorAddingComment)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
startEditing(comment) {
|
startEditing(comment) {
|
||||||
|
|
@ -946,7 +955,7 @@ export default {
|
||||||
try {
|
try {
|
||||||
const response = await this.$axios.$patch(`/api/items/${this.libraryItemId}/comments/${comment.id}`, {
|
const response = await this.$axios.$patch(`/api/items/${this.libraryItemId}/comments/${comment.id}`, {
|
||||||
text: this.editCommentText.trim(),
|
text: this.editCommentText.trim(),
|
||||||
rating: this.editRating || null
|
rating: this.editRating
|
||||||
})
|
})
|
||||||
|
|
||||||
const index = this.comments.findIndex((c) => c.id === comment.id)
|
const index = this.comments.findIndex((c) => c.id === comment.id)
|
||||||
|
|
|
||||||
|
|
@ -1105,17 +1105,17 @@
|
||||||
"ToastUserPasswordMustChange": "New password cannot match old password",
|
"ToastUserPasswordMustChange": "New password cannot match old password",
|
||||||
"ToastUserRootRequireName": "Must enter a root username",
|
"ToastUserRootRequireName": "Must enter a root username",
|
||||||
"LabelComments": "Comments",
|
"LabelComments": "Comments",
|
||||||
"PlaceholderAddComment": "Add a comment... (Ctrl+Enter to post)",
|
"PlaceholderAddComment": "Add a rating... (Ctrl+Enter to post)",
|
||||||
"ButtonPost": "Post",
|
"ButtonPost": "Post",
|
||||||
"MessageNoComments": "No comments yet. Be the first to comment!",
|
"MessageNoComments": "No rating yet. Be the first to rate!",
|
||||||
"MessageCommentAdded": "Comment added successfully",
|
"MessageCommentAdded": "Rating added successfully",
|
||||||
"MessageCommentUpdated": "Comment updated successfully",
|
"MessageCommentUpdated": "Rating updated successfully",
|
||||||
"MessageCommentDeleted": "Comment deleted successfully",
|
"MessageCommentDeleted": "Rating deleted successfully",
|
||||||
"ErrorLoadingComments": "Error loading comments",
|
"ErrorLoadingComments": "Error loading ratings",
|
||||||
"ErrorAddingComment": "Error adding comment",
|
"ErrorAddingComment": "Error adding ratings",
|
||||||
"ErrorUpdatingComment": "Error updating comment",
|
"ErrorUpdatingComment": "Error updating ratings",
|
||||||
"ErrorDeletingComment": "Error deleting comment",
|
"ErrorDeletingComment": "Error deleting ratings",
|
||||||
"ConfirmDeleteComment": "Are you sure you want to delete this comment?",
|
"ConfirmDeleteComment": "Are you sure you want to delete this rating?",
|
||||||
"LabelRating": "Rating",
|
"LabelRating": "Rating",
|
||||||
"LabelStarRating": "{0} stars",
|
"LabelStarRating": "{0} stars",
|
||||||
"LabelNoRating": "No rating",
|
"LabelNoRating": "No rating",
|
||||||
|
|
|
||||||
|
|
@ -1170,7 +1170,6 @@ class LibraryItemController {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
|
||||||
* @param {RequestWithUser} req
|
* @param {RequestWithUser} req
|
||||||
* @param {Response} res
|
* @param {Response} res
|
||||||
* @param {NextFunction} next
|
* @param {NextFunction} next
|
||||||
|
|
@ -1193,6 +1192,11 @@ class LibraryItemController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Allow comment operations for all authenticated users
|
||||||
|
if (req.path.includes('/comments')) {
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
|
||||||
if (req.path.includes('/play')) {
|
if (req.path.includes('/play')) {
|
||||||
// allow POST requests using /play and /play/:episodeId
|
// allow POST requests using /play and /play/:episodeId
|
||||||
} else if (req.method == 'DELETE' && !req.user.canDelete) {
|
} else if (req.method == 'DELETE' && !req.user.canDelete) {
|
||||||
|
|
@ -1240,6 +1244,18 @@ class LibraryItemController {
|
||||||
*/
|
*/
|
||||||
async addComment(req, res) {
|
async addComment(req, res) {
|
||||||
try {
|
try {
|
||||||
|
// Check if user already has a comment for this item
|
||||||
|
const existingComment = await Database.commentModel.findOne({
|
||||||
|
where: {
|
||||||
|
libraryItemId: req.params.id,
|
||||||
|
userId: req.user.id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (existingComment) {
|
||||||
|
return res.status(400).json({ error: 'You have already submitted a review for this item' })
|
||||||
|
}
|
||||||
|
|
||||||
const comment = await Database.commentModel.create({
|
const comment = await Database.commentModel.create({
|
||||||
text: req.body.text,
|
text: req.body.text,
|
||||||
rating: req.body.rating,
|
rating: req.body.rating,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue