feat: split victories into tabbed sections (place/nomination/judge)

Add type field to VictoryItem, tabbed UI in trainer profile,
and type dropdown in admin form.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 16:31:00 +03:00
parent d3bb43af80
commit 1b391cdde6
3 changed files with 76 additions and 142 deletions

View File

@@ -657,80 +657,6 @@ export function ValidatedLinkField({ value, onChange, onValidate, validationKey,
);
}
// --- Place/Nomination Select ---
const PLACE_OPTIONS = [
{ value: "1 место", label: "1 место", icon: "🥇" },
{ value: "2 место", label: "2 место", icon: "🥈" },
{ value: "3 место", label: "3 место", icon: "🥉" },
{ value: "4 место", label: "4 место" },
{ value: "5 место", label: "5 место" },
{ value: "6 место", label: "6 место" },
{ value: "Финалист", label: "Финалист", icon: "🏅" },
{ value: "Полуфиналист", label: "Полуфиналист" },
{ value: "Лауреат", label: "Лауреат", icon: "🏆" },
{ value: "Номинант", label: "Номинант" },
{ value: "Участник", label: "Участник" },
{ value: "Победитель", label: "Победитель", icon: "🏆" },
{ value: "Гран-при", label: "Гран-при", icon: "👑" },
{ value: "Best Show", label: "Best Show", icon: "⭐" },
{ value: "Vice Champion", label: "Vice Champion" },
{ value: "Champion", label: "Champion", icon: "🏆" },
];
interface PlaceSelectProps {
value: string;
onChange: (value: string) => void;
}
function PlaceSelect({ value, onChange }: PlaceSelectProps) {
const [open, setOpen] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!open) return;
function handle(e: MouseEvent) {
if (containerRef.current && !containerRef.current.contains(e.target as Node)) setOpen(false);
}
document.addEventListener("mousedown", handle);
return () => document.removeEventListener("mousedown", handle);
}, [open]);
const selected = PLACE_OPTIONS.find((o) => o.value === value);
return (
<div ref={containerRef} className="relative w-28 shrink-0">
<button
type="button"
onClick={() => setOpen(!open)}
className={`w-full rounded-md border bg-neutral-800 px-2 py-1.5 text-left text-sm transition-colors ${
open ? "border-gold" : "border-white/10"
} ${value ? "text-white" : "text-neutral-500"}`}
>
{selected ? `${selected.icon ? selected.icon + " " : ""}${selected.label}` : "Место..."}
</button>
{open && (
<div className="absolute z-50 mt-1 w-44 rounded-lg border border-white/10 bg-neutral-800 shadow-xl overflow-hidden">
<div className="max-h-52 overflow-y-auto">
{PLACE_OPTIONS.map((opt) => (
<button
key={opt.value}
type="button"
onClick={() => { onChange(opt.value); setOpen(false); }}
className={`w-full px-3 py-1.5 text-left text-sm transition-colors hover:bg-white/5 ${
opt.value === value ? "text-gold bg-gold/5" : "text-white"
}`}
>
{opt.icon && <span className="mr-1.5">{opt.icon}</span>}
{opt.label}
</button>
))}
</div>
</div>
)}
</div>
);
}
interface VictoryItemListFieldProps {
label: string;
items: VictoryItem[];
@@ -744,7 +670,7 @@ interface VictoryItemListFieldProps {
export function VictoryItemListField({ label, items, onChange, cityErrors, citySuggestions, onCitySearch, onCitySelect, onLinkValidate }: VictoryItemListFieldProps) {
function add() {
onChange([...items, { place: "", category: "", competition: "" }]);
onChange([...items, { type: "place", place: "", category: "", competition: "" }]);
}
function remove(index: number) {
@@ -762,9 +688,21 @@ export function VictoryItemListField({ label, items, onChange, cityErrors, cityS
{items.map((item, i) => (
<div key={i} className="rounded-lg border border-white/10 bg-neutral-800/50 p-2.5 space-y-1.5">
<div className="flex gap-1.5">
<PlaceSelect
<select
value={item.type || "place"}
onChange={(e) => update(i, "type", e.target.value)}
className="w-32 shrink-0 rounded-md border border-white/10 bg-neutral-800 px-2 py-1.5 text-sm text-white outline-none focus:border-gold transition-colors"
>
<option value="place">Место</option>
<option value="nomination">Номинация</option>
<option value="judge">Судейство</option>
</select>
<input
type="text"
value={item.place || ""}
onChange={(v) => update(i, "place", v)}
onChange={(e) => update(i, "place", e.target.value)}
placeholder="1 место, финалист..."
className="w-28 shrink-0 rounded-md border border-white/10 bg-neutral-800 px-2.5 py-1.5 text-sm text-white placeholder-neutral-600 outline-none focus:border-gold transition-colors"
/>
<input
type="text"