gohorsejobs/frontend/src/components/location-picker.tsx
NANDO9322 1f9b54d719 fix: resolve problemas de cadastro, seletor de localização e swagger
- Corrige violação de restrição de role no Registro de Candidato (usa 'candidate' em minúsculo)
- Corrige erro de chave duplicada para slug da empresa adicionando timestamp ao workspace do candidato
- Corrige crash no LocationPicker tratando respostas nulas no frontend e retornando arrays vazios no backend
- Corrige documentação do Swagger para o endpoint de Login e adiciona definição de segurança BearerAuth
2026-01-05 13:30:02 -03:00

174 lines
7.4 KiB
TypeScript

"use client";
import { useState, useEffect, useRef } from "react";
import { locationsApi, Country, LocationSearchResult } from "@/lib/api";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { MapPin, Loader2, Globe } from "lucide-react";
interface LocationPickerProps {
value: string; // The final string "City, State, Country"
onChange: (value: string) => void;
}
export function LocationPicker({ value, onChange }: LocationPickerProps) {
const [countries, setCountries] = useState<Country[]>([]);
const [selectedCountry, setSelectedCountry] = useState<string>(""); // ID as string
// Search
const [query, setQuery] = useState("");
const [results, setResults] = useState<LocationSearchResult[]>([]);
const [loading, setLoading] = useState(false);
const [showResults, setShowResults] = useState(false);
const wrapperRef = useRef<HTMLDivElement>(null);
// Initial Load
useEffect(() => {
locationsApi.listCountries().then(res => {
const data = res || [];
setCountries(data);
// Default to Brazil if available (User is Brazilian based on context)
const br = data.find(c => c.iso2 === "BR");
if (br) setSelectedCountry(br.id.toString());
}).catch(console.error);
}, []);
// Helper to get country name
const getCountryName = (id: string) => countries.find(c => c.id.toString() === id)?.name || "";
// Debounce Search
useEffect(() => {
if (!selectedCountry || query.length < 2) {
setResults([]);
return;
}
const timeout = setTimeout(() => {
setLoading(true);
locationsApi.search(query, selectedCountry)
.then(res => setResults(res || []))
.catch(err => {
console.error("Search failed", err);
setResults([]);
})
.finally(() => setLoading(false));
}, 500);
return () => clearTimeout(timeout);
}, [query, selectedCountry]);
// Click Outside to close
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (wrapperRef.current && !wrapperRef.current.contains(event.target as Node)) {
setShowResults(false);
}
}
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
const handleSelectResult = (item: LocationSearchResult) => {
const countryName = getCountryName(selectedCountry);
let finalString = "";
if (item.type === 'city') {
// City, State, Country
finalString = `${item.name}, ${item.region_name}, ${countryName}`;
} else {
// State, Country
finalString = `${item.name}, ${countryName}`;
}
onChange(finalString);
setQuery(item.name); // Updates visible input to just name or full string?
// User image shows "City or state" as input.
// Usually we show the full formatted string or just the name.
// Let's show full string to be clear.
setQuery(finalString);
setShowResults(false);
};
return (
<div className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{/* Country Select (Right in design but logical Left in flow, design shows separate) */}
{/* User image: Left: City/State (Input with target icon), Right: Country (Dropdown) */}
{/* City/State Search (2/3 width) */}
<div className="md:col-span-2 relative" ref={wrapperRef}>
<Label className="mb-1.5 block">Localização da Vaga</Label>
<div className="relative">
<MapPin className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
<Input
value={query}
onChange={(e) => {
setQuery(e.target.value);
setShowResults(true);
// If user types manually conform to standard?
// We can allow free text, but search helps.
onChange(e.target.value);
}}
onFocus={() => setShowResults(true)}
placeholder={selectedCountry ? "Busque cidade ou estado..." : "Selecione o país primeiro"}
className="pl-10"
disabled={!selectedCountry}
/>
{loading && (
<div className="absolute right-3 top-3">
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
</div>
)}
</div>
{/* Results Dropdown */}
{showResults && results?.length > 0 && (
<div className="absolute z-10 w-full mt-1 bg-background border rounded-md shadow-lg max-h-60 overflow-auto">
{results.map((item) => (
<button
key={`${item.type}-${item.id}`}
onClick={() => handleSelectResult(item)}
className="w-full text-left px-4 py-2 hover:bg-muted text-sm flex flex-col"
>
<span className="font-medium">{item.name}</span>
<span className="text-xs text-muted-foreground">
{item.type === 'city' ? `${item.region_name}, Cidade` : `Estado/Região`}
</span>
</button>
))}
</div>
)}
</div>
{/* Country Select (1/3 width) */}
<div>
<Label className="mb-1.5 block">País</Label>
<Select
value={selectedCountry}
onValueChange={(val) => {
setSelectedCountry(val);
setQuery(""); // Clear location on country change
onChange("");
}}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Selecione" />
</SelectTrigger>
<SelectContent>
{Array.isArray(countries) && countries.map((c) => (
<SelectItem key={c.id} value={c.id.toString()}>
<span className="flex items-center gap-2">
<span>{c.emoji}</span>
<span>{c.name}</span>
</span>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
</div>
);
}