Compare commits

..

No commits in common. 'master' and 'add-recipe-form' have entirely different histories.

2
.gitignore vendored

@ -21,7 +21,7 @@ yarn-error.log
*.sublime-workspace
# Visual Studio Code
.vscode/
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json

@ -0,0 +1,4 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846
"recommendations": ["angular.ng-template"]
}

@ -0,0 +1,20 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "ng serve",
"type": "chrome",
"request": "launch",
"preLaunchTask": "npm: start",
"url": "http://localhost:4200/"
},
{
"name": "ng test",
"type": "chrome",
"request": "launch",
"preLaunchTask": "npm: test",
"url": "http://localhost:9876/debug.html"
}
]
}

42
.vscode/tasks.json vendored

@ -0,0 +1,42 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "start",
"isBackground": true,
"problemMatcher": {
"owner": "typescript",
"pattern": "$tsc",
"background": {
"activeOnStart": true,
"beginsPattern": {
"regexp": "(.*?)"
},
"endsPattern": {
"regexp": "bundle generation complete"
}
}
}
},
{
"type": "npm",
"script": "test",
"isBackground": true,
"problemMatcher": {
"owner": "typescript",
"pattern": "$tsc",
"background": {
"activeOnStart": true,
"beginsPattern": {
"regexp": "(.*?)"
},
"endsPattern": {
"regexp": "bundle generation complete"
}
}
}
}
]
}

@ -25,7 +25,6 @@
"src/assets"
],
"styles": [
"@angular/material/prebuilt-themes/indigo-pink.css",
"src/styles.css"
],
"scripts": []
@ -85,7 +84,6 @@
"src/assets"
],
"styles": [
"@angular/material/prebuilt-themes/indigo-pink.css",
"src/styles.css"
],
"scripts": []
@ -93,8 +91,5 @@
}
}
}
},
"cli": {
"analytics": false
}
}

1412
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -11,18 +11,13 @@
"private": true,
"dependencies": {
"@angular/animations": "^17.3.0",
"@angular/cdk": "^17.3.10",
"@angular/common": "^17.3.0",
"@angular/compiler": "^17.3.0",
"@angular/core": "^17.3.0",
"@angular/forms": "^17.3.0",
"@angular/material": "^17.3.10",
"@angular/platform-browser": "^17.3.0",
"@angular/platform-browser-dynamic": "^17.3.0",
"@angular/router": "^17.3.0",
"@jsverse/transloco": "^7.4.2",
"@ngx-translate/core": "^15.0.0",
"@ngx-translate/http-loader": "^8.0.0",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.14.3"
@ -40,4 +35,4 @@
"karma-jasmine-html-reporter": "~2.1.0",
"typescript": "~5.4.2"
}
}
}

@ -1,40 +0,0 @@
nav {
border: 3px solid black;
padding: 0 20px;
ul {
display: flex;
flex-direction: row;
width: 100%;
justify-content: space-between;
padding: 0;
li {
display: flex;
align-items: center;
list-style: none;
a {
color: inherit;
text-decoration: none;
font-weight: bold;
font-size: 1.3rem;
}
}
select {
font-size: 2rem;
appearance: none;
border: none;
background-color: white;
}
}
}
#page-wrapper {
width: 70%;
border: 3px solid black;
margin: 1rem auto;
padding: 1rem;
}

@ -1,20 +1,6 @@
<nav>
<ul>
<li><a routerLink="/" routerLinkActive="active" ariaCurrentWhenActive="page">{{ 'title' | transloco }}</a></li>
<li><a routerLink="/cart" routerLinkActive="active" ariaCurrentWhenActive="page">{{ 'cart'}}</a></li>
<li *ngIf="isLogged; else elseBlock"><a routerLink="/logout" routerLinkActive="active" ariaCurrentWhenActive="page">{{ 'logout' | transloco }}</a></li>
<ng-template #elseBlock>
<li><a routerLink="/login" routerLinkActive="active" ariaCurrentWhenActive="page">{{ 'login' | transloco }}</a></li>
</ng-template>
<h1>Ratatouille</h1>
<select (change)="changeLanguage($event)">
<option value="en">🇬🇧</option>
<option value="fr">🇫🇷</option>
<option value="ru">🇷🇺</option>
</select>
</ul>
</nav>
<div id="page-wrapper">
<router-outlet></router-outlet>
<div class="wrapper">
<app-recipe-list></app-recipe-list>
<app-recipe-form></app-recipe-form>
</div>

