Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend/bruno/Users/Patch Preferences.bru
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ body:json {
"searchLocation": "São Paulo, Brasil",
"searchLanguage": "pt",
"remoteOnly": true,
"jobTypes": ["full-time", "contract"],
"jobTypes": ["Remoto", "Híbrido"],
"emailNotifications": false
}
}
Expand Down
2 changes: 1 addition & 1 deletion backend/bruno/Users/Post Preferences.bru
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ body:json {
"searchLocation": "São Paulo, Brasil",
"searchLanguage": "pt",
"remoteOnly": true,
"jobTypes": ["full-time", "contract"],
"jobTypes": ["Remoto", "Híbrido"],
"emailNotifications": false
}
}
Expand Down
4 changes: 3 additions & 1 deletion backend/src/modules/users/schemas/user.schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,15 @@ export const updateProfileSchema = z

// ── Preferences ───────────────────────────────────────────────────────────────

const jobTypePreferenceSchema = z.enum(["Remoto", "Híbrido", "Presencial"]);

export const updatePreferencesSchema = z
.object({
keywords: z.array(z.string().min(1)).max(20),
searchLocation: z.string().min(1).max(100).nullable(),
searchLanguage: z.string().length(2).nullable(),
remoteOnly: z.boolean(),
jobTypes: z.array(z.string().min(1)).max(10),
jobTypes: z.array(jobTypePreferenceSchema).max(3),
emailNotifications: z.boolean(),
careerChecklist: z
.array(
Expand Down
38 changes: 32 additions & 6 deletions frontend/src/domains/new_dashboard/NewDashboardPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { useUserDashboardData } from "./hooks/useUserDashboardData";
import type {
CareerChecklist,
Job,
JobModelFilter,
JobStatus,
MatchSort,
NewJob,
Expand All @@ -29,6 +30,11 @@ import {
type ContinentFilter,
type CountryFilter,
} from "./utils/locationFilters";
import {
getModelFilterFromJobTypes,
modelFilterMatchesJob,
modelFilterToApiFilter,
} from "./utils/jobModelFilters";
import { parseSearchKeywords } from "./utils/searchKeywords";

function getSection(pathname: string) {
Expand Down Expand Up @@ -149,7 +155,7 @@ export default function NewDashboardPage() {
const navigate = useNavigate();

const [searchQuery, setSearchQuery] = useState("");
const [filterType, setFilterType] = useState("Todos");
const [filterType, setFilterType] = useState<JobModelFilter>("Todos");
const [filterLevel, setFilterLevel] = useState("Todos");
const [continentFilter, setContinentFilter] =
useState<ContinentFilter>("Todos");
Expand All @@ -159,12 +165,14 @@ export default function NewDashboardPage() {
const [isAddJobOpen, setIsAddJobOpen] = useState(false);
const [toast, setToast] = useState("");
const checklistSaveTimeout = useRef<number | null>(null);
const hasUserSelectedModelFilter = useRef(false);
const showToast = useCallback((message: string) => setToast(message), []);
const {
userProfile,
setUserProfile,
searchPreferences,
setSearchPreferences,
isLoadingUserData,
isSavingProfile,
isSavingPreferences,
saveUserProfile,
Expand Down Expand Up @@ -196,6 +204,13 @@ export default function NewDashboardPage() {
),
[recommendedJobs, userProfile.technologyExperiences],
);
const displayedRecommendedJobs = useMemo(
() =>
matchedRecommendedJobs.filter((job) =>
modelFilterMatchesJob(job, filterType),
),
[filterType, matchedRecommendedJobs],
);
const selectedJob =
[...matchedTrackedJobs, ...matchedRecommendedJobs].find(
(job) => job.id === selectedJobId,
Expand Down Expand Up @@ -230,12 +245,19 @@ export default function NewDashboardPage() {
const handleSavePreferences = async (preferences: SearchPreferences) => {
try {
await saveSearchPreferences(preferences);
hasUserSelectedModelFilter.current = false;
setFilterType(getModelFilterFromJobTypes(preferences.jobTypes));
showToast("Preferências de busca atualizadas.");
} catch {
// O hook já notificou a falha preservando as preferências editadas.
}
};

const handleFilterTypeChange = useCallback((value: JobModelFilter) => {
hasUserSelectedModelFilter.current = true;
setFilterType(value);
}, []);

const handleCareerChecklistChange = useCallback(
(careerChecklist: CareerChecklist[]) => {
const nextPreferences = {
Expand Down Expand Up @@ -313,9 +335,7 @@ export default function NewDashboardPage() {
const typedKeywords = parseSearchKeywords(searchQuery);
const filters = {
...(filterLevel !== "Todos" ? { level: filterLevel } : {}),
...(filterType !== "Todos"
? { type: filterType, model: filterType }
: {}),
...modelFilterToApiFilter(filterType),
...(continentFilter !== "Todos" ? { continent: continentFilter } : {}),
...(countryFilter !== "Todos"
? { country: countryFilter, location: countryFilter }
Expand Down Expand Up @@ -347,6 +367,12 @@ export default function NewDashboardPage() {
searchQuery,
]);

useEffect(() => {
if (isLoadingUserData || hasUserSelectedModelFilter.current) return;

setFilterType(getModelFilterFromJobTypes(searchPreferences.jobTypes));
}, [isLoadingUserData, searchPreferences.jobTypes]);

// Busca automática: dispara ao digitar (com debounce) ou ao trocar
// qualquer filtro, sem precisar clicar em "Buscar vagas".
const isFirstSearchRun = useRef(true);
Expand Down Expand Up @@ -398,11 +424,11 @@ export default function NewDashboardPage() {
case "vagas":
return (
<JobTab
jobs={matchedRecommendedJobs}
jobs={displayedRecommendedJobs}
searchQuery={searchQuery}
setSearchQuery={setSearchQuery}
filterType={filterType}
setFilterType={setFilterType}
setFilterType={handleFilterTypeChange}
filterLevel={filterLevel}
setFilterLevel={setFilterLevel}
continentFilter={continentFilter}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Search } from "lucide-react";
import { jobLevels, jobTypes } from "../../constants";
import type { MatchSort } from "../../types";
import { jobLevels } from "../../constants";
import type { JobModelFilter, MatchSort } from "../../types";
import { jobModelFilterOptions } from "../../utils/jobModelFilters";
import {
continentOptions,
countryOptions,
Expand All @@ -11,8 +12,8 @@ import {
interface JobFilterProps {
searchQuery: string;
setSearchQuery: (value: string) => void;
filterType: string;
setFilterType: (value: string) => void;
filterType: JobModelFilter;
setFilterType: (value: JobModelFilter) => void;
filterLevel: string;
setFilterLevel: (value: string) => void;
continentFilter: ContinentFilter;
Expand Down Expand Up @@ -51,13 +52,19 @@ export function JobFilter({

<select
value={filterType}
onChange={(event) => setFilterType(event.target.value)}
onChange={(event) =>
setFilterType(event.target.value as JobModelFilter)
}
className="h-11 rounded-lg border border-input bg-background px-3 text-sm outline-none transition-colors focus:border-ring"
>
<option value="Todos">Modelo (Todos)</option>
{jobTypes.map((type) => (
<option key={type}>{type}</option>
))}
{jobModelFilterOptions
.filter((option) => option.value !== "Todos")
.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>

<select
Expand Down
29 changes: 23 additions & 6 deletions frontend/src/domains/new_dashboard/components/jobs/JobTab.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,28 @@
import { Search } from "lucide-react";
import type { Job, JobStatus, MatchSort, SearchPreferences } from "../../types";
import type {
Job,
JobModelFilter,
JobStatus,
MatchSort,
SearchPreferences,
} from "../../types";
import {
type ContinentFilter,
type CountryFilter,
} from "../../utils/locationFilters";
import {
getModelFilterFromJobTypes,
jobModelFilterOptions,
} from "../../utils/jobModelFilters";
import { JobFilter } from "./JobFilter";
import { JobTable } from "./JobTable";

interface JobTabProps {
jobs: Job[];
searchQuery: string;
setSearchQuery: (value: string) => void;
filterType: string;
setFilterType: (value: string) => void;
filterType: JobModelFilter;
setFilterType: (value: JobModelFilter) => void;
filterLevel: string;
setFilterLevel: (value: string) => void;
continentFilter: ContinentFilter;
Expand Down Expand Up @@ -59,6 +69,13 @@ export function JobTab({
onOpenJob,
onStatusChange,
}: JobTabProps) {
const preferredModelFilter = searchPreferences
? getModelFilterFromJobTypes(searchPreferences.jobTypes)
: "Todos";
const preferredModelLabel = jobModelFilterOptions.find(
(option) => option.value === preferredModelFilter,
)?.label;

return (
<div className="mx-auto flex w-full max-w-[1680px] flex-col gap-6 px-6 py-8 lg:px-8">
<div className="flex flex-col gap-4 md:flex-row md:items-start md:justify-between">
Expand Down Expand Up @@ -97,10 +114,10 @@ export function JobTab({
setMatchSort={setMatchSort}
/>

{searchPreferences?.remoteOnly && (
{preferredModelFilter !== "Todos" && preferredModelLabel && (
<p className="text-sm font-semibold text-emerald-600">
Filtro de busca ativo: Apenas oportunidades remotas habilitado nas
preferências.
Preferência ativa: {preferredModelLabel.toLowerCase()} como padrão
nesta busca.
</p>
)}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import type { SearchPreferences } from "../../types";
import type { JobModelFilter, SearchPreferences } from "../../types";
import {
getJobTypesFromModelFilter,
getModelFilterFromJobTypes,
jobModelFilterOptions,
} from "../../utils/jobModelFilters";

interface PreferencesFormProps {
searchPreferences: SearchPreferences;
Expand All @@ -13,6 +18,10 @@ export function PreferencesForm({
isSaving = false,
onSave,
}: PreferencesFormProps) {
const preferredModelFilter = getModelFilterFromJobTypes(
searchPreferences.jobTypes,
);

return (
<section className="rounded-2xl border border-border bg-card p-6 shadow-sm">
<h2 className="text-[18px] font-bold">Preferências de Carreira</h2>
Expand Down Expand Up @@ -43,24 +52,34 @@ export function PreferencesForm({
/>
</label>

<div className="flex flex-col justify-center gap-3">
<label className="flex cursor-pointer select-none items-start gap-3 text-xs font-bold text-muted-foreground">
<input
type="checkbox"
checked={searchPreferences.remoteOnly}
onChange={(event) =>
<div className="flex flex-col justify-center gap-4">
<label>
<span className="mb-2 block text-xs font-bold uppercase text-muted-foreground">
Vagas Exibidas Inicialmente
</span>
<select
value={preferredModelFilter}
onChange={(event) => {
const jobTypes = getJobTypesFromModelFilter(
event.target.value as JobModelFilter,
);
setSearchPreferences((current) => ({
...current,
remoteOnly: event.target.checked,
}))
}
className="mt-0.5 h-4 w-4 rounded accent-primary"
/>
<span>
<span className="block text-foreground">Apenas Oportunidades Remotas</span>
<span className="mt-1 block font-normal">
Esconde vagas híbridas ou presenciais da listagem principal.
</span>
jobTypes,
remoteOnly:
jobTypes.length === 1 && jobTypes[0] === "Remoto",
}));
}}
className="h-10 w-full rounded-lg border border-input bg-card px-4 text-sm outline-none focus:border-ring"
>
{jobModelFilterOptions.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
<span className="mt-2 block text-xs text-muted-foreground">
Define o filtro padrão aplicado automaticamente na tela de vagas.
</span>
</label>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ export const initialPreferences: SearchPreferences = {
keywords: ["React", "Frontend", "Fullstack"],
searchLocation: "São Paulo, SP",
remoteOnly: true,
jobTypes: ["Remoto"],
emailNotifications: true,
careerChecklist: [],
};
Loading
Loading