Integra login/cadastro com API, toast de sucesso e redirecionamentos
This commit is contained in:
parent
71bc1b6b6e
commit
b66eb96879
|
|
@ -16,6 +16,7 @@
|
|||
"@angular/platform-server": "^20.3.0",
|
||||
"@angular/router": "^20.3.0",
|
||||
"@angular/ssr": "^20.3.10",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"bootstrap": "^5.3.8",
|
||||
"bootstrap-icons": "^1.13.1",
|
||||
"express": "^5.1.0",
|
||||
|
|
@ -27,6 +28,7 @@
|
|||
"@angular/build": "^20.3.10",
|
||||
"@angular/cli": "^20.3.10",
|
||||
"@angular/compiler-cli": "^20.3.0",
|
||||
"@types/bootstrap": "^5.2.10",
|
||||
"@types/express": "^5.0.1",
|
||||
"@types/jasmine": "~5.1.0",
|
||||
"@types/node": "^20.17.19",
|
||||
|
|
@ -3587,6 +3589,16 @@
|
|||
"@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": {
|
||||
"version": "3.4.38",
|
||||
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@
|
|||
"@angular/platform-server": "^20.3.0",
|
||||
"@angular/router": "^20.3.0",
|
||||
"@angular/ssr": "^20.3.10",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"bootstrap": "^5.3.8",
|
||||
"bootstrap-icons": "^1.13.1",
|
||||
"express": "^5.1.0",
|
||||
|
|
@ -42,6 +43,7 @@
|
|||
"@angular/build": "^20.3.10",
|
||||
"@angular/cli": "^20.3.10",
|
||||
"@angular/compiler-cli": "^20.3.0",
|
||||
"@types/bootstrap": "^5.2.10",
|
||||
"@types/express": "^5.0.1",
|
||||
"@types/jasmine": "~5.1.0",
|
||||
"@types/node": "^20.17.19",
|
||||
|
|
|
|||
|
|
@ -1,13 +1,26 @@
|
|||
import { ApplicationConfig, provideBrowserGlobalErrorListeners, provideZoneChangeDetection } from '@angular/core';
|
||||
import {
|
||||
ApplicationConfig,
|
||||
provideBrowserGlobalErrorListeners,
|
||||
provideZoneChangeDetection
|
||||
} from '@angular/core';
|
||||
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 { provideClientHydration, withEventReplay } from '@angular/platform-browser';
|
||||
import { authInterceptor } from './interceptors/auth.interceptor';
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
provideBrowserGlobalErrorListeners(),
|
||||
provideZoneChangeDetection({ eventCoalescing: true }),
|
||||
provideRouter(routes), provideClientHydration(withEventReplay())
|
||||
provideRouter(routes),
|
||||
provideClientHydration(withEventReplay()),
|
||||
|
||||
// ✅ HttpClient com fetch + interceptor
|
||||
provideHttpClient(
|
||||
withFetch(),
|
||||
withInterceptors([authInterceptor])
|
||||
),
|
||||
]
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,9 +2,11 @@ import { Routes } from '@angular/router';
|
|||
import { Home } from './pages/home/home';
|
||||
import { Register } from './pages/register/register';
|
||||
import { LoginComponent } from './pages/login/login';
|
||||
import { Geral } from './pages/geral/geral';
|
||||
|
||||
export const routes: Routes = [
|
||||
{ path: '', component: Home },
|
||||
{ path: "register", component: Register },
|
||||
{ path: "login", component: LoginComponent },
|
||||
{ path: "geral", component: Geral },
|
||||
];
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
background: linear-gradient(90deg, #030FAA 0%, #6066FF 45%, #C91EB5 100%);
|
||||
padding: 10px 32px; /* bem mais baixo que antes */
|
||||
box-sizing: border-box;
|
||||
margin-top: -0.5px;
|
||||
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
|
@ -0,0 +1 @@
|
|||
<p>geral works!</p>
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-geral',
|
||||
imports: [],
|
||||
templateUrl: './geral.html',
|
||||
styleUrl: './geral.scss',
|
||||
})
|
||||
export class Geral {
|
||||
|
||||
}
|
||||
|
|
@ -36,6 +36,11 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Erro da API -->
|
||||
<div *ngIf="apiError" class="alert alert-danger py-2 mb-3">
|
||||
{{ apiError }}
|
||||
</div>
|
||||
|
||||
<!-- Botão Entrar -->
|
||||
<button
|
||||
type="submit"
|
||||
|
|
@ -47,3 +52,22 @@
|
|||
</form>
|
||||
</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>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
/* Wrapper para centralizar o card entre header e footer */
|
||||
.login-wrapper {
|
||||
min-height: calc(100vh - 60px);
|
||||
min-height: calc(100vh - 69.2px);
|
||||
|
||||
display: flex;
|
||||
justify-content: center; /* login fica no centro */
|
||||
|
|
@ -15,36 +15,64 @@
|
|||
padding-bottom: 100px; /* mesmo “respiro” do cadastro */
|
||||
padding-left: 12px;
|
||||
|
||||
background: url('../../../assets/wallpaper/registro_qualidade3.png')
|
||||
no-repeat right center;
|
||||
background-size: contain;
|
||||
background-color: #efefef;
|
||||
|
||||
@media (max-width: 992px) {
|
||||
/* 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) {
|
||||
.login-wrapper {
|
||||
padding-top: 24px;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
padding-bottom: 80px;
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
min-height: calc(100vh - 40px);
|
||||
padding-top: 24px;
|
||||
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 {
|
||||
background-color: transparent;
|
||||
border-radius: 10px;
|
||||
border: 2px solid #c91eb5;
|
||||
|
||||
max-width: 500px; /* aumenta a largura como no cadastro */
|
||||
max-width: 480px; /* antes 500px */
|
||||
width: 100%;
|
||||
min-height: 380px; /* um pouco menor que o cadastro, mas folgado */
|
||||
padding: 28px 24px; /* mesmo “respiro” interno */
|
||||
min-height: 360px; /* antes 380px */
|
||||
padding: 26px 22px; /* antes 28px 24px */
|
||||
box-sizing: border-box;
|
||||
|
||||
backdrop-filter: blur(6px);
|
||||
|
|
@ -52,10 +80,79 @@
|
|||
|
||||
.mb-3,
|
||||
.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 */
|
||||
.login-title {
|
||||
display: flex;
|
||||
|
|
@ -80,14 +177,14 @@
|
|||
color: #000000;
|
||||
}
|
||||
|
||||
/* Inputs iguais aos do cadastro (borda azul, maiores) */
|
||||
/* Inputs iguais aos do cadastro (borda azul) */
|
||||
.form-control {
|
||||
height: 38px; /* antes 34px */
|
||||
height: 38px;
|
||||
border-radius: 8px;
|
||||
border: 2px solid #6066ff;
|
||||
font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
font-weight: 500;
|
||||
font-size: 14px; /* antes 13px */
|
||||
font-size: 14px;
|
||||
color: #000000;
|
||||
|
||||
&::placeholder {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,8 @@
|
|||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import {
|
||||
FormBuilder,
|
||||
FormGroup,
|
||||
Validators,
|
||||
ReactiveFormsModule
|
||||
} from '@angular/forms';
|
||||
import { Component, ElementRef, ViewChild, Inject, PLATFORM_ID } from '@angular/core';
|
||||
import { CommonModule, isPlatformBrowser } from '@angular/common';
|
||||
import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import { AuthService } from '../../services/auth.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-login',
|
||||
|
|
@ -15,18 +12,56 @@ import {
|
|||
styleUrls: ['./login.scss']
|
||||
})
|
||||
export class LoginComponent {
|
||||
|
||||
loginForm: FormGroup;
|
||||
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({
|
||||
username: ['', [Validators.required]], // “Usuário” do protótipo
|
||||
username: ['', [Validators.required]], // aqui é email
|
||||
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 {
|
||||
this.apiError = '';
|
||||
|
||||
if (this.loginForm.invalid) {
|
||||
this.loginForm.markAllAsTouched();
|
||||
return;
|
||||
|
|
@ -34,30 +69,35 @@ export class LoginComponent {
|
|||
|
||||
this.isSubmitting = true;
|
||||
|
||||
// Simulação de envio – preparado para API futura
|
||||
console.log('Dados de login:', this.loginForm.value);
|
||||
const v = this.loginForm.value;
|
||||
|
||||
// FUTURO: aqui você injeta seu AuthService e chama a API .NET:
|
||||
/*
|
||||
this.authService.login(this.loginForm.value).subscribe({
|
||||
next: () => { ... },
|
||||
error: () => { ... },
|
||||
complete: () => this.isSubmitting = false
|
||||
});
|
||||
*/
|
||||
const payload = {
|
||||
email: v.username,
|
||||
password: v.password
|
||||
};
|
||||
|
||||
this.authService.login(payload).subscribe({
|
||||
next: async (res) => {
|
||||
this.isSubmitting = false;
|
||||
|
||||
const nome = this.getNameFromToken(res.token);
|
||||
await this.showToast(`Bem-vindo, ${nome}!`);
|
||||
|
||||
setTimeout(() => {
|
||||
this.router.navigate(['/geral']);
|
||||
}, 900);
|
||||
},
|
||||
error: (err) => {
|
||||
this.isSubmitting = false;
|
||||
alert('Login enviado (simulado). Depois conectamos na API .NET 😉');
|
||||
}, 800);
|
||||
this.apiError = err?.error ?? 'Erro ao fazer login.';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
hasError(field: string, error?: string): boolean {
|
||||
const control = this.loginForm.get(field);
|
||||
if (!control) return false;
|
||||
if (error) {
|
||||
return control.touched && control.hasError(error);
|
||||
}
|
||||
if (error) return control.touched && control.hasError(error);
|
||||
return control.touched && control.invalid;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,12 +6,11 @@
|
|||
<!-- Barra / título -->
|
||||
<div class="register-title mb-4">
|
||||
<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>
|
||||
<h2 class="mb-0">Cadastre-se</h2>
|
||||
</div>
|
||||
|
||||
|
||||
<form [formGroup]="registerForm" (ngSubmit)="onSubmit()">
|
||||
|
||||
<!-- Nome completo -->
|
||||
|
|
@ -87,7 +86,12 @@
|
|||
</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
|
||||
type="submit"
|
||||
class="btn btn-primary w-100 register-btn mb-3"
|
||||
|
|
@ -106,3 +110,22 @@
|
|||
</form>
|
||||
</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>
|
||||
|
|
|
|||
|
|
@ -4,64 +4,164 @@
|
|||
|
||||
/* Wrapper para centralizar o card entre header e footer */
|
||||
.register-wrapper {
|
||||
/* ocupa quase a tela toda, mas sem exagero */
|
||||
min-height: calc(100vh - 60px);
|
||||
/* mesma altura base do login */
|
||||
min-height: calc(100vh - 69.2px);
|
||||
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
justify-content: center; /* igual ao login */
|
||||
align-items: center;
|
||||
|
||||
/* respiro mais equilibrado */
|
||||
/* mesmos paddings do login */
|
||||
padding-top: 32px;
|
||||
padding-right: 12px;
|
||||
padding-bottom: 100px; /* ainda empurra o footer, mas menos que 140 */
|
||||
padding-left: 5vw;
|
||||
padding-bottom: 100px;
|
||||
padding-left: 12px;
|
||||
|
||||
background: url('../../../assets/wallpaper/registro_qualidade2.png')
|
||||
no-repeat right center;
|
||||
background-size: contain;
|
||||
background-color: #efefef;
|
||||
|
||||
/* responsivo: em telas menores, centraliza o card e reduz paddings */
|
||||
@media (max-width: 992px) {
|
||||
/* 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) {
|
||||
.register-wrapper {
|
||||
justify-content: center;
|
||||
padding-top: 24px;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
padding-bottom: 80px;
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
min-height: calc(100vh - 40px);
|
||||
padding-top: 24px;
|
||||
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 {
|
||||
background-color: transparent;
|
||||
border-radius: 10px;
|
||||
border: 2px solid #c91eb5;
|
||||
|
||||
max-width: 500px; /* antes 340px ✅ aumenta a largura do card */
|
||||
/* ➜ menor que antes */
|
||||
max-width: 460px;
|
||||
width: 100%;
|
||||
min-height: 500px; /* leve ajuste pra não ficar “apertado” */
|
||||
padding: 28px 24px; /* um pouco mais de “respiro” interno */
|
||||
min-height: 430px;
|
||||
padding: 22px 20px;
|
||||
box-sizing: border-box;
|
||||
margin-left: 150px;
|
||||
|
||||
margin-left: 0;
|
||||
|
||||
backdrop-filter: blur(6px);
|
||||
-webkit-backdrop-filter: blur(6px);
|
||||
|
||||
.mb-3,
|
||||
.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 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -78,7 +178,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
/* Ícone ao lado do título */
|
||||
.register-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -86,26 +185,29 @@
|
|||
color: #c91eb5;
|
||||
|
||||
i {
|
||||
font-size: 32px;
|
||||
font-size: 35px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Labels */
|
||||
|
||||
/* ========================= */
|
||||
/* FORM / INPUTS / BOTÕES */
|
||||
/* ========================= */
|
||||
|
||||
.form-label {
|
||||
font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
font-weight: 500;
|
||||
font-size: 14px; /* um pouco maior pra acompanhar o card */
|
||||
font-size: 14px;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
/* Inputs com borda azul do protótipo – levemente maiores */
|
||||
.form-control {
|
||||
height: 38px; /* antes 34px */
|
||||
height: 38px;
|
||||
border-radius: 8px;
|
||||
border: 2px solid #6066ff;
|
||||
font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
font-weight: 500;
|
||||
font-size: 14px; /* antes 13px */
|
||||
font-size: 14px;
|
||||
color: #000000;
|
||||
|
||||
&::placeholder {
|
||||
|
|
@ -113,13 +215,12 @@
|
|||
}
|
||||
}
|
||||
|
||||
/* Botão principal (CADASTRAR) – rosa sólido */
|
||||
.register-btn {
|
||||
border-radius: 40px;
|
||||
border: none;
|
||||
font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
font-weight: 600;
|
||||
font-size: 14px; /* um tic maior */
|
||||
font-size: 14px;
|
||||
letter-spacing: 0.5px;
|
||||
text-transform: uppercase;
|
||||
padding: 9px 0;
|
||||
|
|
@ -131,7 +232,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
/* Botão "JÁ TEM UMA CONTA? ENTRE" – azul sólido */
|
||||
.login-btn {
|
||||
border-radius: 40px;
|
||||
border: none;
|
||||
|
|
@ -149,7 +249,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
/* Mensagens de erro */
|
||||
.text-danger.small {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,30 @@
|
|||
// src/app/pages/register.component.ts
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component, ElementRef, ViewChild, Inject, PLATFORM_ID } from '@angular/core';
|
||||
import { CommonModule, isPlatformBrowser } from '@angular/common';
|
||||
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({
|
||||
selector: 'app-register',
|
||||
standalone: true,
|
||||
imports: [CommonModule, ReactiveFormsModule, RouterLink],
|
||||
templateUrl: './register.html',
|
||||
styleUrls: ['./register.scss'] // ou .scss
|
||||
styleUrls: ['./register.scss']
|
||||
})
|
||||
export class Register {
|
||||
|
||||
registerForm: FormGroup;
|
||||
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({
|
||||
fullName: ['', [Validators.required, Validators.minLength(3)]],
|
||||
email: ['', [Validators.required, Validators.email]],
|
||||
|
|
@ -28,14 +36,26 @@ export class Register {
|
|||
});
|
||||
}
|
||||
|
||||
// Validador para confirmar senha
|
||||
private passwordsMatchValidator(group: FormGroup) {
|
||||
const pass = group.get('password')?.value;
|
||||
const confirm = group.get('confirmPassword')?.value;
|
||||
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 {
|
||||
this.apiError = '';
|
||||
|
||||
if (this.registerForm.invalid) {
|
||||
this.registerForm.markAllAsTouched();
|
||||
return;
|
||||
|
|
@ -43,33 +63,40 @@ export class Register {
|
|||
|
||||
this.isSubmitting = true;
|
||||
|
||||
// Por enquanto: apenas exibe os dados e "simula" o envio
|
||||
console.log('Dados de cadastro:', this.registerForm.value);
|
||||
const v = this.registerForm.value;
|
||||
|
||||
// TODO FUTURO: quando a API .NET estiver pronta,
|
||||
// você injeta um AuthService e envia para o backend:
|
||||
/*
|
||||
this.authService.register(this.registerForm.value).subscribe({
|
||||
next: () => { ... },
|
||||
error: () => { ... },
|
||||
complete: () => this.isSubmitting = false
|
||||
});
|
||||
*/
|
||||
const payload = {
|
||||
name: v.fullName,
|
||||
email: v.email,
|
||||
phone: v.phone,
|
||||
password: v.password,
|
||||
confirmPassword: v.confirmPassword
|
||||
};
|
||||
|
||||
// Simulação de atraso só para UX (remova se quiser)
|
||||
setTimeout(() => {
|
||||
this.authService.register(payload).subscribe({
|
||||
next: async () => {
|
||||
this.isSubmitting = false;
|
||||
this.registerForm.reset();
|
||||
alert('Cadastro realizado (simulado). Quando a API estiver pronta, isso irá de fato registrar o usuário.');
|
||||
}, 800);
|
||||
|
||||
// Se você não quer manter "logado" após cadastrar:
|
||||
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 {
|
||||
const control = this.registerForm.get(field);
|
||||
if (!control) return false;
|
||||
if (error) {
|
||||
return control.touched && control.hasError(error);
|
||||
}
|
||||
if (error) return control.touched && control.hasError(error);
|
||||
return control.touched && control.invalid;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 |
|
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 |
|
|
@ -0,0 +1,4 @@
|
|||
export const environment = {
|
||||
production: false,
|
||||
apiUrl: 'https://localhost:7205'
|
||||
};
|
||||
Loading…
Reference in New Issue