@ -1,64 +1,27 @@
import { Component } from '@angular/core';
import { RouterOutlet, RouterLink } from '@angular/router';
import { RecipeFormComponent } from './components/recipe-form/recipe-form.component';
import { RouterOutlet } from '@angular/router';
import { RecipeFormComponent } from './recipe-form/recipe-form.component';
import { RecipeService } from './services/recipe.service';
import { Recipe } from './model/recipe.model';
import { RecipeListComponent } from './components/recipe-list/recipe-list.component';
import { TranslocoPipe, TranslocoService } from '@jsverse/transloco';
import { NgIf } from '@angular/common';
@Component({
selector: 'app-root',
standalone: true,
imports: [
RouterOutlet,
RouterLink,
RecipeFormComponent,
RecipeListComponent,
TranslocoPipe,
NgIf,
],
providers: [RecipeService, TranslocoService, NgIf],
providers: [RecipeService],
templateUrl: './app.component.html',
styleUrl: './app.component.css'
})
export class AppComponent {
title = 'bromista-nisqa-receta';
isLogged = false;
constructor(protected recipeService: RecipeService, private translocoService: TranslocoService){}
ngOnInit(){
const rawCookie = decodeURIComponent(document.cookie);
const array = rawCookie.split(";");
const name = "isAdmin=";
let res:String = "";
constructor(protected recipeService: RecipeService){}
for(let cookie of array)
{
while (cookie.charAt(0) === ' ') {
cookie = cookie.substring(1);
}
if (cookie.indexOf(name) === 0) {
res = cookie.substring(name.length, cookie.length);
}
}
if (res !== "true" ){
this.isLogged = false
}else
{
this.isLogged = true;
}
}
addRecipe($event: Recipe): void {
this.recipeService.addRecipe($event);
}
changeLanguage(event: Event) {
if (event.target && event.target instanceof HTMLSelectElement) {
const lang = event.target.value;
this.translocoService.setActiveLang(lang);
} else {
console.log('Error on language option value')
}
}
}

@ -1,31 +1,8 @@
import { ApplicationConfig, isDevMode } from '@angular/core';
import { provideRouter, withComponentInputBinding } from '@angular/router';
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideAnimations } from '@angular/platform-browser/animations';
import { routes } from './app.routes';
import { provideHttpClient } from '@angular/common/http';
import { TranslocoHttpLoader } from './transloco-loader';
import { provideTransloco } from '@jsverse/transloco';
import { withFetch } from '@angular/common/http';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes, withComponentInputBinding()), provideHttpClient(), provideTransloco({
config: {
availableLangs: ['en', 'fr', 'ru'],
defaultLang: 'en',
// Remove this option if your application doesn't support changing language in runtime.
reRenderOnLangChange: true,
prodMode: !isDevMode(),
},
loader: TranslocoHttpLoader
}
),
provideAnimations(),
provideHttpClient(
withFetch(),
),
]
providers: [provideRouter(routes)]
};

@ -1,23 +1,3 @@
import { Routes } from '@angular/router'
import { Routes } from '@angular/router';
import { Error404Component } from "./components/errors/errors.component";
import { RecipeFormComponent } from "./components/recipe-form/recipe-form.component";
import { RecipeListComponent } from "./components/recipe-list/recipe-list.component";
import { RecipeDetailComponent } from './components/recipe-detail/recipe-detail.component';
import { LoginComponent } from './components/login/login.component';
import { IngredientListComponent } from './components/ingredient-list/ingredient-list.component';
import { LogoutComponent } from './components/logout/logout.component';
import { AuthGuard } from './auth.guard';
import { SavedRecipeComponent } from './components/saved-recipe/saved-recipe.component';
export const routes: Routes = [
{ path: '', component: RecipeListComponent },
{ path: 'error/:status', component: Error404Component},
{ path: 'recipe/add', component: RecipeFormComponent},
{ path: 'cart', component: SavedRecipeComponent},
{ path: 'recipe/:id', component: RecipeDetailComponent},
{ path: 'ingredients', component: IngredientListComponent, canActivate: [AuthGuard] },
{ path: 'login', component: LoginComponent },
{ path: 'logout', component: LogoutComponent, canActivate: [AuthGuard] },
];
export const routes: Routes = [];

