Compare commits

..

No commits in common. 'master' and 'listIngredients' have entirely different histories.

@ -4,11 +4,11 @@
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="c5a06892-a370-4c6a-a9cf-5d61f93a285b" name="Changes" comment="Adding Command section, local storage exceed problem, adding error managing">
<change afterPath="$PROJECT_DIR$/daidokoro/src/app/service/command.service.ts" afterDir="false" />
<list default="true" id="c5a06892-a370-4c6a-a9cf-5d61f93a285b" name="Changes" comment="Adding Front">
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/daidokoro/src/app/component/command/command.component.html" beforeDir="false" afterPath="$PROJECT_DIR$/daidokoro/src/app/component/command/command.component.html" afterDir="false" />
<change beforePath="$PROJECT_DIR$/daidokoro/src/app/component/command/command.component.ts" beforeDir="false" afterPath="$PROJECT_DIR$/daidokoro/src/app/component/command/command.component.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/daidokoro/.gitignore" beforeDir="false" afterPath="$PROJECT_DIR$/daidokoro/.gitignore" afterDir="false" />
<change beforePath="$PROJECT_DIR$/daidokoro/src/app/component/accueil/accueil.component.html" beforeDir="false" afterPath="$PROJECT_DIR$/daidokoro/src/app/component/accueil/accueil.component.html" afterDir="false" />
<change beforePath="$PROJECT_DIR$/daidokoro/src/app/component/accueil/accueil.component.ts" beforeDir="false" afterPath="$PROJECT_DIR$/daidokoro/src/app/component/accueil/accueil.component.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/daidokoro/src/app/component/recipe-list/recipe-list.component.html" beforeDir="false" afterPath="$PROJECT_DIR$/daidokoro/src/app/component/recipe-list/recipe-list.component.html" afterDir="false" />
<change beforePath="$PROJECT_DIR$/daidokoro/src/app/component/recipe-list/recipe-list.component.ts" beforeDir="false" afterPath="$PROJECT_DIR$/daidokoro/src/app/component/recipe-list/recipe-list.component.ts" afterDir="false" />
</list>
@ -27,7 +27,7 @@
<component name="Git.Settings">
<option name="RECENT_BRANCH_BY_REPOSITORY">
<map>
<entry key="$PROJECT_DIR$" value="formFront" />
<entry key="$PROJECT_DIR$" value="front" />
</map>
</option>
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
@ -87,7 +87,6 @@
<workItem from="1718625479373" duration="4985000" />
<workItem from="1719217543390" duration="31000" />
<workItem from="1719217580949" duration="135000" />
<workItem from="1719755658119" duration="6807000" />
</task>
<task id="LOCAL-00001" summary="Adding Front">
<option name="closed" value="true" />
@ -97,31 +96,7 @@
<option name="project" value="LOCAL" />
<updated>1718630357884</updated>
</task>
<task id="LOCAL-00002" summary="Adding Form front with the new form">
<option name="closed" value="true" />
<created>1719666928847</created>
<option name="number" value="00002" />
<option name="presentableId" value="LOCAL-00002" />
<option name="project" value="LOCAL" />
<updated>1719666928847</updated>
</task>
<task id="LOCAL-00003" summary="Adding Ingredients from API, add and edit working good, front of the ingredients page">
<option name="closed" value="true" />
<created>1719757925681</created>
<option name="number" value="00003" />
<option name="presentableId" value="LOCAL-00003" />
<option name="project" value="LOCAL" />
<updated>1719757925681</updated>
</task>
<task id="LOCAL-00004" summary="Adding recipe-details front">
<option name="closed" value="true" />
<created>1719760748129</created>
<option name="number" value="00004" />
<option name="presentableId" value="LOCAL-00004" />
<option name="project" value="LOCAL" />
<updated>1719760748129</updated>
</task>
<option name="localTasksCounter" value="5" />
<option name="localTasksCounter" value="2" />
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
@ -141,10 +116,6 @@
<component name="VcsManagerConfiguration">
<option name="ADD_EXTERNAL_FILES_SILENTLY" value="true" />
<MESSAGE value="Adding Front" />
<MESSAGE value="Adding Form front with the new form" />
<MESSAGE value="Adding Ingredients from API, add and edit working good, front of the ingredients page" />
<MESSAGE value="Adding recipe-details front" />
<MESSAGE value="Adding Command section, local storage exceed problem, adding error managing" />
<option name="LAST_COMMIT_MESSAGE" value="Adding Command section, local storage exceed problem, adding error managing" />
<option name="LAST_COMMIT_MESSAGE" value="Adding Front" />
</component>
</project>

