feat(catalog): OEM uyumluluk oylaması + Parça Uzmanları liderlik tablosu
Parça satırlarına topluluk oyu eklendi: her OEM kodu için uyumlu/uyumsuz ikilisi (👍/👎, sayaçlı). Oylar oem_votes'a (kullanıcı+kod başına tek oy, fikir değişikliği günceller, puan üretmez), ödüller oem_vote_points ledger'ına yazılır: oy +1, kesin çoğunlukla aynı yönde +2 (kodu ilk oylayan her zaman 3 alır); ödüller oy anında kesinleşir, çoğunluk sonra dönse de geri alınmaz. Aynı koda eşzamanlı oylar advisory lock ile sıralanır. /dashboard/uzmanlar: Trophy Gamification UI Kit'ten (ui.trophy.so, MIT) uyarlanan kürsü + sıralama + puan rozetiyle "Parça Uzmanları" liderlik sayfası; adlar KVKK-maskeli (S*** Y***), cevap kullanıcı id sızdırmaz. Not: sidebar nav linki, tr/en i18n anahtarları ve routeTree 09a9487'de gitmişti; bu commit eksik kalan rota/bileşen/API dosyalarını tamamlayarak dev build'ini düzeltir. Migration: 0015_oem_votes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
79
apps/web/src/components/catalog/oem-vote-buttons.tsx
Normal file
79
apps/web/src/components/catalog/oem-vote-buttons.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import { cn } from "@sase/ui";
|
||||
import { ThumbsDown, ThumbsUp } from "lucide-react";
|
||||
|
||||
export type OemVoteChoice = "compatible" | "incompatible";
|
||||
|
||||
export interface OemVoteSummary {
|
||||
compatible: number;
|
||||
incompatible: number;
|
||||
myVote: OemVoteChoice | null;
|
||||
}
|
||||
|
||||
export interface CastOemVoteResult {
|
||||
oemCode: string;
|
||||
myVote: OemVoteChoice;
|
||||
counts: { compatible: number; incompatible: number };
|
||||
pointsAwarded: number;
|
||||
correct: boolean | null;
|
||||
isNew: boolean;
|
||||
}
|
||||
|
||||
interface OemVoteButtonsProps {
|
||||
summary?: OemVoteSummary;
|
||||
pending?: boolean;
|
||||
onVote: (vote: OemVoteChoice) => void;
|
||||
}
|
||||
|
||||
// Parça satırındaki topluluk oyu ikilisi: uyumlu (👍) / uyumsuz (👎).
|
||||
// Satır tıklaması hotspot grubunu seçtiği için tıklamalar satıra taşmaz.
|
||||
export function OemVoteButtons({ summary, pending, onVote }: OemVoteButtonsProps) {
|
||||
const myVote = summary?.myVote ?? null;
|
||||
const options = [
|
||||
{
|
||||
vote: "compatible" as const,
|
||||
icon: ThumbsUp,
|
||||
count: summary?.compatible ?? 0,
|
||||
title: "Uyumlu — bu OEM kodu doğru",
|
||||
activeClass: "text-green-500",
|
||||
},
|
||||
{
|
||||
vote: "incompatible" as const,
|
||||
icon: ThumbsDown,
|
||||
count: summary?.incompatible ?? 0,
|
||||
title: "Uyumsuz — bu OEM kodu hatalı",
|
||||
activeClass: "text-red-500",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<span className="inline-flex items-center gap-0.5">
|
||||
{options.map(({ vote, icon: Icon, count, title, activeClass }) => {
|
||||
const isActive = myVote === vote;
|
||||
return (
|
||||
<button
|
||||
key={vote}
|
||||
type="button"
|
||||
disabled={pending}
|
||||
title={title}
|
||||
aria-label={title}
|
||||
aria-pressed={isActive}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onVote(vote);
|
||||
}}
|
||||
onKeyDown={(e) => e.stopPropagation()}
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1 rounded px-1.5 py-1 text-xs transition-colors disabled:opacity-50",
|
||||
isActive
|
||||
? cn(activeClass, "font-semibold")
|
||||
: "text-muted-foreground hover:bg-accent hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
<Icon className="size-3.5 shrink-0" fill={isActive ? "currentColor" : "none"} />
|
||||
<span className="tabular-nums">{count}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user