Form Add + add bootstrap

master
Matis MAZINGUE 10 months ago
parent c5b01c39db
commit c65d6b029c

@ -55,9 +55,13 @@
],
"styles": [
"@angular/material/prebuilt-themes/azure-blue.css",
"src/styles.scss"
"src/styles.scss",
"node_modules/bootstrap/dist/css/bootstrap.min.css"
],
"scripts": []
"scripts": [
"node_modules/@popperjs/core/dist/umd/popper.min.js",
"node_modules/bootstrap/dist/js/bootstrap.min.js"
]
},
"configurations": {
"production": {

56
package-lock.json generated

@ -18,6 +18,9 @@
"@angular/platform-browser": "^18.0.0",
"@angular/platform-browser-dynamic": "^18.0.0",
"@angular/router": "^18.0.0",
"bootstrap": "^5.3.0",
"bootstrap-icons": "^1.11.3",
"popper.js": "^1.16.1",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.14.3"
@ -4430,6 +4433,16 @@
"node": ">=14"
}
},
"node_modules/@popperjs/core": {
"version": "2.11.8",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.18.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz",
@ -5659,6 +5672,39 @@
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
"dev": true
},
"node_modules/bootstrap": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.0.tgz",
"integrity": "sha512-UnBV3E3v4STVNQdms6jSGO2CvOkjUMdDAVR2V5N4uCMdaIkaQjbcEAMqRimDHIs4uqBYzDAKCQwCB+97tJgHQw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/twbs"
},
{
"type": "opencollective",
"url": "https://opencollective.com/bootstrap"
}
],
"peerDependencies": {
"@popperjs/core": "^2.11.7"
}
},
"node_modules/bootstrap-icons": {
"version": "1.11.3",
"resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.11.3.tgz",
"integrity": "sha512-+3lpHrCw/it2/7lBL15VR0HEumaBss0+f/Lb6ZvHISn1mlK83jjFpooTLsMWbIjJMDjDjOExMsTxnXSIT4k4ww==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/twbs"
},
{
"type": "opencollective",
"url": "https://opencollective.com/bootstrap"
}
]
},
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@ -10777,6 +10823,16 @@
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
}
},
"node_modules/popper.js": {
"version": "1.16.1",
"resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz",
"integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==",
"deprecated": "You can find the new Popper v2 at @popperjs/core, this package is dedicated to the legacy v1",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
}
},
"node_modules/postcss": {
"version": "8.4.38",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz",

@ -20,6 +20,9 @@
"@angular/platform-browser": "^18.0.0",
"@angular/platform-browser-dynamic": "^18.0.0",
"@angular/router": "^18.0.0",
"bootstrap": "^5.3.0",
"bootstrap-icons": "^1.11.3",
"popper.js": "^1.16.1",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.14.3"
@ -37,4 +40,4 @@
"karma-jasmine-html-reporter": "~2.1.0",
"typescript": "~5.4.2"
}
}
}

@ -1,4 +1,7 @@
<app-menu></app-menu>
<main>
<router-outlet></router-outlet>
</main>
<body class="ms-5">
<app-menu></app-menu>
<main>
<router-outlet></router-outlet>
</main>
</body>