@ -1,29 +0,0 @@
import { inject } from "@angular/core";
import { Router } from "@angular/router";
export const AuthGuard = () => {
const router = inject(Router);
const rawCookie = decodeURIComponent(document.cookie);
const array = rawCookie.split(";");
const name = "isAdmin=";
let res:String = "";
for(let cookie of array)
{
while (cookie.charAt(0) === ' ') {
cookie = cookie.substring(1);
}
if (cookie.indexOf(name) === 0) {
res = cookie.substring(name.length, cookie.length);
}
}
console.log(res);
// Check cookie
if(res !== "true") {
router.navigateByUrl('/login')
return false
}
return true
}

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

@ -1,21 +0,0 @@
import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
@Component({
selector: 'app-errors',
standalone: true,
imports: [],
templateUrl: './errors.component.html',
styleUrl: './errors.component.css'
})
export class Error404Component {
status: string | null = "Unknown error";
constructor(private route: ActivatedRoute){}
ngOnInit() {
this.route.paramMap.subscribe(params => {
this.status = params.get('status');
})
}
}

@ -1,37 +0,0 @@
<p>ingredient-list works!</p>
<ng-container *ngIf="ingredients">
<div *ngFor="let ingre of ingredients">
<p>-----------</p>
<p><a>{{ ingre.name}}</a></p>
<p>Description: {{ ingre.description}}</p>
<p><button (click)="edit(ingre)">Modifier</button></p>
</div>
</ng-container>
<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>

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

@ -1,54 +0,0 @@
import { Component } from '@angular/core';
import { Ingredient } from '../../model/ingredient.model';
import { IngredientService } from '../../services/ingredient.service';
import { OnInit } from '@angular/core';
import { NgFor, NgIf } from '@angular/common';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-ingredient-list',
standalone: true,
imports: [NgIf, NgFor, FormsModule],
providers: [],
templateUrl: './ingredient-list.component.html',
styleUrl: './ingredient-list.component.css'
})
export class IngredientListComponent {
public ingredients! : Ingredient[];
newIngredient: Ingredient = { id: 0, name: '', description: '', qty: 0 };
editIngredient: Ingredient | null = null;
constructor(private serviceIngredient : IngredientService){}
ngOnInit(): void
{
this.serviceIngredient.getIngredient().subscribe(ingredients => {
this.ingredients = ingredients;
});
}
edit(ingre: Ingredient){
this.editIngredient = ingre;
}
addIngredient() {
this.serviceIngredient.add(this.newIngredient).subscribe(ingredient => {
this.ingredients.push(ingredient);
this.newIngredient = { id: 0, name: '', description: '', qty: 0}
});
}
updateIngredient() {
if (this.editIngredient) {
this.serviceIngredient.update(this.editIngredient).subscribe(updatedIngredient => {
const index = this.ingredients.findIndex(i => i.id === updatedIngredient.id);
this.ingredients[index] = updatedIngredient;
this.editIngredient = null;
});
}
}
cancelEdit() {
this.editIngredient = null;
}
}

@ -1,15 +0,0 @@
.ingredient-mini {
min-width: 130px;
background-color: #d3d3d3;
padding: 10px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 6px;
.ingredient-mini-title {
font-size: 1.2rem;
font-weight: bold;
}
}

@ -1,4 +0,0 @@
<div class="ingredient-mini">
<span class="ingredient-mini-title">{{ ingredient.name }}</span>
<span>{{ ingredient.qty }}g</span>
</div>

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

@ -1,14 +0,0 @@
import { Component } from '@angular/core';
import { Ingredient } from '../../model/ingredient.model'
import { Input } from '@angular/core';
@Component({
selector: 'app-ingredient-mini',
standalone: true,
imports: [],
templateUrl: './ingredient-mini.component.html',
styleUrl: './ingredient-mini.component.css'
})
export class IngredientMiniComponent {
@Input() ingredient!: Ingredient;
}

