Integra login/cadastro com API, toast de sucesso e redirecionamentos

This commit is contained in:
Eduardo 2025-12-16 00:23:31 -03:00
parent 71bc1b6b6e
commit b66eb96879
22 changed files with 558 additions and 120 deletions

12
package-lock.json generated
View File

@ -16,6 +16,7 @@
"@angular/platform-server": "^20.3.0", "@angular/platform-server": "^20.3.0",
"@angular/router": "^20.3.0", "@angular/router": "^20.3.0",
"@angular/ssr": "^20.3.10", "@angular/ssr": "^20.3.10",
"@popperjs/core": "^2.11.8",
"bootstrap": "^5.3.8", "bootstrap": "^5.3.8",
"bootstrap-icons": "^1.13.1", "bootstrap-icons": "^1.13.1",
"express": "^5.1.0", "express": "^5.1.0",
@ -27,6 +28,7 @@
"@angular/build": "^20.3.10", "@angular/build": "^20.3.10",
"@angular/cli": "^20.3.10", "@angular/cli": "^20.3.10",
"@angular/compiler-cli": "^20.3.0", "@angular/compiler-cli": "^20.3.0",
"@types/bootstrap": "^5.2.10",
"@types/express": "^5.0.1", "@types/express": "^5.0.1",
"@types/jasmine": "~5.1.0", "@types/jasmine": "~5.1.0",
"@types/node": "^20.17.19", "@types/node": "^20.17.19",
@ -3587,6 +3589,16 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/@types/bootstrap": {
"version": "5.2.10",
"resolved": "https://registry.npmjs.org/@types/bootstrap/-/bootstrap-5.2.10.tgz",
"integrity": "sha512-F2X+cd6551tep0MvVZ6nM8v7XgGN/twpdNDjqS1TUM7YFNEtQYWk+dKAnH+T1gr6QgCoGMPl487xw/9hXooa2g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@popperjs/core": "^2.9.2"
}
},
"node_modules/@types/connect": { "node_modules/@types/connect": {
"version": "3.4.38", "version": "3.4.38",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",

View File

@ -31,6 +31,7 @@
"@angular/platform-server": "^20.3.0", "@angular/platform-server": "^20.3.0",
"@angular/router": "^20.3.0", "@angular/router": "^20.3.0",
"@angular/ssr": "^20.3.10", "@angular/ssr": "^20.3.10",
"@popperjs/core": "^2.11.8",
"bootstrap": "^5.3.8", "bootstrap": "^5.3.8",
"bootstrap-icons": "^1.13.1", "bootstrap-icons": "^1.13.1",
"express": "^5.1.0", "express": "^5.1.0",
@ -42,6 +43,7 @@
"@angular/build": "^20.3.10", "@angular/build": "^20.3.10",
"@angular/cli": "^20.3.10", "@angular/cli": "^20.3.10",
"@angular/compiler-cli": "^20.3.0", "@angular/compiler-cli": "^20.3.0",
"@types/bootstrap": "^5.2.10",
"@types/express": "^5.0.1", "@types/express": "^5.0.1",
"@types/jasmine": "~5.1.0", "@types/jasmine": "~5.1.0",
"@types/node": "^20.17.19", "@types/node": "^20.17.19",

View File

@ -1,13 +1,26 @@
import { ApplicationConfig, provideBrowserGlobalErrorListeners, provideZoneChangeDetection } from '@angular/core'; import {
ApplicationConfig,
provideBrowserGlobalErrorListeners,
provideZoneChangeDetection
} from '@angular/core';
import { provideRouter } from '@angular/router'; import { provideRouter } from '@angular/router';
import { provideClientHydration, withEventReplay } from '@angular/platform-browser';
import { provideHttpClient, withFetch, withInterceptors } from '@angular/common/http';
import { routes } from './app.routes'; import { routes } from './app.routes';
import { provideClientHydration, withEventReplay } from '@angular/platform-browser'; import { authInterceptor } from './interceptors/auth.interceptor';
export const appConfig: ApplicationConfig = { export const appConfig: ApplicationConfig = {
providers: [ providers: [
provideBrowserGlobalErrorListeners(), provideBrowserGlobalErrorListeners(),
provideZoneChangeDetection({ eventCoalescing: true }), provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes), provideClientHydration(withEventReplay()) provideRouter(routes),
provideClientHydration(withEventReplay()),
// ✅ HttpClient com fetch + interceptor
provideHttpClient(
withFetch(),
withInterceptors([authInterceptor])
),
] ]
}; };

View File

@ -2,9 +2,11 @@ import { Routes } from '@angular/router';
import { Home } from './pages/home/home'; import { Home } from './pages/home/home';
import { Register } from './pages/register/register'; import { Register } from './pages/register/register';
import { LoginComponent } from './pages/login/login'; import { LoginComponent } from './pages/login/login';
import { Geral } from './pages/geral/geral';
export const routes: Routes = [ export const routes: Routes = [
{ path: '', component: Home }, { path: '', component: Home },
{ path: "register", component: Register }, { path: "register", component: Register },
{ path: "login", component: LoginComponent }, { path: "login", component: LoginComponent },
{ path: "geral", component: Geral },
]; ];

View File

@ -8,6 +8,7 @@
background: linear-gradient(90deg, #030FAA 0%, #6066FF 45%, #C91EB5 100%); background: linear-gradient(90deg, #030FAA 0%, #6066FF 45%, #C91EB5 100%);
padding: 10px 32px; /* bem mais baixo que antes */ padding: 10px 32px; /* bem mais baixo que antes */
box-sizing: border-box; box-sizing: border-box;
margin-top: -0.5px;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;

View File

@ -0,0 +1,13 @@
import { HttpInterceptorFn } from '@angular/common/http';
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const token = localStorage.getItem('token');
if (!token) return next(req);
const authReq = req.clone({
setHeaders: { Authorization: `Bearer ${token}` }
});
return next(authReq);
};

View File

@ -0,0 +1 @@
<p>geral works!</p>

View File

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Geral } from './geral';
describe('Geral', () => {
let component: Geral;
let fixture: ComponentFixture<Geral>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [Geral]
})
.compileComponents();
fixture = TestBed.createComponent(Geral);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,11 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-geral',
imports: [],
templateUrl: './geral.html',
styleUrl: './geral.scss',
})
export class Geral {
}

