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:
@@ -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()
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
Reference in New Issue
Block a user