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:
@@ -392,37 +392,37 @@ export function VictoryListField({ label, items, onChange, placeholder, onLinkVa
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm text-neutral-400 mb-1.5">{label}</label>
|
<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) => (
|
{items.map((item, i) => (
|
||||||
<div key={i} className="rounded-lg border border-white/10 bg-neutral-800/50 p-3 space-y-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-2">
|
<div className="flex items-center gap-1.5">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={item.text}
|
value={item.text}
|
||||||
onChange={(e) => updateText(i, e.target.value)}
|
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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => remove(i)}
|
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} />
|
<X size={14} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 pl-1">
|
<div className="flex items-center gap-1.5">
|
||||||
{item.image ? (
|
{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">
|
<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={12} className="text-gold" />
|
<ImageIcon size={10} className="text-gold" />
|
||||||
<span className="max-w-[120px] truncate">{item.image.split("/").pop()}</span>
|
<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">
|
<button type="button" onClick={() => removeImage(i)} className="text-neutral-500 hover:text-red-400">
|
||||||
<X size={10} />
|
<X size={9} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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">
|
<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={12} className="animate-spin" /> : <Upload size={12} />}
|
{uploadingIndex === i ? <Loader2 size={10} className="animate-spin" /> : <Upload size={10} />}
|
||||||
{uploadingIndex === i ? "Загрузка..." : "Фото"}
|
{uploadingIndex === i ? "..." : "Фото"}
|
||||||
<input type="file" accept="image/*" onChange={(e) => handleUpload(i, e)} className="hidden" />
|
<input type="file" accept="image/*" onChange={(e) => handleUpload(i, e)} className="hidden" />
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
@@ -516,28 +516,22 @@ export function DateRangeField({ value, onChange }: DateRangeFieldProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-1">
|
||||||
<div className="relative flex-1">
|
<Calendar size={11} className="text-neutral-500 shrink-0" />
|
||||||
<Calendar size={12} className="absolute left-2.5 top-1/2 -translate-y-1/2 text-neutral-500 pointer-events-none" />
|
<input
|
||||||
<input
|
type="date"
|
||||||
type="date"
|
value={start}
|
||||||
value={start}
|
onChange={(e) => handleChange(e.target.value, end)}
|
||||||
onChange={(e) => handleChange(e.target.value, end)}
|
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]"
|
||||||
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]"
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<span className="text-neutral-500 text-xs">—</span>
|
<span className="text-neutral-500 text-xs">—</span>
|
||||||
<div className="relative flex-1">
|
<input
|
||||||
<Calendar size={12} className="absolute left-2.5 top-1/2 -translate-y-1/2 text-neutral-500 pointer-events-none" />
|
type="date"
|
||||||
<input
|
value={end}
|
||||||
type="date"
|
min={start}
|
||||||
value={end}
|
onChange={(e) => handleChange(start, e.target.value)}
|
||||||
min={start}
|
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]"
|
||||||
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]"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -570,7 +564,7 @@ export function CityField({ value, onChange, error, onSearch, suggestions, onSel
|
|||||||
return (
|
return (
|
||||||
<div ref={containerRef} className="relative flex-1">
|
<div ref={containerRef} className="relative flex-1">
|
||||||
<div className="relative">
|
<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
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={value}
|
value={value}
|
||||||
@@ -580,11 +574,11 @@ export function CityField({ value, onChange, error, onSearch, suggestions, onSel
|
|||||||
}}
|
}}
|
||||||
onFocus={() => setFocused(true)}
|
onFocus={() => setFocused(true)}
|
||||||
placeholder="Город, страна"
|
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 ? "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>
|
</div>
|
||||||
{error && <p className="mt-0.5 text-[10px] text-red-400">{error}</p>}
|
{error && <p className="mt-0.5 text-[10px] text-red-400">{error}</p>}
|
||||||
{focused && suggestions && suggestions.length > 0 && (
|
{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>
|
<label className="block text-sm text-neutral-400 mb-1.5">{label}</label>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{items.map((item, i) => (
|
{items.map((item, i) => (
|
||||||
<div key={i} className="rounded-lg border border-white/10 bg-neutral-800/50 p-3 space-y-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-2">
|
<div className="flex gap-1.5">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={item.place || ""}
|
value={item.place || ""}
|
||||||
onChange={(e) => update(i, "place", e.target.value)}
|
onChange={(e) => update(i, "place", e.target.value)}
|
||||||
placeholder="Место (🥇, 1 место...)"
|
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"
|
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
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={item.category || ""}
|
value={item.category || ""}
|
||||||
onChange={(e) => update(i, "category", e.target.value)}
|
onChange={(e) => update(i, "category", e.target.value)}
|
||||||
placeholder="Категория (Exotic Semi-Pro...)"
|
placeholder="Категория"
|
||||||
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"
|
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"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={item.competition || ""}
|
||||||
|
onChange={(e) => update(i, "competition", e.target.value)}
|
||||||
|
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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => remove(i)}
|
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} />
|
<X size={14} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<div className="flex gap-1.5">
|
||||||
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"
|
|
||||||
/>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<CityField
|
<CityField
|
||||||
value={item.location || ""}
|
value={item.location || ""}
|
||||||
onChange={(v) => update(i, "location", v)}
|
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}
|
suggestions={citySuggestions?.index === i ? citySuggestions.items : undefined}
|
||||||
onSelectSuggestion={(v) => onCitySelect?.(i, v)}
|
onSelectSuggestion={(v) => onCitySelect?.(i, v)}
|
||||||
/>
|
/>
|
||||||
<div className="w-56 shrink-0">
|
<DateRangeField
|
||||||
<DateRangeField
|
value={item.date || ""}
|
||||||
value={item.date || ""}
|
onChange={(v) => update(i, "date", v)}
|
||||||
onChange={(v) => update(i, "date", v)}
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 pl-1">
|
<div className="flex items-center gap-1.5">
|
||||||
{item.image ? (
|
{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">
|
<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={12} className="text-gold" />
|
<ImageIcon size={10} className="text-gold" />
|
||||||
<span className="max-w-[120px] truncate">{item.image.split("/").pop()}</span>
|
<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">
|
<button type="button" onClick={() => removeImage(i)} className="text-neutral-500 hover:text-red-400">
|
||||||
<X size={10} />
|
<X size={9} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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">
|
<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={12} className="animate-spin" /> : <Upload size={12} />}
|
{uploadingIndex === i ? <Loader2 size={10} className="animate-spin" /> : <Upload size={10} />}
|
||||||
{uploadingIndex === i ? "Загрузка..." : "Фото"}
|
{uploadingIndex === i ? "..." : "Фото"}
|
||||||
<input type="file" accept="image/*" onChange={(e) => handleUpload(i, e)} className="hidden" />
|
<input type="file" accept="image/*" onChange={(e) => handleUpload(i, e)} className="hidden" />
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -88,24 +88,16 @@ export default function TeamMemberEditorPage() {
|
|||||||
);
|
);
|
||||||
const results = await res.json();
|
const results = await res.json();
|
||||||
const cities = results
|
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>) => {
|
.map((r: Record<string, unknown>) => {
|
||||||
const addr = r.address as Record<string, string> | undefined;
|
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 || "";
|
const country = addr?.country || "";
|
||||||
return country ? `${city}, ${country}` : city;
|
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);
|
setCitySuggestions(cities.length > 0 ? { index, items: cities } : null);
|
||||||
if (cities.length === 0 && query.length >= 3) {
|
setCityErrors((prev) => { const n = { ...prev }; delete n[index]; return n; });
|
||||||
setCityErrors((prev) => ({ ...prev, [index]: "Город не найден" }));
|
|
||||||
} else {
|
|
||||||
setCityErrors((prev) => { const n = { ...prev }; delete n[index]; return n; });
|
|
||||||
}
|
|
||||||
} catch {
|
} catch {
|
||||||
setCitySuggestions(null);
|
setCitySuggestions(null);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user