Compare commits

..

27 Commits

Author SHA1 Message Date
Aurian JAULT 4fcb9dc607 fixed ingredient name display
11 months ago
Aurian JAULT b985dd6fd1 Ended project
11 months ago
Aurian JAULT 007772da75 Login/logOut fixed
11 months ago
Aurian JAULT aa1f297106 Security and login
11 months ago
remrem 62dfca2838 Merge branch 'master' of https://codefirst.iut.uca.fr/git/aurian.jault/bromista-nisqa-receta
12 months ago
remrem 5b8b6f33ee pagination
12 months ago
remrem aaaa3a3a53 Merge pull request 'add component mini-ingredient for use in recipe detail' (#7) from mini-ingredient into master
12 months ago
remrem 2295d02452 add component mini-ingredient for use in recipe detail
12 months ago
remrem 5cedd5a720 ui for language change + translate
12 months ago
remrem 88d5451a2c typo
12 months ago
remrem b630f74622 use mat-angular for form ui
12 months ago
remrem a9905dcf9a Merge pull request 'translation' (#6) from translation into master
12 months ago
remrem e3533445d2 replace href by routerLink
12 months ago
remrem c6141226a2 partially working translation
12 months ago
remrem 657461163b nav-bar minimal style
12 months ago
remrem c415c3c19f Merge pull request 'add truncate pipe for recipe-mini description' (#5) from truncate-pipe into master
12 months ago
remrem 8e49b63a99 add truncate pipe for recipe-mini description
12 months ago
remrem b2c05f1440 Merge pull request 'fix/structure' (#4) from fix/structure into master
12 months ago
remrem 398a2ceeef Merge remote-tracking branch 'origin' into fix/structure
12 months ago
remrem 91c7389a62 remove .vscode/ and add to .gitignore
12 months ago
remrem 9d52ef64cc Merge pull request 'routing' (#3) from routing into master
12 months ago
remrem 77add72cab Merge remote-tracking branch 'origin' into routing
12 months ago
remrem 8971a4e0f1 add routing + details for recipe + errors components
12 months ago
remrem b198c891e6 Merge pull request 'add basic routing and nav-bar' (#2) from routing into master
12 months ago
remrem a41065a4bc Merge remote-tracking branch 'origin' into routing
12 months ago
remrem 2ae3528919 add basic routing and nav-bar
12 months ago
remrem 2b64147432 Merge pull request 'add-recipe-form' (#1) from add-recipe-form into master
12 months ago

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

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

@ -1,20 +0,0 @@
{
// 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

@ -1,42 +0,0 @@
{
// 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,6 +25,7 @@
"src/assets"
],
"styles": [
"@angular/material/prebuilt-themes/indigo-pink.css",
"src/styles.css"
],
"scripts": []
@ -84,6 +85,7 @@
"src/assets"
],
"styles": [
"@angular/material/prebuilt-themes/indigo-pink.css",
"src/styles.css"
],
"scripts": []
@ -91,5 +93,8 @@
}
}
}
},
"cli": {
"analytics": false
}
}

1408
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -11,13 +11,18 @@
"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"
@ -35,4 +40,4 @@
"karma-jasmine-html-reporter": "~2.1.0",
"typescript": "~5.4.2"
}
}
}

@ -0,0 +1,40 @@
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,6 +1,20 @@
<h1>Ratatouille</h1>
<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>
<div class="wrapper">
<app-recipe-list></app-recipe-list>
<app-recipe-form></app-recipe-form>
<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>

@ -1,27 +1,64 @@
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { RecipeFormComponent } from './recipe-form/recipe-form.component';
import { RouterOutlet, RouterLink } from '@angular/router';
import { RecipeFormComponent } from './components/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],
providers: [RecipeService, TranslocoService, NgIf],
templateUrl: './app.component.html',
styleUrl: './app.component.css'
})
export class AppComponent {
title = 'bromista-nisqa-receta';
isLogged = false;
constructor(protected recipeService: RecipeService, private translocoService: TranslocoService){}
constructor(protected recipeService: RecipeService){}
ngOnInit(){
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);
}
}
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,8 +1,31 @@
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { ApplicationConfig, isDevMode } from '@angular/core';
import { provideRouter, withComponentInputBinding } 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)]
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(),
),
]
};

@ -1,3 +1,23 @@
import { Routes } from '@angular/router';
import { Routes } from '@angular/router'
export const routes: Routes = [];
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] },
];

@ -0,0 +1,29 @@
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
}

@ -0,0 +1,23 @@
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();
});
});

@ -0,0 +1,21 @@
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');
})
}
}

@ -0,0 +1,37 @@
<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>

@ -0,0 +1,23 @@
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();
});
});

@ -0,0 +1,54 @@
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;
}
}

@ -0,0 +1,15 @@
.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;
}
}

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

@ -0,0 +1,23 @@
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();
});
});

@ -0,0 +1,14 @@
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;
}

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

@ -0,0 +1,23 @@
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();
});
});

@ -0,0 +1,21 @@
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');
}
}

@ -0,0 +1,23 @@
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();
});
});

@ -0,0 +1,20 @@
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('/');
}
}

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

@ -0,0 +1,11 @@
<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>

@ -0,0 +1,23 @@
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();
});
});

@ -0,0 +1,39 @@
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'])
}
});
}
}

@ -0,0 +1,20 @@
.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%;
}
}

