diff --git a/backend/package.json b/backend/package.json index 75f70a49..488e46d0 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,7 +1,7 @@ { "name": "backend", "version": "1.0.0", - "description": "Backend para scraping e API de vagas do LinkedIn", + "description": "Backend para o projeto ", "main": "index.ts", "scripts": { "start": "tsx src/server.ts", @@ -31,19 +31,19 @@ "axios", "cheerio" ], - "author": "Bene Tesla Dev", + "author": "Projeto Open Source - Bene Tesla Dev", "license": "ISC", "dependencies": { "argon2": "^0.44.0", - "axios": "^1.13.6", + "axios": "^1.17.0", "cheerio": "^1.2.0", "cors": "^2.8.6", - "dotenv": "^17.3.1", + "dotenv": "^17.4.2", "express": "^5.2.1", "iron-session": "^8.0.4", "pdfkit": "^0.18.0", "redis": "^5.12.1", - "swagger-jsdoc": "^6.2.8", + "swagger-jsdoc": "^6.3.0", "swagger-ui-express": "^5.0.1", "xlsx": "^0.18.5", "zod": "^4.4.3" @@ -52,13 +52,13 @@ "node": ">=22.0.0" }, "devDependencies": { - "@types/node": "^25.6.0", + "@types/node": "^25.9.1", "@types/pg": "^8.20.0", "@types/supertest": "^7.2.0", - "@vitest/coverage-v8": "^3.2.4", + "@vitest/coverage-v8": "^4.1.8", "nodemon": "^3.1.14", - "supertest": "^7.1.4", - "typescript": "^6.0.2", - "vitest": "^3.2.4" + "supertest": "^7.2.2", + "typescript": "^6.0.3", + "vitest": "^4.1.8" } } diff --git a/backend/tests/integration/routes/auth.routes.test.ts b/backend/tests/integration/routes/auth.routes.test.ts index 74f164c8..e964233a 100644 --- a/backend/tests/integration/routes/auth.routes.test.ts +++ b/backend/tests/integration/routes/auth.routes.test.ts @@ -9,7 +9,11 @@ const mockAuthService = vi.hoisted(() => ({ })); vi.mock("../../../src/modules/auth/auth.service", () => ({ - AuthService: vi.fn(() => mockAuthService), + AuthService: class { + constructor() { + return mockAuthService; + } + }, })); // ── CredentialsService mock ─────────────────────────────────────────────────── @@ -20,7 +24,11 @@ const mockCredentialsService = vi.hoisted(() => ({ })); vi.mock("../../../src/modules/auth/credentials.service", () => ({ - CredentialsService: vi.fn(() => mockCredentialsService), + CredentialsService: class { + constructor() { + return mockCredentialsService; + } + }, })); // ── iron-session ────────────────────────────────────────────────────────────── diff --git a/backend/tests/integration/routes/savedJobs.routes.test.ts b/backend/tests/integration/routes/savedJobs.routes.test.ts index fcd817ad..49e1cdc4 100644 --- a/backend/tests/integration/routes/savedJobs.routes.test.ts +++ b/backend/tests/integration/routes/savedJobs.routes.test.ts @@ -12,7 +12,11 @@ const mockSavedJobsService = vi.hoisted(() => ({ })); vi.mock("../../../src/modules/savedJobs/savedJobs.service", () => ({ - SavedJobsService: vi.fn(() => mockSavedJobsService), + SavedJobsService: class { + constructor() { + return mockSavedJobsService; + } + }, })); // ── iron-session ────────────────────────────────────────────────────────────── diff --git a/backend/tests/integration/routes/user.routes.test.ts b/backend/tests/integration/routes/user.routes.test.ts index 08188699..9311970a 100644 --- a/backend/tests/integration/routes/user.routes.test.ts +++ b/backend/tests/integration/routes/user.routes.test.ts @@ -12,7 +12,11 @@ const mockUsersService = vi.hoisted(() => ({ })); vi.mock("../../../src/modules/users/users.service", () => ({ - UsersService: vi.fn(() => mockUsersService), + UsersService: class { + constructor() { + return mockUsersService; + } + }, })); // ── iron-session ────────────────────────────────────────────────────────────── diff --git a/backend/tests/unit/middleware/errorHandler.test.ts b/backend/tests/unit/middleware/errorHandler.test.ts new file mode 100644 index 00000000..31e8ac61 --- /dev/null +++ b/backend/tests/unit/middleware/errorHandler.test.ts @@ -0,0 +1,47 @@ +import { describe, expect, it, vi } from "vitest"; +import { errorHandler } from "../../../src/middleware/errorHandler"; + +describe("errorHandler", () => { + it("deve retornar 403 para erro de CORS", () => { + const json = vi.fn(); + const status = vi.fn(() => ({ json })); + + const res = { + status, + }; + + errorHandler( + new Error("Origin not allowed by CORS"), + {} as any, + res as any, + vi.fn(), + ); + + expect(status).toHaveBeenCalledWith(403); + expect(json).toHaveBeenCalledWith({ + message: "Origem não permitida.", + }); + }); + + it("deve retornar 500 para erros genéricos", () => { + const json = vi.fn(); + const status = vi.fn(() => ({ json })); + + const res = { + status, + }; + + errorHandler( + new Error("Erro qualquer"), + {} as any, + res as any, + vi.fn(), + ); + + expect(status).toHaveBeenCalledWith(500); + expect(json).toHaveBeenCalledWith({ + message: "Erro interno.", + error: "Erro qualquer", + }); + }); +}); \ No newline at end of file diff --git a/backend/tests/unit/modules/savedJobs/savedJobs.controller.test.ts b/backend/tests/unit/modules/savedJobs/savedJobs.controller.test.ts new file mode 100644 index 00000000..4653d227 --- /dev/null +++ b/backend/tests/unit/modules/savedJobs/savedJobs.controller.test.ts @@ -0,0 +1,327 @@ + +import { getIronSession } from "iron-session"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { SavedJobsController } from "../../../../src/modules/savedJobs/savedJobs.controller"; + +vi.mock("iron-session", () => ({ + getIronSession: vi.fn(), +})); + +const mockService = { + getAll: vi.fn(), + getById: vi.fn(), + create: vi.fn(), + update: vi.fn(), + delete: vi.fn(), +}; + +function createMockResponse() { + const res = {} as any; + + res.status = vi.fn().mockReturnValue(res); + res.json = vi.fn().mockReturnValue(res); + res.send = vi.fn().mockReturnValue(res); + + return res; +} + +describe("SavedJobsController", () => { + let controller: SavedJobsController; + + beforeEach(() => { + vi.clearAllMocks(); + controller = new SavedJobsController(mockService as any); + }); + + describe("getAll", () => { + it("retorna 401 quando não autenticado", async () => { + (getIronSession as any).mockResolvedValue({}); + + const req = {} as any; + const res = createMockResponse(); + + await controller.getAll(req, res); + + expect(res.status).toHaveBeenCalledWith(401); + }); + + it("retorna vagas", async () => { + (getIronSession as any).mockResolvedValue({ + userId: "user-1", + }); + + mockService.getAll.mockResolvedValue([{ id: "1" }]); + + const req = {} as any; + const res = createMockResponse(); + + await controller.getAll(req, res); + + expect(mockService.getAll).toHaveBeenCalledWith("user-1"); + expect(res.json).toHaveBeenCalledWith([{ id: "1" }]); + }); + + it("retorna erro 500", async () => { + (getIronSession as any).mockResolvedValue({ + userId: "user-1", + }); + + mockService.getAll.mockRejectedValue( + new Error("database error"), + ); + + const req = {} as any; + const res = createMockResponse(); + + await controller.getAll(req, res); + + expect(res.status).toHaveBeenCalledWith(500); + }); + }); + + describe("getById", () => { + it("retorna 401", async () => { + (getIronSession as any).mockResolvedValue({}); + + const req = { + params: { id: "1" }, + } as any; + + const res = createMockResponse(); + + await controller.getById(req, res); + + expect(res.status).toHaveBeenCalledWith(401); + }); + + it("retorna 404", async () => { + (getIronSession as any).mockResolvedValue({ + userId: "user-1", + }); + + mockService.getById.mockResolvedValue(null); + + const req = { + params: { id: "1" }, + } as any; + + const res = createMockResponse(); + + await controller.getById(req, res); + + expect(res.status).toHaveBeenCalledWith(404); + }); + + it("retorna vaga", async () => { + (getIronSession as any).mockResolvedValue({ + userId: "user-1", + }); + + mockService.getById.mockResolvedValue({ + id: "1", + }); + + const req = { + params: { id: "1" }, + } as any; + + const res = createMockResponse(); + + await controller.getById(req, res); + + expect(res.json).toHaveBeenCalled(); + }); + }); + + describe("create", () => { + it("retorna 401", async () => { + (getIronSession as any).mockResolvedValue({}); + + const req = { + body: {}, + } as any; + + const res = createMockResponse(); + + await controller.create(req, res); + + expect(res.status).toHaveBeenCalledWith(401); + }); + + it("retorna 400 quando body inválido", async () => { + (getIronSession as any).mockResolvedValue({ + userId: "user-1", + }); + + const req = { + body: { + jobLink: "link-invalido", + }, + } as any; + + const res = createMockResponse(); + + await controller.create(req, res); + + expect(res.status).toHaveBeenCalledWith(400); + }); + + it("cria vaga", async () => { + (getIronSession as any).mockResolvedValue({ + userId: "user-1", + }); + + mockService.create.mockResolvedValue({ + id: "1", + }); + + const req = { + body: { + jobLink: "https://google.com", + }, + } as any; + + const res = createMockResponse(); + + await controller.create(req, res); + + expect(res.status).toHaveBeenCalledWith(201); + }); + + it("retorna 409 quando vaga já existe", async () => { + (getIronSession as any).mockResolvedValue({ + userId: "user-1", + }); + + mockService.create.mockRejectedValue( + new Error("Vaga já salva."), + ); + + const req = { + body: { + jobLink: "https://google.com", + }, + } as any; + + const res = createMockResponse(); + + await controller.create(req, res); + + expect(res.status).toHaveBeenCalledWith(409); + }); + }); + + describe("update", () => { + it("retorna 401", async () => { + (getIronSession as any).mockResolvedValue({}); + + const req = { + params: { id: "1" }, + } as any; + + const res = createMockResponse(); + + await controller.update(req, res); + + expect(res.status).toHaveBeenCalledWith(401); + }); + + it("retorna 400 para payload inválido", async () => { + (getIronSession as any).mockResolvedValue({ + userId: "user-1", + }); + + const req = { + params: { id: "1" }, + body: { + jobLink: "url inválida", + }, + } as any; + + const res = createMockResponse(); + + await controller.update(req, res); + + expect(res.status).toHaveBeenCalledWith(400); + }); + + it("atualiza vaga", async () => { + (getIronSession as any).mockResolvedValue({ + userId: "user-1", + }); + + mockService.update.mockResolvedValue({ + id: "1", + }); + + const req = { + params: { id: "1" }, + body: { + notes: "teste", + }, + } as any; + + const res = createMockResponse(); + + await controller.update(req, res); + + expect(res.json).toHaveBeenCalled(); + }); + }); + + describe("delete", () => { + it("retorna 401", async () => { + (getIronSession as any).mockResolvedValue({}); + + const req = { + params: { id: "1" }, + } as any; + + const res = createMockResponse(); + + await controller.delete(req, res); + + expect(res.status).toHaveBeenCalledWith(401); + }); + + it("remove vaga", async () => { + (getIronSession as any).mockResolvedValue({ + userId: "user-1", + }); + + mockService.delete.mockResolvedValue(undefined); + + const req = { + params: { id: "1" }, + } as any; + + const res = createMockResponse(); + + await controller.delete(req, res); + + expect(res.status).toHaveBeenCalledWith(204); + expect(res.send).toHaveBeenCalled(); + }); + + it("retorna erro desconhecido", async () => { + (getIronSession as any).mockResolvedValue({ + userId: "user-1", + }); + + mockService.delete.mockRejectedValue("erro"); + + const req = { + params: { id: "1" }, + } as any; + + const res = createMockResponse(); + + await controller.delete(req, res); + + expect(res.status).toHaveBeenCalledWith(500); + expect(res.json).toHaveBeenCalledWith({ + error: "Erro desconhecido", + }); + }); + }); +}); \ No newline at end of file diff --git a/backend/tests/unit/modules/users/user.controller.test.ts b/backend/tests/unit/modules/users/user.controller.test.ts new file mode 100644 index 00000000..707e714c --- /dev/null +++ b/backend/tests/unit/modules/users/user.controller.test.ts @@ -0,0 +1,272 @@ +import { getIronSession } from "iron-session"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { UsersController } from "../../../../src/modules/users/users.controller"; +import { UsersService } from "../../../../src/modules/users/users.service"; + +vi.mock("iron-session", () => ({ + getIronSession: vi.fn(), +})); + +describe("UsersController", () => { + let controller: UsersController; + let usersService: Partial; + let req: any; + let res: any; + + beforeEach(() => { + usersService = { + getUserById: vi.fn(), + updateProfile: vi.fn(), + getPreferences: vi.fn(), + createPreferences: vi.fn(), + updatePreferences: vi.fn(), + }; + + controller = new UsersController(usersService as UsersService); + + res = { + status: vi.fn().mockReturnThis(), + json: vi.fn().mockReturnThis(), + }; + + req = { + body: {}, + }; + + vi.clearAllMocks(); + }); + + const mockSession = (session: any) => { + vi.mocked(getIronSession).mockResolvedValue(session); + }; + + describe("getProfile", () => { + it("retorna 401 sem autenticação", async () => { + mockSession({}); + + await controller.getProfile(req, res); + + expect(res.status).toHaveBeenCalledWith(401); + }); + + it("retorna 404 quando usuário não existe", async () => { + mockSession({ userId: "1" }); + + vi.mocked(usersService.getUserById!).mockResolvedValue(null); + + await controller.getProfile(req, res); + + expect(res.status).toHaveBeenCalledWith(404); + }); + + it("retorna usuário", async () => { + const user = { id: "1", username: "bene" }; + + mockSession({ userId: "1" }); + + vi.mocked(usersService.getUserById!).mockResolvedValue(user); + + await controller.getProfile(req, res); + + expect(res.json).toHaveBeenCalledWith(user); + }); + + it("retorna erro 500", async () => { + mockSession({ userId: "1" }); + + vi.mocked(usersService.getUserById!).mockRejectedValue( + new Error("db error"), + ); + + await controller.getProfile(req, res); + + expect(res.status).toHaveBeenCalledWith(500); + }); + + it("retorna erro desconhecido", async () => { + mockSession({ userId: "1" }); + + vi.mocked(usersService.getUserById!).mockRejectedValue("erro"); + + await controller.getProfile(req, res); + + expect(res.status).toHaveBeenCalledWith(500); + expect(res.json).toHaveBeenCalledWith({ + error: "Erro desconhecido", + }); + }); + }); + + describe("updateProfile", () => { + it("retorna 400 para username inválido", async () => { + mockSession({ userId: "1" }); + + req.body = { + username: "@@@", + }; + + await controller.updateProfile(req, res); + + expect(res.status).toHaveBeenCalledWith(400); + }); + + it("atualiza perfil", async () => { + mockSession({ userId: "1" }); + + req.body = { + username: "bene_dev", + displayName: "Bene", + avatarUrl: "https://site.com/avatar.png", + }; + + const updated = { + id: "1", + ...req.body, + }; + + vi.mocked(usersService.updateProfile!).mockResolvedValue(updated); + + await controller.updateProfile(req, res); + + expect(usersService.updateProfile).toHaveBeenCalledWith( + "1", + req.body, + ); + + expect(res.json).toHaveBeenCalledWith(updated); + }); + + it("retorna erro do service", async () => { + mockSession({ userId: "1" }); + + req.body = { + username: "bene_dev", + }; + + vi.mocked(usersService.updateProfile!).mockRejectedValue( + new Error("update failed"), + ); + + await controller.updateProfile(req, res); + + expect(res.status).toHaveBeenCalledWith(500); + }); + }); + + describe("getPreferences", () => { + it("retorna 404 quando preferências não existem", async () => { + mockSession({ userId: "1" }); + + vi.mocked(usersService.getPreferences!).mockResolvedValue(null); + + await controller.getPreferences(req, res); + + expect(res.status).toHaveBeenCalledWith(404); + }); + + it("retorna preferências", async () => { + const prefs = { + keywords: ["java"], + }; + + mockSession({ userId: "1" }); + + vi.mocked(usersService.getPreferences!).mockResolvedValue(prefs); + + await controller.getPreferences(req, res); + + expect(res.json).toHaveBeenCalledWith(prefs); + }); + }); + + describe("createPreferences", () => { + it("retorna 401 sem sessão", async () => { + mockSession({}); + + await controller.createPreferences(req, res); + + expect(res.status).toHaveBeenCalledWith(401); + }); + + it("retorna 400 para payload inválido", async () => { + mockSession({ userId: "1" }); + + req.body = { + searchLanguage: "pt-br", + }; + + await controller.createPreferences(req, res); + + expect(res.status).toHaveBeenCalledWith(400); + }); + + it("cria preferências", async () => { + mockSession({ userId: "1" }); + + req.body = { + keywords: ["java", "spring"], + searchLanguage: "pt", + remoteOnly: true, + }; + + vi.mocked(usersService.createPreferences!).mockResolvedValue(req.body); + + await controller.createPreferences(req, res); + + expect(res.status).toHaveBeenCalledWith(201); + }); + }); + + describe("updatePreferences", () => { + it("retorna 400 para payload inválido", async () => { + mockSession({ userId: "1" }); + + req.body = { + keywords: Array(30).fill("java"), + }; + + await controller.updatePreferences(req, res); + + expect(res.status).toHaveBeenCalledWith(400); + }); + + it("atualiza preferências", async () => { + mockSession({ userId: "1" }); + + req.body = { + keywords: ["java"], + searchLanguage: "pt", + remoteOnly: true, + emailNotifications: true, + }; + + vi.mocked(usersService.updatePreferences!).mockResolvedValue(req.body); + + await controller.updatePreferences(req, res); + + expect(usersService.updatePreferences).toHaveBeenCalledWith( + "1", + req.body, + ); + + expect(res.json).toHaveBeenCalledWith(req.body); + }); + + it("retorna erro do service", async () => { + mockSession({ userId: "1" }); + + req.body = { + keywords: ["java"], + }; + + vi.mocked(usersService.updatePreferences!).mockRejectedValue( + new Error("update failed"), + ); + + await controller.updatePreferences(req, res); + + expect(res.status).toHaveBeenCalledWith(500); + }); + }); +}); \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index e344c337..710e8043 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -15,6 +15,7 @@ "dependencies": { "@fontsource-variable/geist": "^5.2.9", "@lottiefiles/dotlottie-react": "^0.19.4", + "@unpic/react": "^1.0.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "framer-motion": "^12.40.0", @@ -22,33 +23,34 @@ "lucide-react": "^0.577.0", "motion": "^12.40.0", "radix-ui": "^1.4.3", - "react": "^19.2.4", - "react-dom": "^19.2.4", + "react": "^19.2.7", + "react-dom": "^19.2.7", "react-icons": "^5.6.0", - "react-router-dom": "^7.15.1", - "rolldown": "^1.0.0-rc.10", - "shadcn": "^4.8.1", + "react-router-dom": "^7.17.0", + "rolldown": "^1.1.0", + "shadcn": "^4.10.0", + "swiper": "^12.2.0", "tailwind-merge": "^3.6.0", "tw-animate-css": "^1.4.0" }, "devDependencies": { "@eslint/js": "^9.39.4", - "@testing-library/jest-dom": "^6.6.3", - "@testing-library/react": "^16.1.0", - "@types/react": "^19.2.14", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@types/react": "^19.2.16", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^6.0.1", - "@vitest/coverage-v8": "^3.2.4", - "autoprefixer": "^10.4.27", + "@vitejs/plugin-react": "^6.0.2", + "@vitest/coverage-v8": "^4.1.8", + "autoprefixer": "^10.5.0", "eslint": "^9.39.4", - "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.2", - "globals": "^17.4.0", + "globals": "^17.6.0", "jsdom": "^25.0.1", - "postcss": "^8.5.8", - "tailwindcss": "^3.4.17", + "postcss": "^8.5.15", + "tailwindcss": "^3.4.19", "typescript": "^5.9.3", - "typescript-eslint": "^8.46.1", - "vite": "^8.0.1" + "typescript-eslint": "^8.60.1", + "vite": "^8.0.16" } } diff --git a/frontend/public/facebook.png b/frontend/public/facebook.png new file mode 100644 index 00000000..75796ac3 Binary files /dev/null and b/frontend/public/facebook.png differ diff --git a/frontend/public/google.png b/frontend/public/google.png new file mode 100644 index 00000000..494acede Binary files /dev/null and b/frontend/public/google.png differ diff --git a/frontend/public/leftSide.png b/frontend/public/leftSide.png new file mode 100644 index 00000000..db8e973f Binary files /dev/null and b/frontend/public/leftSide.png differ diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 00fc5917..57f06596 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,9 +1,11 @@ import { useState, useEffect } from "react"; import { Route, Routes } from "react-router-dom"; -import LandingPage from "./pages/LandingPage"; -import Dashboard from "./pages/Dashboard"; +import LandingPage from "./pages/dashboard/LandingPage"; +import Dashboard from "./pages/dashboard/Dashboard"; import NotFound from "./not_found"; import Loading from "./Loading"; +import LoginPage from "./pages/login/LoginPage"; +import RegisterPage from "./pages/register/RegisterPage"; function App() { const [appCarregando, setAppCarregando] = useState(true); @@ -24,6 +26,8 @@ function App() { } /> } /> + } /> + }/> } /> ); diff --git a/frontend/src/components/landing/CTASection.tsx b/frontend/src/components/landing/CTASection.tsx index 403eaa61..0436844d 100644 --- a/frontend/src/components/landing/CTASection.tsx +++ b/frontend/src/components/landing/CTASection.tsx @@ -1,21 +1,21 @@ import { motion } from "framer-motion"; import { Link } from "react-router-dom"; -import { ArrowRight, Sparkles } from "lucide-react"; +import { ArrowRight } from "lucide-react"; export function CTASection() { return ( -
+
-
-
-
+
+
+
@@ -27,11 +27,10 @@ export function CTASection() { whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} transition={{ duration: 0.7, delay: 0.1 }} - className="text-4xl md:text-6xl font-bold dark:text-white mb-6 leading-tight" - style={{ fontFamily: "'Space Grotesk', sans-serif" }} + className="text-4xl md:text-6xl font-extrabold text-gray-900 dark:text-white mb-6 leading-tight tracking-tight" > Pronto para transformar sua{" "} - + busca por vagas ? @@ -42,10 +41,15 @@ export function CTASection() { whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} transition={{ duration: 0.7, delay: 0.2 }} - className="text-lg md:text-xl text-gray-400 max-w-2xl mx-auto mb-10 leading-relaxed" + className="text-lg md:text-xl text-gray-500 dark:text-gray-400 max-w-2xl mx-auto mb-10 leading-relaxed font-medium" > - Junte-se a centenas de desenvolvedores que já estão usando o Jobs - Scraper para encontrar as melhores oportunidades de forma automatizada. + Junte-se a centenas de desenvolvedores que já estão usando o{" "} + + < + Cand!Date! + > + {" "} + para encontrar as melhores oportunidades de forma automatizada. Acessar o Dashboard + Explorar funcionalidades diff --git a/frontend/src/components/landing/FeaturesSection.tsx b/frontend/src/components/landing/FeaturesSection.tsx index 6a76ae14..e4a1e2e7 100644 --- a/frontend/src/components/landing/FeaturesSection.tsx +++ b/frontend/src/components/landing/FeaturesSection.tsx @@ -1,53 +1,121 @@ import { Globe, Filter, Zap, Brain, Download, LayoutDashboard } from "lucide-react"; -import { motion } from "framer-motion"; +import { motion, Variants } from "framer-motion"; const features = [ - { title: "Busca Global Automatizada", description: "Varremos LinkedIn, Indeed, Glassdoor e dezenas de outras plataformas em segundos.", icon: Globe }, - { title: "Filtros por Tecnologia", description: "React, Python, Go, AWS... Filtre por stack, senioridade e modelo de trabalho.", icon: Filter }, - { title: "Atualizações em Tempo Real", description: "Nosso scraper roda continuamente. Receba vagas novas assim que são publicadas.", icon: Zap }, - { title: "Análise com IA", description: "Inteligência artificial categoriza e ranqueia as vagas com base no seu perfil.", icon: Brain }, - { title: "Exportação de Dados", description: "Exporte suas vagas favoritas em XLSX, CSV ou PDF para compartilhar offline.", icon: Download }, - { title: "Dashboard Interativo", description: "Visualize tendências de mercado, salários e tecnologias mais demandadas.", icon: LayoutDashboard }, + { + title: "Busca Global Automatizada", + description: "Varremos LinkedIn, Indeed, Glassdoor e dezenas de outras plataformas em segundos.", + icon: Globe, + colorClass: "bg-blue-50 dark:bg-blue-950/30 text-blue-500 dark:text-blue-400 group-hover:ring-blue-500/30" + }, + { + title: "Filtros por Tecnologia", + description: "React, Python, Go, AWS... Filtre por stack, senioridade e modelo de trabalho.", + icon: Filter, + colorClass: "bg-amber-50 dark:bg-amber-950/30 text-amber-500 dark:text-amber-400 group-hover:ring-amber-500/30" + }, + { + title: "Atualizações em Tempo Real", + description: "Nosso scraper roda continuamente. Receba vagas novas assim que são publicadas.", + icon: Zap, + colorClass: "bg-purple-50 dark:bg-purple-950/30 text-purple-500 dark:text-purple-400 group-hover:ring-purple-500/30" + }, + { + title: "Análise com IA", + description: "Inteligência artificial categoriza e ranqueia as vagas com base no seu perfil.", + icon: Brain, + colorClass: "bg-blue-50 dark:bg-blue-950/30 text-blue-500 dark:text-blue-400 group-hover:ring-blue-500/30" + }, + { + title: "Exportação de Dados", + description: "Exporte suas vagas favoritas in XLSX, CSV ou PDF para compartilhar offline.", + icon: Download, + colorClass: "bg-amber-50 dark:bg-amber-950/30 text-amber-500 dark:text-amber-400 group-hover:ring-amber-500/30" + }, + { + title: "Dashboard Interativo", + description: "Visualize tendências de mercado, salários e tecnologias mais demandadas.", + icon: LayoutDashboard, + colorClass: "bg-purple-50 dark:bg-purple-950/30 text-purple-500 dark:text-purple-400 group-hover:ring-purple-500/30" + }, ]; +const containerVariants: Variants = { + hidden: {}, + visible: { + transition: { + staggerChildren: 0.1 + } + } +}; + +const cardVariants: Variants = { + hidden: { opacity: 0, y: 20 }, + visible: { opacity: 1, y: 0, transition: { duration: 0.5, ease: "easeOut" } } +}; + export function FeaturesSection() { return (
-
- {/* Section Header */} -
+
+
+ +
+ +
Tudo que você precisa para encontrar o emprego ideal Nossa plataforma foi construída especificamente para simplificar o mercado de tecnologia global.
- {/* Features Grid */} -
- {features.map((feat, index) => ( -
-
- -
-

{feat.title}

-

{feat.description}

-
- ))} -
+ + {features.map((feat, index) => { + const Icon = feat.icon; + return ( + +
+
+ +
+ +

+ {feat.title} +

+ +

+ {feat.description} +

+
+
+ ); + })} +
); diff --git a/frontend/src/components/landing/Footer.tsx b/frontend/src/components/landing/Footer.tsx index 6f88540f..b79194f0 100644 --- a/frontend/src/components/landing/Footer.tsx +++ b/frontend/src/components/landing/Footer.tsx @@ -9,17 +9,10 @@ export function Footer() {
-
- Painel de Vagas - Painel de Vagas +
+ < + Cand!Date! + >

diff --git a/frontend/src/components/landing/HeroSection.tsx b/frontend/src/components/landing/HeroSection.tsx index 72ce984f..61cacc55 100644 --- a/frontend/src/components/landing/HeroSection.tsx +++ b/frontend/src/components/landing/HeroSection.tsx @@ -32,7 +32,7 @@ function StarsBackground() { {STATIC_STARS.map((star) => ( - -

+ d +
A forma mais rápida de{" "} - + encontrar vagas {" "} de TI no mundo @@ -100,7 +100,9 @@ export function HeroSection() { transition={{ duration: 0.7, delay: 0.25 }} className="text-lg md:text-xl text-gray-500 dark:text-gray-400 max-w-2xl mx-auto mb-10 leading-relaxed font-medium" > - O Painel de Vagas varre dezenas de plataformas automaticamente, transforma candidatos em profissionais preparados e entrega as melhores oportunidades filtradas para você. + O < + Cand!Date! + > varre dezenas de plataformas automaticamente, transforma candidatos em profissionais preparados e entrega as melhores oportunidades filtradas para você. Comece Agora + - + Como funciona - + - - - - + {/* Slates para azul clarinho */} + {/* Indigo suave */} + {/* Azul sutil */} + {/* Roxo clarinho de base */} -
-

+

+

Usado por profissionais que passaram em empresas como :

@@ -150,7 +153,7 @@ export function HeroSection() { key={`${company.name}-${index}`} src={company.logo} alt={company.name} - className="h-8 md:h-10 w-32 md:w-40 object-contain" + className="h-8 md:h-10 w-32 md:w-40 object-contain dark:brightness-0 dark:invert opacity-70 dark:opacity-60 hover:opacity-100 transition-opacity" /> ))} diff --git a/frontend/src/components/landing/HowItWorks.tsx b/frontend/src/components/landing/HowItWorks.tsx index bb1920fd..c06f1065 100644 --- a/frontend/src/components/landing/HowItWorks.tsx +++ b/frontend/src/components/landing/HowItWorks.tsx @@ -7,42 +7,53 @@ const steps = [ title: "Configure seus filtros", description: "Defina suas tecnologias preferidas, nível de senioridade, modelo de trabalho e localização desejada.", icon: Settings, + themeClass: "text-blue-500 dark:text-blue-400", + bgClass: "group-hover:bg-blue-500/10 group-hover:border-blue-500/30", + dotClass: "border-blue-500 bg-blue-500" }, { number: "PASSO 02", title: "O robô busca por você", description: "Nosso scraper varre dezenas de plataformas automaticamente e coleta as vagas que combinam com seu perfil.", icon: Bot, + themeClass: "text-amber-500 dark:text-amber-400", + bgClass: "group-hover:bg-amber-500/10 group-hover:border-amber-500/30", + dotClass: "border-amber-500 bg-amber-500" }, { number: "PASSO 03", title: "Receba e candidate-se", description: "Visualize as vagas filtradas no dashboard, exporte relatórios e candidate-se com confiança.", icon: Rocket, + themeClass: "text-purple-500 dark:text-purple-400", + bgClass: "group-hover:bg-purple-500/10 group-hover:border-purple-500/30", + dotClass: "border-purple-500 bg-purple-500" }, ]; export function HowItWorks() { return (
-
+
+ +
- Como funciona ? + Como funciona? Em apenas 3 passos, você está pronto para receber as melhores vagas automaticamente. @@ -50,7 +61,7 @@ export function HowItWorks() {
-
+
{steps.map((step, idx) => ( -
- +
+
-
+
- + {step.number} -

+

{step.title}

-

+

{step.description}

diff --git a/frontend/src/components/landing/Navbar.tsx b/frontend/src/components/landing/Navbar.tsx index 4cf7757c..6c8808eb 100644 --- a/frontend/src/components/landing/Navbar.tsx +++ b/frontend/src/components/landing/Navbar.tsx @@ -13,40 +13,49 @@ const navLinks = [ name: "Funcionalidades", href: "/#features", sectionId: "features", - icon: Sparkles + icon: Sparkles, + activeColor: "text-blue-500 bg-blue-500/10 ring-blue-500/20 dark:text-blue-400" + }, + { + name: "Time", + href: "/#time", + sectionId: "time", + icon: Users2, + activeColor: "text-amber-500 bg-amber-500/10 ring-amber-500/20 dark:text-amber-400" }, { name: "Como Funciona", href: "/#how-it-works", sectionId: "how-it-works", - icon: Workflow + icon: Workflow, + activeColor: "text-purple-500 bg-purple-500/10 ring-purple-500/20 dark:text-purple-400" }, { name: "Pronto?", href: "/#status", - sectionId: "pronto", - icon: RadioTower + sectionId: "status", + icon: RadioTower, + activeColor: "text-blue-500 bg-blue-500/10 ring-blue-500/20 dark:text-blue-400" }, - { - name: "Time", - href: "/#time", - sectionId: "time", - icon: Users2, - } ]; export function Navbar() { const [isScrolledPastHero, setIsScrolledPastHero] = useState(false); const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); - const [activeSection, setActiveSection] = useState(navLinks[0].name); + const [activeSection, setActiveSection] = useState(""); const { resolvedTheme, toggleTheme } = useTheme(); - const isDark = resolvedTheme === "dark"; useEffect(() => { - const root = document.documentElement; + document.documentElement.style.scrollBehavior = "smooth"; + return () => { + document.documentElement.style.scrollBehavior = "auto"; + }; + }, []); + useEffect(() => { + const root = document.documentElement; if (isDark) { root.classList.add("dark"); } else { @@ -56,7 +65,7 @@ export function Navbar() { useEffect(() => { const handleScroll = () => - setIsScrolledPastHero(window.scrollY > window.innerHeight * 0.8); + setIsScrolledPastHero(window.scrollY > 20); window.addEventListener("scroll", handleScroll, { passive: true }); return () => window.removeEventListener("scroll", handleScroll); }, []); @@ -70,6 +79,11 @@ export function Navbar() { (acc, [id, ratio]) => (ratio > acc.ratio ? { id, ratio } : acc), { id: "", ratio: 0 } ); + + if (window.scrollY < 100) { + setActiveSection(""); + return; + } if (best.id) { const link = navLinks.find((l) => l.sectionId === best.id); if (link) setActiveSection(link.name); @@ -91,21 +105,41 @@ export function Navbar() { return () => observers.forEach((o) => o.disconnect()); }, []); + const handleNavLinkClick = (e: React.MouseEvent, sectionId: string, linkName: string) => { + const el = document.getElementById(sectionId); + if (el) { + e.preventDefault(); + el.scrollIntoView({ behavior: "smooth" }); + setActiveSection(linkName); + setIsMobileMenuOpen(false); + } + }; + return (