Skip to content

Commit 5a3c965

Browse files
committed
feat: add the new landing page
1 parent 70305c5 commit 5a3c965

22 files changed

Lines changed: 10859 additions & 117 deletions

frontend/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@
1515
"dependencies": {
1616
"class-variance-authority": "^0.7.1",
1717
"clsx": "^2.1.1",
18+
"framer-motion": "^12.40.0",
1819
"lucide-react": "^0.577.0",
1920
"react": "^19.2.4",
2021
"react-dom": "^19.2.4",
2122
"react-icons": "^5.6.0",
23+
"react-router-dom": "^7.15.1",
2224
"rolldown": "^1.0.0-rc.10",
2325
"tailwind-merge": "^3.5.0"
2426
},

frontend/src/App.tsx

Lines changed: 7 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -1,113 +1,13 @@
1-
import { JobsFiltersCard } from "@/components/JobsFiltersCard";
2-
import { JobsHeaderCard } from "@/components/JobsHeaderCard";
3-
import { JobsTableCard } from "@/components/JobsTableCard";
4-
import { Button } from "@/components/ui/button";
5-
import { useJobsData } from "@/hooks/useJobsData";
6-
import { useJobsFiltering } from "@/hooks/useJobsFiltering";
7-
import { useJobsPagination } from "@/hooks/useJobsPagination";
8-
import type { JobsMeta } from "@/types/jobs";
9-
import { useCallback, type SetStateAction } from "react";
10-
import { FiRefreshCw } from "react-icons/fi";
11-
12-
function formatDate(timestamp: JobsMeta["modifiedAt"]): string {
13-
if (!timestamp) {
14-
return "-";
15-
}
16-
return new Date(timestamp).toLocaleString("pt-BR");
17-
}
1+
import { Route, Routes } from "react-router-dom";
2+
import LandingPage from "./pages/LandingPage";
3+
import Dashboard from "./pages/Dashboard";
184

195
function App() {
20-
const { files, selectedFile, setSelectedFile, jobs, meta, loading, scraping, error, triggerScraper } =
21-
useJobsData();
22-
23-
const { search, setSearch, keywordFilter, setKeywordFilter, keywords, filteredJobs } = useJobsFiltering(jobs);
24-
25-
const { currentPage, setCurrentPage, pageSize, setPageSize, resetPagination, totalPages, paginatedJobs } =
26-
useJobsPagination({
27-
filteredJobs,
28-
});
29-
30-
const handleSearchChange = useCallback(
31-
(value: SetStateAction<string>) => {
32-
setSearch((previous) => (typeof value === "function" ? value(previous) : value));
33-
resetPagination();
34-
},
35-
[setSearch, resetPagination],
36-
);
37-
38-
const handleKeywordFilterChange = useCallback(
39-
(value: SetStateAction<string[]>) => {
40-
setKeywordFilter((previous) => (typeof value === "function" ? value(previous) : value));
41-
resetPagination();
42-
},
43-
[setKeywordFilter, resetPagination],
44-
);
45-
46-
const handleSelectedFileChange = useCallback(
47-
(value: SetStateAction<string>) => {
48-
setSelectedFile((previous) => (typeof value === "function" ? value(previous) : value));
49-
resetPagination();
50-
},
51-
[setSelectedFile, resetPagination],
52-
);
53-
54-
const handlePageSizeChange = useCallback(
55-
(value: number) => {
56-
setPageSize(value);
57-
resetPagination();
58-
},
59-
[setPageSize, resetPagination],
60-
);
61-
62-
const handleScraper = useCallback(() => {
63-
void triggerScraper();
64-
}, [triggerScraper]);
65-
666
return (
67-
<main className="relative min-h-screen overflow-hidden bg-background px-4 transition-colors duration-300 md:px-8">
68-
<div className="pointer-events-none absolute inset-0 -z-10 bg-[radial-gradient(circle_at_20%_15%,rgba(236,195,117,0.35),transparent_30%),radial-gradient(circle_at_80%_10%,rgba(92,151,191,0.28),transparent_35%),radial-gradient(circle_at_50%_95%,rgba(201,120,99,0.22),transparent_40%)] dark:bg-[radial-gradient(circle_at_20%_15%,rgba(240,180,95,0.18),transparent_30%),radial-gradient(circle_at_80%_10%,rgba(92,151,191,0.18),transparent_35%),radial-gradient(circle_at_50%_95%,rgba(201,120,99,0.15),transparent_40%)]" />
69-
70-
<section className="mx-auto flex w-full flex-col gap-6">
71-
<JobsHeaderCard />
72-
73-
<JobsFiltersCard
74-
search={search}
75-
setSearch={handleSearchChange}
76-
keywordFilter={keywordFilter}
77-
setKeywordFilter={handleKeywordFilterChange}
78-
keywords={keywords}
79-
selectedFile={selectedFile}
80-
setSelectedFile={handleSelectedFileChange}
81-
files={files}
82-
meta={meta}
83-
actions={
84-
<>
85-
<Button
86-
onClick={handleScraper}
87-
disabled={scraping}
88-
className="h-12 md:h-14 w-full sm:w-auto rounded-xl md:rounded-2xl bg-[#0c6b35] px-6 text-base text-white shadow-sm hover:bg-[#0a5b2d] whitespace-nowrap flex items-center gap-2" >
89-
<FiRefreshCw className={`h-4 w-4 ${scraping ? "animate-spin" : ""}`} />
90-
{scraping ? "Buscando vagas..." : "Buscar vagas"}
91-
</Button>
92-
</>
93-
}
94-
/>
95-
96-
<JobsTableCard
97-
meta={meta}
98-
filteredJobs={filteredJobs}
99-
paginatedJobs={paginatedJobs}
100-
loading={loading || scraping}
101-
error={error}
102-
formatDate={formatDate}
103-
currentPage={currentPage}
104-
totalPages={totalPages}
105-
pageSize={pageSize}
106-
onPageChange={setCurrentPage}
107-
onPageSizeChange={handlePageSizeChange}
108-
/>
109-
</section>
110-
</main>
7+
<Routes>
8+
<Route path="/" element={<LandingPage />} />
9+
<Route path="/app" element={<Dashboard />} />
10+
</Routes>
11111
);
11212
}
11313