@ -0,0 +1,47 @@
<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,17 +1,30 @@
import { Component } from '@angular/core';
import { FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Recipe } from '../model/recipe.model';
import { Ingredient } from '../model/ingredient.model';
import { Recipe } from '../../model/recipe.model';
import { CommonModule } from '@angular/common';
import { ReactiveFormsModule } from '@angular/forms';
import { RecipeService } from '../services/recipe.service';
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';
@Component({
selector: 'app-recipe-form',
standalone: true,
imports: [
CommonModule,
ReactiveFormsModule
ReactiveFormsModule,
TranslocoPipe,
MatFormFieldModule,
MatInputModule,
MatButtonModule,
MatSelectModule
],
templateUrl: './recipe-form.component.html',
styleUrls: ['./recipe-form.component.css']
@ -20,16 +33,20 @@ import { RecipeService } from '../services/recipe.service';
export class RecipeFormComponent {
recipeForm: FormGroup;
base64Image: string | ArrayBuffer | null = null;
ingredientsOptions = ['Champignon', 'Tomata', 'Mozarella'];
defaultOption: string = this.ingredientsOptions[2];
ingredientsOptions! : Ingredient[];
defaultOption: string = 'Veuillez choisir';
filename: string = '';
constructor(private fb: FormBuilder, private recipeService: RecipeService) {
constructor(private fb: FormBuilder, private recipeService: RecipeService, private router: Router,private serviceIngredients: IngredientService) {
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 {
@ -51,6 +68,7 @@ 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;
@ -73,8 +91,8 @@ export class RecipeFormComponent {
}))
};
console.log('Recipe added:', newRecipe);
this.recipeService.addRecipe(newRecipe);
this.recipeForm.reset();
let newId = this.recipeService.addRecipe(newRecipe);
this.router.navigate(['recipe', newId - 1]);
} else {
console.log('Form is invalid');
}

@ -0,0 +1,37 @@
#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,4 +1,11 @@
<p>recipe-list works!</p>
<div>
<app-recipe-mini *ngFor="let recipe of recipes" [recipe]="recipe"></app-recipe-mini>
<h1>{{ 'recipe.list' | transloco }}</h1>
<div id="recipe-list">
<app-recipe-mini *ngFor="let recipe of paginatedRecipes" [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,22 +3,36 @@ 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],
imports: [NgFor, RecipeMiniComponent, TranslocoPipe, MatPaginatorModule, RouterLink],
templateUrl: './recipe-list.component.html',
styleUrl: './recipe-list.component.css'
})
export class RecipeListComponent {
recipes : Recipe[] = [];
recipes: Recipe[] = [];
paginatedRecipes: Recipe[] = [];
length?: number;
pageSize = 10;
pageIndex = 0;
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,10 +1,9 @@
.recipe-card {
border: 1px solid #ddd;
border-radius: 8px;
border: 2px solid black;
/* border-radius: 8px; */
overflow: hidden;
width: 300px;
margin: 10px;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
/* box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); */
}
.recipe-image img {
@ -28,6 +27,22 @@
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;
@ -59,6 +74,7 @@ button span {
position: relative;
z-index: 1;
}
button i {
position: absolute;
inset: 0;
@ -105,9 +121,11 @@ button:hover i::after {
0% {
transform: translateX(0);
}
50% {
transform: translateX(5px);
}
100% {
transform: translateX(0);
}
@ -117,9 +135,11 @@ button:hover i::after {
0% {
box-shadow: #27272c;
}
50% {
box-shadow: 0 0 25px var(--clr);
}
100% {
box-shadow: #27272c;
}

@ -1,10 +1,12 @@
<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}}</p>
<button (click)="navigateToRecipe()" style="--clr:#39FF14"><span>Voir la recette</span><i></i></button>
<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>
</div>
</div>

@ -1,19 +1,25 @@
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: [],
imports: [TruncatePipe, TranslocoPipe, RouterModule],
templateUrl: './recipe-mini.component.html',
styleUrl: './recipe-mini.component.css'
})
export class RecipeMiniComponent {
@Input() recipe!: Recipe;
navigateToRecipe(){
console.log("TODO");
constructor(protected serviceCommande: CommandeService){}
addRecipe(recipe: Recipe)
{
this.serviceCommande.addRecipe(recipe);
}
}

@ -0,0 +1,21 @@
.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;
}

@ -0,0 +1,4 @@
<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>

@ -0,0 +1,23 @@
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();
});
});

@ -0,0 +1,29 @@
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,4 +2,5 @@ export interface Ingredient{
id: number;
name: string;
qty: number;
description: string;
}

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

@ -0,0 +1,20 @@
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}`;
}
}

@ -1,32 +0,0 @@
<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>

@ -0,0 +1,38 @@
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);
}
}

@ -0,0 +1,25 @@
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,6 +10,10 @@ 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) || "[]";
@ -18,10 +22,12 @@ export class RecipeService {
}
// Add a new recipe
addRecipe(recipe: Recipe): void {
addRecipe(recipe: Recipe): number {
this.getRecipes();
this.recipes.push(recipe);
recipe.id = this.recipes.length
let newId = this.recipes.push(recipe);
localStorage.setItem(this.localStorageKey, JSON.stringify(this.recipes));
return newId;
}
// Clear all recipes (for example, if needed)

@ -0,0 +1,12 @@
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`);
}
}

@ -0,0 +1,15 @@
{
"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"
}

@ -0,0 +1,15 @@
{
"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"
}

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

@ -1,13 +1,18 @@
<!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 +1,3 @@
/* You can add global styles to this file, and also import other style files */
:root {
font-family: "Helvetica";
}

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