diff --git a/package-lock.json b/package-lock.json index 5e2437a..2b20021 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 35691de..403a9f7 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/app/app.config.ts b/src/app/app.config.ts index 969812a..e4018c5 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -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]) + ), ] }; diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 213a3e4..92fc1d4 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -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 }, ]; diff --git a/src/app/components/footer/footer.scss b/src/app/components/footer/footer.scss index fa49a5a..ab19d6c 100644 --- a/src/app/components/footer/footer.scss +++ b/src/app/components/footer/footer.scss @@ -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; diff --git a/src/app/interceptors/auth.interceptor.ts b/src/app/interceptors/auth.interceptor.ts new file mode 100644 index 0000000..a8753c8 --- /dev/null +++ b/src/app/interceptors/auth.interceptor.ts @@ -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); +}; diff --git a/src/app/pages/geral/geral.html b/src/app/pages/geral/geral.html new file mode 100644 index 0000000..7f35267 --- /dev/null +++ b/src/app/pages/geral/geral.html @@ -0,0 +1 @@ +

geral works!

diff --git a/src/app/pages/geral/geral.scss b/src/app/pages/geral/geral.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/pages/geral/geral.spec.ts b/src/app/pages/geral/geral.spec.ts new file mode 100644 index 0000000..8cbf6c0 --- /dev/null +++ b/src/app/pages/geral/geral.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { Geral } from './geral'; + +describe('Geral', () => { + let component: Geral; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [Geral] + }) + .compileComponents(); + + fixture = TestBed.createComponent(Geral); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/pages/geral/geral.ts b/src/app/pages/geral/geral.ts new file mode 100644 index 0000000..452ad36 --- /dev/null +++ b/src/app/pages/geral/geral.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-geral', + imports: [], + templateUrl: './geral.html', + styleUrl: './geral.scss', +}) +export class Geral { + +} diff --git a/src/app/pages/login/login.html b/src/app/pages/login/login.html index 42ae1e8..394c7b1 100644 --- a/src/app/pages/login/login.html +++ b/src/app/pages/login/login.html @@ -36,6 +36,11 @@ + +
+ {{ apiError }} +
+ + +
+ {{ toastMessage }} +
+ + diff --git a/src/app/pages/login/login.scss b/src/app/pages/login/login.scss index 89a68be..390de2c 100644 --- a/src/app/pages/login/login.scss +++ b/src/app/pages/login/login.scss @@ -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 { diff --git a/src/app/pages/login/login.ts b/src/app/pages/login/login.ts index 4235426..616e55b 100644 --- a/src/app/pages/login/login.ts +++ b/src/app/pages/login/login.ts @@ -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; + this.apiError = err?.error ?? 'Erro ao fazer login.'; + } }); - */ - - setTimeout(() => { - this.isSubmitting = false; - alert('Login enviado (simulado). Depois conectamos na API .NET 😉'); - }, 800); } 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; } } diff --git a/src/app/pages/register/register.html b/src/app/pages/register/register.html index 886f256..1dcb6f4 100644 --- a/src/app/pages/register/register.html +++ b/src/app/pages/register/register.html @@ -5,13 +5,12 @@
- - - -

Cadastre-se

+ + + +

Cadastre-se

-
@@ -87,7 +86,12 @@ - + +
+ {{ apiError }} +
+ + + +
+ {{ toastMessage }} +
+ + diff --git a/src/app/pages/register/register.scss b/src/app/pages/register/register.scss index b69c58c..fba7b44 100644 --- a/src/app/pages/register/register.scss +++ b/src/app/pages/register/register.scss @@ -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; } diff --git a/src/app/pages/register/register.ts b/src/app/pages/register/register.ts index f6905de..ecd2601 100644 --- a/src/app/pages/register/register.ts +++ b/src/app/pages/register/register.ts @@ -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 + }; + + this.authService.register(payload).subscribe({ + next: async () => { + this.isSubmitting = false; + + // 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.'; + } }); - */ - - // Simulação de atraso só para UX (remova se quiser) - setTimeout(() => { - this.isSubmitting = false; - this.registerForm.reset(); - alert('Cadastro realizado (simulado). Quando a API estiver pronta, isso irá de fato registrar o usuário.'); - }, 800); } 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; } } diff --git a/src/app/services/auth.service.ts b/src/app/services/auth.service.ts new file mode 100644 index 0000000..f88487a --- /dev/null +++ b/src/app/services/auth.service.ts @@ -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; + } +} diff --git a/src/assets/wallpaper/mobile.png b/src/assets/wallpaper/mobile.png new file mode 100644 index 0000000..00f83de Binary files /dev/null and b/src/assets/wallpaper/mobile.png differ diff --git a/src/assets/wallpaper/registro_qualidade3.png b/src/assets/wallpaper/registro_login.png similarity index 100% rename from src/assets/wallpaper/registro_qualidade3.png rename to src/assets/wallpaper/registro_login.png diff --git a/src/assets/wallpaper/registro_qualidade.png b/src/assets/wallpaper/registro_qualidade.png deleted file mode 100644 index 4d3d12f..0000000 Binary files a/src/assets/wallpaper/registro_qualidade.png and /dev/null differ diff --git a/src/assets/wallpaper/registro_qualidade2.png b/src/assets/wallpaper/registro_qualidade2.png deleted file mode 100644 index e560b09..0000000 Binary files a/src/assets/wallpaper/registro_qualidade2.png and /dev/null differ diff --git a/src/environments/environment.ts b/src/environments/environment.ts new file mode 100644 index 0000000..76c4506 --- /dev/null +++ b/src/environments/environment.ts @@ -0,0 +1,4 @@ +export const environment = { + production: false, + apiUrl: 'https://localhost:7205' +};