feat: add auth page with login/register and enable task3 CORS origin
This commit is contained in:
parent
727cf62f7d
commit
8015b3ff77
13 changed files with 856 additions and 95 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
quarkus.http.port=8081
|
quarkus.http.port=8081
|
||||||
|
|
||||||
quarkus.http.cors.enabled=true
|
quarkus.http.cors.enabled=true
|
||||||
quarkus.http.cors.origins=https://task2.beetech.dev
|
quarkus.http.cors.origins=https://task2.beetech.dev,https://task3.beetech.dev
|
||||||
quarkus.http.cors.methods=GET,PUT,POST,DELETE,PATCH,OPTIONS
|
quarkus.http.cors.methods=GET,PUT,POST,DELETE,PATCH,OPTIONS
|
||||||
quarkus.http.cors.exposed-headers=Content-Disposition
|
quarkus.http.cors.exposed-headers=Content-Disposition
|
||||||
quarkus.http.cors.access-control-max-age=24H
|
quarkus.http.cors.access-control-max-age=24H
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,7 @@ services:
|
||||||
DB_USER: beetask
|
DB_USER: beetask
|
||||||
DB_PASSWORD: beetask
|
DB_PASSWORD: beetask
|
||||||
DB_URL: jdbc:postgresql://postgres:5432/beetask
|
DB_URL: jdbc:postgresql://postgres:5432/beetask
|
||||||
QUARKUS_HTTP_CORS_ORIGINS: https://task2.beetech.dev
|
QUARKUS_HTTP_CORS_ORIGINS: "https://task2.beetech.dev,https://task3.beetech.dev"
|
||||||
MP_JWT_VERIFY_ISSUER: ${MP_JWT_VERIFY_ISSUER}
|
MP_JWT_VERIFY_ISSUER: ${MP_JWT_VERIFY_ISSUER}
|
||||||
SMALLRYE_JWT_SIGN_KEY: ${SMALLRYE_JWT_SIGN_KEY}
|
SMALLRYE_JWT_SIGN_KEY: ${SMALLRYE_JWT_SIGN_KEY}
|
||||||
MP_JWT_VERIFY_PUBLICKEY: ${MP_JWT_VERIFY_PUBLICKEY}
|
MP_JWT_VERIFY_PUBLICKEY: ${MP_JWT_VERIFY_PUBLICKEY}
|
||||||
|
|
@ -94,7 +94,7 @@ services:
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
- "traefik.docker.network=proxy"
|
- "traefik.docker.network=proxy"
|
||||||
- "traefik.http.routers.backend.rule=Host(`api-task2.beetech.dev`)"
|
- "traefik.http.routers.backend.rule=Host(`api-task2.beetech.dev`) || Host(`api-task3.beetech.dev`)"
|
||||||
- "traefik.http.routers.backend.entrypoints=web"
|
- "traefik.http.routers.backend.entrypoints=web"
|
||||||
- "traefik.http.services.backend.loadbalancer.server.port=8081"
|
- "traefik.http.services.backend.loadbalancer.server.port=8081"
|
||||||
|
|
||||||
|
|
@ -125,7 +125,7 @@ services:
|
||||||
context: ./new-frontend
|
context: ./new-frontend
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
args:
|
args:
|
||||||
VITE_API_URL: ${VITE_API_URL:-https://api-task3.beetech.dev}
|
VITE_API_URL: ${VITE_API_URL:-https://api-task2.beetech.dev}
|
||||||
container_name: bee-task-frontend2
|
container_name: bee-task-frontend2
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
expose:
|
expose:
|
||||||
|
|
|
||||||
80
new-frontend/package-lock.json
generated
80
new-frontend/package-lock.json
generated
|
|
@ -15,7 +15,8 @@
|
||||||
"framer-motion": "^14.1.0",
|
"framer-motion": "^14.1.0",
|
||||||
"lucide-react": "^1.55.0",
|
"lucide-react": "^1.55.0",
|
||||||
"react": "^19.2.8",
|
"react": "^19.2.8",
|
||||||
"react-dom": "^19.2.8"
|
"react-dom": "^19.2.8",
|
||||||
|
"react-router-dom": "^7.18.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/vite": "^4.3.3",
|
"@tailwindcss/vite": "^4.3.3",
|
||||||
|
|
@ -1074,21 +1075,6 @@
|
||||||
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@react-spectrum/provider": {
|
|
||||||
"version": "3.11.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@react-spectrum/provider/-/provider-3.11.1.tgz",
|
|
||||||
"integrity": "sha512-TsoNdVdmlQ7L+75ILq5Yb3+wp/I1AtIeat0o+Y+ZBxP+TtWpwT1ZtCB5l3cplFVzHzOpZlzO0VaDrDP9ElGYDw==",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@adobe/react-spectrum": "^3.47.0",
|
|
||||||
"@swc/helpers": "^0.5.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
|
|
||||||
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@react-stately/color": {
|
"node_modules/@react-stately/color": {
|
||||||
"version": "3.10.1",
|
"version": "3.10.1",
|
||||||
"resolved": "https://registry.npmjs.org/@react-stately/color/-/color-3.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/@react-stately/color/-/color-3.10.1.tgz",
|
||||||
|
|
@ -2065,7 +2051,7 @@
|
||||||
"version": "19.3.0",
|
"version": "19.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz",
|
||||||
"integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==",
|
"integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.2.2"
|
"csstype": "^3.2.2"
|
||||||
|
|
@ -2075,7 +2061,7 @@
|
||||||
"version": "19.3.0",
|
"version": "19.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz",
|
||||||
"integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==",
|
"integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^19.3.0"
|
"@types/react": "^19.3.0"
|
||||||
|
|
@ -2290,6 +2276,19 @@
|
||||||
"integrity": "sha512-OXAPGFRNeLFnUfqDtloYdxkwsJoIdXe28+bjbpJiPqyei2HPa3VHmMCWa0Qe62+U4Ftf9Hj7hRssOkxz7WiWbg==",
|
"integrity": "sha512-OXAPGFRNeLFnUfqDtloYdxkwsJoIdXe28+bjbpJiPqyei2HPa3VHmMCWa0Qe62+U4Ftf9Hj7hRssOkxz7WiWbg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/cookie": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/csstype": {
|
"node_modules/csstype": {
|
||||||
"version": "3.2.3",
|
"version": "3.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||||
|
|
@ -3036,6 +3035,44 @@
|
||||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/react-router": {
|
||||||
|
"version": "7.18.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.4.tgz",
|
||||||
|
"integrity": "sha512-PUPQcMhMGRAslLcvtlPz/kmzBEWPhLdgLFrL7pLNepBL6dX0lWj4WD2cUYVgYCuT3jxvghYFg81cDTj44DhetQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cookie": "^1.0.1",
|
||||||
|
"set-cookie-parser": "^2.6.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=18",
|
||||||
|
"react-dom": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-router-dom": {
|
||||||
|
"version": "7.18.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.4.tgz",
|
||||||
|
"integrity": "sha512-yrfmJHIpDG7taCpqKjT1G5B6q3O2K+RN8/fgNf0lTjCwiPbQ0ei6vXX9ZjQR+7ld8Tr7Z5xmyMnZ8YJrphWQUw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"react-router": "7.18.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=18",
|
||||||
|
"react-dom": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-stately": {
|
"node_modules/react-stately": {
|
||||||
"version": "3.51.0",
|
"version": "3.51.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-stately/-/react-stately-3.51.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-stately/-/react-stately-3.51.0.tgz",
|
||||||
|
|
@ -3118,6 +3155,12 @@
|
||||||
"integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==",
|
"integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/set-cookie-parser": {
|
||||||
|
"version": "2.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
|
||||||
|
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/simple-swizzle": {
|
"node_modules/simple-swizzle": {
|
||||||
"version": "0.2.4",
|
"version": "0.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz",
|
||||||
|
|
@ -3173,6 +3216,7 @@
|
||||||
"version": "4.3.3",
|
"version": "4.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz",
|
||||||
"integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==",
|
"integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/tapable": {
|
"node_modules/tapable": {
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,8 @@
|
||||||
"framer-motion": "^14.1.0",
|
"framer-motion": "^14.1.0",
|
||||||
"lucide-react": "^1.55.0",
|
"lucide-react": "^1.55.0",
|
||||||
"react": "^19.2.8",
|
"react": "^19.2.8",
|
||||||
"react-dom": "^19.2.8"
|
"react-dom": "^19.2.8",
|
||||||
|
"react-router-dom": "^7.18.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/vite": "^4.3.3",
|
"@tailwindcss/vite": "^4.3.3",
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,20 @@
|
||||||
import { Navbar } from "./components/landing/Navbar"
|
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
|
||||||
import { Hero } from "./components/landing/Hero"
|
import { AuthProvider } from './contexts/AuthContext'
|
||||||
import { Stats } from "./components/landing/Stats"
|
import { LandingPage } from './pages/LandingPage'
|
||||||
import { Features } from "./components/landing/Features"
|
import { AuthPage } from './pages/AuthPage'
|
||||||
import { FeatureTabs } from "./components/landing/FeatureTabs"
|
|
||||||
import { Workflow } from "./components/landing/Workflow"
|
|
||||||
import { Testimonials } from "./components/landing/Testimonials"
|
|
||||||
import { Faq } from "./components/landing/Faq"
|
|
||||||
import { CtaSection } from "./components/landing/CtaSection"
|
|
||||||
import { Footer } from "./components/landing/Footer"
|
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-app-bg text-text-main font-sans selection:bg-brand-soft selection:text-brand flex flex-col">
|
<BrowserRouter>
|
||||||
<Navbar />
|
<AuthProvider>
|
||||||
<main className="flex-1">
|
<Routes>
|
||||||
<Hero />
|
<Route path="/" element={<LandingPage />} />
|
||||||
<Stats />
|
<Route path="/login" element={<AuthPage />} />
|
||||||
<Features />
|
<Route path="/register" element={<AuthPage />} />
|
||||||
<FeatureTabs />
|
<Route path="*" element={<Navigate to="/" replace />} />
|
||||||
<Workflow />
|
</Routes>
|
||||||
<Testimonials />
|
</AuthProvider>
|
||||||
<Faq />
|
</BrowserRouter>
|
||||||
<CtaSection />
|
|
||||||
</main>
|
|
||||||
<Footer />
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,16 @@
|
||||||
import { useState, type FormEvent } from "react"
|
import { useState, type FormEvent } from "react"
|
||||||
|
import { useNavigate } from "react-router-dom"
|
||||||
import { Button } from "@heroui/react"
|
import { Button } from "@heroui/react"
|
||||||
import { ArrowRight, Sparkles, CheckCircle2, ShieldCheck } from "lucide-react"
|
import { ArrowRight, Sparkles, ShieldCheck } from "lucide-react"
|
||||||
|
|
||||||
export const CtaSection = () => {
|
export const CtaSection = () => {
|
||||||
const [email, setEmail] = useState("")
|
const [email, setEmail] = useState("")
|
||||||
const [submitted, setSubmitted] = useState(false)
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const handleSubmit = (e: FormEvent) => {
|
const handleSubmit = (e: FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (email) {
|
if (email) {
|
||||||
setSubmitted(true)
|
navigate(`/register?mode=register&email=${encodeURIComponent(email)}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -36,38 +37,26 @@ export const CtaSection = () => {
|
||||||
organizar projetos e gerenciar seu squad com precisão cirúrgica.
|
organizar projetos e gerenciar seu squad com precisão cirúrgica.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{submitted ? (
|
<form
|
||||||
<div className="bg-white/15 backdrop-blur-md border border-white/20 p-6 rounded-2xl max-w-md mx-auto space-y-2">
|
onSubmit={handleSubmit}
|
||||||
<div className="flex items-center justify-center gap-2 text-white font-bold text-lg">
|
className="flex flex-col sm:flex-row items-center justify-center gap-3 max-w-md mx-auto pt-2"
|
||||||
<CheckCircle2 className="w-6 h-6 text-accent" />
|
>
|
||||||
Workspace em preparação!
|
<input
|
||||||
</div>
|
type="email"
|
||||||
<p className="text-xs text-white/80">
|
required
|
||||||
Enviamos o link de ativação para <strong>{email}</strong>.
|
placeholder="Seu melhor e-mail profissional"
|
||||||
</p>
|
value={email}
|
||||||
</div>
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
) : (
|
className="w-full sm:w-80 px-4 py-3 rounded-xl bg-white/10 backdrop-blur-md border border-white/20 text-white placeholder-white/60 focus:outline-none focus:ring-2 focus:ring-accent text-sm"
|
||||||
<form
|
/>
|
||||||
onSubmit={handleSubmit}
|
<Button
|
||||||
className="flex flex-col sm:flex-row items-center justify-center gap-3 max-w-md mx-auto pt-2"
|
type="submit"
|
||||||
|
className="w-full sm:w-auto bg-accent hover:bg-[#b45309] text-white font-bold text-sm px-6 py-3 rounded-xl shadow-lg shadow-black/20 flex items-center justify-center gap-2 transition-all duration-200"
|
||||||
>
|
>
|
||||||
<input
|
Criar Conta
|
||||||
type="email"
|
<ArrowRight className="w-4 h-4" />
|
||||||
required
|
</Button>
|
||||||
placeholder="Seu melhor e-mail profissional"
|
</form>
|
||||||
value={email}
|
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
|
||||||
className="w-full sm:w-80 px-4 py-3 rounded-xl bg-white/10 backdrop-blur-md border border-white/20 text-white placeholder-white/60 focus:outline-none focus:ring-2 focus:ring-accent text-sm"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
className="w-full sm:w-auto bg-accent hover:bg-[#b45309] text-white font-bold text-sm px-6 py-3 rounded-xl shadow-lg shadow-black/20 flex items-center justify-center gap-2 transition-all duration-200"
|
|
||||||
>
|
|
||||||
Criar Conta
|
|
||||||
<ArrowRight className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex flex-wrap items-center justify-center gap-x-6 gap-y-2 text-xs text-white/70 pt-2 font-medium">
|
<div className="flex flex-wrap items-center justify-center gap-x-6 gap-y-2 text-xs text-white/70 pt-2 font-medium">
|
||||||
<span className="flex items-center gap-1.5">
|
<span className="flex items-center gap-1.5">
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { useState, useEffect } from "react"
|
import { useState, useEffect } from "react"
|
||||||
|
import { Link } from "react-router-dom"
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
|
|
@ -80,12 +81,12 @@ export const Hero = () => {
|
||||||
|
|
||||||
{/* Action Buttons */}
|
{/* Action Buttons */}
|
||||||
<div className="flex flex-col sm:flex-row items-center justify-center lg:justify-start gap-4 pt-2">
|
<div className="flex flex-col sm:flex-row items-center justify-center lg:justify-start gap-4 pt-2">
|
||||||
<a href="#comecar" className="w-full sm:w-auto">
|
<Link to="/register" className="w-full sm:w-auto">
|
||||||
<Button className="w-full sm:w-auto bg-brand hover:bg-brand-strong text-white font-bold text-base px-7 py-3.5 rounded-xl shadow-xl shadow-brand/20 transition-all duration-200 hover:scale-[1.02] active:scale-[0.98] flex items-center justify-center gap-2.5">
|
<Button className="w-full sm:w-auto bg-brand hover:bg-brand-strong text-white font-bold text-base px-7 py-3.5 rounded-xl shadow-xl shadow-brand/20 transition-all duration-200 hover:scale-[1.02] active:scale-[0.98] flex items-center justify-center gap-2.5">
|
||||||
Criar Workspace Grátis
|
Criar Workspace Grátis
|
||||||
<ArrowRight className="w-4 h-4" />
|
<ArrowRight className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</a>
|
</Link>
|
||||||
<a href="#preview" className="w-full sm:w-auto">
|
<a href="#preview" className="w-full sm:w-auto">
|
||||||
<Button className="w-full sm:w-auto bg-surface hover:bg-surface-muted text-text-main font-semibold text-base px-6 py-3.5 rounded-xl border border-border-soft shadow-xs transition-colors duration-150 flex items-center justify-center gap-2">
|
<Button className="w-full sm:w-auto bg-surface hover:bg-surface-muted text-text-main font-semibold text-base px-6 py-3.5 rounded-xl border border-border-soft shadow-xs transition-colors duration-150 flex items-center justify-center gap-2">
|
||||||
<Play className="w-4 h-4 text-accent fill-accent" />
|
<Play className="w-4 h-4 text-accent fill-accent" />
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { Link } from "react-router-dom"
|
||||||
import { Button, Chip, ChipLabel } from "@heroui/react"
|
import { Button, Chip, ChipLabel } from "@heroui/react"
|
||||||
import { ArrowRight } from "lucide-react"
|
import { ArrowRight } from "lucide-react"
|
||||||
|
|
||||||
|
|
@ -6,7 +7,7 @@ export const Navbar = () => {
|
||||||
<header className="sticky top-0 z-50 backdrop-blur-md bg-surface/85 border-b border-border-soft transition-all duration-200">
|
<header className="sticky top-0 z-50 backdrop-blur-md bg-surface/85 border-b border-border-soft transition-all duration-200">
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-20 flex items-center justify-between">
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-20 flex items-center justify-between">
|
||||||
{/* Brand */}
|
{/* Brand */}
|
||||||
<div className="flex items-center gap-3">
|
<Link to="/" className="flex items-center gap-3">
|
||||||
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-accent to-brand flex items-center justify-center shadow-md shadow-accent/20">
|
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-accent to-brand flex items-center justify-center shadow-md shadow-accent/20">
|
||||||
<span className="text-xl select-none">🐝</span>
|
<span className="text-xl select-none">🐝</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -23,36 +24,36 @@ export const Navbar = () => {
|
||||||
by BeeTech Group
|
by BeeTech Group
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Link>
|
||||||
|
|
||||||
{/* Navigation Links */}
|
{/* Navigation Links */}
|
||||||
<nav className="hidden md:flex items-center gap-8 text-sm font-medium text-text-muted">
|
<nav className="hidden md:flex items-center gap-8 text-sm font-medium text-text-muted">
|
||||||
<a
|
<a
|
||||||
href="#features"
|
href="/#features"
|
||||||
className="hover:text-brand transition-colors duration-150 flex items-center gap-1.5"
|
className="hover:text-brand transition-colors duration-150 flex items-center gap-1.5"
|
||||||
>
|
>
|
||||||
Funcionalidades
|
Funcionalidades
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="#time-tracking"
|
href="/#time-tracking"
|
||||||
className="hover:text-brand transition-colors duration-150"
|
className="hover:text-brand transition-colors duration-150"
|
||||||
>
|
>
|
||||||
Time Tracking
|
Time Tracking
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="#workspaces"
|
href="/#workspaces"
|
||||||
className="hover:text-brand transition-colors duration-150"
|
className="hover:text-brand transition-colors duration-150"
|
||||||
>
|
>
|
||||||
Workspaces
|
Workspaces
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="#analytics"
|
href="/#analytics"
|
||||||
className="hover:text-brand transition-colors duration-150"
|
className="hover:text-brand transition-colors duration-150"
|
||||||
>
|
>
|
||||||
Analytics & Gestão
|
Analytics & Gestão
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="#faq"
|
href="/#faq"
|
||||||
className="hover:text-brand transition-colors duration-150"
|
className="hover:text-brand transition-colors duration-150"
|
||||||
>
|
>
|
||||||
FAQ
|
FAQ
|
||||||
|
|
@ -61,20 +62,20 @@ export const Navbar = () => {
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<a
|
<Link
|
||||||
href="#login"
|
to="/login"
|
||||||
className="text-sm font-semibold text-text-main hover:text-brand px-3 py-2 transition-colors"
|
className="text-sm font-semibold text-text-main hover:text-brand px-3 py-2 transition-colors"
|
||||||
>
|
>
|
||||||
Entrar
|
Entrar
|
||||||
</a>
|
</Link>
|
||||||
<a href="#comecar">
|
<Link to="/register">
|
||||||
<Button
|
<Button
|
||||||
className="bg-brand hover:bg-brand-strong text-white font-semibold text-sm px-5 py-2.5 rounded-xl shadow-lg shadow-brand/25 transition-all duration-200 hover:scale-[1.02] active:scale-[0.98] flex items-center gap-2"
|
className="bg-brand hover:bg-brand-strong text-white font-semibold text-sm px-5 py-2.5 rounded-xl shadow-lg shadow-brand/25 transition-all duration-200 hover:scale-[1.02] active:scale-[0.98] flex items-center gap-2"
|
||||||
>
|
>
|
||||||
Começar Agora
|
Começar Agora
|
||||||
<ArrowRight className="w-4 h-4" />
|
<ArrowRight className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</a>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
|
||||||
186
new-frontend/src/contexts/AuthContext.tsx
Normal file
186
new-frontend/src/contexts/AuthContext.tsx
Normal file
|
|
@ -0,0 +1,186 @@
|
||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
useContext,
|
||||||
|
useState,
|
||||||
|
useEffect,
|
||||||
|
useCallback,
|
||||||
|
useMemo,
|
||||||
|
type ReactNode,
|
||||||
|
} from 'react'
|
||||||
|
import {
|
||||||
|
authService,
|
||||||
|
type LoginResponse,
|
||||||
|
type CreateAccountResponse,
|
||||||
|
type OrganizationMembership,
|
||||||
|
} from '../services/authService'
|
||||||
|
|
||||||
|
interface AuthContextType {
|
||||||
|
user: LoginResponse | null
|
||||||
|
isAuthenticated: boolean
|
||||||
|
isLoading: boolean
|
||||||
|
activeOrg: OrganizationMembership | null
|
||||||
|
login: (email: string, password: string) => Promise<LoginResponse>
|
||||||
|
register: (name: string, email: string, password: string) => Promise<CreateAccountResponse>
|
||||||
|
logout: () => void
|
||||||
|
refreshUser: () => Promise<void>
|
||||||
|
setActiveOrg: (id: number) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const STORAGE_KEYS = {
|
||||||
|
USER: 'user',
|
||||||
|
ACTIVE_ORG_ID: 'activeOrgId',
|
||||||
|
} as const
|
||||||
|
|
||||||
|
const AuthContext = createContext<AuthContextType | undefined>(undefined)
|
||||||
|
|
||||||
|
export const AuthProvider = ({ children }: { children: ReactNode }) => {
|
||||||
|
const [user, setUser] = useState<LoginResponse | null>(null)
|
||||||
|
const [activeOrgId, setActiveOrgId] = useState<number | null>(null)
|
||||||
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
|
|
||||||
|
const setupActiveOrg = useCallback((organizations: OrganizationMembership[]) => {
|
||||||
|
const storedOrgId = localStorage.getItem(STORAGE_KEYS.ACTIVE_ORG_ID)
|
||||||
|
const parsedStoredId = storedOrgId ? Number(storedOrgId) : null
|
||||||
|
|
||||||
|
const isValidOrg = organizations.some((org) => org.id === parsedStoredId)
|
||||||
|
|
||||||
|
if (isValidOrg && parsedStoredId !== null) {
|
||||||
|
setActiveOrgId(parsedStoredId)
|
||||||
|
} else if (organizations.length > 0) {
|
||||||
|
const firstOrgId = organizations[0].id
|
||||||
|
setActiveOrgId(firstOrgId)
|
||||||
|
localStorage.setItem(STORAGE_KEYS.ACTIVE_ORG_ID, firstOrgId.toString())
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const logout = useCallback(() => {
|
||||||
|
localStorage.removeItem(STORAGE_KEYS.USER)
|
||||||
|
localStorage.removeItem(STORAGE_KEYS.ACTIVE_ORG_ID)
|
||||||
|
setUser(null)
|
||||||
|
setActiveOrgId(null)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const renewToken = useCallback(async (refreshToken: string) => {
|
||||||
|
try {
|
||||||
|
const response = await authService.refresh(refreshToken)
|
||||||
|
response.issuedAt = new Date().toISOString()
|
||||||
|
localStorage.setItem(STORAGE_KEYS.USER, JSON.stringify(response))
|
||||||
|
setUser(response)
|
||||||
|
} catch {
|
||||||
|
logout()
|
||||||
|
}
|
||||||
|
}, [logout])
|
||||||
|
|
||||||
|
const checkTokenExpiration = useCallback(
|
||||||
|
(userData: LoginResponse) => {
|
||||||
|
const issuedAt = new Date(userData.issuedAt).getTime()
|
||||||
|
const expiresAt = issuedAt + userData.expiresIn * 1000
|
||||||
|
const now = Date.now()
|
||||||
|
|
||||||
|
if (now >= expiresAt) {
|
||||||
|
logout()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Se expirar em menos de 5 min, renova
|
||||||
|
if (expiresAt - now < 300000) {
|
||||||
|
renewToken(userData.refreshToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
[logout, renewToken]
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const storedUser = localStorage.getItem(STORAGE_KEYS.USER)
|
||||||
|
if (storedUser) {
|
||||||
|
try {
|
||||||
|
const userData: LoginResponse = JSON.parse(storedUser)
|
||||||
|
if (checkTokenExpiration(userData)) {
|
||||||
|
setUser(userData)
|
||||||
|
setupActiveOrg(userData.organizations)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
localStorage.removeItem(STORAGE_KEYS.USER)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setIsLoading(false)
|
||||||
|
}, [checkTokenExpiration, setupActiveOrg])
|
||||||
|
|
||||||
|
// Verificação periódica a cada 60 segundos
|
||||||
|
useEffect(() => {
|
||||||
|
if (!user) return
|
||||||
|
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
checkTokenExpiration(user)
|
||||||
|
}, 60000)
|
||||||
|
|
||||||
|
return () => clearInterval(interval)
|
||||||
|
}, [user, checkTokenExpiration])
|
||||||
|
|
||||||
|
const login = async (email: string, password: string) => {
|
||||||
|
const response = await authService.login(email, password)
|
||||||
|
response.issuedAt = new Date().toISOString()
|
||||||
|
localStorage.setItem(STORAGE_KEYS.USER, JSON.stringify(response))
|
||||||
|
setUser(response)
|
||||||
|
setupActiveOrg(response.organizations)
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
const register = async (name: string, email: string, password: string) => {
|
||||||
|
return authService.register(name, email, password)
|
||||||
|
}
|
||||||
|
|
||||||
|
const refreshUser = async () => {
|
||||||
|
try {
|
||||||
|
const response = await authService.me()
|
||||||
|
const storedUser = localStorage.getItem(STORAGE_KEYS.USER)
|
||||||
|
if (storedUser) {
|
||||||
|
const userData = JSON.parse(storedUser)
|
||||||
|
const updatedUser = { ...userData, ...response }
|
||||||
|
localStorage.setItem(STORAGE_KEYS.USER, JSON.stringify(updatedUser))
|
||||||
|
setUser(updatedUser)
|
||||||
|
setupActiveOrg(updatedUser.organizations)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignora erro
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const setActiveOrg = (id: number) => {
|
||||||
|
setActiveOrgId(id)
|
||||||
|
localStorage.setItem(STORAGE_KEYS.ACTIVE_ORG_ID, id.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeOrg = useMemo(
|
||||||
|
() => user?.organizations.find((org) => org.id === activeOrgId) || null,
|
||||||
|
[user, activeOrgId]
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthContext.Provider
|
||||||
|
value={{
|
||||||
|
user,
|
||||||
|
isAuthenticated: !!user,
|
||||||
|
isLoading,
|
||||||
|
activeOrg,
|
||||||
|
login,
|
||||||
|
register,
|
||||||
|
logout,
|
||||||
|
refreshUser,
|
||||||
|
setActiveOrg,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</AuthContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useAuth = () => {
|
||||||
|
const context = useContext(AuthContext)
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useAuth must be used within an AuthProvider')
|
||||||
|
}
|
||||||
|
return context
|
||||||
|
}
|
||||||
57
new-frontend/src/lib/api.ts
Normal file
57
new-frontend/src/lib/api.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
const API_BASE_URL = (import.meta.env.VITE_API_URL as string | undefined) ?? 'http://localhost:8081'
|
||||||
|
|
||||||
|
function buildUrl(path: string) {
|
||||||
|
return `${API_BASE_URL}${path}`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RequestOptions = Omit<RequestInit, 'body'> & {
|
||||||
|
body?: unknown
|
||||||
|
skipAuth?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function apiFetch<T>(path: string, options: RequestOptions = {}): Promise<T> {
|
||||||
|
const { body, headers, skipAuth, ...rest } = options
|
||||||
|
|
||||||
|
let authHeader: Record<string, string> = {}
|
||||||
|
if (!skipAuth) {
|
||||||
|
const storedUser = localStorage.getItem('user')
|
||||||
|
if (storedUser) {
|
||||||
|
try {
|
||||||
|
const userData = JSON.parse(storedUser)
|
||||||
|
if (userData?.jwt) {
|
||||||
|
authHeader = { Authorization: `Bearer ${userData.jwt}` }
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignora erro de parsing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isFormData = body instanceof FormData
|
||||||
|
const defaultHeaders: Record<string, string> = isFormData ? {} : { 'Content-Type': 'application/json' }
|
||||||
|
|
||||||
|
const response = await fetch(buildUrl(path), {
|
||||||
|
...rest,
|
||||||
|
headers: {
|
||||||
|
...defaultHeaders,
|
||||||
|
...authHeader,
|
||||||
|
...((headers as Record<string, string>) || {}),
|
||||||
|
},
|
||||||
|
body: isFormData ? body : (body !== undefined && body !== null ? JSON.stringify(body) : undefined),
|
||||||
|
})
|
||||||
|
|
||||||
|
const text = await response.text()
|
||||||
|
if (!response.ok) {
|
||||||
|
let errorMessage = text || 'Não foi possível concluir a requisição.'
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(text)
|
||||||
|
if (parsed.message) errorMessage = parsed.message
|
||||||
|
if (parsed.error) errorMessage = parsed.error
|
||||||
|
} catch {
|
||||||
|
// Usar texto puro
|
||||||
|
}
|
||||||
|
throw new Error(errorMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
return text ? (JSON.parse(text) as T) : (undefined as T)
|
||||||
|
}
|
||||||
409
new-frontend/src/pages/AuthPage.tsx
Normal file
409
new-frontend/src/pages/AuthPage.tsx
Normal file
|
|
@ -0,0 +1,409 @@
|
||||||
|
import { useState, type FormEvent } from 'react'
|
||||||
|
import { Link, useNavigate, useSearchParams } from 'react-router-dom'
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardHeader,
|
||||||
|
CardContent,
|
||||||
|
CardFooter,
|
||||||
|
Button,
|
||||||
|
Input,
|
||||||
|
Chip,
|
||||||
|
ChipLabel,
|
||||||
|
Alert,
|
||||||
|
AlertTitle,
|
||||||
|
AlertDescription,
|
||||||
|
} from '@heroui/react'
|
||||||
|
import {
|
||||||
|
Mail,
|
||||||
|
Lock,
|
||||||
|
User,
|
||||||
|
ArrowRight,
|
||||||
|
Eye,
|
||||||
|
EyeOff,
|
||||||
|
ArrowLeft,
|
||||||
|
ShieldCheck,
|
||||||
|
} from 'lucide-react'
|
||||||
|
import { useAuth } from '../contexts/AuthContext'
|
||||||
|
|
||||||
|
export const AuthPage = () => {
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams()
|
||||||
|
const initialMode = searchParams.get('mode') === 'register' ? 'register' : 'login'
|
||||||
|
const initialEmail = searchParams.get('email') || ''
|
||||||
|
const [mode, setMode] = useState<'login' | 'register'>(initialMode)
|
||||||
|
|
||||||
|
// Form states
|
||||||
|
const [name, setName] = useState('')
|
||||||
|
const [email, setEmail] = useState(initialEmail)
|
||||||
|
const [password, setPassword] = useState('')
|
||||||
|
const [confirmPassword, setConfirmPassword] = useState('')
|
||||||
|
const [showPassword, setShowPassword] = useState(false)
|
||||||
|
|
||||||
|
// Status & UI
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
const [errorMessage, setErrorMessage] = useState<string | null>(null)
|
||||||
|
const [successMessage, setSuccessMessage] = useState<string | null>(null)
|
||||||
|
|
||||||
|
const { login, register } = useAuth()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
const switchMode = (newMode: 'login' | 'register') => {
|
||||||
|
setMode(newMode)
|
||||||
|
setSearchParams({ mode: newMode })
|
||||||
|
setErrorMessage(null)
|
||||||
|
setSuccessMessage(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLogin = async (e: FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setErrorMessage(null)
|
||||||
|
setIsLoading(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await login(email, password)
|
||||||
|
if (!response.organizations || response.organizations.length === 0) {
|
||||||
|
navigate('/organizations')
|
||||||
|
} else {
|
||||||
|
navigate('/dashboard')
|
||||||
|
}
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const msg = err instanceof Error ? err.message : 'Erro ao realizar login. Verifique suas credenciais.'
|
||||||
|
setErrorMessage(msg)
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRegister = async (e: FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setErrorMessage(null)
|
||||||
|
|
||||||
|
if (password.length < 6) {
|
||||||
|
setErrorMessage('A senha deve conter no mínimo 6 caracteres.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password !== confirmPassword) {
|
||||||
|
setErrorMessage('As senhas digitadas não coincidem.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await register(name, email, password)
|
||||||
|
setSuccessMessage('Conta criada com sucesso! Faça login para acessar seu workspace.')
|
||||||
|
setMode('login')
|
||||||
|
setSearchParams({ mode: 'login' })
|
||||||
|
setPassword('')
|
||||||
|
setConfirmPassword('')
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const msg = err instanceof Error ? err.message : 'Erro ao criar conta. Tente novamente.'
|
||||||
|
setErrorMessage(msg)
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-app-bg text-text-main flex flex-col justify-center items-center px-4 py-12 relative overflow-hidden font-sans">
|
||||||
|
{/* Decorative ambient gradients */}
|
||||||
|
<div className="absolute top-0 left-0 w-full h-full overflow-hidden pointer-events-none -z-10">
|
||||||
|
<div className="absolute -top-32 -left-32 w-96 h-96 bg-accent/15 blur-[120px] rounded-full" />
|
||||||
|
<div className="absolute -bottom-32 -right-32 w-96 h-96 bg-brand/15 blur-[120px] rounded-full" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Top back navigation */}
|
||||||
|
<div className="w-full max-w-md mb-6 flex items-center justify-between">
|
||||||
|
<Link
|
||||||
|
to="/"
|
||||||
|
className="inline-flex items-center gap-2 text-xs font-semibold text-text-muted hover:text-brand transition-colors p-2 rounded-xl hover:bg-surface-muted"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="w-4 h-4" />
|
||||||
|
Voltar para a página inicial
|
||||||
|
</Link>
|
||||||
|
<Chip className="bg-brand-soft text-brand text-xs font-semibold px-2 py-0.5 border border-brand/20">
|
||||||
|
<ChipLabel>v3.0</ChipLabel>
|
||||||
|
</Chip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Auth Card */}
|
||||||
|
<Card className="w-full max-w-md bg-surface border border-border-soft rounded-3xl p-6 sm:p-8 shadow-panel relative z-10 transition-all">
|
||||||
|
{/* Card Header & Brand */}
|
||||||
|
<CardHeader className="p-0 pb-6 flex flex-col items-center text-center space-y-3">
|
||||||
|
<div className="w-12 h-12 rounded-2xl bg-gradient-to-br from-accent to-brand flex items-center justify-center shadow-md shadow-accent/20">
|
||||||
|
<span className="text-2xl select-none">🐝</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl sm:text-3xl font-extrabold tracking-tight text-text-main">
|
||||||
|
Bee<span className="text-accent">Task</span>
|
||||||
|
</h1>
|
||||||
|
<p className="text-xs sm:text-sm text-text-muted mt-1">
|
||||||
|
{mode === 'login'
|
||||||
|
? 'Acesse seu painel com segurança e foco'
|
||||||
|
: 'Crie seu workspace corporativo em segundos'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mode Switcher Tabs */}
|
||||||
|
<div className="w-full grid grid-cols-2 p-1 bg-surface-muted rounded-xl border border-border-soft mt-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => switchMode('login')}
|
||||||
|
className={`py-2 text-xs sm:text-sm font-bold rounded-lg transition-all ${
|
||||||
|
mode === 'login'
|
||||||
|
? 'bg-surface text-brand shadow-xs border border-border-soft'
|
||||||
|
: 'text-text-muted hover:text-text-main'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Entrar
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => switchMode('register')}
|
||||||
|
className={`py-2 text-xs sm:text-sm font-bold rounded-lg transition-all ${
|
||||||
|
mode === 'register'
|
||||||
|
? 'bg-surface text-brand shadow-xs border border-border-soft'
|
||||||
|
: 'text-text-muted hover:text-text-main'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Criar Conta
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
{/* Feedback Alerts */}
|
||||||
|
<CardContent className="p-0 space-y-5">
|
||||||
|
{errorMessage && (
|
||||||
|
<Alert className="bg-danger-soft border border-danger/30 text-danger rounded-xl p-3 text-xs flex items-start gap-2">
|
||||||
|
<div className="flex-1">
|
||||||
|
<AlertTitle className="font-bold">Atenção</AlertTitle>
|
||||||
|
<AlertDescription className="text-xs mt-0.5">{errorMessage}</AlertDescription>
|
||||||
|
</div>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{successMessage && (
|
||||||
|
<Alert className="bg-success-soft border border-success/30 text-success rounded-xl p-3 text-xs flex items-start gap-2">
|
||||||
|
<div className="flex-1">
|
||||||
|
<AlertTitle className="font-bold">Sucesso</AlertTitle>
|
||||||
|
<AlertDescription className="text-xs mt-0.5">{successMessage}</AlertDescription>
|
||||||
|
</div>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Mode Form */}
|
||||||
|
{mode === 'login' ? (
|
||||||
|
<form onSubmit={handleLogin} className="space-y-4">
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<label className="text-xs font-bold text-text-main">
|
||||||
|
E-mail Profissional
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<div className="absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none text-text-muted">
|
||||||
|
<Mail className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
type="email"
|
||||||
|
required
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
placeholder="seu.email@empresa.com"
|
||||||
|
className="w-full pl-10 pr-4 py-2.5 bg-surface-muted/60 border border-border-soft rounded-xl text-sm text-text-main placeholder-text-muted/60 focus:outline-none focus:ring-2 focus:ring-brand/20 focus:border-brand transition-all"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<label className="text-xs font-bold text-text-main">
|
||||||
|
Senha de Acesso
|
||||||
|
</label>
|
||||||
|
<a
|
||||||
|
href="#esqueci"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
alert('Entre em contato com o administrador do seu Workspace para redefinir sua senha.')
|
||||||
|
}}
|
||||||
|
className="text-[11px] font-semibold text-brand hover:underline"
|
||||||
|
>
|
||||||
|
Esqueceu?
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div className="relative">
|
||||||
|
<div className="absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none text-text-muted">
|
||||||
|
<Lock className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
type={showPassword ? 'text' : 'password'}
|
||||||
|
required
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
placeholder="••••••••"
|
||||||
|
className="w-full pl-10 pr-10 py-2.5 bg-surface-muted/60 border border-border-soft rounded-xl text-sm text-text-main placeholder-text-muted/60 focus:outline-none focus:ring-2 focus:ring-brand/20 focus:border-brand transition-all font-mono"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
|
className="absolute inset-y-0 right-0 pr-3.5 flex items-center text-text-muted hover:text-text-main transition-colors"
|
||||||
|
>
|
||||||
|
{showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
isDisabled={isLoading}
|
||||||
|
className="w-full bg-brand hover:bg-brand-strong text-white font-bold text-sm py-3 rounded-xl shadow-lg shadow-brand/20 transition-all duration-200 flex items-center justify-center gap-2 mt-2"
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
Acessar Workspace
|
||||||
|
<ArrowRight className="w-4 h-4" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
) : (
|
||||||
|
<form onSubmit={handleRegister} className="space-y-4">
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<label className="text-xs font-bold text-text-main">
|
||||||
|
Nome Completo
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<div className="absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none text-text-muted">
|
||||||
|
<User className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
placeholder="Ex: Carlos Oliveira"
|
||||||
|
className="w-full pl-10 pr-4 py-2.5 bg-surface-muted/60 border border-border-soft rounded-xl text-sm text-text-main placeholder-text-muted/60 focus:outline-none focus:ring-2 focus:ring-brand/20 focus:border-brand transition-all"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<label className="text-xs font-bold text-text-main">
|
||||||
|
E-mail Corporativo
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<div className="absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none text-text-muted">
|
||||||
|
<Mail className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
type="email"
|
||||||
|
required
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
placeholder="seu.email@empresa.com"
|
||||||
|
className="w-full pl-10 pr-4 py-2.5 bg-surface-muted/60 border border-border-soft rounded-xl text-sm text-text-main placeholder-text-muted/60 focus:outline-none focus:ring-2 focus:ring-brand/20 focus:border-brand transition-all"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<label className="text-xs font-bold text-text-main">
|
||||||
|
Crie uma Senha
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<div className="absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none text-text-muted">
|
||||||
|
<Lock className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
type={showPassword ? 'text' : 'password'}
|
||||||
|
required
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
placeholder="Mínimo 6 dígitos"
|
||||||
|
className="w-full pl-10 pr-10 py-2.5 bg-surface-muted/60 border border-border-soft rounded-xl text-sm text-text-main placeholder-text-muted/60 focus:outline-none focus:ring-2 focus:ring-brand/20 focus:border-brand transition-all font-mono"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
|
className="absolute inset-y-0 right-0 pr-3.5 flex items-center text-text-muted hover:text-text-main transition-colors"
|
||||||
|
>
|
||||||
|
{showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<label className="text-xs font-bold text-text-main">
|
||||||
|
Confirme sua Senha
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<div className="absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none text-text-muted">
|
||||||
|
<Lock className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
type={showPassword ? 'text' : 'password'}
|
||||||
|
required
|
||||||
|
value={confirmPassword}
|
||||||
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||||
|
placeholder="Repita a senha"
|
||||||
|
className="w-full pl-10 pr-4 py-2.5 bg-surface-muted/60 border border-border-soft rounded-xl text-sm text-text-main placeholder-text-muted/60 focus:outline-none focus:ring-2 focus:ring-brand/20 focus:border-brand transition-all font-mono"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
isDisabled={isLoading}
|
||||||
|
className="w-full bg-accent hover:bg-[#b45309] text-white font-bold text-sm py-3 rounded-xl shadow-lg shadow-accent/20 transition-all duration-200 flex items-center justify-center gap-2 mt-2"
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
Criar Minha Conta
|
||||||
|
<ArrowRight className="w-4 h-4" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
|
||||||
|
{/* Footer Links & Info */}
|
||||||
|
<CardFooter className="p-0 pt-6 mt-6 border-t border-border-soft flex flex-col items-center space-y-3 text-center">
|
||||||
|
<p className="text-xs text-text-muted">
|
||||||
|
{mode === 'login' ? (
|
||||||
|
<>
|
||||||
|
Ainda não tem conta no Bee Task?{' '}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => switchMode('register')}
|
||||||
|
className="font-bold text-brand hover:underline"
|
||||||
|
>
|
||||||
|
Cadastre-se grátis
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
Já possui uma conta ativa?{' '}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => switchMode('login')}
|
||||||
|
className="font-bold text-brand hover:underline"
|
||||||
|
>
|
||||||
|
Fazer login
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-center gap-2 text-[11px] text-text-muted font-medium pt-1">
|
||||||
|
<ShieldCheck className="w-3.5 h-3.5 text-brand" />
|
||||||
|
<span>Autenticação segura com JWT Quarkus & MinIO S3</span>
|
||||||
|
</div>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
29
new-frontend/src/pages/LandingPage.tsx
Normal file
29
new-frontend/src/pages/LandingPage.tsx
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { Navbar } from "../components/landing/Navbar"
|
||||||
|
import { Hero } from "../components/landing/Hero"
|
||||||
|
import { Stats } from "../components/landing/Stats"
|
||||||
|
import { Features } from "../components/landing/Features"
|
||||||
|
import { FeatureTabs } from "../components/landing/FeatureTabs"
|
||||||
|
import { Workflow } from "../components/landing/Workflow"
|
||||||
|
import { Testimonials } from "../components/landing/Testimonials"
|
||||||
|
import { Faq } from "../components/landing/Faq"
|
||||||
|
import { CtaSection } from "../components/landing/CtaSection"
|
||||||
|
import { Footer } from "../components/landing/Footer"
|
||||||
|
|
||||||
|
export const LandingPage = () => {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-app-bg text-text-main font-sans selection:bg-brand-soft selection:text-brand flex flex-col">
|
||||||
|
<Navbar />
|
||||||
|
<main className="flex-1">
|
||||||
|
<Hero />
|
||||||
|
<Stats />
|
||||||
|
<Features />
|
||||||
|
<FeatureTabs />
|
||||||
|
<Workflow />
|
||||||
|
<Testimonials />
|
||||||
|
<Faq />
|
||||||
|
<CtaSection />
|
||||||
|
</main>
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
54
new-frontend/src/services/authService.ts
Normal file
54
new-frontend/src/services/authService.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
import { apiFetch } from '../lib/api'
|
||||||
|
|
||||||
|
export type OrganizationMembership = {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
roles: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LoginResponse = {
|
||||||
|
name: string
|
||||||
|
email: string
|
||||||
|
photo: string
|
||||||
|
jwt: string
|
||||||
|
refreshToken: string
|
||||||
|
expiresIn: number // seconds
|
||||||
|
issuedAt: string // ISO date
|
||||||
|
organizations: OrganizationMembership[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CreateAccountResponse = {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
email: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const authService = {
|
||||||
|
async login(email: string, password: string): Promise<LoginResponse> {
|
||||||
|
return apiFetch<LoginResponse>('/auth/login', {
|
||||||
|
method: 'POST',
|
||||||
|
body: { email, password },
|
||||||
|
skipAuth: true,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
async register(name: string, email: string, password: string): Promise<CreateAccountResponse> {
|
||||||
|
return apiFetch<CreateAccountResponse>('/auth/register', {
|
||||||
|
method: 'POST',
|
||||||
|
body: { name, email, password },
|
||||||
|
skipAuth: true,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
async refresh(refreshToken: string): Promise<LoginResponse> {
|
||||||
|
return apiFetch<LoginResponse>('/auth/refresh', {
|
||||||
|
method: 'POST',
|
||||||
|
body: { refreshToken },
|
||||||
|
skipAuth: true,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
async me(): Promise<LoginResponse> {
|
||||||
|
return apiFetch<LoginResponse>('/auth/me')
|
||||||
|
},
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue