-
Notifications
You must be signed in to change notification settings - Fork 28
feat: initialize Vagas Full application with Electron, React, and Nod… #20
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,4 +17,7 @@ coverage/ | |
| frontend/coverage/ | ||
| backend/coverage/ | ||
| *.lcov | ||
| .nyc_output/ | ||
| .nyc_output/ | ||
|
|
||
| # Dist-electron | ||
| dist-electron/ | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,13 +3,15 @@ import express from "express"; | |
| import { existsSync, mkdirSync, readdirSync, statSync } from "fs"; | ||
| import path from "path"; | ||
| import XLSX from "xlsx"; | ||
| import { run as runScraper } from "./app.js"; | ||
|
|
||
| /** | ||
| * @param {{ outputDir?: string }} [options] | ||
| */ | ||
| export function createJobsApiApp(options = {}) { | ||
| const outputDir = options.outputDir ?? path.resolve(process.cwd(), "output"); | ||
| const app = express(); | ||
| let activeScraperRun = null; | ||
|
|
||
| app.use(cors()); | ||
|
|
||
|
|
@@ -99,5 +101,33 @@ export function createJobsApiApp(options = {}) { | |
| } | ||
| }); | ||
|
|
||
| app.post("/api/scraper/run", async (_req, res) => { | ||
| if (activeScraperRun) { | ||
| return res.status(409).json({ | ||
| message: "O scraper ja esta em execucao.", | ||
| }); | ||
| } | ||
|
|
||
| try { | ||
| activeScraperRun = runScraper(); | ||
| await activeScraperRun; | ||
|
|
||
| const files = listXlsxFiles(); | ||
| return res.json({ | ||
| ok: true, | ||
| file: files[0]?.file ?? null, | ||
| modifiedAt: files[0]?.modifiedAt ?? null, | ||
| totalFiles: files.length, | ||
| }); | ||
|
Comment on lines
+104
to
+121
|
||
| } catch (error) { | ||
| return res.status(500).json({ | ||
| message: "Erro ao executar o scraper.", | ||
| error: error?.message || "Erro desconhecido", | ||
| }); | ||
| } finally { | ||
| activeScraperRun = null; | ||
| } | ||
| }); | ||
|
|
||
| return app; | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,11 +1,35 @@ | ||||||||||||||||||||||||||||||||||||||||
| import "dotenv/config"; | ||||||||||||||||||||||||||||||||||||||||
| import express from "express"; | ||||||||||||||||||||||||||||||||||||||||
| import { existsSync } from "fs"; | ||||||||||||||||||||||||||||||||||||||||
| import path from "path"; | ||||||||||||||||||||||||||||||||||||||||
| import { createJobsApiApp } from "./jobsApiApp.js"; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const PORT = Number(process.env.PORT || 3001); | ||||||||||||||||||||||||||||||||||||||||
| const app = createJobsApiApp({ | ||||||||||||||||||||||||||||||||||||||||
| outputDir: path.resolve(process.cwd(), "output"), | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| // When running inside Electron, main.js sets ELECTRON_OUTPUT_DIR to a | ||||||||||||||||||||||||||||||||||||||||
| // writable user-data path. Fall back to the CWD-relative folder for dev/CLI. | ||||||||||||||||||||||||||||||||||||||||
| const outputDir = process.env.ELECTRON_OUTPUT_DIR | ||||||||||||||||||||||||||||||||||||||||
| ? process.env.ELECTRON_OUTPUT_DIR | ||||||||||||||||||||||||||||||||||||||||
| : path.resolve(process.cwd(), "output"); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const app = createJobsApiApp({ outputDir }); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| // ── Electron static-file serving ─────────────────────────────────────────── | ||||||||||||||||||||||||||||||||||||||||
| // When ELECTRON_STATIC_DIR is set, serve the React production build so that | ||||||||||||||||||||||||||||||||||||||||
| // the Electron window can load everything from http://localhost:<PORT>. | ||||||||||||||||||||||||||||||||||||||||
| const staticDir = process.env.ELECTRON_STATIC_DIR; | ||||||||||||||||||||||||||||||||||||||||
| if (staticDir && existsSync(staticDir)) { | ||||||||||||||||||||||||||||||||||||||||
| app.use(express.static(staticDir)); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| // SPA catch-all: return index.html for any non-API route so that React | ||||||||||||||||||||||||||||||||||||||||
| // Router (if used in future) works correctly. | ||||||||||||||||||||||||||||||||||||||||
| app.use((req, res) => { | ||||||||||||||||||||||||||||||||||||||||
| if (req.path.startsWith("/api")) { | ||||||||||||||||||||||||||||||||||||||||
| return res.status(404).json({ error: "Rota não encontrada." }); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| res.sendFile(path.join(staticDir, "index.html")); | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+26
to
+31
|
||||||||||||||||||||||||||||||||||||||||
| app.use((req, res) => { | |
| if (req.path.startsWith("/api")) { | |
| return res.status(404).json({ error: "Rota não encontrada." }); | |
| } | |
| res.sendFile(path.join(staticDir, "index.html")); | |
| }); | |
| app.get("*", (req, res) => { | |
| if (req.path.startsWith("/api")) { | |
| return res.status(404).json({ message: "Rota não encontrada." }); | |
| } | |
| res.sendFile(path.join(staticDir, "index.html")); | |
| }); | |
| app.head("*", (req, res) => { | |
| if (req.path.startsWith("/api")) { | |
| return res.status(404).json({ message: "Rota não encontrada." }); | |
| } | |
| res.sendFile(path.join(staticDir, "index.html")); | |
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| <!DOCTYPE html> | ||
| <html lang="pt-BR"> | ||
| <head> | ||
| <meta charset="UTF-8" /> | ||
| <!-- | ||
| Strict CSP: no inline scripts, no external resources. | ||
| All styling is inline CSS only (allowed by 'unsafe-inline' for style-src). | ||
| --> | ||
| <meta http-equiv="Content-Security-Policy" | ||
| content="default-src 'none'; style-src 'unsafe-inline'" /> | ||
| <title>Vagas Full – Carregando</title> | ||
| <style> | ||
| *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } | ||
|
|
||
| body { | ||
| display: flex; | ||
| align-items: center; | ||
| justify-content: center; | ||
| height: 100vh; | ||
| background: #0f172a; /* slate-900 */ | ||
| font-family: system-ui, -apple-system, "Segoe UI", sans-serif; | ||
| color: #f1f5f9; /* slate-100 */ | ||
| user-select: none; | ||
| -webkit-app-region: drag; /* makes frameless window draggable */ | ||
| } | ||
|
|
||
| .card { | ||
| display: flex; | ||
| flex-direction: column; | ||
| align-items: center; | ||
| gap: 20px; | ||
| padding: 40px 48px; | ||
| background: #1e293b; /* slate-800 */ | ||
| border: 1px solid #334155; /* slate-700 */ | ||
| border-radius: 16px; | ||
| width: 420px; | ||
| text-align: center; | ||
| } | ||
|
|
||
| .logo { | ||
| font-size: 26px; | ||
| font-weight: 700; | ||
| letter-spacing: -0.5px; | ||
| color: #38bdf8; /* sky-400 */ | ||
| } | ||
|
|
||
| .spinner { | ||
| width: 44px; | ||
| height: 44px; | ||
| border: 4px solid #334155; | ||
| border-top-color: #38bdf8; /* sky-400 */ | ||
| border-radius: 50%; | ||
| animation: spin 0.85s linear infinite; | ||
| } | ||
|
|
||
| @keyframes spin { | ||
| to { transform: rotate(360deg); } | ||
| } | ||
|
|
||
| .status { | ||
| font-size: 16px; | ||
| font-weight: 600; | ||
| color: #e2e8f0; /* slate-200 */ | ||
| } | ||
|
|
||
| .hint { | ||
| font-size: 13px; | ||
| color: #64748b; /* slate-500 */ | ||
| line-height: 1.5; | ||
| } | ||
| </style> | ||
| </head> | ||
| <body> | ||
| <div class="card"> | ||
| <div class="logo">Vagas Full</div> | ||
| <div class="spinner"></div> | ||
| <p class="status">Buscando vagas no LinkedIn...</p> | ||
| <p class="hint">Isso pode levar alguns instantes.<br>Aguarde enquanto os dados são coletados.</p> | ||
| </div> | ||
| </body> | ||
| </html> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
app.use(cors())com configuração padrão permite qualquer origem. Com a adição dePOST /api/scraper/run, isso facilita que qualquer site aberto no navegador do usuário acione o scraper se o servidor estiver acessível. Sugestão: configurar CORS de forma restritiva (por exemplo, condicionar por env no modo Electron e permitir apenas a origem esperada) e/ou adicionar algum mecanismo simples de autenticação/nonce para endpoints de execução.