Skip to content

Commit 70a15e4

Browse files
committed
fix(PAV-85): corrige mensagem de filtro ativo nas vagas
1 parent 4c2a9e7 commit 70a15e4

12 files changed

Lines changed: 252 additions & 136 deletions

File tree

backend/src/lib/cache.ts

Lines changed: 65 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -197,13 +197,35 @@ export type CacheJobIndexFilters = {
197197
country?: string;
198198
state?: string;
199199
city?: string;
200-
type?: string;
201-
model?: string;
200+
type?: string | string[];
201+
model?: string | string[];
202202
contract?: string;
203203
};
204204

205-
export function cacheJobIndexKeys(filters: CacheJobIndexFilters): string[] {
206-
const entries: Array<[string, string | undefined]> = [
205+
function filterValues(value: string | string[] | undefined): string[] {
206+
const values = Array.isArray(value) ? value : [value];
207+
208+
return values
209+
.flatMap((item) => item?.split(",") ?? [])
210+
.map((item) => item.trim())
211+
.filter(Boolean);
212+
}
213+
214+
function cacheJobIndexKey(kind: string, value: string): string {
215+
const normalized =
216+
kind === "level"
217+
? normalizeLevelIndexValue(value)
218+
: normalizeIndexValue(value);
219+
220+
if (!normalized || normalized === "todos" || normalized === "all") {
221+
return "";
222+
}
223+
224+
return `scraper:jobs:${kind}:${normalized}`;
225+
}
226+
227+
function cacheJobIndexKeyGroups(filters: CacheJobIndexFilters): string[][] {
228+
const entries: Array<[string, string | string[] | undefined]> = [
207229
["level", filters.level],
208230
["location", filters.location],
209231
["continent", filters.continent],
@@ -215,52 +237,64 @@ export function cacheJobIndexKeys(filters: CacheJobIndexFilters): string[] {
215237
];
216238

217239
return entries
218-
.map(([kind, value]) => {
219-
const normalized =
220-
kind === "level"
221-
? normalizeLevelIndexValue(value ?? "")
222-
: normalizeIndexValue(value ?? "");
223-
if (!normalized || normalized === "todos" || normalized === "all") {
224-
return "";
225-
}
226-
return `scraper:jobs:${kind}:${normalized}`;
227-
})
228-
.filter(Boolean);
240+
.map(([kind, value]) =>
241+
filterValues(value)
242+
.map((item) => cacheJobIndexKey(kind, item))
243+
.filter(Boolean),
244+
)
245+
.filter((group) => group.length > 0);
246+
}
247+
248+
export function cacheJobIndexKeys(filters: CacheJobIndexFilters): string[] {
249+
return cacheJobIndexKeyGroups(filters).flatMap((group) => group);
229250
}
230251

231252
export async function cacheSearchJobIds(
232253
filters: CacheJobIndexFilters,
233254
): Promise<string[]> {
234255
const client = await getCache();
235256
const keywordKeys = keywordSearchKeys(filters.keywords ?? []);
236-
const filterKeys = cacheJobIndexKeys(filters);
257+
const tempKeys: string[] = [];
237258

238-
if (keywordKeys.length === 0 && filterKeys.length === 0) {
239-
return await client.sMembers("scraper:jobs:index");
240-
}
259+
const filterKeys = await Promise.all(
260+
cacheJobIndexKeyGroups(filters).map(async (group) => {
261+
if (group.length === 1) return group[0];
241262

242-
if (keywordKeys.length === 0) {
243-
if (filterKeys.length === 1) return await client.sMembers(filterKeys[0]);
244-
return (await client.sendCommand(["SINTER", ...filterKeys])) as string[];
245-
}
263+
const tempKey = `scraper:jobs:filter:${randomUUID()}`;
264+
tempKeys.push(tempKey);
265+
await client.sendCommand(["SUNIONSTORE", tempKey, ...group]);
266+
await client.expire(tempKey, 30);
267+
return tempKey;
268+
}),
269+
);
246270

247-
if (keywordKeys.length === 1) {
248-
const keys = [keywordKeys[0], ...filterKeys];
249-
if (keys.length === 1) return await client.sMembers(keys[0]);
250-
return (await client.sendCommand(["SINTER", ...keys])) as string[];
251-
}
271+
try {
272+
if (keywordKeys.length === 0 && filterKeys.length === 0) {
273+
return await client.sMembers("scraper:jobs:index");
274+
}
252275

253-
const tempKey = `scraper:jobs:search:${randomUUID()}`;
276+
if (keywordKeys.length === 0) {
277+
if (filterKeys.length === 1) return await client.sMembers(filterKeys[0]);
278+
return (await client.sendCommand(["SINTER", ...filterKeys])) as string[];
279+
}
280+
281+
if (keywordKeys.length === 1) {
282+
const keys = [keywordKeys[0], ...filterKeys];
283+
if (keys.length === 1) return await client.sMembers(keys[0]);
284+
return (await client.sendCommand(["SINTER", ...keys])) as string[];
285+
}
286+
287+
const tempKey = `scraper:jobs:search:${randomUUID()}`;
288+
tempKeys.push(tempKey);
254289

255-
try {
256290
await client.sendCommand(["SUNIONSTORE", tempKey, ...keywordKeys]);
257291
await client.expire(tempKey, 30);
258292

259293
const keys = [tempKey, ...filterKeys];
260294
if (keys.length === 1) return await client.sMembers(keys[0]);
261295
return (await client.sendCommand(["SINTER", ...keys])) as string[];
262296
} finally {
263-
await client.del(tempKey);
297+
await Promise.all(tempKeys.map((key) => client.del(key)));
264298
}
265299
}
266300

backend/src/routes/jobs.routes.ts

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,15 @@ function firstQueryValue(value: unknown): string {
3535
return typeof value === "string" ? value.trim() : "";
3636
}
3737

38+
function queryValues(value: unknown): string[] {
39+
const values = Array.isArray(value) ? value : [value];
40+
41+
return values
42+
.flatMap((item) => (typeof item === "string" ? item.split(",") : []))
43+
.map((item) => item.trim())
44+
.filter(Boolean);
45+
}
46+
3847
function normalizeComparable(value: string): string {
3948
return value
4049
.normalize("NFD")
@@ -209,16 +218,22 @@ function matchesLocationFilter(jobLocation: string, location: string): boolean {
209218
return normalizedLocation.includes(location);
210219
}
211220

221+
function getTypeFilters(query: Request["query"]): string[] {
222+
const rawTypes = queryValues(query.model).length > 0
223+
? queryValues(query.model)
224+
: queryValues(query.type);
225+
226+
return [...new Set(rawTypes.map(normalizeComparable).filter(Boolean))];
227+
}
228+
212229
function filterJobs(jobs: unknown[], query: Request["query"]): unknown[] {
213230
const level = normalizeLevelFilter(firstQueryValue(query.level));
214231
const location = normalizeComparable(
215232
firstQueryValue(query.country) || firstQueryValue(query.location),
216233
);
217-
const type = normalizeComparable(
218-
firstQueryValue(query.model) || firstQueryValue(query.type),
219-
);
234+
const types = getTypeFilters(query);
220235

221-
if (!level && !location && !type) return jobs;
236+
if (!level && !location && types.length === 0) return jobs;
222237

223238
return jobs.filter((job) => {
224239
const candidate = job as SearchJob;
@@ -227,7 +242,8 @@ function filterJobs(jobs: unknown[], query: Request["query"]): unknown[] {
227242

228243
const matchesLevel = !level || inferJobLevel(title) === level;
229244
const matchesLocation = matchesLocationFilter(jobLocation, location);
230-
const matchesType = !type || inferJobType(candidate) === type;
245+
const matchesType =
246+
types.length === 0 || types.includes(inferJobType(candidate));
231247

232248
return matchesLevel && matchesLocation && matchesType;
233249
});
@@ -387,8 +403,8 @@ jobsRoutes.get("/search", async (req: Request, res: Response) => {
387403
country: firstQueryValue(req.query.country),
388404
state: firstQueryValue(req.query.state),
389405
city: firstQueryValue(req.query.city),
390-
type: firstQueryValue(req.query.type),
391-
model: firstQueryValue(req.query.model),
406+
type: queryValues(req.query.type),
407+
model: queryValues(req.query.model),
392408
contract:
393409
firstQueryValue(req.query.contract) ||
394410
firstQueryValue(req.query.contractType) ||

backend/tests/unit/app.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -321,8 +321,8 @@ describe("jobsApiApp", () => {
321321
country: "",
322322
state: "",
323323
city: "",
324-
type: "Remoto",
325-
model: "",
324+
type: ["Remoto"],
325+
model: [],
326326
contract: "",
327327
});
328328
expect(mocks.cacheSearchKeywords).not.toHaveBeenCalled();

backend/tests/unit/libs/cache.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,33 @@ describe("Valkey Cache Lib", () => {
286286
);
287287
expect(result).toEqual(["job_1", "job_2"]);
288288
});
289+
290+
it("deve unir múltiplos modelos antes de intersectar com outros filtros", async () => {
291+
mockClientInstance.sendCommand
292+
.mockResolvedValueOnce(2)
293+
.mockResolvedValueOnce(["job_1", "job_2"]);
294+
295+
const result = await cacheSearchJobIds({
296+
model: ["Remoto", "Híbrido"],
297+
level: "Sênior",
298+
});
299+
300+
expect(mockClientInstance.sendCommand).toHaveBeenNthCalledWith(1, [
301+
"SUNIONSTORE",
302+
expect.stringMatching(/^scraper:jobs:filter:/),
303+
"scraper:jobs:model:remoto",
304+
"scraper:jobs:model:hibrido",
305+
]);
306+
expect(mockClientInstance.sendCommand).toHaveBeenNthCalledWith(2, [
307+
"SINTER",
308+
"scraper:jobs:level:senior",
309+
expect.stringMatching(/^scraper:jobs:filter:/),
310+
]);
311+
expect(mockClientInstance.del).toHaveBeenCalledWith(
312+
expect.stringMatching(/^scraper:jobs:filter:/),
313+
);
314+
expect(result).toEqual(["job_1", "job_2"]);
315+
});
289316
});
290317

291318
describe("cacheGetJobsByIds", () => {

frontend/src/domains/auth/presentation/components/LoginFormPanel.tsx

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,15 @@ export default function RightSide() {
168168
<span className="text-purple-500">!</span>
169169
<span className="text-blue-500 font-light">&gt;</span>
170170
</h2>
171-
171+
<p className="mt-4 text-sm text-gray-500 dark:text-neutral-400 font-medium">
172+
Novo por aqui?{" "}
173+
<a
174+
href="/register"
175+
className="font-semibold bg-gradient-to-r from-blue-600 to-purple-500 bg-clip-text text-transparent dark:from-blue-400 dark:to-purple-400 underline underline-offset-2 hover:opacity-80 transition-opacity"
176+
>
177+
Cadastre-se
178+
</a>
179+
</p>
172180
</div>
173181
</div>
174182
<form
@@ -274,15 +282,6 @@ export default function RightSide() {
274282
>
275283
{isLoading ? "Entrando..." : "Entrar"}
276284
</motion.button>
277-
<p className="mt-4 text-sm text-center text-gray-500 dark:text-neutral-400 font-medium">
278-
Novo por aqui?{" "}
279-
<a
280-
href="/register"
281-
className="font-semibold bg-gradient-to-r from-blue-600 to-purple-500 bg-clip-text text-transparent dark:from-blue-400 dark:to-purple-400 underline underline-offset-2 hover:opacity-80 transition-opacity"
282-
>
283-
Cadastre-se
284-
</a>
285-
</p>
286285
</form>
287286
<div className="relative z-10 w-full max-w-2xl mx-auto">
288287
<div className="relative mb-6">

frontend/src/domains/marketing/presentation/components/Footer.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { Github } from "lucide-react";
2+
import LogoWhite from "@/shared/assets/logo-painel-vagas.svg";
3+
import LogoBlack from "@/shared/assets/black.svg";
24

35
export function Footer() {
46
return (
@@ -26,7 +28,7 @@ export function Footer() {
2628
<ul className="space-y-4 text-gray-500 dark:text-gray-400">
2729
<li><a href="#features" className="hover:text-emerald-500 dark:hover:text-emerald-400 transition-colors">Funcionalidades</a></li>
2830
<li><a href="#how-it-works" className="hover:text-emerald-500 dark:hover:text-emerald-400 transition-colors">Como Funciona</a></li>
29-
{/* <li><a href="#pricing" className="hover:text-emerald-500 dark:hover:text-emerald-400 transition-colors">Preços</a></li> */}
31+
<li><a href="#pricing" className="hover:text-emerald-500 dark:hover:text-emerald-400 transition-colors">Preços</a></li>
3032
<li><a href="#status" className="hover:text-emerald-500 dark:hover:text-emerald-400 transition-colors">Status</a></li>
3133
</ul>
3234
</div>

frontend/src/domains/marketing/presentation/components/HeroSection.tsx

Lines changed: 60 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,73 @@
1-
import { motion } from "framer-motion";
1+
import { motion, useScroll, useTransform } from "framer-motion";
22
import { ArrowRight, Play } from "lucide-react";
33
import { useRef } from "react";
44
import { Link } from "react-router-dom";
55

6+
const STATIC_STARS = Array.from({ length: 80 }).map((_, i) => {
7+
const random = (min: number, max: number) => Math.random() * (max - min) + min;
8+
return {
9+
id: i,
10+
top: `${random(0, 100)}%`,
11+
left: `${random(0, 100)}%`,
12+
size: random(1, 3),
13+
delay: random(0, 5),
14+
duration: random(2, 6),
15+
};
16+
});
17+
18+
function StarsBackground() {
19+
return (
20+
<div className="absolute inset-0 overflow-hidden pointer-events-none">
21+
{STATIC_STARS.map((star) => (
22+
<motion.div
23+
key={star.id}
24+
className="absolute rounded-full bg-blue-900/30 dark:bg-white"
25+
style={{
26+
top: star.top,
27+
left: star.left,
28+
width: star.size,
29+
height: star.size,
30+
}}
31+
animate={{
32+
opacity: [0.2, 1, 0.2],
33+
scale: [1, 1.2, 1],
34+
}}
35+
transition={{
36+
duration: star.duration,
37+
repeat: Infinity,
38+
delay: star.delay,
39+
ease: "easeInOut",
40+
}}
41+
/>
42+
))}
43+
</div>
44+
);
45+
}
46+
647
export function HeroSection() {
748
const ref = useRef(null);
49+
const { scrollYProgress } = useScroll({
50+
target: ref,
51+
offset: ["start start", "end start"],
52+
});
53+
54+
const backgroundY = useTransform(scrollYProgress, [0, 1], ["0%", "50%"]);
55+
const textY = useTransform(scrollYProgress, [0, 1], ["0%", "80%"]);
856

957
return (
10-
<section
11-
ref={ref}
12-
className="relative min-h-[90vh] flex flex-col items-center justify-start pt-16 md:pt-24 pb-0 overflow-hidden bg-transparent font-sans"
13-
>
58+
<section ref={ref} className="relative min-h-[90vh] flex flex-col items-center justify-start pt-16 md:pt-24 pb-0 overflow-hidden bg-transparent font-sans">
1459
<motion.div
60+
style={{ y: backgroundY }}
1561
className="absolute inset-0 pointer-events-none w-full h-[150%]
1662
bg-[linear-gradient(to_right,rgba(0,0,0,0.03)_1px,transparent_1px),linear-gradient(to_bottom,rgba(0,0,0,0.03)_1px,transparent_1px)]
1763
dark:bg-[linear-gradient(to_right,rgba(255,255,255,0.02)_1px,transparent_1px),linear-gradient(to_bottom,rgba(255,255,255,0.02)_1px,transparent_1px)]
1864
bg-[size:60px_60px]"
19-
></motion.div>
65+
>
66+
<StarsBackground />
67+
</motion.div>
2068
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-[800px] h-[600px] bg-gradient-to-br from-blue-500/10 via-purple-500/5 to-transparent blur-[120px] rounded-full pointer-events-none" />
2169

22-
<motion.div className="relative z-10 max-w-5xl mx-auto px-6 text-center">
70+
<motion.div style={{ y: textY }} className="relative z-10 max-w-5xl mx-auto px-6 text-center">
2371
<motion.h1
2472
initial={{ opacity: 0, y: 30 }}
2573
animate={{ opacity: 1, y: 0 }}
@@ -40,11 +88,8 @@ export function HeroSection() {
4088
className="text-lg md:text-xl text-gray-500 dark:text-gray-400 max-w-2xl mx-auto mb-10 leading-relaxed font-medium"
4189
>
4290
O <span className="text-blue-500 font-light">&lt;</span>
43-
Cand<span className="text-amber-500">!</span>Date
44-
<span className="text-purple-500">!</span>
45-
<span className="text-blue-500 font-light">&gt;</span> varre dezenas
46-
de plataformas automaticamente, transforma candidatos em profissionais
47-
preparados e entrega as melhores oportunidades filtradas para você.
91+
Cand<span className="text-amber-500">!</span>Date<span className="text-purple-500">!</span>
92+
<span className="text-blue-500 font-light">&gt;</span> varre dezenas de plataformas automaticamente, transforma candidatos em profissionais preparados e entrega as melhores oportunidades filtradas para você.
4893
</motion.p>
4994

5095
<motion.div
@@ -70,6 +115,9 @@ export function HeroSection() {
70115
</a>
71116
</motion.div>
72117
</motion.div>
118+
119+
120+
73121
</section>
74122
);
75123
}

0 commit comments

Comments
 (0)