feat(stars): add DELETE endpoint and support rating removal

- Implement DELETE /api/stars to remove star ratings by mediaId
- Update media's star count and average rating on deletion
- Modify frontend rating handlers to support unstarring by sending DELETE requests
- Update multiple pages to handle rating removal when rating is zero
- Adjust photo viewer to toggle rating off if same rating clicked
- Improve error handling and success responses for star rating API interactions
This commit is contained in:
tigeren 2025-09-21 15:06:52 +00:00
parent 5fac68e809
commit b8b46fcf0e
8 changed files with 127 additions and 31 deletions

Binary file not shown.

View File

@ -73,6 +73,37 @@ export async function POST(request: Request) {
WHERE id = ?
`).run(mediaId, mediaId, mediaId);
return NextResponse.json({ success: true });
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}
export async function DELETE(request: Request) {
try {
const db = getDatabase();
const { mediaId } = await request.json();
if (!mediaId) {
return NextResponse.json({ error: 'mediaId is required' }, { status: 400 });
}
// Delete star rating by media_id
const result = db.prepare(`DELETE FROM stars WHERE media_id = ?`).run(mediaId);
if (result.changes === 0) {
return NextResponse.json({ error: 'No rating found for this media' }, { status: 404 });
}
// Update media star count and average rating
db.prepare(`
UPDATE media
SET
star_count = (SELECT COUNT(*) FROM stars WHERE media_id = ?),
avg_rating = COALESCE((SELECT AVG(rating) FROM stars WHERE media_id = ?), 0.0)
WHERE id = ?
`).run(mediaId, mediaId, mediaId);
return NextResponse.json({ success: true });
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 });

View File

@ -61,11 +61,21 @@ export default function BookmarksPage() {
const handleRate = async (id: number, rating: number) => {
try {
await fetch(`/api/stars/${id}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ rating })
});
if (rating === 0) {
// For unstarring (rating = 0), delete the existing rating
await fetch(`/api/stars`, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ mediaId: id })
});
} else {
// For setting/updating a rating
await fetch(`/api/stars`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ mediaId: id, rating })
});
}
} catch (error) {
console.error('Error rating item:', error);
}

View File

@ -215,16 +215,28 @@ const FolderViewerPage = () => {
// Handle rating operations
const handleRate = async (mediaId: number, rating: number) => {
try {
const response = await fetch(`/api/stars`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ mediaId, rating })
});
if (response.ok) {
console.log('Rating added successfully');
if (rating === 0) {
// For unstarring (rating = 0), delete the existing rating
await fetch(`/api/stars`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ mediaId })
});
} else {
// For setting/updating a rating
const response = await fetch(`/api/stars`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ mediaId, rating })
});
if (response.ok) {
console.log('Rating added successfully');
}
}
} catch (error) {
console.error('Error rating item:', error);

View File

@ -85,11 +85,21 @@ export default function PhotosPage() {
const handleRate = async (photoId: number, rating: number) => {
try {
await fetch(`/api/stars/${photoId}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ rating })
});
if (rating === 0) {
// For unstarring (rating = 0), delete the existing rating
await fetch(`/api/stars`, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ mediaId: photoId })
});
} else {
// For setting/updating a rating
await fetch(`/api/stars`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ mediaId: photoId, rating })
});
}
} catch (error) {
console.error('Error rating photo:', error);
}

View File

@ -57,11 +57,21 @@ const TextsPage = () => {
const handleRate = async (textId: number, rating: number) => {
try {
await fetch(`/api/stars/${textId}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ rating })
});
if (rating === 0) {
// For unstarring (rating = 0), delete the existing rating
await fetch(`/api/stars`, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ mediaId: textId })
});
} else {
// For setting/updating a rating
await fetch(`/api/stars`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ mediaId: textId, rating })
});
}
} catch (error) {
console.error('Error rating text:', error);
}

View File

@ -49,11 +49,31 @@ const VideosPage = () => {
const handleRate = async (videoId: number, rating: number) => {
try {
await fetch(`/api/stars/${videoId}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ rating })
});
if (rating === 0) {
// For unstarring (rating = 0), we need to delete the existing rating
// First, get the current rating record to find the star ID
const getResponse = await fetch(`/api/stars/${videoId}`);
if (getResponse.ok) {
const data = await getResponse.json();
if (data.hasRating) {
// We need to get the actual star record ID to delete it
// Since the API structure doesn't return star ID, we'll use a different approach
// Delete by media_id instead of star id
await fetch(`/api/stars`, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ mediaId: videoId })
});
}
}
} else {
// For setting/updating a rating
await fetch(`/api/stars`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ mediaId: videoId, rating })
});
}
} catch (error) {
console.error('Error rating video:', error);
}

View File

@ -130,7 +130,10 @@ export default function PhotoViewer({
const handleRate = (rating: number) => {
if (onRate && 'id' in photo && photo.id !== undefined) {
onRate(photo.id, rating);
const currentRating = Math.round(getAvgRating());
// If clicking the same star as current rating, cancel the rating
const newRating = currentRating === rating ? 0 : rating;
onRate(photo.id, newRating);
}
};