@ -12,72 +12,9 @@
![BuyMeACoffee](https://img.shields.io/badge/Buy%20Me%20a%20Coffee-ffdd00?style=for-the-badge&logo=buy-me-a-coffee&logoColor=black)
</div>
# Daidokoro
## Lancer l'application
Pour lancer l'application, veuillez suivre les étapes suivantes :
1. Cloner le repository du projet.
2. Se rendre dans le dossier `daidokoro`.
3. Exécuter la commande suivante : `ng serve --open`.
## Présentation rapide du projet
Daidokoro est un projet réalisé en Angular. Il s'agit d'une application web de gestion de recettes et d'ingrédients.
Les fonctionnalités incluent la gestion des utilisateurs, la consultation, l'ajout, la modification et la suppression d'ingrédients, ainsi que la création et la commande de recettes.
Le projet est conçu pour être intuitif et facile à utiliser, avec une interface utilisateur agréable et des fonctionnalités robustes.
## Prérequis demandés
Pour ce projet, les fonctionnalités suivantes étaient requises :
- Une barre de navigation commune à tout le site avec :
- Une page de connexion (/login) permettant de créer un cookie ou un local storage avec une valeur `isAdmin = true` et une redirection vers la page des ingrédients. Le bouton de connexion n'apparaît que si la valeur `isAdmin` est `false` ou inexistante.
- Une page de déconnexion (/logout) ayant le comportement inverse de la connexion.
- Un bouton vers la page des ingrédients avec une guard qui vérifie la valeur `isAdmin`. Si `true`, l'accès est permis, sinon, redirection vers la page d'accueil.
- Listing des ingrédients existants.
- Ajout d'un ingrédient.
- Modification d'un ingrédient.
- Connexion à une API pour les ingrédients, remplaçant le stub actuel.
- Sur la page d'accueil :
- Liste de recettes chargées depuis le local storage avec une image enregistrée ou un placeholder (`placehold.co/50x50`).
- Pagination de la liste de recettes.
- Un bouton pour ajouter une recette, faisant apparaître un formulaire avec :
- Nom
- Image
- Description
- Liste d'ingrédients avec possibilité de rajouter et de supprimer des ingrédients, au moins un devant être sélectionné.
- Un bouton "Commander" sur chaque recette qui ajoute la recette commandée à une liste "Commande en cours" sur la page d'accueil.
- Un bouton "Détail" redirigeant vers `/recettes/:id` et affichant la recette avec tous ses attributs.
## Réalisation des prérequis
Nous avons mis en œuvre toutes les fonctionnalités demandées :
- **Barre de navigation :** Elle est présente sur toutes les pages et comprend les boutons de connexion et de déconnexion. La gestion de `isAdmin` est effectuée via le local storage.
- **Guard d'accès aux ingrédients :** Nous avons implémenté une guard qui vérifie si l'utilisateur est connecté avant de permettre l'accès à la page des ingrédients.
- **Gestion des ingrédients :** Nous avons créé des pages pour lister, ajouter et modifier les ingrédients via l'API.
- **Page d'accueil :** La liste des recettes est chargée depuis le local storage et paginée. Un formulaire d'ajout de recette permet de créer de nouvelles recettes avec tous les champs requis. Chaque recette a des boutons "Commander" et "Détail".
## Fonctionnalités supplémentaires
En plus des fonctionnalités de base demandées, nous avons :
- Implémenté un style personnalisé pour l'application afin d'améliorer l'expérience utilisateur.
## Difficultés
Nous avons aussi rencontrer certaines difficultés :
- L'application a du mal à stocker dans le local storage à partir d'un certain quota, il est limité.
- Nous avons tenté de déployer l'application mais sans succès, le hub de codefirst était surchargé.
## Organisation du travail
Nous avons utilisé un système de branches et de merge requests pour organiser notre travail de manière efficace. Chaque fonctionnalité a été développée sur une branche dédiée et intégrée dans la branche principale via des merge requests. Cela nous a permis de travailler en parallèle tout en maintenant un code base stable et propre.
## Conclusion
Le projet Daidokoro a été une expérience enrichissante, nous permettant d'acquérir des connaissances en Angular et en gestion de projets.
# :bookmark: Présentation
このプロジェクトはレシピを閲覧・追加できるサイトを目指したプロジェクトです!あなたの作品を世界と共有しましょう!
# :construction: Développeurs

@ -8,6 +8,4 @@ RUN npm install -g @angular/cli
RUN npm install
EXPOSE 4200
ENTRYPOINT ["ng", "serve", "--host", "0.0.0.0"]
ENTRYPOINT ["ng", "serve", "--host", "0.0.0.0","--port","80"]

@ -26,10 +26,38 @@
background-color: #ddd;
color: black;
}
.content {
padding: 20px;
display: flex;
gap: 20px;
}
.recipe-container, .form-container {
background-color: #bab6b6;
color: black;
padding: 20px;
flex: 1;
}
.recipe-container {
display: flex;
flex-direction: column;
}
.recipe-container h2 {
font-size: 3em;
margin-bottom: 10px;
}
.form-container {
display: none;
flex-direction: column;
}
.show-form .form-container {
display: flex;
}
.show-form .recipe-container, .show-form .form-container {
flex: 1;
}
.button-container {
margin-top: 20px;
}
</style>
</head>
<body>
@ -46,7 +74,6 @@
</div>
</div>
<router-outlet></router-outlet>
</body>

