diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties
index 290eab3..38589ce 100644
--- a/backend/src/main/resources/application.properties
+++ b/backend/src/main/resources/application.properties
@@ -1,7 +1,7 @@
quarkus.http.port=8081
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.exposed-headers=Content-Disposition
quarkus.http.cors.access-control-max-age=24H
diff --git a/docker-compose.yml b/docker-compose.yml
index 0ecc5b0..8041ea5 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -76,7 +76,7 @@ services:
DB_USER: beetask
DB_PASSWORD: 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}
SMALLRYE_JWT_SIGN_KEY: ${SMALLRYE_JWT_SIGN_KEY}
MP_JWT_VERIFY_PUBLICKEY: ${MP_JWT_VERIFY_PUBLICKEY}
@@ -94,7 +94,7 @@ services:
labels:
- "traefik.enable=true"
- "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.services.backend.loadbalancer.server.port=8081"
@@ -125,7 +125,7 @@ services:
context: ./new-frontend
dockerfile: Dockerfile
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
restart: unless-stopped
expose:
diff --git a/new-frontend/package-lock.json b/new-frontend/package-lock.json
index fbe6116..e16dd0c 100644
--- a/new-frontend/package-lock.json
+++ b/new-frontend/package-lock.json
@@ -15,7 +15,8 @@
"framer-motion": "^14.1.0",
"lucide-react": "^1.55.0",
"react": "^19.2.8",
- "react-dom": "^19.2.8"
+ "react-dom": "^19.2.8",
+ "react-router-dom": "^7.18.4"
},
"devDependencies": {
"@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"
}
},
- "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": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/@react-stately/color/-/color-3.10.1.tgz",
@@ -2065,7 +2051,7 @@
"version": "19.3.0",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz",
"integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"csstype": "^3.2.2"
@@ -2075,7 +2061,7 @@
"version": "19.3.0",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz",
"integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"peerDependencies": {
"@types/react": "^19.3.0"
@@ -2290,6 +2276,19 @@
"integrity": "sha512-OXAPGFRNeLFnUfqDtloYdxkwsJoIdXe28+bjbpJiPqyei2HPa3VHmMCWa0Qe62+U4Ftf9Hj7hRssOkxz7WiWbg==",
"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": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
@@ -3036,6 +3035,44 @@
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"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": {
"version": "3.51.0",
"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==",
"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": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz",
@@ -3173,6 +3216,7 @@
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz",
"integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==",
+ "dev": true,
"license": "MIT"
},
"node_modules/tapable": {
diff --git a/new-frontend/package.json b/new-frontend/package.json
index 768b43b..7c536a8 100644
--- a/new-frontend/package.json
+++ b/new-frontend/package.json
@@ -17,7 +17,8 @@
"framer-motion": "^14.1.0",
"lucide-react": "^1.55.0",
"react": "^19.2.8",
- "react-dom": "^19.2.8"
+ "react-dom": "^19.2.8",
+ "react-router-dom": "^7.18.4"
},
"devDependencies": {
"@tailwindcss/vite": "^4.3.3",
diff --git a/new-frontend/src/App.tsx b/new-frontend/src/App.tsx
index 6f84862..b943950 100644
--- a/new-frontend/src/App.tsx
+++ b/new-frontend/src/App.tsx
@@ -1,30 +1,20 @@
-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"
+import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
+import { AuthProvider } from './contexts/AuthContext'
+import { LandingPage } from './pages/LandingPage'
+import { AuthPage } from './pages/AuthPage'
export function App() {
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+ } />
+ } />
+ } />
+ } />
+
+
+
)
}
diff --git a/new-frontend/src/components/landing/CtaSection.tsx b/new-frontend/src/components/landing/CtaSection.tsx
index a39618b..606dec2 100644
--- a/new-frontend/src/components/landing/CtaSection.tsx
+++ b/new-frontend/src/components/landing/CtaSection.tsx
@@ -1,15 +1,16 @@
import { useState, type FormEvent } from "react"
+import { useNavigate } from "react-router-dom"
import { Button } from "@heroui/react"
-import { ArrowRight, Sparkles, CheckCircle2, ShieldCheck } from "lucide-react"
+import { ArrowRight, Sparkles, ShieldCheck } from "lucide-react"
export const CtaSection = () => {
const [email, setEmail] = useState("")
- const [submitted, setSubmitted] = useState(false)
+ const navigate = useNavigate()
const handleSubmit = (e: FormEvent) => {
e.preventDefault()
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.
- {submitted ? (
-
-
-
- Workspace em preparação!
-
-
- Enviamos o link de ativação para {email}.
-
-
- ) : (
-
- )}
+ Criar Conta
+
+
+
diff --git a/new-frontend/src/components/landing/Hero.tsx b/new-frontend/src/components/landing/Hero.tsx
index 276d24b..9d3c656 100644
--- a/new-frontend/src/components/landing/Hero.tsx
+++ b/new-frontend/src/components/landing/Hero.tsx
@@ -1,4 +1,5 @@
import { useState, useEffect } from "react"
+import { Link } from "react-router-dom"
import {
Button,
Card,
@@ -80,12 +81,12 @@ export const Hero = () => {
{/* Action Buttons */}
diff --git a/new-frontend/src/contexts/AuthContext.tsx b/new-frontend/src/contexts/AuthContext.tsx
new file mode 100644
index 0000000..17b57ab
--- /dev/null
+++ b/new-frontend/src/contexts/AuthContext.tsx
@@ -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
+ register: (name: string, email: string, password: string) => Promise
+ logout: () => void
+ refreshUser: () => Promise
+ setActiveOrg: (id: number) => void
+}
+
+const STORAGE_KEYS = {
+ USER: 'user',
+ ACTIVE_ORG_ID: 'activeOrgId',
+} as const
+
+const AuthContext = createContext(undefined)
+
+export const AuthProvider = ({ children }: { children: ReactNode }) => {
+ const [user, setUser] = useState(null)
+ const [activeOrgId, setActiveOrgId] = useState(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 (
+
+ {children}
+
+ )
+}
+
+export const useAuth = () => {
+ const context = useContext(AuthContext)
+ if (!context) {
+ throw new Error('useAuth must be used within an AuthProvider')
+ }
+ return context
+}
diff --git a/new-frontend/src/lib/api.ts b/new-frontend/src/lib/api.ts
new file mode 100644
index 0000000..6a58420
--- /dev/null
+++ b/new-frontend/src/lib/api.ts
@@ -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 & {
+ body?: unknown
+ skipAuth?: boolean
+}
+
+export async function apiFetch(path: string, options: RequestOptions = {}): Promise {
+ const { body, headers, skipAuth, ...rest } = options
+
+ let authHeader: Record = {}
+ 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 = isFormData ? {} : { 'Content-Type': 'application/json' }
+
+ const response = await fetch(buildUrl(path), {
+ ...rest,
+ headers: {
+ ...defaultHeaders,
+ ...authHeader,
+ ...((headers as Record) || {}),
+ },
+ 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)
+}
diff --git a/new-frontend/src/pages/AuthPage.tsx b/new-frontend/src/pages/AuthPage.tsx
new file mode 100644
index 0000000..06d2ccf
--- /dev/null
+++ b/new-frontend/src/pages/AuthPage.tsx
@@ -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(null)
+ const [successMessage, setSuccessMessage] = useState(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 (
+
+ {/* Decorative ambient gradients */}
+
+
+ {/* Top back navigation */}
+
+
+
+ Voltar para a página inicial
+
+
+ v3.0
+
+
+
+ {/* Main Auth Card */}
+
+ {/* Card Header & Brand */}
+
+
+ 🐝
+
+
+
+
+ BeeTask
+
+
+ {mode === 'login'
+ ? 'Acesse seu painel com segurança e foco'
+ : 'Crie seu workspace corporativo em segundos'}
+
+
+
+ {/* Mode Switcher Tabs */}
+
+
+
+
+
+
+ {/* Feedback Alerts */}
+
+ {errorMessage && (
+
+
+
Atenção
+
{errorMessage}
+
+
+ )}
+
+ {successMessage && (
+
+
+
Sucesso
+
{successMessage}
+
+
+ )}
+
+ {/* Mode Form */}
+ {mode === 'login' ? (
+
+ ) : (
+
+ )}
+
+
+ {/* Footer Links & Info */}
+
+
+ {mode === 'login' ? (
+ <>
+ Ainda não tem conta no Bee Task?{' '}
+
+ >
+ ) : (
+ <>
+ Já possui uma conta ativa?{' '}
+
+ >
+ )}
+
+
+
+
+ Autenticação segura com JWT Quarkus & MinIO S3
+
+
+
+
+ )
+}
diff --git a/new-frontend/src/pages/LandingPage.tsx b/new-frontend/src/pages/LandingPage.tsx
new file mode 100644
index 0000000..3146ec6
--- /dev/null
+++ b/new-frontend/src/pages/LandingPage.tsx
@@ -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 (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/new-frontend/src/services/authService.ts b/new-frontend/src/services/authService.ts
new file mode 100644
index 0000000..74e6632
--- /dev/null
+++ b/new-frontend/src/services/authService.ts
@@ -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 {
+ return apiFetch('/auth/login', {
+ method: 'POST',
+ body: { email, password },
+ skipAuth: true,
+ })
+ },
+
+ async register(name: string, email: string, password: string): Promise {
+ return apiFetch('/auth/register', {
+ method: 'POST',
+ body: { name, email, password },
+ skipAuth: true,
+ })
+ },
+
+ async refresh(refreshToken: string): Promise {
+ return apiFetch('/auth/refresh', {
+ method: 'POST',
+ body: { refreshToken },
+ skipAuth: true,
+ })
+ },
+
+ async me(): Promise {
+ return apiFetch('/auth/me')
+ },
+}