add AddProduct component with form for product creation and associated styles

This commit is contained in:
Vincent Guillet
2025-10-14 14:53:13 +02:00
parent f04f9fd93f
commit 8c3de85f36
9 changed files with 396 additions and 24 deletions

View File

@@ -0,0 +1,32 @@
.auth-wrap {
min-height: 100vh;
display: grid;
place-items: center;
padding: 16px;
}
.auth-card {
width: 100%;
max-width: 520px;
}
.form-grid {
display: grid;
gap: 16px;
margin-top: 16px;
}
.actions {
display: flex;
margin: 8px;
button {
display: inline-flex;
align-items: center;
gap: 8px;
}
}
.ml-8 {
margin-left: 8px;
}

View File

@@ -0,0 +1,148 @@
<section class="auth-wrap">
<mat-card class="auth-card">
<mat-card-header>
<mat-card-title>Gestion des produits</mat-card-title>
<mat-card-subtitle>Create a new account</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<form [formGroup]="addProductForm" (ngSubmit)="onProductAdd()" class="form-grid">
<!-- Title -->
<mat-form-field appearance="outline">
<mat-label>Titre</mat-label>
<input matInput
id="title"
name="title"
formControlName="title"
type="text"
placeholder="Ceci est un titre"
required>
@if (isFieldInvalid('title')) {
<mat-error>{{ getFieldError('title') }}</mat-error>
}
</mat-form-field>
<!-- Description -->
<mat-form-field appearance="outline">
<mat-label>Description</mat-label>
<input matInput
id="description"
name="description"
formControlName="description"
type="text"
required>
@if (isFieldInvalid('description')) {
<mat-error>{{ getFieldError('description') }}</mat-error>
}
</mat-form-field>
<!-- Category -->
<mat-form-field appearance="outline">
<mat-label>Catégorie</mat-label>
<mat-select disableRipple>
<mat-option value="1">Option 1</mat-option>
<mat-option value="2">Option 2</mat-option>
<mat-option value="3">Option 3</mat-option>
</mat-select>
</mat-form-field>
<!-- Condition -->
<mat-form-field appearance="outline">
<mat-label>État</mat-label>
<mat-select disableRipple>
<mat-option value="1">Option 1</mat-option>
<mat-option value="2">Option 2</mat-option>
<mat-option value="3">Option 3</mat-option>
</mat-select>
</mat-form-field>
<!-- Brand -->
<mat-form-field appearance="outline">
<mat-label>Marque</mat-label>
<mat-select disableRipple>
@for (brand of brands; track brand.name) {
<mat-option [value]="brand">{{ brand.name }}</mat-option>
}
</mat-select>
</mat-form-field>
<!-- Platform -->
<mat-form-field appearance="outline">
<mat-label>Plateforme</mat-label>
<mat-select disableRipple>
<mat-option value="1">Option 1</mat-option>
<mat-option value="2">Option 2</mat-option>
<mat-option value="3">Option 3</mat-option>
</mat-select>
</mat-form-field>
<!-- Complete state -->
<mat-checkbox formControlName="complete" id="complete">
Complet
</mat-checkbox>
@if (isFieldInvalid('complete')) {
<div class="mat-caption mat-error">{{ getFieldError('complete') }}</div>
}
<!-- manual included -->
<mat-checkbox formControlName="manual" id="manual">
Avec notice
</mat-checkbox>
@if (isFieldInvalid('manual')) {
<div class="mat-caption mat-error">{{ getFieldError('manual') }}</div>
}
<!-- Price -->
<mat-form-field appearance="outline">
<mat-label>Prix TTC</mat-label>
<input matInput
id="price"
name="price"
formControlName="price"
type="text"
required>
@if (isFieldInvalid('price')) {
<mat-error>{{ getFieldError('price') }}</mat-error>
}
</mat-form-field>
<!-- Quantity -->
<mat-form-field appearance="outline">
<mat-label>Quantité</mat-label>
<input matInput
id="quantity"
name="quantity"
formControlName="quantity"
type="text"
required>
@if (isFieldInvalid('quantity')) {
<mat-error>{{ getFieldError('quantity') }}</mat-error>
}
</mat-form-field>
<!-- Submit Button -->
<div class="actions">
<button mat-raised-button color="primary"
type="submit"
[disabled]="isLoading || addProductForm.invalid">
@if (isLoading) {
<mat-progress-spinner diameter="16" mode="indeterminate"></mat-progress-spinner>
<span class="ml-8">Ajout du produit…</span>
} @else {
Ajouter le produit
}
</button>
</div>
</form>
</mat-card-content>
<mat-divider></mat-divider>
<mat-card-actions align="end">
<span class="mat-body-small">
<a [routerLink]="'/login'">Voir la liste des produits</a>
</span>
</mat-card-actions>
</mat-card>
</section>