frontend/src/assets/amazon.png

91.6 KB
Loading

frontend/src/assets/google.png

43.4 KB
Loading

frontend/src/assets/meta.png

4.19 KB
Loading

frontend/src/assets/meta.svg

Lines changed: 25 additions & 0 deletions
Loading

frontend/src/assets/meta02.png

5.15 KB
Loading

frontend/src/assets/uber.png

102 KB
Loading
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { motion } from "framer-motion";
2+
import { Link } from "react-router-dom";
3+
import { ArrowRight, Sparkles } from "lucide-react";
4+
5+
export function CTASection() {
6+
return (
7+
<section className="relative py-24 md:py-32 åoverflow-hidden">
8+
<div className="absolute inset-0 pointer-events-none">
9+
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[800px] h-[400px] bg-emerald-500/10 blur-[150px] rounded-full" />
10+
<div className="absolute top-0 left-0 w-[400px] h-[400px] bg-emerald-900/20 blur-[120px] rounded-full" />
11+
<div className="absolute bottom-0 right-0 w-[400px] h-[400px] bg-emerald-900/20 blur-[120px] rounded-full" />
12+
</div>
13+
14+
<div
15+
className="absolute inset-0 opacity-[0.03] pointer-events-none"
16+
style={{
17+
backgroundImage:
18+
"linear-gradient(to right, #fff 1px, transparent 1px), linear-gradient(to bottom, #fff 1px, transparent 1px)",
19+
backgroundSize: "40px 40px",
20+
}}
21+
/>
22+
23+
<div className="relative z-10 max-w-4xl mx-auto px-6 text-center">
24+
<motion.div
25+
initial={{ opacity: 0, y: 30 }}
26+
whileInView={{ opacity: 1, y: 0 }}
27+
viewport={{ once: true }}
28+
transition={{ duration: 0.7 }}
29+
className="inline-flex items-center gap-2 bg-emerald-500/10 border border-emerald-500/20 text-emerald-400 text-sm font-medium px-4 py-2 rounded-full mb-8"
30+
>
31+
<Sparkles className="w-4 h-4" />
32+
Comece agora, é gratuito
33+
</motion.div>
34+
35+
<motion.h2
36+
initial={{ opacity: 0, y: 30 }}
37+
whileInView={{ opacity: 1, y: 0 }}
38+
viewport={{ once: true }}
39+
transition={{ duration: 0.7, delay: 0.1 }}
40+
className="text-4xl md:text-6xl font-bold dark:text-white mb-6 leading-tight"
41+
style={{ fontFamily: "'Space Grotesk', sans-serif" }}
42+
>
43+
Pronto para transformar sua{" "}
44+
<span className="bg-gradient-to-r from-emerald-400 to-green-300 bg-clip-text text-transparent">
45+
busca por vagas
46+
</span>
47+
?
48+
</motion.h2>
49+
50+
<motion.p
51+
initial={{ opacity: 0, y: 30 }}
52+
whileInView={{ opacity: 1, y: 0 }}
53+
viewport={{ once: true }}
54+
transition={{ duration: 0.7, delay: 0.2 }}
55+
className="text-lg md:text-xl text-gray-400 max-w-2xl mx-auto mb-10 leading-relaxed"
56+
>
57+
Junte-se a centenas de desenvolvedores que já estão usando o Jobs
58+
Scraper para encontrar as melhores oportunidades de forma automatizada.
59+
</motion.p>
60+
61+
<motion.div
62+
initial={{ opacity: 0, y: 30 }}
63+
whileInView={{ opacity: 1, y: 0 }}
64+
viewport={{ once: true }}
65+
transition={{ duration: 0.7, delay: 0.3 }}
66+
className="flex flex-col sm:flex-row items-center justify-center gap-4"
67+
>
68+
<Link
69+
to="/app"
70+
className="group inline-flex items-center gap-2 bg-emerald-500 hover:bg-emerald-400 text-gray-950 font-semibold text-base py-4 px-10 rounded-full transition-all duration-300 shadow-lg shadow-emerald-500/25 hover:shadow-xl hover:shadow-emerald-400/30 hover:scale-[1.02]"
71+
>
72+
Acessar o Dashboard
73+
<ArrowRight className="w-4 h-4 transition-transform group-hover:translate-x-1" />
74+
</Link>
75+
<a
76+
href="#features"
77+
className="inline-flex items-center gap-2 text-gray-400 hover:text-black dark:hover:text-white font-medium text-base py-4 px-8 rounded-full border border-gray-800 hover:border-gray-600 transition-all duration-300"
78+
>
79+
Explorar funcionalidades
80+
</a>
81+
</motion.div>
82+
</div>
83+
</section>
84+
);
85+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { Globe, Filter, Zap, Brain, Download, LayoutDashboard } from "lucide-react";
2+
import { motion } from "framer-motion";
3+
4+
const features = [
5+
{ title: "Busca Global Automatizada", description: "Varremos LinkedIn, Indeed, Glassdoor e dezenas de outras plataformas em segundos.", icon: Globe },
6+
{ title: "Filtros por Tecnologia", description: "React, Python, Go, AWS... Filtre por stack, senioridade e modelo de trabalho.", icon: Filter },
7+
{ title: "Atualizações em Tempo Real", description: "Nosso scraper roda continuamente. Receba vagas novas assim que são publicadas.", icon: Zap },
8+
{ title: "Análise com IA", description: "Inteligência artificial categoriza e ranqueia as vagas com base no seu perfil.", icon: Brain },
9+
{ title: "Exportação de Dados", description: "Exporte suas vagas favoritas em XLSX, CSV ou PDF para compartilhar offline.", icon: Download },
10+
{ title: "Dashboard Interativo", description: "Visualize tendências de mercado, salários e tecnologias mais demandadas.", icon: LayoutDashboard },
11+
];
12+
13+
export function FeaturesSection() {
14+
return (
15+
<section id="features" className="py-24 relative overflow-hidden bg-transparent">
16+
<div className="max-w-7xl mx-auto px-6">
17+
{/* Section Header */}
18+
<div className="text-center mb-16">
19+
<motion.h2
20+
initial={{ opacity: 0, y: 20 }}
21+
whileInView={{ opacity: 1, y: 0 }}
22+
viewport={{ once: true, margin: "-50px" }}
23+
transition={{ duration: 0.5 }}
24+
className="text-3xl md:text-4xl lg:text-5xl font-bold text-gray-900 dark:text-white mb-4"
25+
>
26+
Tudo que você precisa para encontrar o emprego ideal
27+
</motion.h2>
28+
<motion.p
29+
initial={{ opacity: 0, y: 20 }}
30+
whileInView={{ opacity: 1, y: 0 }}
31+
viewport={{ once: true, margin: "-50px" }}
32+
transition={{ duration: 0.5, delay: 0.1 }}
33+
className="text-gray-500 dark:text-gray-400 text-lg max-w-2xl mx-auto"
34+
>
35+
Nossa plataforma foi construída especificamente para simplificar o mercado de tecnologia global.
36+
</motion.p>
37+
</div>
38+
39+
{/* Features Grid */}
40+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
41+
{features.map((feat, index) => (
42+
<div key={index} className="p-6 rounded-2xl border border-gray-200 dark:border-neutral-800 bg-white/50 dark:bg-neutral-900/30 backdrop-blur-sm">
43+
<div className="w-10 h-10 rounded-xl bg-emerald-50 dark:bg-emerald-950/50 flex items-center justify-center mb-4">
44+
<feat.icon className="text-[#0c6b35] dark:text-emerald-400 w-5 h-5" />
45+
</div>
46+
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-2">{feat.title}</h3>
47+
<p className="text-gray-500 dark:text-gray-400 leading-relaxed text-sm">{feat.description}</p>
48+
</div>
49+
))}
50+
</div>
51+
</div>
52+
</section>
53+
);
54+
}

0 commit comments

Comments
 (0)