@ -1,2 +0,0 @@
<p>login works!</p>
<button (click)="setCookie()"> Log Me ! </button>

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

@ -1,21 +0,0 @@
import { Component } from '@angular/core';
import { Router } from "@angular/router";
import { Inject } from '@angular/core';
@Component({
selector: 'app-login',
standalone: true,
imports: [],
templateUrl: './login.component.html',
styleUrl: './login.component.css'
})
export class LoginComponent {
constructor(@Inject(Router) private router: Router) {}
setCookie():void{
document.cookie = "isAdmin=true";
this.router.navigateByUrl('/ingredients');
}
}

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

@ -1,20 +0,0 @@
import { Component } from '@angular/core';
import { Inject } from '@angular/core';
import { Router } from '@angular/router';
@Component({
selector: 'app-logout',
standalone: true,
imports: [],
templateUrl: './logout.component.html',
styleUrl: './logout.component.css'
})
export class LogoutComponent {
constructor(@Inject(Router) private router: Router) {}
ngOnInit(){
document.cookie = "isAdmin=false";
this.router.navigateByUrl('/');
}
}

@ -1,11 +0,0 @@
img {
max-height: 300px;
height: 300px;
}
#recipe-list {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 1rem;
}

@ -1,11 +0,0 @@
<div id="recipe-detail">
<img src="{{ recipe.image }}" />
<h1>{{ recipe.name }}</h1>
<p>{{ recipe.description }}</p>
<h2>{{ 'recipe.ingredients' | transloco }}</h2>
<div id="recipe-list">
<app-ingredient-mini *ngFor="let ingredient of recipe.ingredients" [ingredient]="ingredient"></app-ingredient-mini>
</div>
</div>

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

@ -1,39 +0,0 @@
import { Component} from '@angular/core';
import { Recipe } from '../../model/recipe.model';
import { RecipeService } from '../../services/recipe.service';
import { Router, ActivatedRoute } from '@angular/router';
import { TranslocoPipe } from '@jsverse/transloco';
import { IngredientMiniComponent } from '../ingredient-mini/ingredient-mini.component';
import { NgFor } from '@angular/common';
@Component({
selector: 'app-recipe-detail',
standalone: true,
imports: [NgFor, TranslocoPipe, IngredientMiniComponent],
templateUrl: './recipe-detail.component.html',
styleUrl: './recipe-detail.component.css'
})
export class RecipeDetailComponent {
recipe!: Recipe;
id: string | null = null;
constructor(private router: Router, private route: ActivatedRoute, private recipeService: RecipeService) {
this.recipeService.getRecipes();
}
ngOnInit() {
this.route.paramMap.subscribe(params => {
this.id = params.get('id');
if (this.id) {
this.recipe = this.recipeService.getRecipeById(Number(this.id))!;
console.log(this.recipe);
}
if (this.recipe === undefined) {
this.router.navigate(['/error/404'])
}
});
}
}

@ -1,20 +0,0 @@
.mat-mdc-form-field {
width: 40%;
min-width: 300px;
}
.form-submit {
margin-top: 1rem;
}
.form-row {
display: flex;
flex-direction: row;
align-items: center;
gap: 1rem;
.mat-mdc-form-field {
width: 30%;
}
}