@ -1,4 +1,4 @@
import {Component, NgModule} from '@angular/core';
import { Component } from '@angular/core';
import {Router, RouterLink, RouterLinkActive, RouterOutlet} from '@angular/router';
import {RecipeFormComponent} from "./component/recipe-form/recipe-form.component";
import {AccueilComponent} from "./component/accueil/accueil.component";
@ -13,6 +13,7 @@ import {LinkService} from "./service/link.service";
standalone: true,
imports: [RouterOutlet, AccueilComponent, RecipeFormComponent, NgForOf, NgIf, RecipeListComponent, RouterLink, RouterLinkActive],
templateUrl: './app.component.html',
styleUrl: './app.component.css'
})
export class AppComponent {
links : Link[] = this.linkService.getLinks()
@ -21,12 +22,12 @@ export class AppComponent {
}
onClickLogin(event: Event): void {
event.preventDefault();
event.preventDefault(); // Prevent the default anchor behavior
this.loginService.login();
}
onClickLogout(event: Event): void {
event.preventDefault();
event.preventDefault(); // Prevent the default anchor behavior
this.loginService.logout();
}

@ -2,12 +2,7 @@ import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import {provideRouter, withComponentInputBinding} from '@angular/router';
import { routes } from './app.routes';
import { provideHttpClient } from "@angular/common/http";
export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes,withComponentInputBinding()),
provideHttpClient()
]
providers: [provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes,withComponentInputBinding())]
};

@ -3,11 +3,9 @@ import {AuthGuard} from "./guard.guard";
import {IngredientsComponent} from "./component/ingredients/ingredients.component";
import {AccueilComponent} from "./component/accueil/accueil.component";
import {RecipeDetailComponent} from "./component/recipe-detail/recipe-detail.component";
import {CommandComponent} from "./component/command/command.component";
export const routes: Routes = [
{path: 'ingredients', component:IngredientsComponent,canActivate: [AuthGuard]},
{path: 'recipe/:id', component: RecipeDetailComponent},
{path: '', component:AccueilComponent},
{path: 'command', component: CommandComponent}
{path: '', component:AccueilComponent}
];

@ -1,51 +0,0 @@
<style>
.command-list {
font-family: Arial, sans-serif;
background-color: #f0f0f0;
margin: 0;
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
}
.recipe-item {
background-color: #fff;
border-radius: 10px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
margin: 10px 0;
padding: 10px 20px;
width: 80%;
display: flex;
justify-content: space-between;
align-items: center;
}
.recipe-item h3 {
margin: 0;
}
.recipe-item button {
background-color: #ff5252;
color: #fff;
border: none;
border-radius: 5px;
padding: 5px 10px;
cursor: pointer;
}
.recipe-item button:hover {
background-color: #ff0000;
}
</style>
<div class="command-list">
<h2>Ma Commande</h2>
<div *ngFor="let recipe of recipes" class="recipe-item">
<img src="{{recipe.$image}}" width="10%" alt="ImageRecipe"
onerror="this.onerror=null;this.src='https://placehold.co/100x100/black/white?text=Not+Found';">
<h3>{{ recipe.$name }}</h3>
<button (click)="removeRecipe(recipe)">Retirer</button>
</div>
<p *ngIf="recipes.length === 0">Aucune recette dans la commande.</p>
</div>

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

@ -1,25 +0,0 @@
import { Component } from '@angular/core';
import { CommandService } from '../../service/command.service';
import { Recipe } from '../../model/recipe.model';
import {NgFor, NgIf} from '@angular/common';
import { RouterLink } from '@angular/router';
@Component({
selector: 'app-command',
standalone: true,
imports: [NgFor, RouterLink, NgIf],
templateUrl: './command.component.html'
})
export class CommandComponent {
recipes: Recipe[] = [];
errorMessage: string | null = null;
constructor(private commandService: CommandService) {
this.recipes = this.commandService.getRecipes();
}
removeRecipe(recipe: Recipe) {
this.commandService.removeRecipe(recipe);
this.recipes = this.commandService.getRecipes();
}
}

