add AddProduct component with form for product creation and associated styles
This commit is contained in:
32
client/src/app/pages/add-product/add-product.component.css
Normal file
32
client/src/app/pages/add-product/add-product.component.css
Normal 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;
|
||||
}
|
||||
148
client/src/app/pages/add-product/add-product.component.html
Normal file
148
client/src/app/pages/add-product/add-product.component.html
Normal 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>
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
163
client/src/app/pages/add-product/add-product.component.ts
Normal file
163
client/src/app/pages/add-product/add-product.component.ts
Normal 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 '';
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,15 @@
|
||||
<div class="home-container">
|
||||
@if (getUser(); as user) {
|
||||
<h1>Welcome, {{ user.firstName }}!</h1>
|
||||
<p>What would you like to do today?</p>
|
||||
<h1>Bonjour, {{ user.firstName }}!</h1>
|
||||
<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 {
|
||||
<h1>Welcome to the demo</h1>
|
||||
<p>Create an account or sign in to get started.</p>
|
||||
<h2>Gestion des produits</h2>
|
||||
<div class="home-actions">
|
||||
<button mat-flat-button [routerLink]="'/login'">Login</button>
|
||||
<button mat-raised-button [routerLink]="'/register'">Sign Up</button>
|
||||
<button mat-flat-button [routerLink]="'/login'">Se connecter</button>
|
||||
<button mat-raised-button [routerLink]="'/register'">S'inscrire</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -2,13 +2,15 @@ import {Component, inject} from '@angular/core';
|
||||
import {MatButton} from '@angular/material/button';
|
||||
import {AuthService} from '../../services/auth/auth.service';
|
||||
import {RouterLink} from '@angular/router';
|
||||
import {AddProductComponent} from '../add-product/add-product.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-home',
|
||||
standalone: true,
|
||||
imports: [
|
||||
MatButton,
|
||||
RouterLink
|
||||
RouterLink,
|
||||
AddProductComponent
|
||||
],
|
||||
templateUrl: './home.component.html',
|
||||
styleUrl: './home.component.css'
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
<div id="container">
|
||||
<form (submit)="login()" [formGroup]="loginFormGroup">
|
||||
<h3>Sign in</h3>
|
||||
<h3>Se connecter</h3>
|
||||
<mat-form-field>
|
||||
<mat-label>Username</mat-label>
|
||||
<mat-label>Nom d'utilisateur</mat-label>
|
||||
<input matInput formControlName="username">
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<mat-label>Password</mat-label>
|
||||
<mat-label>Mot de passe</mat-label>
|
||||
<input matInput type="password" formControlName="password">
|
||||
</mat-form-field>
|
||||
<button mat-flat-button [disabled]="loginFormGroup.invalid">Login</button>
|
||||
<button mat-flat-button [disabled]="loginFormGroup.invalid">Se connecter</button>
|
||||
@if (invalidCredentials) {
|
||||
<mat-error>Invalid credentials</mat-error>
|
||||
<mat-error>Nom d'utilisateur ou mot de passe invalide</mat-error>
|
||||
}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -51,9 +51,11 @@ export class LoginComponent implements OnDestroy {
|
||||
this.loginFormGroup.value as Credentials).subscribe({
|
||||
next: (result: User | null | undefined) => {
|
||||
this.navigateHome();
|
||||
alert('Login successful!');
|
||||
},
|
||||
error: (error) => {
|
||||
console.log(error);
|
||||
alert(error.message);
|
||||
this.invalidCredentials = true;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<section class="auth-wrap">
|
||||
<mat-card class="auth-card">
|
||||
<mat-card-header>
|
||||
<mat-card-title>Registration</mat-card-title>
|
||||
<mat-card-subtitle>Create a new account</mat-card-subtitle>
|
||||
<mat-card-title>Inscription</mat-card-title>
|
||||
<mat-card-subtitle>Créer un nouveau compte</mat-card-subtitle>
|
||||
</mat-card-header>
|
||||
|
||||
<mat-card-content>
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
<!-- First Name -->
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>First Name</mat-label>
|
||||
<mat-label>Prénom</mat-label>
|
||||
<input matInput
|
||||
id="firstName"
|
||||
name="firstName"
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
<!-- Last Name -->
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Last Name</mat-label>
|
||||
<mat-label>Nom</mat-label>
|
||||
<input matInput
|
||||
id="lastName"
|
||||
name="lastName"
|
||||
@@ -40,7 +40,7 @@
|
||||
|
||||
<!-- Username -->
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Username</mat-label>
|
||||
<mat-label>Nom d'utilisateur</mat-label>
|
||||
<input matInput
|
||||
id="username"
|
||||
name="username"
|
||||
@@ -70,7 +70,7 @@
|
||||
|
||||
<!-- Password -->
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Password</mat-label>
|
||||
<mat-label>Mot de passe</mat-label>
|
||||
<input matInput
|
||||
id="password"
|
||||
name="password"
|
||||
@@ -85,7 +85,7 @@
|
||||
|
||||
<!-- Password confirmation -->
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Confirm Password</mat-label>
|
||||
<mat-label>Confirmer le mot de passe</mat-label>
|
||||
<input matInput
|
||||
id="confirmPassword"
|
||||
name="confirmPassword"
|
||||
@@ -103,7 +103,7 @@
|
||||
|
||||
<!-- Terms and Conditions -->
|
||||
<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>
|
||||
@if (isFieldInvalid('termsAndConditions')) {
|
||||
<div class="mat-caption mat-error">{{ getFieldError('termsAndConditions') }}</div>
|
||||
@@ -116,9 +116,9 @@
|
||||
[disabled]="isLoading || registerForm.invalid">
|
||||
@if (isLoading) {
|
||||
<mat-progress-spinner diameter="16" mode="indeterminate"></mat-progress-spinner>
|
||||
<span class="ml-8">Signing up…</span>
|
||||
<span class="ml-8">Inscription…</span>
|
||||
} @else {
|
||||
Sign up
|
||||
S'inscrire
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
@@ -129,8 +129,8 @@
|
||||
|
||||
<mat-card-actions align="end">
|
||||
<span class="mat-body-small">
|
||||
Already have an account?
|
||||
<a [routerLink]="'/login'">Sign in</a>
|
||||
Vous avez déjà un compte ?
|
||||
<a [routerLink]="'/login'">Se connecter</a>
|
||||
</span>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
||||
|
||||
Reference in New Issue
Block a user