View File

@ -36,6 +36,11 @@
</div> </div>
</div> </div>
<!-- Erro da API -->
<div *ngIf="apiError" class="alert alert-danger py-2 mb-3">
{{ apiError }}
</div>
<!-- Botão Entrar --> <!-- Botão Entrar -->
<button <button
type="submit" type="submit"
@ -47,3 +52,22 @@
</form> </form>
</div> </div>
</div> </div>
<!-- Toast (Sucesso Login) -->
<div class="toast-container position-fixed top-0 end-0 p-3" style="z-index: 2000;">
<div
#successToast
class="toast text-bg-success"
role="alert"
aria-live="assertive"
aria-atomic="true"
>
<div class="toast-header">
<strong class="me-auto">LineGestão</strong>
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Fechar"></button>
</div>
<div class="toast-body">
{{ toastMessage }}
</div>
</div>
</div>

View File

@ -4,7 +4,7 @@
/* Wrapper para centralizar o card entre header e footer */ /* Wrapper para centralizar o card entre header e footer */
.login-wrapper { .login-wrapper {
min-height: calc(100vh - 60px); min-height: calc(100vh - 69.2px);
display: flex; display: flex;
justify-content: center; /* login fica no centro */ justify-content: center; /* login fica no centro */
@ -15,36 +15,64 @@
padding-bottom: 100px; /* mesmo “respiro” do cadastro */ padding-bottom: 100px; /* mesmo “respiro” do cadastro */
padding-left: 12px; padding-left: 12px;
background: url('../../../assets/wallpaper/registro_qualidade3.png')
no-repeat right center;
background-size: contain;
background-color: #efefef; background-color: #efefef;
/* IMAGEM DESKTOP (PADRÃO) */
background-image: url('../../../assets/wallpaper/registro_login.png');
background-repeat: no-repeat;
background-position: right top;
background-size: cover;
/* 🔑 fundo preso ao viewport (igual cadastro) */
background-attachment: fixed;
}
/* NOTEBOOKS / TABLETS */
@media (max-width: 992px) { @media (max-width: 992px) {
.login-wrapper {
padding-top: 24px;
padding-left: 16px; padding-left: 16px;
padding-right: 16px; padding-right: 16px;
padding-bottom: 80px;
}
@media (max-width: 576px) {
min-height: calc(100vh - 40px);
padding-top: 24px;
padding-bottom: 60px; padding-bottom: 60px;
background-position: center top;
background-size: cover;
background-attachment: scroll;
}
}
/* CELULARES IMAGEM MOBILE */
@media (max-width: 576px) {
.login-wrapper {
min-height: calc(100vh - 40px);
padding-top: 20px;
padding-bottom: 32px;
padding-left: 16px;
padding-right: 16px;
background-image: url('../../../assets/wallpaper/mobile.png');
background-repeat: no-repeat;
background-position: center top;
background-size: cover;
background-attachment: scroll;
} }
} }
/* Card principal mesmo estilo glass do cadastro, /* ========================= */
mas SEM margin-left e centralizado pelo flex */ /* CARD DE LOGIN */
/* ========================= */
/* Desktop grande (monitor) */
.login-card { .login-card {
background-color: transparent; background-color: transparent;
border-radius: 10px; border-radius: 10px;
border: 2px solid #c91eb5; border: 2px solid #c91eb5;
max-width: 500px; /* aumenta a largura como no cadastro */ max-width: 480px; /* antes 500px */
width: 100%; width: 100%;
min-height: 380px; /* um pouco menor que o cadastro, mas folgado */ min-height: 360px; /* antes 380px */
padding: 28px 24px; /* mesmo “respiro” interno */ padding: 26px 22px; /* antes 28px 24px */
box-sizing: border-box; box-sizing: border-box;
backdrop-filter: blur(6px); backdrop-filter: blur(6px);
@ -52,10 +80,79 @@
.mb-3, .mb-3,
.mb-4 { .mb-4 {
margin-bottom: 0.9rem; margin-bottom: 0.8rem; /* antes 0.9rem */
} }
} }
/* NOTEB00KS (≤1440px) deixa ainda mais compacto */
@media (max-width: 1440px) {
.login-card {
max-width: 430px;
min-height: 330px;
padding: 22px 20px;
}
.login-title h2 {
font-size: 30px; /* levemente menor */
}
}
/* notebooks / tablets (≤992px) */
@media (max-width: 992px) {
.login-card {
max-width: 400px;
min-height: 310px;
padding: 20px 18px;
}
.login-title h2 {
font-size: 26px;
}
.form-control {
height: 36px;
font-size: 13px;
}
.login-btn-submit {
font-size: 13px;
padding: 7px 0;
}
.mb-3,
.mb-4 {
margin-bottom: 0.7rem;
}
}
/* celulares (≤576px) bem enxuto */
@media (max-width: 576px) {
.login-card {
max-width: 340px;
min-height: auto;
padding: 18px 14px;
}
.login-title h2 {
font-size: 24px;
}
.form-control {
height: 34px;
font-size: 13px;
}
.login-btn-submit {
font-size: 12.5px;
padding: 7px 0;
}
}
/* ========================= */
/* TIPOGRAFIA E FORM */
/* ========================= */
/* Título centralizado rosa */ /* Título centralizado rosa */
.login-title { .login-title {
display: flex; display: flex;
@ -80,14 +177,14 @@
color: #000000; color: #000000;
} }
/* Inputs iguais aos do cadastro (borda azul, maiores) */ /* Inputs iguais aos do cadastro (borda azul) */
.form-control { .form-control {
height: 38px; /* antes 34px */ height: 38px;
border-radius: 8px; border-radius: 8px;
border: 2px solid #6066ff; border: 2px solid #6066ff;
font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
font-weight: 500; font-weight: 500;
font-size: 14px; /* antes 13px */ font-size: 14px;
color: #000000; color: #000000;
&::placeholder { &::placeholder {

View File

@ -1,11 +1,8 @@
import { Component } from '@angular/core'; import { Component, ElementRef, ViewChild, Inject, PLATFORM_ID } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule, isPlatformBrowser } from '@angular/common';
import { import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
FormBuilder, import { Router } from '@angular/router';
FormGroup, import { AuthService } from '../../services/auth.service';
Validators,
ReactiveFormsModule
} from '@angular/forms';
@Component({ @Component({
selector: 'app-login', selector: 'app-login',
@ -15,18 +12,56 @@ import {
styleUrls: ['./login.scss'] styleUrls: ['./login.scss']
}) })
export class LoginComponent { export class LoginComponent {
loginForm: FormGroup; loginForm: FormGroup;
isSubmitting = false; isSubmitting = false;
apiError = '';
constructor(private fb: FormBuilder) { toastMessage = '';
@ViewChild('successToast') successToast!: ElementRef;
constructor(
private fb: FormBuilder,
private authService: AuthService,
private router: Router,
@Inject(PLATFORM_ID) private platformId: object
) {
this.loginForm = this.fb.group({ this.loginForm = this.fb.group({
username: ['', [Validators.required]], // “Usuário” do protótipo username: ['', [Validators.required]], // aqui é email
password: ['', [Validators.required, Validators.minLength(6)]] password: ['', [Validators.required, Validators.minLength(6)]]
}); });
} }
private async showToast(message: string) {
this.toastMessage = message;
// ✅ SSR-safe: só roda no browser
if (!isPlatformBrowser(this.platformId)) return;
const bs = await import('bootstrap');
const toast = new bs.Toast(this.successToast.nativeElement, { autohide: true, delay: 1500 });
toast.show();
}
private getNameFromToken(token: string): string {
try {
const payload = token.split('.')[1];
const base64 = payload.replace(/-/g, '+').replace(/_/g, '/');
const json = decodeURIComponent(
atob(base64)
.split('')
.map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
.join('')
);
const data = JSON.parse(json);
return data?.name ?? 'usuário';
} catch {
return 'usuário';
}
}
onSubmit(): void { onSubmit(): void {
this.apiError = '';
if (this.loginForm.invalid) { if (this.loginForm.invalid) {
this.loginForm.markAllAsTouched(); this.loginForm.markAllAsTouched();
return; return;
@ -34,30 +69,35 @@ export class LoginComponent {
this.isSubmitting = true; this.isSubmitting = true;
// Simulação de envio preparado para API futura const v = this.loginForm.value;
console.log('Dados de login:', this.loginForm.value);
// FUTURO: aqui você injeta seu AuthService e chama a API .NET: const payload = {
/* email: v.username,
this.authService.login(this.loginForm.value).subscribe({ password: v.password
next: () => { ... }, };
error: () => { ... },
complete: () => this.isSubmitting = false this.authService.login(payload).subscribe({
}); next: async (res) => {
*/ this.isSubmitting = false;
const nome = this.getNameFromToken(res.token);
await this.showToast(`Bem-vindo, ${nome}!`);
setTimeout(() => { setTimeout(() => {
this.router.navigate(['/geral']);
}, 900);
},
error: (err) => {
this.isSubmitting = false; this.isSubmitting = false;
alert('Login enviado (simulado). Depois conectamos na API .NET 😉'); this.apiError = err?.error ?? 'Erro ao fazer login.';
}, 800); }
});
} }
hasError(field: string, error?: string): boolean { hasError(field: string, error?: string): boolean {
const control = this.loginForm.get(field); const control = this.loginForm.get(field);
if (!control) return false; if (!control) return false;
if (error) { if (error) return control.touched && control.hasError(error);
return control.touched && control.hasError(error);
}
return control.touched && control.invalid; return control.touched && control.invalid;
} }
} }

View File

@ -6,12 +6,11 @@
<!-- Barra / título --> <!-- Barra / título -->
<div class="register-title mb-4"> <div class="register-title mb-4">
<span class="register-icon"> <span class="register-icon">
<i class="bi bi-box-arrow-in-right"></i> <!-- ícone bootstrap --> <i class="bi bi-box-arrow-in-right"></i>
</span> </span>
<h2 class="mb-0">Cadastre-se</h2> <h2 class="mb-0">Cadastre-se</h2>
</div> </div>
<form [formGroup]="registerForm" (ngSubmit)="onSubmit()"> <form [formGroup]="registerForm" (ngSubmit)="onSubmit()">
<!-- Nome completo --> <!-- Nome completo -->
@ -87,7 +86,12 @@
</div> </div>
</div> </div>
<!-- Botão Cadastrar (gradiente) --> <!-- Erro da API -->
<div *ngIf="apiError" class="alert alert-danger py-2 mb-3">
{{ apiError }}
</div>
<!-- Botão Cadastrar -->
<button <button
type="submit" type="submit"
class="btn btn-primary w-100 register-btn mb-3" class="btn btn-primary w-100 register-btn mb-3"
@ -106,3 +110,22 @@
</form> </form>
</div> </div>
</div> </div>
<!-- Toast (Sucesso Cadastro) -->
<div class="toast-container position-fixed top-0 end-0 p-3" style="z-index: 2000;">
<div
#successToast
class="toast text-bg-success"
role="alert"
aria-live="assertive"
aria-atomic="true"
>
<div class="toast-header">
<strong class="me-auto">LineGestão</strong>
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Fechar"></button>
</div>
<div class="toast-body">
{{ toastMessage }}
</div>
</div>
</div>

View File

@ -4,64 +4,164 @@
/* Wrapper para centralizar o card entre header e footer */ /* Wrapper para centralizar o card entre header e footer */
.register-wrapper { .register-wrapper {
/* ocupa quase a tela toda, mas sem exagero */ /* mesma altura base do login */
min-height: calc(100vh - 60px); min-height: calc(100vh - 69.2px);
display: flex; display: flex;
justify-content: flex-start; justify-content: center; /* igual ao login */
align-items: center; align-items: center;
/* respiro mais equilibrado */ /* mesmos paddings do login */
padding-top: 32px; padding-top: 32px;
padding-right: 12px; padding-right: 12px;
padding-bottom: 100px; /* ainda empurra o footer, mas menos que 140 */ padding-bottom: 100px;
padding-left: 5vw; padding-left: 12px;
background: url('../../../assets/wallpaper/registro_qualidade2.png')
no-repeat right center;
background-size: contain;
background-color: #efefef; background-color: #efefef;
/* responsivo: em telas menores, centraliza o card e reduz paddings */ /* mesma imagem/config do login */
background-image: url('../../../assets/wallpaper/registro_login.png');
background-repeat: no-repeat;
background-position: right top;
background-size: cover;
/* 🔑 fundo preso ao viewport → não “dá zoom” por causa da altura maior */
background-attachment: fixed;
}
/* NOTEBOOKS / TABLETS */
@media (max-width: 992px) { @media (max-width: 992px) {
.register-wrapper {
justify-content: center; justify-content: center;
padding-top: 24px;
padding-left: 16px; padding-left: 16px;
padding-right: 16px; padding-right: 16px;
padding-bottom: 80px;
}
@media (max-width: 576px) {
min-height: calc(100vh - 40px);
padding-top: 24px;
padding-bottom: 60px; padding-bottom: 60px;
background-position: center top;
background-size: cover;
background-attachment: scroll; /* evita bug em telas menores */
}
}
/* CELULARES IMAGEM MOBILE */
@media (max-width: 576px) {
.register-wrapper {
min-height: calc(100vh - 40px);
padding-top: 20px;
padding-bottom: 32px;
padding-left: 16px;
padding-right: 16px;
background-image: url('../../../assets/wallpaper/mobile.png');
background-repeat: no-repeat;
background-position: center top;
background-size: cover;
background-attachment: scroll;
} }
} }
/* ========================= */
/* CARD DE CADASTRO (MENOR) */
/* ========================= */
/* Card principal AGORA MAIS LARGO */ /* Desktop grande (monitor) */
.register-card { .register-card {
background-color: transparent; background-color: transparent;
border-radius: 10px; border-radius: 10px;
border: 2px solid #c91eb5; border: 2px solid #c91eb5;
max-width: 500px; /* antes 340px ✅ aumenta a largura do card */ /* ➜ menor que antes */
max-width: 460px;
width: 100%; width: 100%;
min-height: 500px; /* leve ajuste pra não ficar “apertado” */ min-height: 430px;
padding: 28px 24px; /* um pouco mais de “respiro” interno */ padding: 22px 20px;
box-sizing: border-box; box-sizing: border-box;
margin-left: 150px;
margin-left: 0;
backdrop-filter: blur(6px); backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
.mb-3, .mb-3,
.mb-4 { .mb-4 {
margin-bottom: 0.9rem; /* mantém espaçamento confortável */ margin-bottom: 0.8rem; /* menos espaço entre os blocos */
} }
} }
/* Título: ícone + texto lado a lado */ /* NOTEBOOKS (≤1440px) ainda mais compacto */
@media (max-width: 1440px) {
.register-card {
max-width: 420px;
min-height: 400px;
padding: 20px 18px;
}
.register-title h2 {
font-size: 30px;
}
}
/* notebooks / tablets (≤992px) */
@media (max-width: 992px) {
.register-card {
max-width: 380px;
min-height: 360px;
padding: 18px 16px;
}
.register-title h2 {
font-size: 26px;
}
.form-control {
height: 36px;
font-size: 13px;
}
.register-btn,
.login-btn {
font-size: 13px;
padding: 7px 0;
}
.mb-3,
.mb-4 {
margin-bottom: 0.7rem;
}
}
/* celulares (≤576px) compacto, mas confortável */
@media (max-width: 576px) {
.register-card {
max-width: 330px;
min-height: auto;
padding: 18px 14px;
}
.register-title h2 {
font-size: 24px;
}
.form-control {
height: 34px;
font-size: 13px;
}
.register-btn,
.login-btn {
font-size: 12.5px;
padding: 7px 0;
}
}
/* ========================= */
/* TÍTULO + ÍCONE */
/* ========================= */
.register-title { .register-title {
display: flex; display: flex;
align-items: center; align-items: center;
@ -78,7 +178,6 @@
} }
} }
/* Ícone ao lado do título */
.register-icon { .register-icon {
display: flex; display: flex;
align-items: center; align-items: center;
@ -86,26 +185,29 @@
color: #c91eb5; color: #c91eb5;
i { i {
font-size: 32px; font-size: 35px;
} }
} }
/* Labels */
/* ========================= */
/* FORM / INPUTS / BOTÕES */
/* ========================= */
.form-label { .form-label {
font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
font-weight: 500; font-weight: 500;
font-size: 14px; /* um pouco maior pra acompanhar o card */ font-size: 14px;
color: #000000; color: #000000;
} }
/* Inputs com borda azul do protótipo levemente maiores */
.form-control { .form-control {
height: 38px; /* antes 34px */ height: 38px;
border-radius: 8px; border-radius: 8px;
border: 2px solid #6066ff; border: 2px solid #6066ff;
font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
font-weight: 500; font-weight: 500;
font-size: 14px; /* antes 13px */ font-size: 14px;
color: #000000; color: #000000;
&::placeholder { &::placeholder {
@ -113,13 +215,12 @@
} }
} }
/* Botão principal (CADASTRAR) rosa sólido */
.register-btn { .register-btn {
border-radius: 40px; border-radius: 40px;
border: none; border: none;
font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
font-weight: 600; font-weight: 600;
font-size: 14px; /* um tic maior */ font-size: 14px;
letter-spacing: 0.5px; letter-spacing: 0.5px;
text-transform: uppercase; text-transform: uppercase;
padding: 9px 0; padding: 9px 0;
@ -131,7 +232,6 @@
} }
} }
/* Botão "JÁ TEM UMA CONTA? ENTRE" azul sólido */
.login-btn { .login-btn {
border-radius: 40px; border-radius: 40px;
border: none; border: none;
@ -149,7 +249,6 @@
} }
} }
/* Mensagens de erro */
.text-danger.small { .text-danger.small {
font-size: 11px; font-size: 11px;
} }

