fix: compact victory cards, fix date picker overflow, improve city autocomplete

- Merge place/category/competition into single row for compact layout
- Inline date range picker (no wrapper div causing overflow)
- Remove restrictive Nominatim filter — show all location results
- Reduce padding/gaps across all bio fields for denser layout

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 15:47:52 +03:00
parent 627781027b
commit 5030edd0d6
2 changed files with 62 additions and 78 deletions

View File

@@ -392,37 +392,37 @@ export function VictoryListField({ label, items, onChange, placeholder, onLinkVa
return (
<div>
<label className="block text-sm text-neutral-400 mb-1.5">{label}</label>
<div className="space-y-3">
<div className="space-y-2">
{items.map((item, i) => (
<div key={i} className="rounded-lg border border-white/10 bg-neutral-800/50 p-3 space-y-2">
<div className="flex items-center gap-2">
<div key={i} className="rounded-lg border border-white/10 bg-neutral-800/50 p-2.5 space-y-1.5">
<div className="flex items-center gap-1.5">
<input
type="text"
value={item.text}
onChange={(e) => updateText(i, e.target.value)}
className="flex-1 rounded-lg border border-white/10 bg-neutral-800 px-4 py-2 text-sm text-white outline-none focus:border-gold transition-colors"
className="flex-1 rounded-md border border-white/10 bg-neutral-800 px-2.5 py-1.5 text-sm text-white outline-none focus:border-gold transition-colors"
/>
<button
type="button"
onClick={() => remove(i)}
className="shrink-0 rounded-lg p-2 text-neutral-500 hover:text-red-400 transition-colors"
className="shrink-0 rounded-md p-1.5 text-neutral-500 hover:text-red-400 transition-colors"
>
<X size={14} />
</button>
</div>
<div className="flex items-center gap-2 pl-1">
<div className="flex items-center gap-1.5">
{item.image ? (
<div className="flex items-center gap-1.5 rounded-md bg-neutral-700/50 px-2 py-1 text-xs text-neutral-300">
<ImageIcon size={12} className="text-gold" />
<span className="max-w-[120px] truncate">{item.image.split("/").pop()}</span>
<div className="flex items-center gap-1 rounded bg-neutral-700/50 px-1.5 py-0.5 text-[11px] text-neutral-300">
<ImageIcon size={10} className="text-gold" />
<span className="max-w-[80px] truncate">{item.image.split("/").pop()}</span>
<button type="button" onClick={() => removeImage(i)} className="text-neutral-500 hover:text-red-400">
<X size={10} />
<X size={9} />
</button>
</div>
) : (
<label className="flex cursor-pointer items-center gap-1.5 rounded-md px-2 py-1 text-xs text-neutral-500 hover:text-neutral-300 transition-colors">
{uploadingIndex === i ? <Loader2 size={12} className="animate-spin" /> : <Upload size={12} />}
{uploadingIndex === i ? "Загрузка..." : "Фото"}
<label className="flex cursor-pointer items-center gap-1 rounded px-1.5 py-0.5 text-[11px] text-neutral-500 hover:text-neutral-300 transition-colors">
{uploadingIndex === i ? <Loader2 size={10} className="animate-spin" /> : <Upload size={10} />}
{uploadingIndex === i ? "..." : "Фото"}
<input type="file" accept="image/*" onChange={(e) => handleUpload(i, e)} className="hidden" />
</label>
)}
@@ -516,29 +516,23 @@ export function DateRangeField({ value, onChange }: DateRangeFieldProps) {
}
return (
<div className="flex items-center gap-2">
<div className="relative flex-1">
<Calendar size={12} className="absolute left-2.5 top-1/2 -translate-y-1/2 text-neutral-500 pointer-events-none" />
<div className="flex items-center gap-1">
<Calendar size={11} className="text-neutral-500 shrink-0" />
<input
type="date"
value={start}
onChange={(e) => handleChange(e.target.value, end)}
className="w-full rounded-lg border border-white/10 bg-neutral-800 pl-7 pr-2 py-2 text-sm text-white outline-none focus:border-gold transition-colors [color-scheme:dark]"
className="w-[130px] rounded-md border border-white/10 bg-neutral-800 px-1.5 py-1.5 text-xs text-white outline-none focus:border-gold transition-colors [color-scheme:dark]"
/>
</div>
<span className="text-neutral-500 text-xs"></span>
<div className="relative flex-1">
<Calendar size={12} className="absolute left-2.5 top-1/2 -translate-y-1/2 text-neutral-500 pointer-events-none" />
<input
type="date"
value={end}
min={start}
onChange={(e) => handleChange(start, e.target.value)}
placeholder="(один день)"
className="w-full rounded-lg border border-white/10 bg-neutral-800 pl-7 pr-2 py-2 text-sm text-white outline-none focus:border-gold transition-colors [color-scheme:dark]"
className="w-[130px] rounded-md border border-white/10 bg-neutral-800 px-1.5 py-1.5 text-xs text-white outline-none focus:border-gold transition-colors [color-scheme:dark]"
/>
</div>
</div>
);
}
@@ -570,7 +564,7 @@ export function CityField({ value, onChange, error, onSearch, suggestions, onSel
return (
<div ref={containerRef} className="relative flex-1">
<div className="relative">
<MapPin size={12} className="absolute left-2.5 top-1/2 -translate-y-1/2 text-neutral-500 pointer-events-none" />
<MapPin size={11} className="absolute left-2 top-1/2 -translate-y-1/2 text-neutral-500 pointer-events-none" />
<input
type="text"
value={value}
@@ -580,11 +574,11 @@ export function CityField({ value, onChange, error, onSearch, suggestions, onSel
}}
onFocus={() => setFocused(true)}
placeholder="Город, страна"
className={`w-full rounded-lg border bg-neutral-800 pl-7 pr-3 py-2 text-sm text-white placeholder-neutral-600 outline-none transition-colors ${
className={`w-full rounded-md border bg-neutral-800 pl-6 pr-3 py-1.5 text-sm text-white placeholder-neutral-600 outline-none transition-colors ${
error ? "border-red-500/50" : "border-white/10 focus:border-gold"
}`}
/>
{error && <AlertCircle size={12} className="absolute right-2.5 top-1/2 -translate-y-1/2 text-red-400" />}
{error && <AlertCircle size={12} className="absolute right-2 top-1/2 -translate-y-1/2 text-red-400" />}
</div>
{error && <p className="mt-0.5 text-[10px] text-red-400">{error}</p>}
{focused && suggestions && suggestions.length > 0 && (
@@ -716,38 +710,38 @@ export function VictoryItemListField({ label, items, onChange, cityErrors, cityS
<label className="block text-sm text-neutral-400 mb-1.5">{label}</label>
<div className="space-y-3">
{items.map((item, i) => (
<div key={i} className="rounded-lg border border-white/10 bg-neutral-800/50 p-3 space-y-2">
<div className="flex gap-2">
<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">
<input
type="text"
value={item.place || ""}
onChange={(e) => update(i, "place", e.target.value)}
placeholder="Место (🥇, 1 место...)"
className="w-32 rounded-lg border border-white/10 bg-neutral-800 px-3 py-2 text-sm text-white placeholder-neutral-600 outline-none focus:border-gold transition-colors"
placeholder="Место (🥇, 1...)"
className="w-24 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"
value={item.category || ""}
onChange={(e) => update(i, "category", e.target.value)}
placeholder="Категория (Exotic Semi-Pro...)"
className="flex-1 rounded-lg border border-white/10 bg-neutral-800 px-3 py-2 text-sm text-white placeholder-neutral-600 outline-none focus:border-gold transition-colors"
placeholder="Категория"
className="flex-1 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"
/>
<button
type="button"
onClick={() => remove(i)}
className="shrink-0 rounded-lg p-2 text-neutral-500 hover:text-red-400 transition-colors"
>
<X size={14} />
</button>
</div>
<input
type="text"
value={item.competition || ""}
onChange={(e) => update(i, "competition", e.target.value)}
placeholder="Чемпионат (REVOLUTION 2025...)"
className="w-full rounded-lg border border-white/10 bg-neutral-800 px-3 py-2 text-sm text-white placeholder-neutral-600 outline-none focus:border-gold transition-colors"
placeholder="Чемпионат"
className="flex-1 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"
/>
<div className="flex gap-2">
<button
type="button"
onClick={() => remove(i)}
className="shrink-0 rounded-md p-1.5 text-neutral-500 hover:text-red-400 transition-colors"
>
<X size={14} />
</button>
</div>
<div className="flex gap-1.5">
<CityField
value={item.location || ""}
onChange={(v) => update(i, "location", v)}
@@ -756,26 +750,24 @@ export function VictoryItemListField({ label, items, onChange, cityErrors, cityS
suggestions={citySuggestions?.index === i ? citySuggestions.items : undefined}
onSelectSuggestion={(v) => onCitySelect?.(i, v)}
/>
<div className="w-56 shrink-0">
<DateRangeField
value={item.date || ""}
onChange={(v) => update(i, "date", v)}
/>
</div>
</div>
<div className="flex items-center gap-2 pl-1">
<div className="flex items-center gap-1.5">
{item.image ? (
<div className="flex items-center gap-1.5 rounded-md bg-neutral-700/50 px-2 py-1 text-xs text-neutral-300">
<ImageIcon size={12} className="text-gold" />
<span className="max-w-[120px] truncate">{item.image.split("/").pop()}</span>
<div className="flex items-center gap-1 rounded bg-neutral-700/50 px-1.5 py-0.5 text-[11px] text-neutral-300">
<ImageIcon size={10} className="text-gold" />
<span className="max-w-[80px] truncate">{item.image.split("/").pop()}</span>
<button type="button" onClick={() => removeImage(i)} className="text-neutral-500 hover:text-red-400">
<X size={10} />
<X size={9} />
</button>
</div>
) : (
<label className="flex cursor-pointer items-center gap-1.5 rounded-md px-2 py-1 text-xs text-neutral-500 hover:text-neutral-300 transition-colors">
{uploadingIndex === i ? <Loader2 size={12} className="animate-spin" /> : <Upload size={12} />}
{uploadingIndex === i ? "Загрузка..." : "Фото"}
<label className="flex cursor-pointer items-center gap-1 rounded px-1.5 py-0.5 text-[11px] text-neutral-500 hover:text-neutral-300 transition-colors">
{uploadingIndex === i ? <Loader2 size={10} className="animate-spin" /> : <Upload size={10} />}
{uploadingIndex === i ? "..." : "Фото"}
<input type="file" accept="image/*" onChange={(e) => handleUpload(i, e)} className="hidden" />
</label>
)}

View File

@@ -88,24 +88,16 @@ export default function TeamMemberEditorPage() {
);
const results = await res.json();
const cities = results
.filter((r: Record<string, unknown>) => {
const type = r.type as string;
const cls = r.class as string;
return cls === "place" || type === "city" || type === "town" || type === "village" || type === "administrative";
})
.map((r: Record<string, unknown>) => {
const addr = r.address as Record<string, string> | undefined;
const city = addr?.city || addr?.town || addr?.village || (r.name as string);
const city = addr?.city || addr?.town || addr?.village || addr?.state || (r.name as string);
const country = addr?.country || "";
return country ? `${city}, ${country}` : city;
})
.filter((v: string, i: number, a: string[]) => a.indexOf(v) === i);
.filter((v: string, i: number, a: string[]) => a.indexOf(v) === i)
.slice(0, 6);
setCitySuggestions(cities.length > 0 ? { index, items: cities } : null);
if (cities.length === 0 && query.length >= 3) {
setCityErrors((prev) => ({ ...prev, [index]: "Город не найден" }));
} else {
setCityErrors((prev) => { const n = { ...prev }; delete n[index]; return n; });
}
} catch {
setCitySuggestions(null);
}