Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ backend/coverage/

# Dist-electron
dist-electron/.sfdx/
pnpm-lock.yaml
7 changes: 7 additions & 0 deletions backend/drizzle/meta/_journal.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@
"when": 1779191875793,
"tag": "0003_hot_miek",
"breakpoints": true
},
{
"idx": 4,
"version": "7",
"when": 1780927293836,
"tag": "0004_small_psylocke",
"breakpoints": true
}
]
}
3 changes: 3 additions & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"cheerio": "^1.2.0",
"cors": "^2.8.6",
"dotenv": "^17.4.2",
"drizzle-orm": "^0.45.2",
"express": "^5.2.1",
"iron-session": "^8.0.4",
"pdfkit": "^0.18.0",
Expand All @@ -56,8 +57,10 @@
"@types/pg": "^8.20.0",
"@types/supertest": "^7.2.0",
"@vitest/coverage-v8": "^4.1.8",
"drizzle-kit": "^0.31.10",
"nodemon": "^3.1.14",
"supertest": "^7.2.2",
"tsx": "^4.22.4",
"typescript": "^6.0.3",
"vitest": "^4.1.8"
}
Expand Down
152 changes: 135 additions & 17 deletions frontend/src/components/login/RegisterSide.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import { useState, FormEvent } from "react";
import { Eye, EyeOff, ArrowLeft } from "lucide-react";
import { Eye, EyeOff, ArrowLeft, Github, Linkedin } from "lucide-react";
import { Image } from "@unpic/react";
import { motion } from "framer-motion";
import PhoneInput from "react-phone-number-input";

import { api } from "@/services/api";
import "react-phone-number-input/style.css";
import { useNavigate } from "react-router-dom";

interface ValidationError {
_errors?: string[];
}

