aboutsummaryrefslogtreecommitdiffstats
path: root/public/projects/angular-small-apps/apps/recipes/src/app/components
diff options
context:
space:
mode:
Diffstat (limited to 'public/projects/angular-small-apps/apps/recipes/src/app/components')
-rw-r--r--public/projects/angular-small-apps/apps/recipes/src/app/components/page-not-found/page-not-found.component.html7
-rw-r--r--public/projects/angular-small-apps/apps/recipes/src/app/components/page-not-found/page-not-found.component.scss0
-rw-r--r--public/projects/angular-small-apps/apps/recipes/src/app/components/page-not-found/page-not-found.component.spec.ts24
-rw-r--r--public/projects/angular-small-apps/apps/recipes/src/app/components/page-not-found/page-not-found.component.ts11
-rw-r--r--public/projects/angular-small-apps/apps/recipes/src/app/components/recipe/recipe.component.html111
-rw-r--r--public/projects/angular-small-apps/apps/recipes/src/app/components/recipe/recipe.component.scss54
-rw-r--r--public/projects/angular-small-apps/apps/recipes/src/app/components/recipe/recipe.component.spec.ts24
-rw-r--r--public/projects/angular-small-apps/apps/recipes/src/app/components/recipe/recipe.component.ts140
-rw-r--r--public/projects/angular-small-apps/apps/recipes/src/app/components/recipes-list/recipes-list.component.html39
-rw-r--r--public/projects/angular-small-apps/apps/recipes/src/app/components/recipes-list/recipes-list.component.scss0
-rw-r--r--public/projects/angular-small-apps/apps/recipes/src/app/components/recipes-list/recipes-list.component.spec.ts24
-rw-r--r--public/projects/angular-small-apps/apps/recipes/src/app/components/recipes-list/recipes-list.component.ts43
-rw-r--r--public/projects/angular-small-apps/apps/recipes/src/app/components/search-results/search-results.component.html61
-rw-r--r--public/projects/angular-small-apps/apps/recipes/src/app/components/search-results/search-results.component.scss0
-rw-r--r--public/projects/angular-small-apps/apps/recipes/src/app/components/search-results/search-results.component.spec.ts24
-rw-r--r--public/projects/angular-small-apps/apps/recipes/src/app/components/search-results/search-results.component.ts44
-rw-r--r--public/projects/angular-small-apps/apps/recipes/src/app/components/search/search.component.html30
-rw-r--r--public/projects/angular-small-apps/apps/recipes/src/app/components/search/search.component.scss49
-rw-r--r--public/projects/angular-small-apps/apps/recipes/src/app/components/search/search.component.spec.ts24
-rw-r--r--public/projects/angular-small-apps/apps/recipes/src/app/components/search/search.component.ts29
-rw-r--r--public/projects/angular-small-apps/apps/recipes/src/app/components/toolbar/toolbar.component.html18
-rw-r--r--public/projects/angular-small-apps/apps/recipes/src/app/components/toolbar/toolbar.component.scss68
-rw-r--r--public/projects/angular-small-apps/apps/recipes/src/app/components/toolbar/toolbar.component.spec.ts24
-rw-r--r--public/projects/angular-small-apps/apps/recipes/src/app/components/toolbar/toolbar.component.ts65
24 files changed, 913 insertions, 0 deletions
diff --git a/public/projects/angular-small-apps/apps/recipes/src/app/components/page-not-found/page-not-found.component.html b/public/projects/angular-small-apps/apps/recipes/src/app/components/page-not-found/page-not-found.component.html
new file mode 100644
index 0000000..8be0976
--- /dev/null
+++ b/public/projects/angular-small-apps/apps/recipes/src/app/components/page-not-found/page-not-found.component.html
@@ -0,0 +1,7 @@
+<div class="not-found">
+ <h2>Page not found</h2>
+ <p>This usually happens when you didn't save a recipe from a search.</p>
+ <a routerLink="" class="btn">
+ <span class="btn__icon">&leftarrow; </span>Back to recipes
+ </a>
+</div>
diff --git a/public/projects/angular-small-apps/apps/recipes/src/app/components/page-not-found/page-not-found.component.scss b/public/projects/angular-small-apps/apps/recipes/src/app/components/page-not-found/page-not-found.component.scss
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/public/projects/angular-small-apps/apps/recipes/src/app/components/page-not-found/page-not-found.component.scss
diff --git a/public/projects/angular-small-apps/apps/recipes/src/app/components/page-not-found/page-not-found.component.spec.ts b/public/projects/angular-small-apps/apps/recipes/src/app/components/page-not-found/page-not-found.component.spec.ts
new file mode 100644
index 0000000..d11a45e
--- /dev/null
+++ b/public/projects/angular-small-apps/apps/recipes/src/app/components/page-not-found/page-not-found.component.spec.ts
@@ -0,0 +1,24 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { PageNotFoundComponent } from './page-not-found.component';
+
+describe('PageNotFoundComponent', () => {
+ let component: PageNotFoundComponent;
+ let fixture: ComponentFixture<PageNotFoundComponent>;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [PageNotFoundComponent],
+ }).compileComponents();
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(PageNotFoundComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/public/projects/angular-small-apps/apps/recipes/src/app/components/page-not-found/page-not-found.component.ts b/public/projects/angular-small-apps/apps/recipes/src/app/components/page-not-found/page-not-found.component.ts
new file mode 100644
index 0000000..8c9e8b0
--- /dev/null
+++ b/public/projects/angular-small-apps/apps/recipes/src/app/components/page-not-found/page-not-found.component.ts
@@ -0,0 +1,11 @@
+import { Component, OnInit } from '@angular/core';
+
+@Component({
+ templateUrl: './page-not-found.component.html',
+ styleUrls: ['./page-not-found.component.scss'],
+})
+export class PageNotFoundComponent implements OnInit {
+ constructor() {}
+
+ ngOnInit(): void {}
+}
diff --git a/public/projects/angular-small-apps/apps/recipes/src/app/components/recipe/recipe.component.html b/public/projects/angular-small-apps/apps/recipes/src/app/components/recipe/recipe.component.html
new file mode 100644
index 0000000..9f875d9
--- /dev/null
+++ b/public/projects/angular-small-apps/apps/recipes/src/app/components/recipe/recipe.component.html
@@ -0,0 +1,111 @@
+<article class="recipe">
+ <header class="recipe__header">
+ <toolbar (isEditionMode)="isContentEditable($event)"></toolbar>
+ <h2 class="recipe__title">
+ <ng-container
+ *ngIf="isEditable; then editableTitle; else staticTitle"
+ ></ng-container>
+ <ng-template #editableTitle>
+ <input
+ type="text"
+ value="{{ recipe.strMeal }}"
+ name="strMeal"
+ (input)="updateRecipe($event)"
+ class="recipe__field"
+ />
+ </ng-template>
+ <ng-template #staticTitle>{{ recipe.strMeal }}</ng-template>
+ </h2>
+ <dl class="meta">
+ <dt class="meta__term">Category:</dt>
+ <dd class="meta__description">
+ <ng-container
+ *ngIf="isEditable; then editableCategory; else staticCategory"
+ ></ng-container>
+ <ng-template #staticCategory>
+ {{ recipe.strCategory }}
+ </ng-template>
+ <ng-template #editableCategory>
+ <input
+ type="text"
+ name="strCategory"
+ value="{{ recipe.strCategory }}"
+ (input)="updateRecipe($event)"
+ class="recipe__field"
+ />
+ </ng-template>
+ </dd>
+ </dl>
+ </header>
+ <div class="recipe__body">
+ <div class="recipe__preview">
+ <h3>Preview</h3>
+ <img
+ [src]="getPreview()"
+ alt="{{ recipe.strMeal }} preview"
+ class="recipe__thumb"
+ />
+ <ng-container *ngIf="isEditable">
+ <label class="recipe__label btn" for="recipe-thumb">
+ Upload a new image
+ <input
+ type="file"
+ name="recipe-thumb"
+ id="recipe-thumb"
+ accept="image/png, image/jpeg"
+ class="recipe__field recipe__field--file"
+ (change)="updatePreview($event)"
+ />
+ </label>
+ </ng-container>
+ </div>
+ <div class="recipe__ingredients">
+ <h3>Ingredients</h3>
+ <ul class="ingredients-list">
+ <li
+ *ngFor="let ingredient of getIngredients(); index as i"
+ class="ingredients-list__item"
+ >
+ <ng-container
+ *ngIf="isEditable; then editableIngredients; else staticIngredients"
+ ></ng-container>
+ <ng-template #staticIngredients>
+ {{ ingredient }}
+ </ng-template>
+ <ng-template #editableIngredients>
+ <input
+ type="text"
+ name="strIngredient{{ i + 1 }}"
+ value="{{ ingredient }}"
+ (input)="updateRecipe($event)"
+ class="recipe__field"
+ />
+ </ng-template>
+ </li>
+ </ul>
+ </div>
+ <div class="recipe__instructions">
+ <h3>Instructions</h3>
+ <ng-container
+ *ngIf="isEditable; then editableInstructions; else staticInstructions"
+ ></ng-container>
+ <ng-template #editableInstructions>
+ <textarea
+ textareaResize
+ name="strInstructions"
+ (input)="updateRecipe($event)"
+ class="recipe__field recipe__field--textarea"
+ >{{ recipe.strInstructions }}</textarea
+ >
+ </ng-template>
+ <ng-template #staticInstructions>{{
+ recipe.strInstructions
+ }}</ng-template>
+ </div>
+ </div>
+ <footer class="recipe__footer">
+ <a routerLink="" class="btn">
+ <span class="btn__icon">&leftarrow; </span>Back to your recipes
+ </a>
+ </footer>
+</article>
diff --git a/public/projects/angular-small-apps/apps/recipes/src/app/components/recipe/recipe.component.scss b/public/projects/angular-small-apps/apps/recipes/src/app/components/recipe/recipe.component.scss
new file mode 100644
index 0000000..82294b8
--- /dev/null
+++ b/public/projects/angular-small-apps/apps/recipes/src/app/components/recipe/recipe.component.scss
@@ -0,0 +1,54 @@
+.recipe {
+ background: #fff;
+ border: 1px solid #d7d7d7;
+ border-radius: 5px;
+ box-shadow: 0 0 5px -3px #000000a5;
+ padding: clamp(4rem, 3vw, 5rem) clamp(1rem, 3vw, 2rem) clamp(2rem, 3vw, 4rem);
+ position: relative;
+
+ &__body {
+ display: flex;
+ flex-flow: row wrap;
+ align-items: flex-start;
+ gap: clamp(1rem, 3vw, 2rem);
+ margin: 1rem 0;
+ }
+
+ &__thumb {
+ max-width: min(100vw - 4rem, 300px);
+ }
+
+ &__instructions {
+ flex: 0 0 100%;
+ white-space: pre-line;
+ }
+
+ &__footer {
+ margin: 2rem 0 0;
+ }
+
+ &__field {
+ all: inherit;
+ width: 100%;
+
+ &--textarea {
+ width: 100%;
+ min-height: 10rem;
+ }
+
+ &--file {
+ display: none;
+ }
+ }
+
+ &__label {
+ display: block;
+ cursor: pointer;
+ width: max-content;
+ margin: auto;
+ }
+}
+
+.ingredients-list {
+ padding: 0 1rem;
+}
diff --git a/public/projects/angular-small-apps/apps/recipes/src/app/components/recipe/recipe.component.spec.ts b/public/projects/angular-small-apps/apps/recipes/src/app/components/recipe/recipe.component.spec.ts
new file mode 100644
index 0000000..55a83e9
--- /dev/null
+++ b/public/projects/angular-small-apps/apps/recipes/src/app/components/recipe/recipe.component.spec.ts
@@ -0,0 +1,24 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { RecipeComponent } from './recipe.component';
+
+describe('RecipeComponent', () => {
+ let component: RecipeComponent;
+ let fixture: ComponentFixture<RecipeComponent>;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [RecipeComponent],
+ }).compileComponents();
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(RecipeComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/public/projects/angular-small-apps/apps/recipes/src/app/components/recipe/recipe.component.ts b/public/projects/angular-small-apps/apps/recipes/src/app/components/recipe/recipe.component.ts
new file mode 100644
index 0000000..070220f
--- /dev/null
+++ b/public/projects/angular-small-apps/apps/recipes/src/app/components/recipe/recipe.component.ts
@@ -0,0 +1,140 @@
+import { Component, Input, OnInit, ViewEncapsulation } from '@angular/core';
+import { ActivatedRoute, Event, Router } from '@angular/router';
+import { Recipes } from 'src/app/shared/recipes';
+import { LocalStorageService } from 'src/app/shared/services/local-storage.service';
+import { RecipesService } from 'src/app/shared/services/recipes.service';
+
+@Component({
+ templateUrl: './recipe.component.html',
+ styleUrls: ['./recipe.component.scss'],
+ encapsulation: ViewEncapsulation.Emulated,
+})
+export class RecipeComponent implements OnInit {
+ recipe: Partial<Recipes> = {};
+ isEditable: boolean = false;
+ savedRecipes: Recipes[] = this.storage.get('recipes');
+ preview: string = this.recipe.strMealThumb!;
+
+ constructor(
+ private storage: LocalStorageService,
+ private route: ActivatedRoute,
+ private recipes: RecipesService,
+ private router: Router
+ ) {}
+
+ ngOnInit(): void {
+ const slug = this.route.snapshot.paramMap.get('slug') || '';
+ this.setRecipe(slug);
+ this.preview = this.recipe.strMealThumb!;
+ }
+
+ setRecipe(slug: string): void {
+ const allRecipes = this.storage.get('recipes');
+ const filteredRecipes = allRecipes.filter(
+ (meal: Recipes) => meal.slug === slug
+ );
+
+ if (filteredRecipes.length === 0) {
+ const recipeId = history.state?.id;
+ if (recipeId) {
+ this.recipes.getRecipeById(recipeId).subscribe((recipes: any) => {
+ this.recipe = recipes.meals[0];
+ this.preview = this.recipe.strMealThumb!;
+ });
+ } else {
+ this.router.navigateByUrl('/404');
+ }
+ } else {
+ this.recipe = filteredRecipes[0] ? filteredRecipes[0] : {};
+ }
+ }
+
+ getIngredients(): string[] {
+ const ingredients = [];
+
+ for (let i = 1; i <= 20; i++) {
+ const currentIngredient = `strIngredient${i}` as keyof Recipes;
+ let ingredient;
+
+ if (this.recipe[currentIngredient]) {
+ const currentMeasure = `strMeasure${i}` as keyof Recipes;
+
+ if (this.recipe[currentMeasure]) {
+ ingredient = `${this.recipe[currentMeasure]} ${this.recipe[currentIngredient]}`;
+ } else {
+ ingredient = `${this.recipe[currentIngredient]}`;
+ }
+ }
+
+ if (ingredient) ingredients.push(ingredient);
+ }
+
+ return ingredients;
+ }
+
+ isContentEditable(value: boolean): void {
+ this.isEditable = value;
+ }
+
+ updateRecipe(e: any) {
+ const recipeProperty = e.target.name as keyof Recipes;
+
+ if (!recipeProperty) {
+ return;
+ }
+
+ this.recipe[recipeProperty] = e.target.value;
+
+ if (recipeProperty.startsWith('strIngredient')) {
+ const ingredientNumber = recipeProperty.replace('strIngredient', '');
+ const measureProperty = `strMeasure${ingredientNumber}` as keyof Recipes;
+ this.recipe[measureProperty] = undefined;
+ }
+
+ const updatedRecipes = this.savedRecipes.map((recipe: Recipes) => {
+ if (recipe.idMeal === this.recipe.idMeal) {
+ return { ...this.recipe };
+ }
+ return recipe;
+ });
+ this.storage.set('recipes', updatedRecipes);
+ }
+
+ getPreview() {
+ return this.preview;
+ }
+
+ getImage(img: any) {
+ var canvas = document.createElement('canvas');
+ canvas.width = img.width;
+ canvas.height = img.height;
+
+ var ctx = canvas.getContext('2d')!;
+ ctx.drawImage(img, 0, 0);
+
+ var dataURL = canvas.toDataURL('image/png');
+
+ return dataURL.replace(/^data:image\/(png|jpg);base64,/, '');
+ }
+
+ updatePreview(e: any) {
+ const newPreview = e.target.files[0];
+
+ if (FileReader && newPreview) {
+ const fileReader = new FileReader();
+ fileReader.onload = (reader) => {
+ if (reader.target?.result) {
+ const updatedRecipes = this.savedRecipes.map((recipe: Recipes) => {
+ if (recipe.idMeal === this.recipe.idMeal) {
+ return { ...recipe, strMealThumb: reader.target?.result };
+ }
+ return recipe;
+ });
+ this.preview = reader.target?.result as string;
+ this.storage.set('recipes', updatedRecipes);
+ }
+ };
+ fileReader.readAsDataURL(newPreview);
+ }
+ }
+}
diff --git a/public/projects/angular-small-apps/apps/recipes/src/app/components/recipes-list/recipes-list.component.html b/public/projects/angular-small-apps/apps/recipes/src/app/components/recipes-list/recipes-list.component.html
new file mode 100644
index 0000000..463b4f3
--- /dev/null
+++ b/public/projects/angular-small-apps/apps/recipes/src/app/components/recipes-list/recipes-list.component.html
@@ -0,0 +1,39 @@
+<ul class="recipes-list">
+ <li *ngFor="let recipe of recipes | async" class="recipes-list__item">
+ <article class="card">
+ <header class="card__header">
+ <img
+ src="{{ recipe.strMealThumb }}"
+ alt="{{ recipe.strMeal }} picture"
+ class="card__thumb"
+ />
+ <h2 class="card__title">{{ recipe.strMeal }}</h2>
+ </header>
+ <div class="card__body">
+ {{
+ recipe.strInstructions.length > 120
+ ? (recipe.strInstructions | slice: 0:120) + "&hellip;"
+ : recipe.strInstructions
+ }}
+ </div>
+ <footer class="card__footer">
+ <dl class="meta">
+ <div class="meta__item">
+ <dt class="meta__term">Category:</dt>
+ <dd class="meta__description">{{ recipe.strCategory }}</dd>
+ </div>
+ <div class="meta__item" *ngIf="recipe.strTags">
+ <dt class="meta__term">Tags:</dt>
+ <dd class="meta__description">
+ {{ recipe.strTags | formatComma }}
+ </dd>
+ </div>
+ </dl>
+ <a [routerLink]="['/recipe/', recipe.slug]" class="btn">
+ Read more
+ <span class="btn__icon"> &rightarrow;</span>
+ </a>
+ </footer>
+ </article>
+ </li>
+</ul>
diff --git a/public/projects/angular-small-apps/apps/recipes/src/app/components/recipes-list/recipes-list.component.scss b/public/projects/angular-small-apps/apps/recipes/src/app/components/recipes-list/recipes-list.component.scss
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/public/projects/angular-small-apps/apps/recipes/src/app/components/recipes-list/recipes-list.component.scss
diff --git a/public/projects/angular-small-apps/apps/recipes/src/app/components/recipes-list/recipes-list.component.spec.ts b/public/projects/angular-small-apps/apps/recipes/src/app/components/recipes-list/recipes-list.component.spec.ts
new file mode 100644
index 0000000..d35b397
--- /dev/null
+++ b/public/projects/angular-small-apps/apps/recipes/src/app/components/recipes-list/recipes-list.component.spec.ts
@@ -0,0 +1,24 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { RecipesListComponent } from './recipes-list.component';
+
+describe('RecipesListComponent', () => {
+ let component: RecipesListComponent;
+ let fixture: ComponentFixture<RecipesListComponent>;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [RecipesListComponent],
+ }).compileComponents();
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(RecipesListComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/public/projects/angular-small-apps/apps/recipes/src/app/components/recipes-list/recipes-list.component.ts b/public/projects/angular-small-apps/apps/recipes/src/app/components/recipes-list/recipes-list.component.ts
new file mode 100644
index 0000000..499b116
--- /dev/null
+++ b/public/projects/angular-small-apps/apps/recipes/src/app/components/recipes-list/recipes-list.component.ts
@@ -0,0 +1,43 @@
+import { Component, OnInit } from '@angular/core';
+import { BehaviorSubject } from 'rxjs';
+import { Recipes } from 'src/app/shared/recipes';
+import { LocalStorageService } from 'src/app/shared/services/local-storage.service';
+import { RecipesService } from 'src/app/shared/services/recipes.service';
+import { slugify } from 'src/app/shared/utilities/slugify';
+
+@Component({
+ templateUrl: './recipes-list.component.html',
+ styleUrls: ['./recipes-list.component.scss'],
+})
+export class RecipesListComponent implements OnInit {
+ private _recipes = new BehaviorSubject<Recipes[]>([]);
+ public recipes = this._recipes.asObservable();
+
+ constructor(
+ private storage: LocalStorageService,
+ private recipesService: RecipesService
+ ) {}
+
+ ngOnInit(): void {
+ if (this.storage.get('recipes')) {
+ this._recipes.next(this.storage.get('recipes'));
+ } else {
+ this.setRecipes();
+ this.saveRecipes();
+ }
+ }
+
+ setRecipes(): any {
+ this.recipesService.getAllRecipes().subscribe((recipes: any) => {
+ const allRecipes: Recipes[] = recipes.meals;
+ const allRecipesWithSlug = allRecipes.map((recipe: Recipes) => {
+ return { ...recipe, slug: slugify(recipe.strMeal) };
+ });
+ this._recipes.next(allRecipesWithSlug);
+ });
+ }
+
+ saveRecipes(): void {
+ this.recipes.subscribe((recipes) => this.storage.set('recipes', recipes));
+ }
+}
diff --git a/public/projects/angular-small-apps/apps/recipes/src/app/components/search-results/search-results.component.html b/public/projects/angular-small-apps/apps/recipes/src/app/components/search-results/search-results.component.html
new file mode 100644
index 0000000..53b0d07
--- /dev/null
+++ b/public/projects/angular-small-apps/apps/recipes/src/app/components/search-results/search-results.component.html
@@ -0,0 +1,61 @@
+<ng-container
+ *ngIf="recipes && recipes.length > 0; then recipesList; else notFound"
+></ng-container>
+<ng-template #recipesList>
+ <ul class="recipes-list">
+ <li *ngFor="let recipe of recipes" class="recipes-list__item">
+ <article class="card">
+ <header class="card__header">
+ <img
+ src="{{ recipe.strMealThumb }}"
+ alt="{{ recipe.strMeal }} picture"
+ class="card__thumb"
+ />
+ <h2 class="card__title">{{ recipe.strMeal }}</h2>
+ </header>
+ <div *ngIf="recipe.strInstructions" class="card__body">
+ {{
+ recipe.strInstructions.length > 120
+ ? (recipe.strInstructions | slice: 0:120) + "&hellip;"
+ : recipe.strInstructions
+ }}
+ </div>
+ <footer class="card__footer">
+ <dl class="meta">
+ <div class="meta__item" *ngIf="recipe.strCategory">
+ <dt class="meta__term">Category:</dt>
+ <dd class="meta__description">{{ recipe.strCategory }}</dd>
+ </div>
+ <div class="meta__item" *ngIf="recipe.strTags">
+ <dt class="meta__term">Tags:</dt>
+ <dd class="meta__description">
+ {{ recipe.strTags | formatComma }}
+ </dd>
+ </div>
+ </dl>
+ <a
+ [routerLink]="['/recipe/', recipe.strMeal | slugify]"
+ [state]="{ id: recipe.idMeal }"
+ class="btn"
+ >
+ Read more
+ <span class="btn__icon"> &rightarrow;</span>
+ </a>
+ </footer>
+ </article>
+ </li>
+ </ul>
+ <a routerLink="" class="btn">
+ <span class="btn__icon">&leftarrow; </span>Back to recipes
+ </a>
+</ng-template>
+<ng-template #notFound>
+ <div class="not-found">
+ <div class="not-found__result">
+ No recipe match your query: {{ query }}.
+ </div>
+ <a routerLink="" class="btn">
+ <span class="btn__icon">&leftarrow; </span>Back to your recipes
+ </a>
+ </div>
+</ng-template>
diff --git a/public/projects/angular-small-apps/apps/recipes/src/app/components/search-results/search-results.component.scss b/public/projects/angular-small-apps/apps/recipes/src/app/components/search-results/search-results.component.scss
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/public/projects/angular-small-apps/apps/recipes/src/app/components/search-results/search-results.component.scss
diff --git a/public/projects/angular-small-apps/apps/recipes/src/app/components/search-results/search-results.component.spec.ts b/public/projects/angular-small-apps/apps/recipes/src/app/components/search-results/search-results.component.spec.ts
new file mode 100644
index 0000000..75ad455
--- /dev/null
+++ b/public/projects/angular-small-apps/apps/recipes/src/app/components/search-results/search-results.component.spec.ts
@@ -0,0 +1,24 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { SearchResultsComponent } from './search-results.component';
+
+describe('SearchResultsComponent', () => {
+ let component: SearchResultsComponent;
+ let fixture: ComponentFixture<SearchResultsComponent>;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [SearchResultsComponent],
+ }).compileComponents();
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(SearchResultsComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/public/projects/angular-small-apps/apps/recipes/src/app/components/search-results/search-results.component.ts b/public/projects/angular-small-apps/apps/recipes/src/app/components/search-results/search-results.component.ts
new file mode 100644
index 0000000..4a77732
--- /dev/null
+++ b/public/projects/angular-small-apps/apps/recipes/src/app/components/search-results/search-results.component.ts
@@ -0,0 +1,44 @@
+import { Component, OnInit } from '@angular/core';
+import { ActivatedRoute, Router } from '@angular/router';
+import { Recipes } from 'src/app/shared/recipes';
+import { SearchService } from 'src/app/shared/services/search.service';
+
+@Component({
+ templateUrl: './search-results.component.html',
+ styleUrls: ['./search-results.component.scss'],
+})
+export class SearchResultsComponent implements OnInit {
+ recipes: Recipes[] = [];
+ query: string = '';
+ by: string = '';
+
+ constructor(
+ private search: SearchService,
+ private router: Router,
+ private route: ActivatedRoute
+ ) {
+ this.getRecipes();
+ this.route.params.subscribe((param) => {
+ this.query = param['query'];
+ this.by = param['by'];
+ });
+ }
+
+ ngOnInit(): void {
+ if (this.recipes.length === 0) {
+ const currentURL = this.router.url.replace('%20', ' ');
+ const byParam = currentURL.match(/(?:by=)(\w+)/i);
+ const queryParam = currentURL.match(/(?:query=)([a-zA-Z\s]*)/i);
+ this.query = (queryParam && queryParam[1]) || '';
+ this.by = (byParam && byParam[1]) || '';
+
+ this.search.findResults(this.query, this.by);
+ }
+ }
+
+ getRecipes(): void {
+ this.search.getResults().subscribe((recipes) => {
+ this.recipes = recipes;
+ });
+ }
+}
diff --git a/public/projects/angular-small-apps/apps/recipes/src/app/components/search/search.component.html b/public/projects/angular-small-apps/apps/recipes/src/app/components/search/search.component.html
new file mode 100644
index 0000000..8854d5d
--- /dev/null
+++ b/public/projects/angular-small-apps/apps/recipes/src/app/components/search/search.component.html
@@ -0,0 +1,30 @@
+<form [formGroup]="searchForm" (ngSubmit)="onSubmit()" class="form">
+ <fieldset class="form__fieldset">
+ <legend class="form__legend">Search a new recipe</legend>
+ <div class="form__item">
+ <label for="search-by" class="form__label">By</label>
+ <select
+ id="search-by"
+ name="search-by"
+ class="form__select"
+ formControlName="by"
+ >
+ <option value="name" selected>Name</option>
+ <option value="ingredient">Ingredient</option>
+ <option value="category">Category</option>
+ </select>
+ </div>
+ <div class="form__item">
+ <label for="search-query" class="form__label">Query</label>
+ <input
+ type="search"
+ name="search-query"
+ id="search-query"
+ formControlName="query"
+ class="form__input"
+ placeholder="Keywords..."
+ />
+ </div>
+ <button class="btn" type="submit">Search</button>
+ </fieldset>
+</form>
diff --git a/public/projects/angular-small-apps/apps/recipes/src/app/components/search/search.component.scss b/public/projects/angular-small-apps/apps/recipes/src/app/components/search/search.component.scss
new file mode 100644
index 0000000..0b66d92
--- /dev/null
+++ b/public/projects/angular-small-apps/apps/recipes/src/app/components/search/search.component.scss
@@ -0,0 +1,49 @@
+.form {
+ background: #fff;
+ border: 1px solid #d7d7d7;
+ border-radius: 5px;
+ box-shadow: 0 0 5px -3px #000000a5;
+ padding: 0.5rem;
+ margin-bottom: 1rem;
+
+ &__fieldset {
+ border: none;
+ display: flex;
+ flex-flow: row wrap;
+ align-items: flex-end;
+ gap: 0.5rem;
+ }
+
+ &__legend {
+ font-size: 1.2rem;
+ font-weight: 600;
+ margin: 0 0 0.5rem;
+ padding: 0;
+ }
+
+ &__item {
+ display: flex;
+ flex-flow: column wrap;
+ }
+
+ &__label {
+ color: hsl(0, 0%, 25%);
+ font-size: 0.85rem;
+ font-weight: 600;
+ letter-spacing: 1px;
+ text-transform: uppercase;
+ cursor: pointer;
+ margin-bottom: 0.2rem;
+ }
+
+ &__input,
+ &__select {
+ background: #fff;
+ border: 2px solid #195881;
+ padding: 0.5rem;
+ }
+
+ &__select {
+ cursor: pointer;
+ }
+}
diff --git a/public/projects/angular-small-apps/apps/recipes/src/app/components/search/search.component.spec.ts b/public/projects/angular-small-apps/apps/recipes/src/app/components/search/search.component.spec.ts
new file mode 100644
index 0000000..5e23163
--- /dev/null
+++ b/public/projects/angular-small-apps/apps/recipes/src/app/components/search/search.component.spec.ts
@@ -0,0 +1,24 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { SearchComponent } from './search.component';
+
+describe('SearchComponent', () => {
+ let component: SearchComponent;
+ let fixture: ComponentFixture<SearchComponent>;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [SearchComponent],
+ }).compileComponents();
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(SearchComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/public/projects/angular-small-apps/apps/recipes/src/app/components/search/search.component.ts b/public/projects/angular-small-apps/apps/recipes/src/app/components/search/search.component.ts
new file mode 100644
index 0000000..594a505
--- /dev/null
+++ b/public/projects/angular-small-apps/apps/recipes/src/app/components/search/search.component.ts
@@ -0,0 +1,29 @@
+import { Component } from '@angular/core';
+import { FormBuilder } from '@angular/forms';
+import { Router } from '@angular/router';
+import { SearchService } from 'src/app/shared/services/search.service';
+
+@Component({
+ selector: '.search',
+ templateUrl: './search.component.html',
+ styleUrls: ['./search.component.scss'],
+})
+export class SearchComponent {
+ searchForm = this.formBuilder.group({
+ by: 'name',
+ query: '',
+ });
+
+ constructor(
+ private search: SearchService,
+ private formBuilder: FormBuilder,
+ private router: Router
+ ) {}
+
+ onSubmit(): void {
+ const by = this.searchForm.get('by')?.value;
+ const query = this.searchForm.get('query')?.value;
+ this.search.findResults(query, by);
+ this.router.navigate([`/search`, { by, query }]);
+ }
+}
diff --git a/public/projects/angular-small-apps/apps/recipes/src/app/components/toolbar/toolbar.component.html b/public/projects/angular-small-apps/apps/recipes/src/app/components/toolbar/toolbar.component.html
new file mode 100644
index 0000000..3da30ae
--- /dev/null
+++ b/public/projects/angular-small-apps/apps/recipes/src/app/components/toolbar/toolbar.component.html
@@ -0,0 +1,18 @@
+<div class="toolbar" toolbar>
+ <div class="toggle-edition">
+ <input
+ type="checkbox"
+ name="edition-mode"
+ id="edition-mode"
+ class="toggle-edition__checkbox"
+ [checked]="isEditionEnabled"
+ (click)="toggleEdition()"
+ />
+ <label for="edition-mode" class="toggle-edition__label">{{
+ toggleLabel
+ }}</label>
+ </div>
+ <button *ngIf="shouldDisplaySave()" class="btn" (click)="saveRecipe()">
+ Save
+ </button>
+</div>
diff --git a/public/projects/angular-small-apps/apps/recipes/src/app/components/toolbar/toolbar.component.scss b/public/projects/angular-small-apps/apps/recipes/src/app/components/toolbar/toolbar.component.scss
new file mode 100644
index 0000000..2a954af
--- /dev/null
+++ b/public/projects/angular-small-apps/apps/recipes/src/app/components/toolbar/toolbar.component.scss
@@ -0,0 +1,68 @@
+.toolbar {
+ display: flex;
+ flex-flow: row wrap;
+ justify-content: flex-end;
+ gap: 1rem;
+ padding: 1rem;
+ position: absolute;
+ top: 0;
+ right: 0;
+}
+
+.toggle-edition {
+ --gap: 0.5rem;
+ --toggle-width: 3.5rem;
+
+ display: flex;
+ align-items: center;
+
+ &__label {
+ display: inline-flex;
+ gap: var(--gap);
+ position: relative;
+ cursor: pointer;
+ color: hsl(0, 0%, 30%);
+ font-size: 0.9rem;
+ font-weight: 600;
+ letter-spacing: 1px;
+ text-transform: uppercase;
+
+ &::before {
+ order: 2;
+ content: "";
+ display: block;
+ background: hsl(0, 0%, 80%);
+ border-radius: 2rem;
+ box-shadow: inset 0 0 2px 0 hsla(0, 0%, 0%, 0.5),
+ 0 0 0 2px hsla(0, 0%, 0%, 0.5);
+ width: var(--toggle-width);
+ padding: 0 0.6rem;
+ }
+
+ &::after {
+ content: "";
+ display: block;
+ width: calc(var(--toggle-width) / 2);
+ background: hsl(0, 85%, 41%);
+ border: 1px solid hsl(0, 0%, 31%);
+ border-radius: 50%;
+ box-shadow: 0 0 0 1px hsla(0, 0%, 36%, 0.5);
+ position: absolute;
+ right: calc((var(--toggle-width) + var(--gap)) / 2);
+ top: -1px;
+ bottom: -1px;
+ transition: all 0.3s ease-in-out 0s;
+ }
+ }
+
+ &__checkbox {
+ opacity: 0;
+
+ &:checked ~ {
+ .toggle-edition__label::after {
+ background: hsl(120, 76%, 31%);
+ right: -1px;
+ }
+ }
+ }
+}
diff --git a/public/projects/angular-small-apps/apps/recipes/src/app/components/toolbar/toolbar.component.spec.ts b/public/projects/angular-small-apps/apps/recipes/src/app/components/toolbar/toolbar.component.spec.ts
new file mode 100644
index 0000000..31ca834
--- /dev/null
+++ b/public/projects/angular-small-apps/apps/recipes/src/app/components/toolbar/toolbar.component.spec.ts
@@ -0,0 +1,24 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { ToolbarComponent } from './toolbar.component';
+
+describe('ToolbarComponent', () => {
+ let component: ToolbarComponent;
+ let fixture: ComponentFixture<ToolbarComponent>;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [ToolbarComponent],
+ }).compileComponents();
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ToolbarComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/public/projects/angular-small-apps/apps/recipes/src/app/components/toolbar/toolbar.component.ts b/public/projects/angular-small-apps/apps/recipes/src/app/components/toolbar/toolbar.component.ts
new file mode 100644
index 0000000..710b967
--- /dev/null
+++ b/public/projects/angular-small-apps/apps/recipes/src/app/components/toolbar/toolbar.component.ts
@@ -0,0 +1,65 @@
+import { Component, EventEmitter, Input, Output } from '@angular/core';
+import { Router } from '@angular/router';
+import { LocalStorageService } from 'src/app/shared/services/local-storage.service';
+import { RecipesService } from 'src/app/shared/services/recipes.service';
+import { slugify } from 'src/app/shared/utilities/slugify';
+
+@Component({
+ selector: 'toolbar',
+ templateUrl: './toolbar.component.html',
+ styleUrls: ['./toolbar.component.scss'],
+})
+export class ToolbarComponent {
+ @Output() isEditionMode = new EventEmitter<boolean>();
+ isEditionEnabled: boolean = false;
+ recipeId = history.state?.id;
+ savedRecipes: object[] = this.storage.get('recipes');
+ toggleLabel: string = 'Edition disabled';
+
+ constructor(
+ private storage: LocalStorageService,
+ private recipes: RecipesService,
+ private router: Router
+ ) {}
+
+ isSavedRecipe(): boolean {
+ let recipeIndex;
+
+ if (this.recipeId) {
+ recipeIndex = this.savedRecipes.findIndex(
+ (recipe: any) => recipe.idMeal === this.recipeId
+ );
+ } else {
+ const slug = this.router.url.replace('/recipe/', '');
+ recipeIndex = this.savedRecipes.findIndex(
+ (recipe: any) => recipe.slug === slug
+ );
+ }
+
+ return recipeIndex === -1 ? false : true;
+ }
+
+ shouldDisplaySave(): boolean {
+ return !this.isSavedRecipe();
+ }
+
+ saveRecipe(): void {
+ this.recipes.getRecipeById(this.recipeId).subscribe((recipe: any) => {
+ const currentRecipe = recipe.meals[0];
+ const newRecipe = {
+ ...currentRecipe,
+ slug: slugify(currentRecipe.strMeal),
+ };
+ this.savedRecipes.push(newRecipe);
+ this.storage.set('recipes', this.savedRecipes);
+ });
+ }
+
+ toggleEdition() {
+ this.isEditionEnabled = !this.isEditionEnabled;
+ this.toggleLabel = this.isEditionEnabled
+ ? 'Edition enabled'
+ : 'Edition disabled';
+ this.isEditionMode.emit(this.isEditionEnabled);
+ }
+}