- 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
174 lines
7.4 KiB
TypeScript
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>
|
|
);
|
|
}
|