const STATIC_STARS = Array.from({ length: 40 }).map((_, i) => {
const random = (min: number, max: number) => Math.random() * (max - min) + min;
Expand Down Expand Up @@ -47,6 +54,8 @@ function StarsBackground() {
}

export default function RegisterSide() {
const navigate = useNavigate();

const [showPassword, setShowPassword] = useState(false);
const [nome, setNome] = useState("");
const [email, setEmail] = useState("");
Expand All @@ -59,6 +68,8 @@ export default function RegisterSide() {
const [telefoneError, setTelefoneError] = useState("");
const [passwordError, setPasswordError] = useState("");
const [cpfError, setCpfError] = useState("");
const [globalError, setGlobalError] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false);

const handleRevealPassword = () => setShowPassword((prev) => !prev);

Expand All @@ -70,13 +81,44 @@ export default function RegisterSide() {
return `${digits.slice(0, 3)}.${digits.slice(3, 6)}.${digits.slice(6, 9)}-${digits.slice(9)}`;
};

const handleSubmit = (e: FormEvent) => {
const handleOAuthLogin = async (provider: "google" | "github" | "linkedin") => {
try {
setGlobalError("");
setIsSubmitting(true);

const response = await api.get(`/auth/${provider}/url`);

if (response.data?.url) {
window.location.href = response.data.url;
} else {
throw new Error("URL de redirecionamento não retornada pelo servidor.");
}
} catch (error: unknown) {
console.error("Erro ao iniciar fluxo OAuth:", error);

let apiError = `Não foi possível conectar ao provedor ${provider}.`;

// Verifica de forma segura se o erro contém um objeto response vindo do Axios
if (error && typeof error === "object" && "response" in error) {
const axiosError = error as { response?: { data?: { error?: string } } };
if (typeof axiosError.response?.data?.error === "string") {
apiError = axiosError.response.data.error;
}
}

setGlobalError(apiError);
setIsSubmitting(false);
}
};

const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
setNomeError("");
setEmailError("");
setTelefoneError("");
setPasswordError("");
setCpfError("");
setGlobalError("");

let isValid = true;

Expand All @@ -103,8 +145,57 @@ export default function RegisterSide() {
setCpfError("Por favor, insira um CPF válido."); isValid = false;
}

if (isValid) {
console.log("Cadastro válido!", { nome, email, telefone, password, cpf });
if (!isValid) return;

setIsSubmitting(true);

try {
const response = await api.post("/auth/register", {
name: nome,
email: email,
phone: telefone,
password: password,
cpf: cpf.replace(/\D/g, ""),
});

console.log("Cadastro efetuado com sucesso!", response.data);
navigate("/app");

} catch (error: unknown) {
console.error("Erro na requisição de cadastro:", error);
setIsSubmitting(false);

if (error && typeof error === "object" && "response" in error) {
const axiosError = error as {
response?: {
data?: {
error?: string | Record<string, ValidationError>;
message?: string;
};
};
};

const data = axiosError.response?.data;

if (data && typeof data.error === "object") {
const validationErrors = data.error as Record<string, ValidationError>;

if (validationErrors.name?._errors?.[0]) setNomeError(validationErrors.name._errors[0]);
if (validationErrors.email?._errors?.[0]) setEmailError(validationErrors.email._errors[0]);
if (validationErrors.phone?._errors?.[0]) setTelefoneError(validationErrors.phone._errors[0]);
if (validationErrors.password?._errors?.[0]) setPasswordError(validationErrors.password._errors[0]);
if (validationErrors.cpf?._errors?.[0]) setCpfError(validationErrors.cpf._errors[0]);
} else {
const fallbackMsg = data?.message || (typeof data?.error === "string" ? data.error : "") || "Erro ao efetuar o cadastro.";
if (fallbackMsg.toLowerCase().includes("email")) {
setEmailError("Este endereço de e-mail já está em uso.");
} else {
setGlobalError(fallbackMsg);
}
}
} else {
setGlobalError("Ocorreu um erro inesperado de conexão.");
}
}
};

Expand Down Expand Up @@ -144,13 +235,20 @@ export default function RegisterSide() {

<form className="relative z-10 space-y-5 w-full max-w-2xl mx-auto my-8" onSubmit={handleSubmit}>

{globalError && (
<div className="p-3.5 bg-red-500/10 border border-red-500/30 text-red-500 rounded-xl text-sm font-semibold text-center backdrop-blur-sm">
{globalError}
</div>
)}

<div>
<label htmlFor="nome" className="block text-sm font-semibold text-gray-700 dark:text-neutral-300 mb-1.5">
Nome
</label>
<input
id="nome"
type="text"
disabled={isSubmitting}
value={nome}
onChange={(e) => setNome(e.target.value)}
placeholder="Bene Santos"
Expand All @@ -168,6 +266,7 @@ export default function RegisterSide() {
<input
id="email"
type="email"
disabled={isSubmitting}
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="lbene17@gmail.com"
Expand All @@ -188,6 +287,7 @@ export default function RegisterSide() {
<PhoneInput
international
defaultCountry="BR"
disabled={isSubmitting}
value={telefone}
onChange={setTelefone}
className="w-full px-4 py-3.5 text-gray-900 dark:text-white bg-transparent focus:outline-none phone-input-custom"
Expand All @@ -205,6 +305,7 @@ export default function RegisterSide() {
<input
id="senha"
type={showPassword ? "text" : "password"}
disabled={isSubmitting}
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Ex: ••••••••••••"
Expand All @@ -214,6 +315,7 @@ export default function RegisterSide() {
/>
<button
type="button"
disabled={isSubmitting}
onClick={handleRevealPassword}
className="absolute inset-y-0 right-0 pr-4 flex items-center text-gray-400 dark:text-neutral-500 hover:text-blue-500 dark:hover:text-blue-400 transition-colors"
>
Expand All @@ -230,6 +332,7 @@ export default function RegisterSide() {
<input
id="cpf"
type="text"
disabled={isSubmitting}
value={cpf}
onChange={(e) => setCpf(formatCpf(e.target.value))}
placeholder="091.000.000-00"
Expand All @@ -242,12 +345,13 @@ export default function RegisterSide() {

<motion.button
type="submit"
whileHover={{ scale: 1.01 }}
whileTap={{ scale: 0.99 }}
className="w-full bg-gradient-to-r from-blue-600 via-purple-600 to-teal-600 hover:opacity-95 text-white py-3.5 px-4 rounded-xl font-bold text-base focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all shadow-md shadow-blue-500/10 cursor-pointer"
disabled={isSubmitting}
whileHover={{ scale: isSubmitting ? 1 : 1.01 }}
whileTap={{ scale: isSubmitting ? 1 : 0.99 }}
className="w-full bg-gradient-to-r from-blue-600 via-purple-600 to-teal-600 hover:opacity-95 text-white py-3.5 px-4 rounded-xl font-bold text-base focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all shadow-md shadow-blue-500/10 cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
>
Cadastrar
</motion.button>
{isSubmitting ? "Cadastrando..." : "Cadastrar"}
</motion.button>
</form>

<div className="relative z-10 w-full max-w-2xl mx-auto">
Expand All @@ -260,18 +364,32 @@ export default function RegisterSide() {
</div>
</div>

{/* Botões Sociais Atualizados com Hover translúcido e bordas combinando */}
<div className="grid grid-cols-3 gap-4">
<button className="flex justify-center items-center py-3 px-4 border border-gray-200 dark:border-neutral-800 rounded-xl bg-white/50 dark:bg-neutral-800/50 hover:bg-gray-50 dark:hover:bg-neutral-700/50 transition-all shadow-sm cursor-pointer">
<button
type="button"
disabled={isSubmitting}
onClick={() => handleOAuthLogin("google")}
className="flex justify-center items-center py-3 px-4 border border-gray-200 dark:border-neutral-800 rounded-xl bg-white/50 dark:bg-neutral-800/50 hover:bg-gray-50 dark:hover:bg-neutral-700/50 transition-all shadow-sm cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
>
<Image src="/google.png" alt="Google" width={20} height={20} className="object-contain" />
</button>
<button className="flex justify-center items-center py-3 px-4 border border-gray-200 dark:border-neutral-800 rounded-xl bg-white/50 dark:bg-neutral-800/50 hover:bg-gray-50 dark:hover:bg-neutral-700/50 transition-all shadow-sm cursor-pointer">
<Image src="/facebook.png" alt="Facebook" width={20} height={20} className="object-contain" />

<button
type="button"
disabled={isSubmitting}
onClick={() => handleOAuthLogin("linkedin")}
className="flex justify-center items-center py-3 px-4 border border-gray-200 dark:border-neutral-800 rounded-xl bg-white/50 dark:bg-neutral-800/50 hover:bg-gray-50 dark:hover:bg-neutral-700/50 transition-all shadow-sm cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
>
<Linkedin className="h-5 text-blue-500" />
</button>
<button className="flex justify-center items-center py-3 px-4 border border-gray-200 dark:border-neutral-800 rounded-xl bg-white/50 dark:bg-neutral-800/50 hover:bg-gray-50 dark:hover:bg-neutral-700/50 transition-all shadow-sm cursor-pointer">
<svg className="h-5 w-5 fill-gray-900 dark:fill-white transition-colors" viewBox="0 0 24 24">
<path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.81-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M15.97 4.17c.66-.81 1.11-1.93.99-3.06-1 .04-2.21.67-2.93 1.49-.62.69-1.16 1.84-1.01 2.96 1.12.09 2.27-.58 2.95-1.39z"/>
</svg>

<button
type="button"
disabled={isSubmitting}
onClick={() => handleOAuthLogin("github")}
className="flex justify-center items-center py-3 px-4 border border-gray-200 dark:border-neutral-800 rounded-xl bg-white/50 dark:bg-neutral-800/50 hover:bg-gray-50 dark:hover:bg-neutral-700/50 transition-all shadow-sm cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
>
<Github className="h-5 text-gray-900 dark:text-white" />
</button>
</div>
</div>
Expand Down
Loading
Loading