- CandidateCard: replace all hardcoded titles and badge text with t() - CandidateListItem: add useTranslation, replace winner/delete/open labels and +/- Notes badge - CandidateForm: add useTranslation, replace all form labels, placeholders, validation errors, submit button - ComparisonTable: move STATUS_LABELS inside component with t(), replace all ATTRIBUTE_ROWS labels, View button, impact row labels - StatusBadge: refactor STATUS_CONFIG to STATUS_ICONS + runtime STATUS_LABELS via t() - CreateThreadModal: replace title, thread name label, category label, placeholder, cancel/submit buttons, error messages - AddToThreadModal: replace modal titles, labels, placeholders, back/cancel/submit buttons, error messages - threads.json: extend candidateForm with category, notes, pros, cons, product link labels and all placeholders
109 lines
2.9 KiB
TypeScript
109 lines
2.9 KiB
TypeScript
import { useEffect, useRef, useState } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { LucideIcon } from "../lib/iconData";
|
|
|
|
const STATUS_ICONS = {
|
|
researching: "search",
|
|
ordered: "truck",
|
|
arrived: "check",
|
|
} as const;
|
|
|
|
type CandidateStatus = keyof typeof STATUS_ICONS;
|
|
|
|
interface StatusBadgeProps {
|
|
status: CandidateStatus;
|
|
onStatusChange: (status: CandidateStatus) => void;
|
|
}
|
|
|
|
export function StatusBadge({ status, onStatusChange }: StatusBadgeProps) {
|
|
const { t } = useTranslation("threads");
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
|
|
const STATUS_LABELS: Record<CandidateStatus, string> = {
|
|
researching: t("statusBadge.researching"),
|
|
ordered: t("statusBadge.ordered"),
|
|
arrived: t("statusBadge.arrived"),
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (!isOpen) return;
|
|
|
|
function handleClickOutside(e: MouseEvent) {
|
|
if (
|
|
containerRef.current &&
|
|
!containerRef.current.contains(e.target as Node)
|
|
) {
|
|
setIsOpen(false);
|
|
}
|
|
}
|
|
|
|
function handleEscape(e: KeyboardEvent) {
|
|
if (e.key === "Escape") {
|
|
setIsOpen(false);
|
|
}
|
|
}
|
|
|
|
document.addEventListener("mousedown", handleClickOutside);
|
|
document.addEventListener("keydown", handleEscape);
|
|
return () => {
|
|
document.removeEventListener("mousedown", handleClickOutside);
|
|
document.removeEventListener("keydown", handleEscape);
|
|
};
|
|
}, [isOpen]);
|
|
|
|
return (
|
|
<div ref={containerRef} className="relative">
|
|
<button
|
|
type="button"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
setIsOpen((prev) => !prev);
|
|
}}
|
|
className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-600 hover:bg-gray-200 transition-colors cursor-pointer"
|
|
>
|
|
<LucideIcon name={STATUS_ICONS[status]} size={14} className="text-gray-500" />
|
|
{STATUS_LABELS[status]}
|
|
</button>
|
|
|
|
{isOpen && (
|
|
<div className="absolute right-0 top-full mt-1 z-20 bg-white border border-gray-200 rounded-lg shadow-lg py-1 min-w-[150px]">
|
|
{(Object.keys(STATUS_ICONS) as CandidateStatus[]).map((key) => {
|
|
const isActive = key === status;
|
|
return (
|
|
<button
|
|
key={key}
|
|
type="button"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onStatusChange(key);
|
|
setIsOpen(false);
|
|
}}
|
|
className={`w-full flex items-center gap-2 px-3 py-1.5 text-sm text-left hover:bg-gray-50 transition-colors ${
|
|
isActive ? "bg-gray-50 font-medium" : ""
|
|
}`}
|
|
>
|
|
<LucideIcon
|
|
name={STATUS_ICONS[key]}
|
|
size={14}
|
|
className={isActive ? "text-gray-700" : "text-gray-400"}
|
|
/>
|
|
<span className={isActive ? "text-gray-900" : "text-gray-600"}>
|
|
{STATUS_LABELS[key]}
|
|
</span>
|
|
{isActive && (
|
|
<LucideIcon
|
|
name="check"
|
|
size={14}
|
|
className="ml-auto text-gray-500"
|
|
/>
|
|
)}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|