@ -2,7 +2,7 @@
.ingredients {
font-family: Arial, sans-serif;
background-color: #bab6b6;
margin: 0;
margin: 0px;
padding: 20px;
display: flex;
flex-wrap: wrap;
@ -18,101 +18,33 @@
width: 200px;
margin: 20px;
}
.ingredient-card:hover {
transform: scale(1.05);
}
.ingredient-image {
width: 100%;
height: 200px;
object-fit: cover;
}
.ingredient-content {
padding: 15px;
}
.ingredient-title {
font-size: 1em;
margin: 0 0 10px;
}
.ingredient-actions {
margin-top: 10px;
}
.ingredient-actions button {
margin-right: 5px;
}
.general-form-add {
margin-top: 2%;
background-color: #bab6b6;
float: left;
width: 49%;
}
.general-form-edit {
margin-top: 2%;
background-color: #bab6b6;
float: right;
width: 49%;
}
.form-group {
margin-bottom: 10px;
margin-right: 2%;
margin-left: 2%
}
.form-group label {
display: block;
margin-bottom: 5px;
}
.form-group input, .form-group textarea {
width: 100%;
padding: 8px;
box-sizing: border-box;
}
.button-form{
margin-left: 2%;
}
</style>
<body>
<div class="ingredients">
<div class="ingredient-card" *ngFor="let ingredient of ingredients">
<div class="ingredient-content">
<h2 class="ingredient-title">{{ ingredient.name }}</h2>
<p>{{ ingredient.description }}</p>
<div class="ingredient-actions">
<button (click)="edit(ingredient)">Modifier</button>
<div class="ingredients">
<div class="ingredient-card" *ngFor="let ingredient of ingredients ">
<img src="image1.jpg" alt="Recette 1" class="ingredient-image"
onerror="this.onerror=null;this.src='https://placehold.co/100x100/black/white?text=Not+Found';">
<div class="ingredient-content">
<h2 class="ingredient-title">{{ ingredient.$name }}</h2>
</div>
</div>
</div>
</div>
<div class="general-form-add">
<h2>Ajouter un ingrédient</h2>
<div class="form-group">
<label for="newName">Nom:</label>
<input id="newName" [(ngModel)]="newIngredient.name" required>
</div>
<div class="form-group">
<label for="newDescription">Description:</label>
<textarea id="newDescription" [(ngModel)]="newIngredient.description" required></textarea>
</div>
<button class="button-form" (click)="addIngredient()">Ajouter</button>
</div>
<div *ngIf="editIngredient" class="general-form-edit">
<h2>Modifier l'ingrédient</h2>
<div class="form-group">
<label for="editName">Nom:</label>
<input id="editName" [(ngModel)]="editIngredient.name">
</div>
<div class="form-group">
<label for="editDescription">Description:</label>
<textarea id="editDescription" [(ngModel)]="editIngredient.description"></textarea>
</div>
<button class="button-form" (click)="updateIngredient()">Enregistrer</button>
<button class="button-form" (click)="cancelEdit()">Annuler</button>
</div>
</body>