@ -1,37 +1,70 @@
<form (ngSubmit)="onSubmit()" #recipeForm="ngForm">
<div>
<label for="name">Name:</label>
<input
type="text"
id="name"
[(ngModel)]="recipe.name"
name="name"
required
/>
<form (ngSubmit)="onSubmit(); recipeForm.ngSubmit.emit()" #recipeForm="ngForm" class="ng-submitted">
<div class="form-group my-2 col-md-4">
<label for="name">Name</label>
<input class="form-control"
placeholder="Name"
type="text"
id="name"
[(ngModel)]="recipe.name"
name="name"
required
#nameField="ngModel"
>
<div *ngIf="nameField.invalid && (nameField.dirty || nameField.touched)" class="text-danger">
<div *ngIf="nameField.errors && nameField.errors['required']">Name is required.</div>
</div>
</div>
<div>
<label for="description">Description:</label>
<textarea
id="description"
[(ngModel)]="recipe.description"
name="description"
required
<div class="form-group my-2 col-md-4">
<label for="description">Description</label>
<textarea class="form-control"
id="description"
[(ngModel)]="recipe.description"
name="description"
required
rows="2"
maxlength="{{ maxDescriptionLength }}"
#descriptionField="ngModel"
></textarea>
<div class="text-muted text-end">Characters left: {{ maxDescriptionLength - recipe.description.length }}</div>
<div *ngIf="descriptionField.invalid && (descriptionField.dirty || descriptionField.touched)" class="text-danger">
<div *ngIf="descriptionField.errors && descriptionField.errors['required']">Description is required.</div>
</div>
</div>
<div>
<label for="image">Image:</label>
<input type="file" id="image" (change)="onFileChange($event)" />
<div *ngIf="imageError" style="color: red">{{ imageError }}</div>
<div class="form-group my-3">
<div class="custom-file">
<label for="image">Image</label>
<input type="file" class="custom-file-input ms-1" id="image" required (change)="onFileChange($event)">
<div *ngIf="imageError" style="color: red">{{ imageError }}</div>
<div *ngIf="recipeForm.submitted && recipeForm.invalid && !recipeForm.controls['image'].valid" class="text-danger">
Image is required.
</div>
</div>
</div>
<div *ngFor="let ingredient of ingredients">
<label>
{{ ingredient.name }}:
<mat-form-field class="my-3 me-3">
<mat-label>Ingredients</mat-label>
<mat-select [(ngModel)]="selectedIngredient" name="selectedIngredient">
<mat-option *ngFor="let ingredient of ingredients" [value]="ingredient.id">
{{ ingredient.name }}
</mat-option>
</mat-select>
</mat-form-field>
<button class="me-3" mat-raised-button type="button" (click)="addIngredient()">Add Ingredient</button>
<div class="mb-3" *ngFor="let ingredientId of getIngredientKeys()">
<div>
{{ findIngredientName(ingredientId) }}
<input
type="number"
[(ngModel)]="ingredientQuantities[ingredient.id]"
name="quantity{{ ingredient.id }}"
[(ngModel)]="ingredientQuantities[ingredientId]"
name="quantity_{{ ingredientId }}"
min="1"
/>
</label>
</div>
</div>
<button type="submit" [disabled]="!recipeForm.form.valid">Add Recipe</button>
<button mat-raised-button type="submit" [disabled]="!recipeForm.form.valid">Add Recipe</button>
</form>

@ -4,11 +4,21 @@ import { CommonModule } from '@angular/common';
import { IngredientService } from '../services/ingredient.service';
import { Ingredient } from '../models/ingredient';
import { Recipe } from '../models/recipe';
import { Router } from '@angular/router';
import { MatButtonModule } from '@angular/material/button';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatSelectModule } from '@angular/material/select';
@Component({
selector: 'app-recipe-add',
standalone: true,
imports: [CommonModule, FormsModule],
imports: [
CommonModule,
FormsModule,
MatButtonModule,
MatFormFieldModule,
MatSelectModule
],
templateUrl: './recipe-add.component.html',
styleUrls: ['./recipe-add.component.scss'],
})
@ -16,9 +26,14 @@ export class RecipeAddComponent implements OnInit {
recipe: Recipe = new Recipe(0, '', '', '', []);
ingredients: Ingredient[] = [];
ingredientQuantities: { [key: number]: number } = {};
selectedIngredient: number | null = null;
imageError: string | null = null;
maxDescriptionLength: number = 200; // Maximum length for description
constructor(private ingredientService: IngredientService) {}
constructor(
private ingredientService: IngredientService,
private router: Router
) {}
ngOnInit(): void {
this.ingredients = this.ingredientService.getIngredients();
@ -48,11 +63,36 @@ export class RecipeAddComponent implements OnInit {
}
}
addIngredient(): void {
if (this.selectedIngredient !== null && !this.ingredientQuantities[this.selectedIngredient]) {
this.ingredientQuantities[this.selectedIngredient] = 1; // Default quantity
}
}
getIngredientKeys(): number[] {
return Object.keys(this.ingredientQuantities).map(key => parseInt(key, 10));
}
findIngredientName(id: number): string {
const ingredient = this.ingredients.find(ingredient => ingredient.id === id);
return ingredient ? ingredient.name : '';
}
onSubmit(): void {
this.recipe.ingredients = Object.keys(this.ingredientQuantities).map(
if (!this.recipe.name.trim() || !this.recipe.description.trim() || !this.recipe.image) {
alert('Please fill out Name, Description, and Image fields.');
return;
}
if (this.recipe.description.length > this.maxDescriptionLength) {
alert(`Description should not exceed ${this.maxDescriptionLength} characters.`);
return;
}
this.recipe.ingredients = this.getIngredientKeys().map(
(id) => {
const ingredient = this.ingredientService.getIngredient(Number(id));
return new Ingredient(Number(id), ingredient.name);
const ingredient = this.ingredientService.getIngredient(id);
return new Ingredient(id, ingredient.name);
}
);
@ -60,5 +100,6 @@ export class RecipeAddComponent implements OnInit {
this.recipe.id = recipes.length ? recipes[recipes.length - 1].id + 1 : 1;
recipes.push(this.recipe);
localStorage.setItem('recipes', JSON.stringify(recipes));
this.router.navigate(['/list']);
}
}

@ -1,4 +1,5 @@
/* You can add global styles to this file, and also import other style files */
@import "bootstrap/dist/css/bootstrap.min.css";
html, body { height: 100%; }
body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }

Loading…
Cancel
Save