View File

@ -1,22 +1,30 @@
// src/app/pages/register.component.ts import { Component, ElementRef, ViewChild, Inject, PLATFORM_ID } from '@angular/core';
import { Component } from '@angular/core'; import { CommonModule, isPlatformBrowser } from '@angular/common';
import { CommonModule } from '@angular/common';
import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms'; import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
import { RouterLink } from '@angular/router'; import { Router, RouterLink } from '@angular/router';
import { AuthService } from '../../services/auth.service';
@Component({ @Component({
selector: 'app-register', selector: 'app-register',
standalone: true, standalone: true,
imports: [CommonModule, ReactiveFormsModule, RouterLink], imports: [CommonModule, ReactiveFormsModule, RouterLink],
templateUrl: './register.html', templateUrl: './register.html',
styleUrls: ['./register.scss'] // ou .scss styleUrls: ['./register.scss']
}) })
export class Register { export class Register {
registerForm: FormGroup; registerForm: FormGroup;
isSubmitting = false; isSubmitting = false;
apiError = '';
constructor(private fb: FormBuilder) { toastMessage = '';
@ViewChild('successToast') successToast!: ElementRef;
constructor(
private fb: FormBuilder,
private authService: AuthService,
private router: Router,
@Inject(PLATFORM_ID) private platformId: object
) {
this.registerForm = this.fb.group({ this.registerForm = this.fb.group({
fullName: ['', [Validators.required, Validators.minLength(3)]], fullName: ['', [Validators.required, Validators.minLength(3)]],
email: ['', [Validators.required, Validators.email]], email: ['', [Validators.required, Validators.email]],
@ -28,14 +36,26 @@ export class Register {
}); });
} }
// Validador para confirmar senha
private passwordsMatchValidator(group: FormGroup) { private passwordsMatchValidator(group: FormGroup) {
const pass = group.get('password')?.value; const pass = group.get('password')?.value;
const confirm = group.get('confirmPassword')?.value; const confirm = group.get('confirmPassword')?.value;
return pass === confirm ? null : { passwordsMismatch: true }; return pass === confirm ? null : { passwordsMismatch: true };
} }
private async showToast(message: string) {
this.toastMessage = message;
// ✅ SSR-safe: só roda no browser
if (!isPlatformBrowser(this.platformId)) return;
const bs = await import('bootstrap');
const toast = new bs.Toast(this.successToast.nativeElement, { autohide: true, delay: 1800 });
toast.show();
}
onSubmit(): void { onSubmit(): void {
this.apiError = '';
if (this.registerForm.invalid) { if (this.registerForm.invalid) {
this.registerForm.markAllAsTouched(); this.registerForm.markAllAsTouched();
return; return;
@ -43,33 +63,40 @@ export class Register {
this.isSubmitting = true; this.isSubmitting = true;
// Por enquanto: apenas exibe os dados e "simula" o envio const v = this.registerForm.value;
console.log('Dados de cadastro:', this.registerForm.value);
// TODO FUTURO: quando a API .NET estiver pronta, const payload = {
// você injeta um AuthService e envia para o backend: name: v.fullName,
/* email: v.email,
this.authService.register(this.registerForm.value).subscribe({ phone: v.phone,
next: () => { ... }, password: v.password,
error: () => { ... }, confirmPassword: v.confirmPassword
complete: () => this.isSubmitting = false };
});
*/
// Simulação de atraso só para UX (remova se quiser) this.authService.register(payload).subscribe({
setTimeout(() => { next: async () => {
this.isSubmitting = false; this.isSubmitting = false;
this.registerForm.reset();
alert('Cadastro realizado (simulado). Quando a API estiver pronta, isso irá de fato registrar o usuário.'); // Se você não quer manter "logado" após cadastrar:
}, 800); localStorage.removeItem('token');
await this.showToast('Cadastro realizado com sucesso! Agora faça login para continuar.');
setTimeout(() => {
this.router.navigate(['/login']);
}, 1000);
},
error: (err) => {
this.isSubmitting = false;
this.apiError = err?.error ?? 'Erro ao cadastrar.';
}
});
} }
hasError(field: string, error?: string): boolean { hasError(field: string, error?: string): boolean {
const control = this.registerForm.get(field); const control = this.registerForm.get(field);
if (!control) return false; if (!control) return false;
if (error) { if (error) return control.touched && control.hasError(error);
return control.touched && control.hasError(error);
}
return control.touched && control.invalid; return control.touched && control.invalid;
} }
} }

View File

@ -0,0 +1,46 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { environment } from '../../environments/environment';
import { tap } from 'rxjs/operators';
export interface RegisterPayload {
name: string;
email: string;
phone: string;
password: string;
confirmPassword: string;
}
export interface LoginPayload {
email: string;
password: string;
}
@Injectable({ providedIn: 'root' })
export class AuthService {
private baseUrl = `${environment.apiUrl}/auth`;
constructor(private http: HttpClient) {}
register(payload: RegisterPayload) {
return this.http.post<{ token: string }>(`${this.baseUrl}/register`, payload)
.pipe(tap(r => localStorage.setItem('token', r.token)));
}
login(payload: LoginPayload) {
return this.http.post<{ token: string }>(`${this.baseUrl}/login`, payload)
.pipe(tap(r => localStorage.setItem('token', r.token)));
}
logout() {
localStorage.removeItem('token');
}
get token(): string | null {
return localStorage.getItem('token');
}
isLoggedIn(): boolean {
return !!this.token;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 MiB

View File

Before

Width:  |  Height:  |  Size: 4.7 MiB

After

Width:  |  Height:  |  Size: 4.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 MiB

View File

@ -0,0 +1,4 @@
export const environment = {
production: false,
apiUrl: 'https://localhost:7205'
};