View File

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

View File

@@ -0,0 +1,163 @@
import {Component, inject, OnDestroy, OnInit} from '@angular/core';
import {
FormBuilder,
FormGroup,
FormsModule,
ReactiveFormsModule,
Validators
} from "@angular/forms";
import {MatButton} from "@angular/material/button";
import {
MatCard,
MatCardActions,
MatCardContent,
MatCardHeader,
MatCardSubtitle,
MatCardTitle
} from "@angular/material/card";
import {MatCheckbox} from "@angular/material/checkbox";
import {MatDivider} from "@angular/material/divider";
import {MatError, MatFormField, MatLabel} from "@angular/material/form-field";
import {MatInput} from "@angular/material/input";
import {MatProgressSpinner} from "@angular/material/progress-spinner";
import {MatOption, MatSelect} from '@angular/material/select';
import {Router, RouterLink} from '@angular/router';
import {Subscription} from 'rxjs';
import {BrandService} from '../../services/brand/brand.service';
import {Brand} from '../../models/brand/brand';
@Component({
selector: 'app-add-product',
standalone: true,
imports: [
FormsModule,
MatButton,
MatCard,
MatCardActions,
MatCardContent,
MatCardHeader,
MatCardSubtitle,
MatCardTitle,
MatCheckbox,
MatDivider,
MatError,
MatFormField,
MatInput,
MatLabel,
MatProgressSpinner,
ReactiveFormsModule,
MatSelect,
MatOption,
RouterLink
],
templateUrl: './add-product.component.html',
styleUrl: './add-product.component.css'
})
export class AddProductComponent implements OnInit, OnDestroy {
addProductForm: FormGroup;
isSubmitted = false;
isLoading = false;
brands: Brand[] = [];
private readonly router: Router = inject(Router);
private addProductSubscription: Subscription | null = null;
private readonly brandService: BrandService = inject(BrandService);
constructor(private readonly formBuilder: FormBuilder) {
this.addProductForm = this.formBuilder.group({
title: ['', [
Validators.required,
Validators.minLength(3),
Validators.maxLength(50),
Validators.pattern('^[a-zA-Z]+$')
]],
description: ['', [
Validators.required,
Validators.minLength(10),
Validators.maxLength(255),
Validators.pattern('^[a-zA-Z]+$')
]],
category: ['', [
Validators.required
]],
condition: ['', [
Validators.required
]],
brand: ['', [
Validators.required
]],
platform: ['', [
Validators.required
]],
complete: [true,
Validators.requiredTrue
],
manual: [true,
Validators.requiredTrue
],
price: ['', [
Validators.required,
Validators.min(0),
Validators.max(9999),
Validators.pattern('^[0-9]+$')
]],
quantity: ['', [
Validators.required,
Validators.min(1),
Validators.max(999),
Validators.pattern('^[0-9]+$')
]]
},
);
}
ngOnInit(): void {
this.brandService.getBrands().subscribe({
next: (brands) => {
this.brands = brands;
},
error: (error) => {
console.error('Error fetching brands:', error);
},
complete: () => {
console.log('Finished fetching brands:', this.brands);
}
});
}
ngOnDestroy(): void {
this.addProductSubscription?.unsubscribe();
}
onProductAdd() {
this.isSubmitted = true;
if (this.addProductForm.valid) {
this.isLoading = true;
const productData = this.addProductForm.value;
alert("Produit ajouté avec succès !");
console.log(productData);
}
}
isFieldInvalid(fieldName: string): boolean {
const field = this.addProductForm.get(fieldName);
return Boolean(field && field.invalid && (field.dirty || field.touched || this.isSubmitted));
}
getFieldError(fieldName: string): string {
const field = this.addProductForm.get(fieldName);
if (field && field.errors) {
if (field.errors['required']) return `Ce champ est obligatoire`;
if (field.errors['email']) return `Format d'email invalide`;
if (field.errors['minlength']) return `Minimum ${field.errors['minlength'].requiredLength} caractères`;
if (field.errors['maxlength']) return `Maximum ${field.errors['maxlength'].requiredLength} caractères`;
}
return '';
}
}

