Add delete button to taste profile tags in Stats

- Backend: DELETE /stats/taste/{tag} removes the row from user_tag_affinity
- API: deleteTasteTag(tag) helper
- Stats UI: × button on each tag chip, faint by default, full opacity on hover;
  invalidates stats query so the tag disappears immediately

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Mattias Tall
2026-05-26 11:07:32 +02:00
parent a4cd32da4a
commit d6dd07e0bd
3 changed files with 33 additions and 5 deletions

View File

@@ -1,10 +1,10 @@
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from sqlalchemy import text from sqlalchemy import text
from ..auth_utils import get_current_user from ..auth_utils import get_current_user
from ..database import get_db from ..database import get_db
from ..models import User from ..models import User, UserTagAffinity
router = APIRouter() router = APIRouter()
@@ -142,3 +142,16 @@ def get_stats(
"top_categories": [dict(r) for r in top_categories], "top_categories": [dict(r) for r in top_categories],
"taste_profile": [dict(r) for r in taste_profile], "taste_profile": [dict(r) for r in taste_profile],
} }
@router.delete("/taste/{tag}", status_code=204)
def delete_taste_tag(
tag: str,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
row = db.query(UserTagAffinity).filter_by(user_id=current_user.id, tag=tag).first()
if not row:
raise HTTPException(status_code=404, detail="Tag not found")
db.delete(row)
db.commit()

View File

@@ -129,6 +129,7 @@ export const getCommunityShelf = () => api.get("/discovery/community");
// Stats // Stats
export const getStats = () => api.get("/stats"); export const getStats = () => api.get("/stats");
export const deleteTasteTag = (tag) => api.delete(`/stats/taste/${encodeURIComponent(tag)}`);
// Admin // Admin
export const getAdminUsers = () => api.get("/admin/users"); export const getAdminUsers = () => api.get("/admin/users");

View File

@@ -1,5 +1,5 @@
import { useQuery } from "@tanstack/react-query"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { getStats } from "../api"; import { getStats, deleteTasteTag } from "../api";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
function fmt(seconds) { function fmt(seconds) {
@@ -22,12 +22,18 @@ function StatCard({ label, value, sub }) {
} }
export default function Stats() { export default function Stats() {
const qc = useQueryClient();
const { data, isLoading } = useQuery({ const { data, isLoading } = useQuery({
queryKey: ["stats"], queryKey: ["stats"],
queryFn: () => getStats().then(r => r.data), queryFn: () => getStats().then(r => r.data),
staleTime: 5 * 60_000, staleTime: 5 * 60_000,
}); });
const deleteTag = useMutation({
mutationFn: (tag) => deleteTasteTag(tag),
onSuccess: () => qc.invalidateQueries({ queryKey: ["stats"] }),
});
if (isLoading) { if (isLoading) {
return ( return (
<div className="flex items-center justify-center py-24"> <div className="flex items-center justify-center py-24">
@@ -178,7 +184,7 @@ export default function Stats() {
<span <span
key={t.tag} key={t.tag}
title={`score: ${t.score.toFixed(1)}`} title={`score: ${t.score.toFixed(1)}`}
className="px-3 py-1 rounded-full text-xs font-medium" className="flex items-center gap-1 px-3 py-1 rounded-full text-xs font-medium"
style={{ style={{
backgroundColor: `rgba(250,204,21,${0.08 + intensity * 0.18})`, backgroundColor: `rgba(250,204,21,${0.08 + intensity * 0.18})`,
color: `hsl(50,95%,${55 + intensity * 20}%)`, color: `hsl(50,95%,${55 + intensity * 20}%)`,
@@ -186,6 +192,14 @@ export default function Stats() {
}} }}
> >
{t.tag} {t.tag}
<button
onClick={() => deleteTag.mutate(t.tag)}
disabled={deleteTag.isPending}
className="opacity-40 hover:opacity-100 transition-opacity leading-none ml-0.5"
title="Remove from taste profile"
>
×
</button>
</span> </span>
); );
})} })}