@ -1,47 +0,0 @@
<h1>{{ 'recipe.add.link' | transloco }}</h1>
<form [formGroup]="recipeForm" (ngSubmit)="onSubmit()">
<p>
<mat-form-field appearance="fill">
<mat-label>Name</mat-label>
<input matInput formControlName="name">
</mat-form-field>
</p>
<p>
<mat-form-field appearance="fill">
<mat-label>Description</mat-label>
<textarea matInput formControlName="description"></textarea>
</mat-form-field>
</p>
<p>
<mat-form-field appearance="fill">
<mat-label>Image</mat-label>
<input matInput type="text" placeholder="No file chosen" [value]="filename" readonly>
<input type="file" formControlName="image" (change)="onFileChange($event)" hidden #fileInput>
<button mat-stroked-button type="button" timeout (click)="fileInput.click()">Browse</button>
</mat-form-field>
</p>
<div formArrayName="ingredients">
<h3>{{ 'recipe.ingredients' | transloco }}</h3>
<div *ngFor="let ingredient of ingredients.controls; let i = index" [formGroupName]="i" class="form-row">
<mat-form-field>
<mat-label>{{ 'recipe.ingredients' | transloco }}</mat-label>
<mat-select formControlName="name">
@for (ingredient of ingredientsOptions; track ingredient) {
<mat-option [value]="ingredient.name">{{ ingredient.name }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field>
<mat-label>Quantity</mat-label>
<input matInput formControlName="qty">
</mat-form-field>
<button mat-stroked-button type="button" (click)="removeIngredient(i)">Remove</button>
<br>
</div>
<button mat-stroked-button type="button" (click)="addIngredient()">{{
'recipe.add.button-ingredients'|transloco }}</button>
</div>
<button class="form-submit" mat-stroked-button type="submit">{{ 'recipe.add.button-recipe' | transloco }}</button>
</form>

@ -1,37 +0,0 @@
#recipe-list {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
gap: 2rem;
}
.pagination {
font-size: 1.2rem;
}
.plus-button {
position: fixed;
bottom: 20px;
right: 20px;
width: 50px;
height: 50px;
background-color: #28a745;
color: white;
border-radius: 50%;
text-align: center;
line-height: 50px;
font-size: 24px;
text-decoration: none;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
transition: background-color 0.3s ease;
z-index: 1000; /* Assure que le bouton est au-dessus des autres éléments */
}
.plus-button:hover {
background-color: #218838;
}
.plus-button:active {
background-color: #1e7e34;
}

@ -1,11 +1,4 @@
<h1>{{ 'recipe.list' | transloco }}</h1>
<div id="recipe-list">
<app-recipe-mini *ngFor="let recipe of paginatedRecipes" [recipe]="recipe"></app-recipe-mini>
<p>recipe-list works!</p>
<div>
<app-recipe-mini *ngFor="let recipe of recipes" [recipe]="recipe"></app-recipe-mini>
</div>
<a routerLink="/recipe/add" class="plus-button">+</a>
<mat-paginator class="pagination" (page)="handlePageEvent($event)" [length]="length" [pageSize]="pageSize"
[pageIndex]="pageIndex">
</mat-paginator>

@ -3,36 +3,22 @@ import { Recipe } from '../../model/recipe.model';
import { RecipeService } from '../../services/recipe.service';
import { NgFor } from '@angular/common';
import { RecipeMiniComponent } from '../recipe-mini/recipe-mini.component';
import { TranslocoPipe } from '@jsverse/transloco';
import { MatPaginatorModule, PageEvent} from '@angular/material/paginator';
import { RouterLink } from '@angular/router';
@Component({
selector: 'app-recipe-list',
standalone: true,
imports: [NgFor, RecipeMiniComponent, TranslocoPipe, MatPaginatorModule, RouterLink],
imports: [NgFor, RecipeMiniComponent],
templateUrl: './recipe-list.component.html',
styleUrl: './recipe-list.component.css'
})
export class RecipeListComponent {
recipes: Recipe[] = [];
paginatedRecipes: Recipe[] = [];
length?: number;
pageSize = 10;
pageIndex = 0;
recipes : Recipe[] = [];
constructor(protected recipeService: RecipeService) { }
constructor(protected recipeService: RecipeService){}
ngOnInit() {
ngOnInit(){
this.recipes = this.recipeService.getRecipes();
this.length = this.recipes.length;
this.paginatedRecipes = this.recipes.slice(this.pageIndex * this.pageSize, (this.pageIndex +1) * this.pageSize)
}
handlePageEvent(e: PageEvent) {
this.length = e.length;
this.pageSize = e.pageSize;
this.pageIndex = e.pageIndex;
this.paginatedRecipes = this.recipes.slice(this.pageIndex * this.pageSize, (this.pageIndex +1) * this.pageSize)
}
}

@ -1,9 +1,10 @@
.recipe-card {
border: 2px solid black;
/* border-radius: 8px; */
border: 1px solid #ddd;
border-radius: 8px;
overflow: hidden;
width: 300px;
/* box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); */
margin: 10px;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.recipe-image img {
@ -27,22 +28,6 @@
color: #666;
}
a {
display: flex;
justify-content: center;
border: 2px solid black;
padding: 10px 20px;
color: white;
background-color: black;
text-decoration: none;
transition: .3s;
}
a:hover {
color: inherit;
background-color: #fff;
}
button {
position: relative;
background: #444;
@ -74,7 +59,6 @@ button span {
position: relative;
z-index: 1;
}
button i {
position: absolute;
inset: 0;
@ -121,11 +105,9 @@ button:hover i::after {
0% {
transform: translateX(0);
}
50% {
transform: translateX(5px);
}
100% {
transform: translateX(0);
}
@ -135,11 +117,9 @@ button:hover i::after {
0% {
box-shadow: #27272c;
}
50% {
box-shadow: 0 0 25px var(--clr);
}
100% {
box-shadow: #27272c;
}

@ -1,12 +1,10 @@
<div class="recipe-card">
<div class="recipe-image">
<img [src]="recipe.image" onerror="this.onerror=null;this.src='https://placehold.co/100x100';"
alt="{{ recipe.name }}">
<img [src]="recipe.image" onerror="this.onerror=null;this.src='https://placehold.co/100x100';" alt="{{recipe.name}}">
</div>
<div class="recipe-details">
<h2>{{ recipe.name }}</h2>
<p>{{ recipe.description | truncate:50:false }}</p>
<a [routerLink]="['/recipe', recipe.id]">{{ 'recipe.see' | transloco }}</a>
<a (click)='addRecipe(recipe)'>Add Recipe to cart</a>
<h2>{{recipe.name}}</h2>
<p>{{recipe.description}}</p>
<button (click)="navigateToRecipe()" style="--clr:#39FF14"><span>Voir la recette</span><i></i></button>
</div>
</div>

@ -1,25 +1,19 @@
import { Component } from '@angular/core';
import { Recipe } from '../../model/recipe.model';
import { Input } from '@angular/core';
import { TruncatePipe } from '../../pipes/truncate.pipe';
import { TranslocoPipe } from '@jsverse/transloco';
import { RouterModule } from '@angular/router';
import { CommandeService } from '../../services/commandes.service';
@Component({
selector: 'app-recipe-mini',
standalone: true,
imports: [TruncatePipe, TranslocoPipe, RouterModule],
imports: [],
templateUrl: './recipe-mini.component.html',
styleUrl: './recipe-mini.component.css'
})
export class RecipeMiniComponent {
@Input() recipe!: Recipe;
constructor(protected serviceCommande: CommandeService){}
addRecipe(recipe: Recipe)
{
this.serviceCommande.addRecipe(recipe);
navigateToRecipe(){
console.log("TODO");
}
}

@ -1,21 +0,0 @@
.button {
display: inline-block;
padding: 10px 20px;
font-size: 16px;
color: white;
background-color: #007BFF;
text-align: center;
text-decoration: none;
border-radius: 5px;
border: none;
cursor: pointer;
transition: background-color 0.3s ease;
}
.button:hover {
background-color: #0056b3;
}
.button:active {
background-color: #004085;
}

@ -1,4 +0,0 @@
<div *ngIf="commandes">
<app-recipe-mini *ngFor="let recipe of commandes" [recipe]="recipe"></app-recipe-mini>
<a class="button" (click)="deleteCart()">Clear recipes</a>
</div>

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

@ -1,29 +0,0 @@
import { Component } from '@angular/core';
import { CommandeService } from '../../services/commandes.service';
import { OnInit } from '@angular/core';
import { Recipe } from '../../model/recipe.model';
import { NgFor, NgIf } from '@angular/common';
import { RecipeMiniComponent } from '../recipe-mini/recipe-mini.component';
@Component({
selector: 'app-saved-recipe',
standalone: true,
imports: [NgIf, NgFor, RecipeMiniComponent],
templateUrl: './saved-recipe.component.html',
styleUrl: './saved-recipe.component.css'
})
export class SavedRecipeComponent {
public commandes?: Recipe[];
constructor(private serviceCommande : CommandeService){}
ngOnInit(){
this.commandes = this.serviceCommande.getRecipe();
}
deleteCart(){
this.commandes = [];
this.serviceCommande.clearRecipes();
}
}

@ -2,5 +2,4 @@ export interface Ingredient{
id: number;
name: string;
qty: number;
description: string;
}

@ -1,8 +0,0 @@
import { TruncatePipe } from './truncate.pipe';
describe('TruncatePipe', () => {
it('create an instance', () => {
const pipe = new TruncatePipe();
expect(pipe).toBeTruthy();
});
});

@ -1,20 +0,0 @@
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'truncate',
standalone: true,
})
export class TruncatePipe implements PipeTransform {
transform(value: string, limit: number = 50, completeWords: boolean = false, ellipsis: string = '...'): string {
if (!value) return '';
if (value.length <= limit) return value;
if (completeWords) {
limit = value.substring(0, limit).lastIndexOf(' ');
}
return `${value.substring(0, limit)}${ellipsis}`;
}
}

@ -0,0 +1,32 @@
<h2>New Recipe</h2>
<form [formGroup]="recipeForm" (ngSubmit)="onSubmit()">
<label for="name">Name:</label>
<input id="name" formControlName="name">
<br>
<label for="description">Description:</label>
<textarea id="description" formControlName="description"></textarea>
<br>
<label for="image">Image URL:</label>
<input id="image" type="file" formControlName="image" (change)="onFileChange($event)">
<br>
<div formArrayName="ingredients">
<h3>Ingredients</h3>
<button type="button" (click)="addIngredient()">Add Ingredient</button>
<div *ngFor="let ingredient of ingredients.controls; let i = index" [formGroupName]="i">
<label for="ingredientName">Ingredient:</label>
<select id="ingredientName" formControlName="name">
<option *ngFor="let option of ingredientsOptions" [value]="option" [selected]="option === defaultOption">
{{option}}</option>
</select>
<label for="ingredientQuantity">Quantity:</label>
<input id="ingredientQuantity" formControlName="qty">
<button type="button" (click)="removeIngredient(i)">Remove</button>
<br>
</div>
</div>
<button type="submit">Add Recipe</button>
</form>

@ -1,30 +1,17 @@
import { Component } from '@angular/core';
import { FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Recipe } from '../../model/recipe.model';
import { Recipe } from '../model/recipe.model';
import { Ingredient } from '../model/ingredient.model';
import { CommonModule } from '@angular/common';
import { ReactiveFormsModule } from '@angular/forms';
import { RecipeService } from '../../services/recipe.service';
import { TranslocoPipe } from '@jsverse/transloco';
import { Ingredient } from '../../model/ingredient.model';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatButtonModule } from '@angular/material/button';
import { MatSelectModule} from '@angular/material/select';
import { Router } from '@angular/router';
import { IngredientService } from '../../services/ingredient.service';
import { RecipeService } from '../services/recipe.service';
@Component({
selector: 'app-recipe-form',
standalone: true,
imports: [
CommonModule,
ReactiveFormsModule,
TranslocoPipe,
MatFormFieldModule,
MatInputModule,
MatButtonModule,
MatSelectModule
ReactiveFormsModule
],
templateUrl: './recipe-form.component.html',
styleUrls: ['./recipe-form.component.css']
@ -33,20 +20,16 @@ import { IngredientService } from '../../services/ingredient.service';
export class RecipeFormComponent {
recipeForm: FormGroup;
base64Image: string | ArrayBuffer | null = null;
ingredientsOptions! : Ingredient[];
defaultOption: string = 'Veuillez choisir';
filename: string = '';
ingredientsOptions = ['Champignon', 'Tomata', 'Mozarella'];
defaultOption: string = this.ingredientsOptions[2];
constructor(private fb: FormBuilder, private recipeService: RecipeService, private router: Router,private serviceIngredients: IngredientService) {
constructor(private fb: FormBuilder, private recipeService: RecipeService) {
this.recipeForm = this.fb.group({
name: ['', Validators.required],
description: ['', Validators.required],
image: ['', Validators.required],
ingredients: this.fb.array([]),
});
this.serviceIngredients.getIngredient().subscribe(ingredients => {
this.ingredientsOptions = ingredients;
});
}
get ingredients(): FormArray {
@ -68,7 +51,6 @@ export class RecipeFormComponent {
const input = event.target as HTMLInputElement;
if (input.files && input.files[0]) {
const file = input.files[0];
this.filename = file.name;
const reader = new FileReader();
reader.onload = () => {
this.base64Image = reader.result;
@ -91,8 +73,8 @@ export class RecipeFormComponent {
}))
};
console.log('Recipe added:', newRecipe);
let newId = this.recipeService.addRecipe(newRecipe);
this.router.navigate(['recipe', newId - 1]);
this.recipeService.addRecipe(newRecipe);
this.recipeForm.reset();
} else {
console.log('Form is invalid');
}

@ -1,38 +0,0 @@
import { Injectable } from "@angular/core";
import { Recipe } from "../model/recipe.model";
@Injectable({
providedIn: 'root'
})
export class CommandeService {
private commande: Recipe[] = [];
private localStorageKey = 'commande';
constructor(){
console.log(this.commande);
}
getRecipeById(id: number) {
return this.commande.find(e => e.id === id);
}
// Get recipes from local storage
getRecipe(): Recipe[] {
const recipesJson = localStorage.getItem(this.localStorageKey) || "[]";
this.commande = JSON.parse(recipesJson) || [];
return this.commande;
}
// Add a new recipe
addRecipe(recipe: Recipe): number {
this.getRecipe();
recipe.id = this.commande.length
let newId = this.commande.push(recipe);
localStorage.setItem(this.localStorageKey, JSON.stringify(this.commande));
return newId;
}
// Clear all recipes (for example, if needed)
clearRecipes(): void {
localStorage.removeItem(this.localStorageKey);
}
}

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

@ -10,10 +10,6 @@ export class RecipeService {
constructor() { }
getRecipeById(id: number) {
return this.recipes.find(e => e.id === id);
}
// Get recipes from local storage
getRecipes(): Recipe[] {
const recipesJson = localStorage.getItem(this.localStorageKey) || "[]";
@ -22,12 +18,10 @@ export class RecipeService {
}
// Add a new recipe
addRecipe(recipe: Recipe): number {
addRecipe(recipe: Recipe): void {
this.getRecipes();
recipe.id = this.recipes.length
let newId = this.recipes.push(recipe);
this.recipes.push(recipe);
localStorage.setItem(this.localStorageKey, JSON.stringify(this.recipes));
return newId;
}
// Clear all recipes (for example, if needed)

@ -1,12 +0,0 @@
import { inject, Injectable } from "@angular/core";
import { Translation, TranslocoLoader } from "@jsverse/transloco";
import { HttpClient } from "@angular/common/http";
@Injectable({ providedIn: 'root' })
export class TranslocoHttpLoader implements TranslocoLoader {
private http = inject(HttpClient);
getTranslation(lang: string) {
return this.http.get<Translation>(`../assets/i18n/${lang}.json`);
}
}

@ -1,15 +0,0 @@
{
"title": "Home",
"recipe": {
"add": {
"link": "Add Recipe",
"button-ingredients": "Add Ingredients",
"button-recipe": "Add Recipe"
},
"see": "View Recipe",
"list": "Recipe List",
"ingredients": "Ingredients"
},
"logout": "logout",
"login": "login"
}

@ -1,15 +0,0 @@
{
"title": "Accueil",
"recipe": {
"add": {
"link": "Ajouter une recette",
"button-ingredients": "Ajouter des ingrédients",
"button-recipe": "Ajouter la recette"
},
"see": "Voir la recette",
"list": "Liste de recettes",
"ingredients": "Ingrédients"
},
"logout": "déconnexion",
"login": "connexion"
}

@ -1,15 +0,0 @@
{
"title": "Домой",
"recipe": {
"add": {
"link": "Добавить рецепт",
"button-ingredients": "Добавить ингредиенты",
"button-recipe": "Добавить рецепт"
},
"see": "Посмотреть рецепт",
"list": "Список рецептов",
"ingredients": "Ингредиенты"
},
"logout": "logout",
"login": "login"
}

@ -1,18 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>BromistaNisqaReceta</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
</head>
<body>
<app-root></app-root>
</body>
</html>
</html>

@ -1,3 +1 @@
:root {
font-family: "Helvetica";
}
/* You can add global styles to this file, and also import other style files */

@ -1,5 +0,0 @@
module.exports = {
rootTranslationsPath: 'src/assets/i18n/',
langs: ['en', 'fr', 'ru'],
keysManager: {}
};
Loading…
Cancel
Save