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 (
|
||||
<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>
|
||||
)}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user