@ -1,62 +1,45 @@
import { Component, OnInit } from '@angular/core';
import { IngredientService } from "../../service/ingredient.service";
import { Ingredient } from "../../model/ingredient.model";
import { NgFor, NgIf } from "@angular/common";
import { FormsModule } from "@angular/forms";
import { HttpClient } from "@angular/common/http";
import {Component, OnInit} from '@angular/core';
import {IngredientService} from "../../service/ingredient.service";
import {Ingredient} from "../../model/ingredient.model";
import {NgFor} from "@angular/common";
import {LoginService} from "../../service/login.service";
import {Link} from "../../model/link.model";
import {LinkService} from "../../service/link.service";
@Component({
selector: 'app-ingredients',
standalone: true,
imports: [
NgFor,
NgIf,
FormsModule
],
providers: [IngredientService, HttpClient],
templateUrl: './ingredients.component.html',
styleUrl: './ingredients.component.css'
})
export class IngredientsComponent implements OnInit {
export class IngredientsComponent implements OnInit{
ingredients: Ingredient[] = [];
newIngredient: Ingredient = { id: '0', name: '', description: '' };
editIngredient: Ingredient | null = null;
links: Link[] = this.linkService.getLinks()
constructor(private ingredientService: IngredientService) {}
constructor(private ingredientService : IngredientService,private loginService: LoginService,private linkService : LinkService) {}
ngOnInit() {
this.loadIngredients();
}
loadIngredients() {
this.ingredientService.getAll().subscribe(ingredients => {
this.ingredients = ingredients;
});
}
addIngredient() {
this.ingredientService.add(this.newIngredient).subscribe(ingredient => {
this.ingredients.push(ingredient);
this.newIngredient = { id: '0', name: '', description: '' };
});
onClickLogin(event: Event): void {
event.preventDefault(); // Prevent the default anchor behavior
this.loginService.login();
}
edit(ingredient: Ingredient) {
this.editIngredient = { ...ingredient };
onClickLogout(event: Event): void {
event.preventDefault(); // Prevent the default anchor behavior
this.loginService.logout();
}
updateIngredient() {
if (this.editIngredient) {
this.ingredientService.update(this.editIngredient).subscribe(updatedIngredient => {
const index = this.ingredients.findIndex(i => i.id === updatedIngredient.id);
this.ingredients[index] = updatedIngredient;
this.editIngredient = null;
});
}
isLogged(): boolean {
return this.loginService.isLogged();
}
cancelEdit() {
this.editIngredient = null;
}
}

@ -1,85 +1,5 @@
<style>
.recipe-details {
font-family: Arial, sans-serif;
background-color: #bab6b6;
margin: 0;
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
}
<h1>{{recipe?.$name}}</h1>
<p> {{recipe?.$createdAt}}</p>
.recipe-card {
background-color: #fff;
border-radius: 10px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
overflow: hidden;
transition: transform 0.2s;
width: 80%;
margin: 20px;
padding: 20px;
}
<h3>{{recipe?.$description}}</h3>
.recipe-card:hover {
transform: scale(1.05);
}
.recipe-image {
width: 100%;
max-width: 300px;
border-radius: 10px;
}
.recipe-content {
padding: 15px;
text-align: center;
}
.recipe-title {
font-size: 2em;
margin: 0 0 10px;
}
.recipe-date {
color: #777;
margin-bottom: 20px;
}
.recipe-description {
font-size: 1.2em;
margin: 20px 0;
}
.ingredient-list {
list-style-type: none;
padding: 0;
}
.ingredient-item {
background-color: #f9f9f9;
border: 1px solid #ddd;
border-radius: 5px;
padding: 10px;
margin: 5px 0;
}
</style>
<div class="recipe-details">
<div class="recipe-card">
<h1 class="recipe-title">{{ recipe?.$name }}</h1>
<p class="recipe-date">{{ recipe?.$createdAt }}</p>
<img [src]="recipe?.$image" alt="{{ recipe?.$name }}" class="recipe-image"
onerror="this.onerror=null;this.src='https://placehold.co/300x300/black/white?text=Not+Found';">
<div class="recipe-content">
<h3 class="recipe-description">{{ recipe?.$description }}</h3>
<h4>Ingrédients</h4>
<ul class="ingredient-list">
<li class="ingredient-item" *ngFor="let ingredient of ingredients">
{{ ingredient.ingredient.name }} : {{ ingredient.quantity }} {{ ingredient.unit }}
</li>
</ul>
</div>
</div>
</div>

@ -3,7 +3,6 @@ import {Recipe} from "../../model/recipe.model";
import {RecipeService} from "../../service/recipe.service";
import {RouterLink, RouterLinkActive, RouterOutlet} from "@angular/router";
import {NgFor} from "@angular/common";
import {QuantifiedIngredientGetter} from "../../model/quantified-ingredient-getter.model";
@Component({
selector: 'app-recipe-detail',
@ -15,7 +14,6 @@ import {QuantifiedIngredientGetter} from "../../model/quantified-ingredient-gett
export class RecipeDetailComponent {
recipe : Recipe|null = null;
ingredients : QuantifiedIngredientGetter[] = [];
constructor(private recipeService : RecipeService) {
}
@ -23,9 +21,6 @@ export class RecipeDetailComponent {
@Input()
set id(id: number) {
this.recipe = this.recipeService.getRecipe(id)!;
this.ingredients = this.recipe.$ingredients;
}
}

@ -1,138 +1,34 @@
<style>
body {
font-family: Arial, sans-serif;
background-color: #bab6b6;
margin: 0;
padding: 20px;
display: flex;
flex-wrap: wrap;
justify-content: center;
}
.form-container {
background-color: #fff;
border-radius: 10px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
padding: 20px;
width: 100%;
}
.form-container h2 {
margin-top: 0;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
}
.form-group input,
.form-group select,
.form-group button {
width: 97%;
padding: 10px;
margin-top: 5px;
border: 1px solid #ccc;
border-radius: 5px;
}
.form-group button {
background-color: black;
color: white;
border: none;
cursor: pointer;
}
.form-group button:hover {
background-color: #525259;
}
.form-group .remove-button {
background-color: red;
}
.form-group .remove-button:hover {
background-color: darkred;
}
.add-ingredient-button {
background-color: #4CAF50;
color: white;
}
.add-ingredient-button:hover {
background-color: #45a049;
}
.submit-button {
background-color: black;
color: white;
}
.submit-button:hover {
background-color: #525259;
}
.error-message {
color: red;
font-size: 0.9em;
margin-top: 5px;
}
.form-group.invalid input,
.form-group.invalid select {
border-color: red;
}
<form [formGroup]="recipeForm" (ngSubmit)="onSubmit()">
<label for="id">ID: </label>
<input id="id" type="number" formControlName="id">
</style>
<body>
<div class="form-container">
<h2>Ajouter une Recette</h2>
<form [formGroup]="recipeForm" (ngSubmit)="onSubmit()">
<div class="form-group" [class.invalid]="recipeForm.get('id')?.invalid && recipeForm.get('id')?.touched">
<label for="id">ID: </label>
<input id="id" type="number" formControlName="id" required>
<div class="error-message" *ngIf="recipeForm.get('id')?.invalid && recipeForm.get('id')?.touched">
L'ID est requis.
</div>
</div>
<div class="form-group" [class.invalid]="recipeForm.get('name')?.invalid && recipeForm.get('name')?.touched">
<label for="name">Name: </label>
<input id="name" type="text" formControlName="name" required>
<div class="error-message" *ngIf="recipeForm.get('name')?.invalid && recipeForm.get('name')?.touched">
Le nom est requis.
</div>
</div>
<div class="form-group" [class.invalid]="recipeForm.get('description')?.invalid && recipeForm.get('description')?.touched">
<label for="description">Description: </label>
<input id="description" type="text" formControlName="description" required>
<div class="error-message" *ngIf="recipeForm.get('description')?.invalid && recipeForm.get('description')?.touched">
La description est requise.
</div>
</div>
<label for="image">Image: </label>
<input id="image" type="file" (change)="onFileChange($event)">
<label for="name">Name: </label>
<input id="name" type="text" formControlName="name">
<br>
<div *ngIf="imageBase64">
<img [src]="imageBase64" alt="Selected Image" style="max-width: 200px; max-height: 200px;">
</div>
<br>
<label for="description">Description: </label>
<input id="description" type="text" formControlName="description">
<div formArrayName="ingredients">
<div *ngFor="let ingredient of ingredients.controls; let i=index" [formGroupName]="i" class="form-group">
<label for="ingredient-quantity">Quantity:</label>
<input id="ingredient-quantity" type="number" formControlName="quantity" required>
<div formArrayName="ingredients">
<div *ngFor="let ingredient of ingredients.controls; let i=index" [formGroupName]="i">
<label for="ingredient-quantity">Quantity:</label>
<input id="ingredient-quantity" type="number" formControlName="quantity">
<label for="ingredient-unit">Unit:</label>
<select id="ingredient-unit" formControlName="unit" required>
<option *ngFor="let unit of unity" [value]="unit">{{ unit }}</option>
</select>
<label for="ingredient-unit">Unit:</label>
<select id="ingredient-unit" formControlName="unit">
<option *ngFor="let unit of unity" [value]="unit">{{ unit }}</option>
</select>
<label for="ingredient-name">Ingredient:</label>
<select id="ingredient-name" formControlName="ingredient" required>
<option *ngFor="let ingredient of ingredientsList" [value]="ingredient.name">{{ ingredient.name }}</option>
</select>
<label for="ingredient-name">Ingredient:</label>
<select id="ingredient-name" formControlName="ingredient">
<option *ngFor="let ingredient of ingredientsList" [value]="ingredient.$name">{{ ingredient.$name }}</option>
</select>
<button type="button" class="remove-button" (click)="removeIngredient(i)">Remove</button>
</div>
<button type="button" (click)="removeIngredient(i)">Remove</button>
</div>
</div>
<button type="button" class="add-ingredient-button" (click)="addIngredient()">Add Ingredient</button>
<button type="button" (click)="addIngredient()">Add Ingredient</button>
<p>Complete the form to enable the button.</p>
<button type="submit" class="submit-button">Submit</button>
</form>
</div>
</body>
<p>Complete the form to enable the button.</p>
<button type="submit" [disabled]="!recipeForm.valid">Submit</button>
</form>

@ -1,99 +1,75 @@
import { Component, EventEmitter, Output } from '@angular/core';
import { Recipe } from "../../model/recipe.model";
import { FormControl, FormGroup, ReactiveFormsModule, FormArray, FormBuilder, Validators } from "@angular/forms";
import {Component, EventEmitter, Output} from '@angular/core';
import {Recipe} from "../../model/recipe.model";
import {FormControl, FormGroup, ReactiveFormsModule, FormArray, FormBuilder} from "@angular/forms";
import { Ingredient } from '../../model/ingredient.model';
import { Unity } from '../../model/unity';
import { IngredientService } from '../../service/ingredient.service';
import {NgFor, NgIf} from "@angular/common";
import {NgFor} from "@angular/common";
@Component({
selector: 'app-recipe-form',
standalone: true,
imports: [ReactiveFormsModule, NgFor, NgIf],
templateUrl: './recipe-form.component.html'
imports: [ReactiveFormsModule,NgFor],
templateUrl: './recipe-form.component.html',
styleUrl: './recipe-form.component.css'
})
export class RecipeFormComponent {
@Output() formSubmitted = new EventEmitter<Recipe>();
ingredientsList: Ingredient[] = [];
unity = Object.values(Unity);
imageBase64: string | null = null;
constructor(private formBuilder: FormBuilder, private ingredientService: IngredientService) { }
constructor(private formBuilder: FormBuilder, private ingredientService : IngredientService){}
ngOnInit(): void {
this.ingredientService.getAll().subscribe(ingredients => {
this.ingredientsList = ingredients;
});
}
recipeForm: FormGroup = this.formBuilder.group({
id: new FormControl('', { nonNullable: true, validators: [Validators.required] }),
name: new FormControl('', { nonNullable: true, validators: [Validators.required] }),
description: new FormControl('', { nonNullable: true, validators: [Validators.required] }),
ingredients: this.formBuilder.array([this.newIngredient()]),
image: new FormControl('', {nonNullable: false})
id: new FormControl('', { nonNullable: true }),
name: new FormControl('', { nonNullable: true }),
description: new FormControl('', { nonNullable: true }),
ingredients: this.formBuilder.array([])
});
get ingredients(): FormArray {
get ingredients():FormArray{
return this.recipeForm.get('ingredients') as FormArray;
}
newIngredient(): FormGroup {
newIngredient(): FormGroup{
return this.formBuilder.group({
quantity: new FormControl(0, { nonNullable: true, validators: [Validators.required] }),
unit: new FormControl('', { nonNullable: true, validators: [Validators.required] }),
ingredient: new FormControl('', { nonNullable: true, validators: [Validators.required] })
quantity: new FormControl(0, { nonNullable: true }),
unit: new FormControl('', { nonNullable: true }),
ingredient: new FormControl('', { nonNullable: true })
});
}
addIngredient() {
this.ingredients.push(this.newIngredient());
}
removeIngredient(index: number) {
if (this.ingredients.length > 1) {
this.ingredients.removeAt(index);
} else {
}
}
onFileChange(event: Event) {
const input = event.target as HTMLInputElement;
if (input.files && input.files[0]) {
const file = input.files[0];
const reader = new FileReader();
reader.onload = () => {
this.imageBase64 = reader.result as string;
this.recipeForm.patchValue({image: this.imageBase64});
};
reader.readAsDataURL(file);
}
this.ingredients.removeAt(index);
}
onSubmit() {
if (this.recipeForm.valid) {
if(this.recipeForm.valid){
const recipe: Recipe = {
$id: parseInt(this.recipeForm.value.id!),
$name: this.recipeForm.value.name!,
$description: this.recipeForm.value.description!,
$createdAt: new Date(),
$ingredients: this.recipeForm.value.ingredients!.map((ingredient: any) => ({
$ingredients: this.recipeForm.value.ingredients!.map((ingredient : any) => ({
quantity: ingredient.quantity,
unit: ingredient.unit,
ingredient: this.ingredientsList.find(ing => ing.name === ingredient.ingredient)
})),
$image: this.recipeForm.value.image
ingredient: this.ingredientsList.find(ing => ing.$name === ingredient.ingredient)
}))
};
this.formSubmitted.emit(recipe);
this.recipeForm.reset()
this.ingredients.clear()
this.imageBase64 = null;
this.addIngredient()
}else{
alert("Veuillez remplir tous les champs")
}
}
}

@ -71,29 +71,25 @@
border: 1px solid #bbb;
border-radius: 5px;
}
.error-message {
color: red;
font-weight: bold;
margin-bottom: 4%;
.page-link.active {
background-color: #333;
color: white;
}
</style>
<body>
<div *ngIf="errorMessageDisplay" class="error-message">{{ errorMessage }}</div>
<div [ngClass]="{'four-column': !isFormVisible, 'two-column': isFormVisible}" class="recipe-container">
<div class="recipe-card" *ngFor="let recipe of paginatedRecipes">
<img [src]="recipe.$image" alt="Recette 1" class="recipe-image"
onerror="this.onerror=null;this.src='https://placehold.co/100x100/black/white?text=Not+Found';">
<img src="image1.jpg" alt="Recette 1" class="recipe-image" onerror="this.onerror=null;this.src='https://placehold.co/100x100/black/white?text=Not+Found';">
<div class="recipe-content">
<h2 class="recipe-title">{{recipe.$name}}</h2>
<p class="recipe-description">{{recipe.$description}}</p>
<button (click)="detailsRecipe(recipe.$id)" class="details-button">Détails</button>
<button (click)="addToCommand(recipe)" class="details-button" >Ajouter à ma commande</button>
</div>
</div>
</div>
<div class="pagination">
<button class="page-link" (click)="previousPage()" [disabled]="currentPage === 0">Précédent</button>
<p>{{ currentPage +1 }}</p>
<!-- <span class="page-link" *ngFor="let page of totalPages; let i = index" [class.active]="currentPage === i" (click)="changePage(i)">{{ i + 1 }}</span>-->
<button class="page-link" (click)="nextPage()" [disabled]="currentPage === totalPages - 1">Suivant</button>
</div>
</body>

@ -1,10 +1,9 @@
import {Component, Input, OnInit} from '@angular/core';
import { RecipeService } from '../../service/recipe.service';
import { Recipe } from '../../model/recipe.model';
import {NgClass, NgIf, NgOptimizedImage} from "@angular/common";
import {NgClass, NgOptimizedImage} from "@angular/common";
import {NgFor} from "@angular/common";
import {Router, RouterLinkActive} from "@angular/router";
import {CommandService} from "../../service/command.service";
@Component({
selector: 'app-recipe-list',
@ -13,23 +12,19 @@ import {CommandService} from "../../service/command.service";
NgOptimizedImage,
NgFor,
NgClass,
RouterLinkActive,
NgIf
RouterLinkActive
],
standalone: true
})
export class RecipeListComponent implements OnInit {
recipes: Recipe[] = [];
errorMessageDisplay: boolean = false;
errorMessage: string | null = null;
@Input() isFormVisible!: boolean;
currentPage: number = 0;
pageSize: number = 4;
totalPages: any = 0;
constructor(private recipeService: RecipeService,private router : Router, private commandService: CommandService) {}
constructor(private recipeService: RecipeService,private router : Router) {}
ngOnInit(): void {
this.recipes = this.recipeService.getRecipes();
@ -60,19 +55,4 @@ export class RecipeListComponent implements OnInit {
detailsRecipe(id : number){
this.router.navigateByUrl('/recipe/'+id);
}
addToCommand(recipe: Recipe) {
try {
this.commandService.addRecipe(recipe);
this.errorMessageDisplay = false;
} catch (e) {
if (e instanceof DOMException && e.name === 'QuotaExceededError') {
this.errorMessageDisplay = true;
this.errorMessage = 'Local Storage saturé, impossible d\'ajouter la recette dans le local storage des commandes.';
} else {
throw e;
}
}
}
}

@ -2,7 +2,5 @@ import {Link} from "../model/link.model";
export var LINKS :Link[] = [
{$name:"Accueil",$link:""},
{$name:"Ingredients",$link:"/ingredients"},
{$name:"Commande",$link:"/command"}
]
{$name:"Ingredients",$link:"/ingredients"}
]

@ -1,6 +1,5 @@
export interface Ingredient {
id : string,
name : string,
description : string,
$id : number,
$name : string
}

@ -1,23 +0,0 @@
import { Unity } from "./unity";
import { Ingredient } from "./ingredient.model";
import { QuantifiedIngredient } from "./quantified-ingredient.model";
export class QuantifiedIngredientGetter implements QuantifiedIngredient {
constructor(
public $quantity: number,
public $unit: Unity,
public $ingredient: Ingredient
) {}
get quantity(): number {
return this.$quantity;
}
get unit(): Unity {
return this.$unit;
}
get ingredient(): Ingredient {
return this.$ingredient;
}
}

@ -1,8 +1,8 @@
import { Unity } from "./unity";
import { Ingredient } from "./ingredient.model";
import {Unity} from "./unity";
import {Ingredient} from "./ingredient.model";
export interface QuantifiedIngredient {
$quantity: number;
$unit: Unity;
$ingredient: Ingredient;
$quantity : number,
$unit : Unity,
$ingredient : Ingredient
}

@ -1,10 +1,9 @@
import {QuantifiedIngredientGetter} from "./quantified-ingredient-getter.model";
import {QuantifiedIngredient} from "./quantified-ingredient.model";
export interface Recipe {
$id : number,
$name : string,
$description: string,
$createdAt : Date,
$ingredients: QuantifiedIngredientGetter[],
$image?: string
$ingredients: QuantifiedIngredient[]
}

@ -1,25 +0,0 @@
import { Injectable } from '@angular/core';
import { Recipe } from "../model/recipe.model";
@Injectable({
providedIn: 'root'
})
export class CommandService {
private recipes: Recipe[] = JSON.parse(localStorage.getItem('command') || '[]');
addRecipe(recipe: Recipe) {
this.recipes.push(recipe);
localStorage.setItem('command', JSON.stringify(this.recipes));
}
getRecipes(): Recipe[] {
return this.recipes;
}
removeRecipe(recipe: Recipe) {
const index = this.recipes.indexOf(recipe);
if (index > -1) {
this.recipes.splice(index, 1);
}
}
}

@ -1,26 +1,15 @@
import { Injectable } from '@angular/core';
import { Ingredient } from "../model/ingredient.model";
import { Observable } from "rxjs";
import { HttpClient } from "@angular/common/http";
import {Ingredient} from "../model/ingredient.model";
import {$INGREDIENTS} from "../data/ingredient.stub";
import {Observable, of} from "rxjs";
@Injectable({
providedIn: 'root'
})
export class IngredientService {
private apiUrl = 'https://664ba07f35bbda10987d9f99.mockapi.io/api/ingredients';
constructor(private http: HttpClient) { }
getAll(): Observable<Ingredient[]> {
return this.http.get<Ingredient[]>(this.apiUrl);
}
add(ingredient: Ingredient): Observable<Ingredient> {
return this.http.post<Ingredient>(this.apiUrl, ingredient);
}
update(ingredient: Ingredient): Observable<Ingredient> {
return this.http.put<Ingredient>(`${this.apiUrl}/${ingredient.id}`, ingredient);
getAll() : Observable<Ingredient[]> {
return of($INGREDIENTS);
}
}

Loading…
Cancel
Save