View File

@@ -1,13 +1,15 @@
<div class="home-container"> <div class="home-container">
@if (getUser(); as user) { @if (getUser(); as user) {
<h1>Welcome, {{ user.firstName }}!</h1> <h1>Bonjour, {{ user.firstName }}!</h1>
<p>What would you like to do today?</p> <p>Que souhaitez-vous faire ?</p>
<br>
<button mat-flat-button [routerLink]="'/add-product'">Ajouter un nouveau produit</button>
<button mat-raised-button [routerLink]="'/products'">Voir la liste des produits</button>
} @else { } @else {
<h1>Welcome to the demo</h1> <h2>Gestion des produits</h2>
<p>Create an account or sign in to get started.</p>
<div class="home-actions"> <div class="home-actions">
<button mat-flat-button [routerLink]="'/login'">Login</button> <button mat-flat-button [routerLink]="'/login'">Se connecter</button>
<button mat-raised-button [routerLink]="'/register'">Sign Up</button> <button mat-raised-button [routerLink]="'/register'">S'inscrire</button>
</div> </div>
} }
</div> </div>

View File

@@ -2,13 +2,15 @@ import {Component, inject} from '@angular/core';
import {MatButton} from '@angular/material/button'; import {MatButton} from '@angular/material/button';
import {AuthService} from '../../services/auth/auth.service'; import {AuthService} from '../../services/auth/auth.service';
import {RouterLink} from '@angular/router'; import {RouterLink} from '@angular/router';
import {AddProductComponent} from '../add-product/add-product.component';
@Component({ @Component({
selector: 'app-home', selector: 'app-home',
standalone: true, standalone: true,
imports: [ imports: [
MatButton, MatButton,
RouterLink RouterLink,
AddProductComponent
], ],
templateUrl: './home.component.html', templateUrl: './home.component.html',
styleUrl: './home.component.css' styleUrl: './home.component.css'

View File

@@ -1,17 +1,17 @@
<div id="container"> <div id="container">
<form (submit)="login()" [formGroup]="loginFormGroup"> <form (submit)="login()" [formGroup]="loginFormGroup">
<h3>Sign in</h3> <h3>Se connecter</h3>
<mat-form-field> <mat-form-field>
<mat-label>Username</mat-label> <mat-label>Nom d'utilisateur</mat-label>
<input matInput formControlName="username"> <input matInput formControlName="username">
</mat-form-field> </mat-form-field>
<mat-form-field> <mat-form-field>
<mat-label>Password</mat-label> <mat-label>Mot de passe</mat-label>
<input matInput type="password" formControlName="password"> <input matInput type="password" formControlName="password">
</mat-form-field> </mat-form-field>
<button mat-flat-button [disabled]="loginFormGroup.invalid">Login</button> <button mat-flat-button [disabled]="loginFormGroup.invalid">Se connecter</button>
@if (invalidCredentials) { @if (invalidCredentials) {
<mat-error>Invalid credentials</mat-error> <mat-error>Nom d'utilisateur ou mot de passe invalide</mat-error>
} }
</form> </form>
</div> </div>

View File

@@ -51,9 +51,11 @@ export class LoginComponent implements OnDestroy {
this.loginFormGroup.value as Credentials).subscribe({ this.loginFormGroup.value as Credentials).subscribe({
next: (result: User | null | undefined) => { next: (result: User | null | undefined) => {
this.navigateHome(); this.navigateHome();
alert('Login successful!');
}, },
error: (error) => { error: (error) => {
console.log(error); console.log(error);
alert(error.message);
this.invalidCredentials = true; this.invalidCredentials = true;
} }
}); });

View File

@@ -1,8 +1,8 @@
<section class="auth-wrap"> <section class="auth-wrap">
<mat-card class="auth-card"> <mat-card class="auth-card">
<mat-card-header> <mat-card-header>
<mat-card-title>Registration</mat-card-title> <mat-card-title>Inscription</mat-card-title>
<mat-card-subtitle>Create a new account</mat-card-subtitle> <mat-card-subtitle>Créer un nouveau compte</mat-card-subtitle>
</mat-card-header> </mat-card-header>
<mat-card-content> <mat-card-content>
@@ -10,7 +10,7 @@
<!-- First Name --> <!-- First Name -->
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-label>First Name</mat-label> <mat-label>Prénom</mat-label>
<input matInput <input matInput
id="firstName" id="firstName"
name="firstName" name="firstName"
@@ -25,7 +25,7 @@
<!-- Last Name --> <!-- Last Name -->
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-label>Last Name</mat-label> <mat-label>Nom</mat-label>
<input matInput <input matInput
id="lastName" id="lastName"
name="lastName" name="lastName"
@@ -40,7 +40,7 @@
<!-- Username --> <!-- Username -->
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-label>Username</mat-label> <mat-label>Nom d'utilisateur</mat-label>
<input matInput <input matInput
id="username" id="username"
name="username" name="username"
@@ -70,7 +70,7 @@
<!-- Password --> <!-- Password -->
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-label>Password</mat-label> <mat-label>Mot de passe</mat-label>
<input matInput <input matInput
id="password" id="password"
name="password" name="password"
@@ -85,7 +85,7 @@
<!-- Password confirmation --> <!-- Password confirmation -->
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-label>Confirm Password</mat-label> <mat-label>Confirmer le mot de passe</mat-label>
<input matInput <input matInput
id="confirmPassword" id="confirmPassword"
name="confirmPassword" name="confirmPassword"
@@ -103,7 +103,7 @@
<!-- Terms and Conditions --> <!-- Terms and Conditions -->
<mat-checkbox formControlName="termsAndConditions" id="iAgree"> <mat-checkbox formControlName="termsAndConditions" id="iAgree">
I agree to the <a href="#" target="_blank" rel="noopener">terms and conditions</a> J'accepte les <a href="#" target="_blank" rel="noopener">conditions générales d'utilisation</a>
</mat-checkbox> </mat-checkbox>
@if (isFieldInvalid('termsAndConditions')) { @if (isFieldInvalid('termsAndConditions')) {
<div class="mat-caption mat-error">{{ getFieldError('termsAndConditions') }}</div> <div class="mat-caption mat-error">{{ getFieldError('termsAndConditions') }}</div>
@@ -116,9 +116,9 @@
[disabled]="isLoading || registerForm.invalid"> [disabled]="isLoading || registerForm.invalid">
@if (isLoading) { @if (isLoading) {
<mat-progress-spinner diameter="16" mode="indeterminate"></mat-progress-spinner> <mat-progress-spinner diameter="16" mode="indeterminate"></mat-progress-spinner>
<span class="ml-8">Signing up</span> <span class="ml-8">Inscription</span>
} @else { } @else {
Sign up S'inscrire
} }
</button> </button>
</div> </div>
@@ -129,8 +129,8 @@
<mat-card-actions align="end"> <mat-card-actions align="end">
<span class="mat-body-small"> <span class="mat-body-small">
Already have an account? Vous avez déjà un compte ?
<a [routerLink]="'/login'">Sign in</a> <a [routerLink]="'/login'">Se connecter</a>
</span> </span>
</mat-card-actions> </mat-card-actions>
</mat-card> </mat-card>