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/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",

View File

@ -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",

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 { 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])
),
]
};

View File

@ -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 },
];

View File

@ -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;

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>
<!-- 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>

View File

@ -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 {

View File

@ -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;
}
}

View File

@ -5,13 +5,12 @@
<!-- Barra / título -->
<div class="register-title mb-4">
<span class="register-icon">
<i class="bi bi-box-arrow-in-right"></i> <!-- ícone bootstrap -->
</span>
<h2 class="mb-0">Cadastre-se</h2>
<span class="register-icon">
<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>

View File

@ -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;
}

View File

@ -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;
}
}

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'
};