aboutsummaryrefslogtreecommitdiffstats
path: root/public/projects/angular-small-apps/apps
diff options
context:
space:
mode:
authorArmand Philippot <git@armandphilippot.com>2022-02-20 16:11:50 +0100
committerArmand Philippot <git@armandphilippot.com>2022-02-20 16:15:08 +0100
commit73a5c7fae9ffbe9ada721148c8c454a643aceebe (patch)
treec8fad013ed9b5dd589add87f8d45cf02bbfc6e91 /public/projects/angular-small-apps/apps
parentb01239fbdcc5bbc5921f73ec0e8fee7bedd5c8e8 (diff)
chore!: restructure repo
I separated public files from the config/dev files. It improves repo readability. I also moved dotenv helper to public/inc directory and extract the Matomo tracker in the same directory.
Diffstat (limited to 'public/projects/angular-small-apps/apps')
-rw-r--r--public/projects/angular-small-apps/apps/recipes/.browserslistrc16
-rw-r--r--public/projects/angular-small-apps/apps/recipes/.editorconfig16
-rw-r--r--public/projects/angular-small-apps/apps/recipes/.gitignore46
-rw-r--r--public/projects/angular-small-apps/apps/recipes/README.md26
-rw-r--r--public/projects/angular-small-apps/apps/recipes/angular.json101
-rw-r--r--public/projects/angular-small-apps/apps/recipes/karma.conf.js41
-rw-r--r--public/projects/angular-small-apps/apps/recipes/package.json41
-rw-r--r--public/projects/angular-small-apps/apps/recipes/src/app/app-routing.module.ts19
-rw-r--r--public/projects/angular-small-apps/apps/recipes/src/app/app.component.html10
-rw-r--r--public/projects/angular-small-apps/apps/recipes/src/app/app.component.scss0
-rw-r--r--public/projects/angular-small-apps/apps/recipes/src/app/app.component.spec.ts33
-rw-r--r--public/projects/angular-small-apps/apps/recipes/src/app/app.component.ts12
-rw-r--r--public/projects/angular-small-apps/apps/recipes/src/app/app.module.ts40
-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
-rw-r--r--public/projects/angular-small-apps/apps/recipes/src/app/shared/directives/textarea.directive.spec.ts8
-rw-r--r--public/projects/angular-small-apps/apps/recipes/src/app/shared/directives/textarea.directive.ts49
-rw-r--r--public/projects/angular-small-apps/apps/recipes/src/app/shared/pipes/format-comma.pipe.spec.ts8
-rw-r--r--public/projects/angular-small-apps/apps/recipes/src/app/shared/pipes/format-comma.pipe.ts18
-rw-r--r--public/projects/angular-small-apps/apps/recipes/src/app/shared/pipes/slugify.pipe.spec.ts8
-rw-r--r--public/projects/angular-small-apps/apps/recipes/src/app/shared/pipes/slugify.pipe.ts19
-rw-r--r--public/projects/angular-small-apps/apps/recipes/src/app/shared/recipes.ts49
-rw-r--r--public/projects/angular-small-apps/apps/recipes/src/app/shared/services/local-storage.service.spec.ts16
-rw-r--r--public/projects/angular-small-apps/apps/recipes/src/app/shared/services/local-storage.service.ts39
-rw-r--r--public/projects/angular-small-apps/apps/recipes/src/app/shared/services/recipes.service.spec.ts16
-rw-r--r--public/projects/angular-small-apps/apps/recipes/src/app/shared/services/recipes.service.ts46
-rw-r--r--public/projects/angular-small-apps/apps/recipes/src/app/shared/services/search.service.spec.ts16
-rw-r--r--public/projects/angular-small-apps/apps/recipes/src/app/shared/services/search.service.ts39
-rw-r--r--public/projects/angular-small-apps/apps/recipes/src/app/shared/utilities/slugify.ts20
-rw-r--r--public/projects/angular-small-apps/apps/recipes/src/assets/.gitkeep0
-rw-r--r--public/projects/angular-small-apps/apps/recipes/src/environments/environment.prod.ts3
-rw-r--r--public/projects/angular-small-apps/apps/recipes/src/environments/environment.ts16
-rw-r--r--public/projects/angular-small-apps/apps/recipes/src/favicon.icobin0 -> 948 bytes
-rw-r--r--public/projects/angular-small-apps/apps/recipes/src/index.html13
-rw-r--r--public/projects/angular-small-apps/apps/recipes/src/main.ts12
-rw-r--r--public/projects/angular-small-apps/apps/recipes/src/polyfills.ts53
-rw-r--r--public/projects/angular-small-apps/apps/recipes/src/styles.scss26
-rw-r--r--public/projects/angular-small-apps/apps/recipes/src/styles/base/_base.scss6
-rw-r--r--public/projects/angular-small-apps/apps/recipes/src/styles/base/_typography.scss9
-rw-r--r--public/projects/angular-small-apps/apps/recipes/src/styles/components/_buttons.scss38
-rw-r--r--public/projects/angular-small-apps/apps/recipes/src/styles/components/_card.scss47
-rw-r--r--public/projects/angular-small-apps/apps/recipes/src/styles/components/_meta.scss9
-rw-r--r--public/projects/angular-small-apps/apps/recipes/src/styles/layout/_footer.scss8
-rw-r--r--public/projects/angular-small-apps/apps/recipes/src/styles/layout/_grid.scss16
-rw-r--r--public/projects/angular-small-apps/apps/recipes/src/styles/layout/_main.scss20
-rw-r--r--public/projects/angular-small-apps/apps/recipes/src/test.ts26
-rw-r--r--public/projects/angular-small-apps/apps/recipes/tsconfig.app.json15
-rw-r--r--public/projects/angular-small-apps/apps/recipes/tsconfig.json32
-rw-r--r--public/projects/angular-small-apps/apps/recipes/tsconfig.spec.json18
71 files changed, 2032 insertions, 0 deletions
diff --git a/public/projects/angular-small-apps/apps/recipes/.browserslistrc b/public/projects/angular-small-apps/apps/recipes/.browserslistrc
new file mode 100644
index 0000000..4f9ac26
--- /dev/null
+++ b/public/projects/angular-small-apps/apps/recipes/.browserslistrc
@@ -0,0 +1,16 @@
+# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
+# For additional information regarding the format and rule options, please see:
+# https://github.com/browserslist/browserslist#queries
+
+# For the full list of supported browsers by the Angular framework, please see:
+# https://angular.io/guide/browser-support
+
+# You can see what browsers were selected by your queries by running:
+# npx browserslist
+
+last 1 Chrome version
+last 1 Firefox version
+last 2 Edge major versions
+last 2 Safari major versions
+last 2 iOS major versions
+Firefox ESR
diff --git a/public/projects/angular-small-apps/apps/recipes/.editorconfig b/public/projects/angular-small-apps/apps/recipes/.editorconfig
new file mode 100644
index 0000000..59d9a3a
--- /dev/null
+++ b/public/projects/angular-small-apps/apps/recipes/.editorconfig
@@ -0,0 +1,16 @@
+# Editor configuration, see https://editorconfig.org
+root = true
+
+[*]
+charset = utf-8
+indent_style = space
+indent_size = 2
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.ts]
+quote_type = single
+
+[*.md]
+max_line_length = off
+trim_trailing_whitespace = false
diff --git a/public/projects/angular-small-apps/apps/recipes/.gitignore b/public/projects/angular-small-apps/apps/recipes/.gitignore
new file mode 100644
index 0000000..105c00f
--- /dev/null
+++ b/public/projects/angular-small-apps/apps/recipes/.gitignore
@@ -0,0 +1,46 @@
+# See http://help.github.com/ignore-files/ for more about ignoring files.
+
+# compiled output
+/dist
+/tmp
+/out-tsc
+# Only exists if Bazel was run
+/bazel-out
+
+# dependencies
+/node_modules
+
+# profiling files
+chrome-profiler-events*.json
+
+# IDEs and editors
+/.idea
+.project
+.classpath
+.c9/
+*.launch
+.settings/
+*.sublime-workspace
+
+# IDE - VSCode
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+.history/*
+
+# misc
+/.angular/cache
+/.sass-cache
+/connect.lock
+/coverage
+/libpeerconnection.log
+npm-debug.log
+yarn-error.log
+testem.log
+/typings
+
+# System Files
+.DS_Store
+Thumbs.db
diff --git a/public/projects/angular-small-apps/apps/recipes/README.md b/public/projects/angular-small-apps/apps/recipes/README.md
new file mode 100644
index 0000000..89a7ca9
--- /dev/null
+++ b/public/projects/angular-small-apps/apps/recipes/README.md
@@ -0,0 +1,26 @@
+# Recipes
+
+A recipes app implemented with Angular.
+
+## How to
+
+### Start the development version
+
+1. Run `yarn run start`
+2. Navigate to `http://localhost:4200/` (browser normally opens automatically).
+
+### Start the build version
+
+1. Run `yarn run build` to build the project
+2. Navigate to `dist/` directory.
+3. Open `index.html`
+
+## Preview
+
+You can see a live preview here: https://demo.armandphilippot.com/#recipe
+
+![Preview](../assets/preview-recipes.jpg)
+
+## License
+
+This project is open-source and available under the [MIT License](../LICENSE).
diff --git a/public/projects/angular-small-apps/apps/recipes/angular.json b/public/projects/angular-small-apps/apps/recipes/angular.json
new file mode 100644
index 0000000..45e9e0a
--- /dev/null
+++ b/public/projects/angular-small-apps/apps/recipes/angular.json
@@ -0,0 +1,101 @@
+{
+ "$schema": "../../node_modules/@angular/cli/lib/config/schema.json",
+ "version": 1,
+ "newProjectRoot": "projects",
+ "projects": {
+ "recipes": {
+ "projectType": "application",
+ "schematics": {
+ "@schematics/angular:component": {
+ "style": "scss"
+ },
+ "@schematics/angular:application": {
+ "strict": true
+ }
+ },
+ "root": "",
+ "sourceRoot": "src",
+ "prefix": "app",
+ "architect": {
+ "build": {
+ "builder": "@angular-devkit/build-angular:browser",
+ "options": {
+ "outputPath": "dist/recipes",
+ "index": "src/index.html",
+ "main": "src/main.ts",
+ "polyfills": "src/polyfills.ts",
+ "tsConfig": "tsconfig.app.json",
+ "inlineStyleLanguage": "scss",
+ "assets": ["src/favicon.ico", "src/assets"],
+ "styles": ["src/styles.scss"],
+ "scripts": []
+ },
+ "configurations": {
+ "production": {
+ "budgets": [
+ {
+ "type": "initial",
+ "maximumWarning": "500kb",
+ "maximumError": "1mb"
+ },
+ {
+ "type": "anyComponentStyle",
+ "maximumWarning": "2kb",
+ "maximumError": "4kb"
+ }
+ ],
+ "fileReplacements": [
+ {
+ "replace": "src/environments/environment.ts",
+ "with": "src/environments/environment.prod.ts"
+ }
+ ],
+ "outputHashing": "all"
+ },
+ "development": {
+ "buildOptimizer": false,
+ "optimization": false,
+ "vendorChunk": true,
+ "extractLicenses": false,
+ "sourceMap": true,
+ "namedChunks": true
+ }
+ },
+ "defaultConfiguration": "production"
+ },
+ "serve": {
+ "builder": "@angular-devkit/build-angular:dev-server",
+ "configurations": {
+ "production": {
+ "browserTarget": "recipes:build:production"
+ },
+ "development": {
+ "browserTarget": "recipes:build:development"
+ }
+ },
+ "defaultConfiguration": "development"
+ },
+ "extract-i18n": {
+ "builder": "@angular-devkit/build-angular:extract-i18n",
+ "options": {
+ "browserTarget": "recipes:build"
+ }
+ },
+ "test": {
+ "builder": "@angular-devkit/build-angular:karma",
+ "options": {
+ "main": "src/test.ts",
+ "polyfills": "src/polyfills.ts",
+ "tsConfig": "tsconfig.spec.json",
+ "karmaConfig": "karma.conf.js",
+ "inlineStyleLanguage": "scss",
+ "assets": ["src/favicon.ico", "src/assets"],
+ "styles": ["src/styles.scss"],
+ "scripts": []
+ }
+ }
+ }
+ }
+ },
+ "defaultProject": "recipes"
+}
diff --git a/public/projects/angular-small-apps/apps/recipes/karma.conf.js b/public/projects/angular-small-apps/apps/recipes/karma.conf.js
new file mode 100644
index 0000000..e743cf7
--- /dev/null
+++ b/public/projects/angular-small-apps/apps/recipes/karma.conf.js
@@ -0,0 +1,41 @@
+// Karma configuration file, see link for more information
+// https://karma-runner.github.io/1.0/config/configuration-file.html
+
+module.exports = function (config) {
+ config.set({
+ basePath: "",
+ frameworks: ["jasmine", "@angular-devkit/build-angular"],
+ plugins: [
+ require("karma-jasmine"),
+ require("karma-firefox-launcher"),
+ require("karma-jasmine-html-reporter"),
+ require("karma-coverage"),
+ require("@angular-devkit/build-angular/plugins/karma"),
+ ],
+ client: {
+ jasmine: {
+ // you can add configuration options for Jasmine here
+ // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
+ // for example, you can disable the random execution with `random: false`
+ // or set a specific seed with `seed: 4321`
+ },
+ clearContext: false, // leave Jasmine Spec Runner output visible in browser
+ },
+ jasmineHtmlReporter: {
+ suppressAll: true, // removes the duplicated traces
+ },
+ coverageReporter: {
+ dir: require("path").join(__dirname, "./coverage/recipes"),
+ subdir: ".",
+ reporters: [{ type: "html" }, { type: "text-summary" }],
+ },
+ reporters: ["progress", "kjhtml"],
+ port: 9876,
+ colors: true,
+ logLevel: config.LOG_INFO,
+ autoWatch: true,
+ browsers: ["Firefox"],
+ singleRun: false,
+ restartOnFileChange: true,
+ });
+};
diff --git a/public/projects/angular-small-apps/apps/recipes/package.json b/public/projects/angular-small-apps/apps/recipes/package.json
new file mode 100644
index 0000000..091ef3f
--- /dev/null
+++ b/public/projects/angular-small-apps/apps/recipes/package.json
@@ -0,0 +1,41 @@
+{
+ "name": "recipes",
+ "description": "A recipes app implemented with Angular.",
+ "version": "0.0.1",
+ "scripts": {
+ "ng": "ng",
+ "start": "ng serve --open",
+ "build": "ng build",
+ "watch": "ng build --watch --configuration development",
+ "test": "ng test"
+ },
+ "private": true,
+ "dependencies": {
+ "@angular/animations": "~13.2.2",
+ "@angular/common": "~13.2.2",
+ "@angular/compiler": "~13.2.2",
+ "@angular/core": "~13.2.2",
+ "@angular/forms": "~13.2.2",
+ "@angular/platform-browser": "~13.2.2",
+ "@angular/platform-browser-dynamic": "~13.2.2",
+ "@angular/router": "~13.2.2",
+ "modern-normalize": "^1.1.0",
+ "rxjs": "~7.5.4",
+ "tslib": "^2.3.0",
+ "zone.js": "~0.11.4"
+ },
+ "devDependencies": {
+ "@angular-devkit/build-angular": "~13.2.3",
+ "@angular/cli": "~13.2.3",
+ "@angular/compiler-cli": "~13.2.2",
+ "@types/jasmine": "~3.10.0",
+ "@types/node": "^17.0.18",
+ "jasmine-core": "~4.0.0",
+ "karma": "~6.3.14",
+ "karma-coverage": "~2.2.0",
+ "karma-firefox-launcher": "^2.1.2",
+ "karma-jasmine": "~4.0.0",
+ "karma-jasmine-html-reporter": "~1.7.0",
+ "typescript": "~4.5.5"
+ }
+}
diff --git a/public/projects/angular-small-apps/apps/recipes/src/app/app-routing.module.ts b/public/projects/angular-small-apps/apps/recipes/src/app/app-routing.module.ts
new file mode 100644
index 0000000..74b4b9d
--- /dev/null
+++ b/public/projects/angular-small-apps/apps/recipes/src/app/app-routing.module.ts
@@ -0,0 +1,19 @@
+import { NgModule } from '@angular/core';
+import { RouterModule, Routes } from '@angular/router';
+import { PageNotFoundComponent } from './components/page-not-found/page-not-found.component';
+import { RecipeComponent } from './components/recipe/recipe.component';
+import { RecipesListComponent } from './components/recipes-list/recipes-list.component';
+import { SearchResultsComponent } from './components/search-results/search-results.component';
+
+const routes: Routes = [
+ { path: '', component: RecipesListComponent },
+ { path: 'recipe/:slug', component: RecipeComponent },
+ { path: 'search', component: SearchResultsComponent },
+ { path: '**', component: PageNotFoundComponent },
+];
+
+@NgModule({
+ imports: [RouterModule.forRoot(routes)],
+ exports: [RouterModule],
+})
+export class AppRoutingModule {}
diff --git a/public/projects/angular-small-apps/apps/recipes/src/app/app.component.html b/public/projects/angular-small-apps/apps/recipes/src/app/app.component.html
new file mode 100644
index 0000000..b5cbafb
--- /dev/null
+++ b/public/projects/angular-small-apps/apps/recipes/src/app/app.component.html
@@ -0,0 +1,10 @@
+<header class="header">
+ <h1 class="branding">{{ title }}</h1>
+</header>
+<main class="main">
+ <div class="search"></div>
+ <router-outlet></router-outlet>
+</main>
+<footer class="footer">
+ <p class="copyright">Recipes App. MIT 2021. Armand Philippot.</p>
+</footer>
diff --git a/public/projects/angular-small-apps/apps/recipes/src/app/app.component.scss b/public/projects/angular-small-apps/apps/recipes/src/app/app.component.scss
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/public/projects/angular-small-apps/apps/recipes/src/app/app.component.scss
diff --git a/public/projects/angular-small-apps/apps/recipes/src/app/app.component.spec.ts b/public/projects/angular-small-apps/apps/recipes/src/app/app.component.spec.ts
new file mode 100644
index 0000000..736e498
--- /dev/null
+++ b/public/projects/angular-small-apps/apps/recipes/src/app/app.component.spec.ts
@@ -0,0 +1,33 @@
+import { TestBed } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+import { AppComponent } from './app.component';
+
+describe('AppComponent', () => {
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [RouterTestingModule],
+ declarations: [AppComponent],
+ }).compileComponents();
+ });
+
+ it('should create the app', () => {
+ const fixture = TestBed.createComponent(AppComponent);
+ const app = fixture.componentInstance;
+ expect(app).toBeTruthy();
+ });
+
+ it(`should have as title 'recipes'`, () => {
+ const fixture = TestBed.createComponent(AppComponent);
+ const app = fixture.componentInstance;
+ expect(app.title).toEqual('recipes');
+ });
+
+ it('should render title', () => {
+ const fixture = TestBed.createComponent(AppComponent);
+ fixture.detectChanges();
+ const compiled = fixture.nativeElement as HTMLElement;
+ expect(compiled.querySelector('.content span')?.textContent).toContain(
+ 'recipes app is running!'
+ );
+ });
+});
diff --git a/public/projects/angular-small-apps/apps/recipes/src/app/app.component.ts b/public/projects/angular-small-apps/apps/recipes/src/app/app.component.ts
new file mode 100644
index 0000000..b25ec74
--- /dev/null
+++ b/public/projects/angular-small-apps/apps/recipes/src/app/app.component.ts
@@ -0,0 +1,12 @@
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'app-root',
+ templateUrl: './app.component.html',
+ styleUrls: ['./app.component.scss'],
+})
+export class AppComponent {
+ title = 'Food Recipes';
+
+ constructor() {}
+}
diff --git a/public/projects/angular-small-apps/apps/recipes/src/app/app.module.ts b/public/projects/angular-small-apps/apps/recipes/src/app/app.module.ts
new file mode 100644
index 0000000..60286a9
--- /dev/null
+++ b/public/projects/angular-small-apps/apps/recipes/src/app/app.module.ts
@@ -0,0 +1,40 @@
+import { NgModule } from '@angular/core';
+import { BrowserModule } from '@angular/platform-browser';
+import { HttpClientModule } from '@angular/common/http';
+
+import { AppRoutingModule } from './app-routing.module';
+import { AppComponent } from './app.component';
+import { RecipesListComponent } from './components/recipes-list/recipes-list.component';
+import { FormatCommaPipe } from './shared/pipes/format-comma.pipe';
+import { RecipeComponent } from './components/recipe/recipe.component';
+import { SearchComponent } from './components/search/search.component';
+import { ReactiveFormsModule } from '@angular/forms';
+import { SearchResultsComponent } from './components/search-results/search-results.component';
+import { SlugifyPipe } from './shared/pipes/slugify.pipe';
+import { PageNotFoundComponent } from './components/page-not-found/page-not-found.component';
+import { ToolbarComponent } from './components/toolbar/toolbar.component';
+import { TextareaDirective } from './shared/directives/textarea.directive';
+
+@NgModule({
+ declarations: [
+ AppComponent,
+ RecipesListComponent,
+ FormatCommaPipe,
+ RecipeComponent,
+ SearchComponent,
+ SearchResultsComponent,
+ SlugifyPipe,
+ PageNotFoundComponent,
+ ToolbarComponent,
+ TextareaDirective,
+ ],
+ imports: [
+ BrowserModule,
+ AppRoutingModule,
+ HttpClientModule,
+ ReactiveFormsModule,
+ ],
+ providers: [],
+ bootstrap: [AppComponent],
+})
+export class AppModule {}
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);
+ }
+}
diff --git a/public/projects/angular-small-apps/apps/recipes/src/app/shared/directives/textarea.directive.spec.ts b/public/projects/angular-small-apps/apps/recipes/src/app/shared/directives/textarea.directive.spec.ts
new file mode 100644
index 0000000..6f3c1e4
--- /dev/null
+++ b/public/projects/angular-small-apps/apps/recipes/src/app/shared/directives/textarea.directive.spec.ts
@@ -0,0 +1,8 @@
+import { TextareaDirective } from './textarea.directive';
+
+describe('TextareaDirective', () => {
+ it('should create an instance', () => {
+ const directive = new TextareaDirective();
+ expect(directive).toBeTruthy();
+ });
+});
diff --git a/public/projects/angular-small-apps/apps/recipes/src/app/shared/directives/textarea.directive.ts b/public/projects/angular-small-apps/apps/recipes/src/app/shared/directives/textarea.directive.ts
new file mode 100644
index 0000000..dbb1dc0
--- /dev/null
+++ b/public/projects/angular-small-apps/apps/recipes/src/app/shared/directives/textarea.directive.ts
@@ -0,0 +1,49 @@
+import {
+ AfterViewInit,
+ Directive,
+ ElementRef,
+ HostListener,
+ Renderer2,
+} from '@angular/core';
+
+@Directive({
+ selector: '[textareaResize]',
+})
+export class TextareaDirective implements AfterViewInit {
+ textareaHeight: string = '';
+
+ constructor(public renderer: Renderer2, public element: ElementRef) {}
+
+ ngAfterViewInit() {
+ this.resize();
+ this.setTextareaHeight();
+ }
+
+ @HostListener('keyup', ['$event']) onKeyDown(e: KeyboardEvent) {
+ const isCut = e.ctrlKey && e.key === 'x';
+ const isDelete = e.key === 'Delete';
+ if (isCut || isDelete) {
+ this.resize('auto');
+ } else {
+ this.resize();
+ }
+ this.setTextareaHeight();
+ }
+
+ setTextareaHeight() {
+ this.renderer.setStyle(
+ this.element.nativeElement,
+ 'height',
+ this.textareaHeight
+ );
+ }
+
+ resize(initialHeight: any = null) {
+ const textarea = this.element.nativeElement;
+ this.textareaHeight = initialHeight ?? textarea.height;
+
+ if (textarea.scrollHeight > textarea.clientHeight) {
+ this.textareaHeight = `${textarea.scrollHeight}px`;
+ }
+ }
+}
diff --git a/public/projects/angular-small-apps/apps/recipes/src/app/shared/pipes/format-comma.pipe.spec.ts b/public/projects/angular-small-apps/apps/recipes/src/app/shared/pipes/format-comma.pipe.spec.ts
new file mode 100644
index 0000000..e8f923d
--- /dev/null
+++ b/public/projects/angular-small-apps/apps/recipes/src/app/shared/pipes/format-comma.pipe.spec.ts
@@ -0,0 +1,8 @@
+import { FormatCommaPipe } from './format-comma.pipe';
+
+describe('FormatCommaPipe', () => {
+ it('create an instance', () => {
+ const pipe = new FormatCommaPipe();
+ expect(pipe).toBeTruthy();
+ });
+});
diff --git a/public/projects/angular-small-apps/apps/recipes/src/app/shared/pipes/format-comma.pipe.ts b/public/projects/angular-small-apps/apps/recipes/src/app/shared/pipes/format-comma.pipe.ts
new file mode 100644
index 0000000..761ae16
--- /dev/null
+++ b/public/projects/angular-small-apps/apps/recipes/src/app/shared/pipes/format-comma.pipe.ts
@@ -0,0 +1,18 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+/*
+ * Add a space after a comma.
+ * Usage:
+ * text | formatComma
+ * Example:
+ * {{ foo,bar,baz | formatComma }}
+ * formats to: foo, bar, baz
+ */
+@Pipe({
+ name: 'formatComma',
+})
+export class FormatCommaPipe implements PipeTransform {
+ transform(text: string): string {
+ return text.replace(/,/g, ', ');
+ }
+}
diff --git a/public/projects/angular-small-apps/apps/recipes/src/app/shared/pipes/slugify.pipe.spec.ts b/public/projects/angular-small-apps/apps/recipes/src/app/shared/pipes/slugify.pipe.spec.ts
new file mode 100644
index 0000000..2048bc8
--- /dev/null
+++ b/public/projects/angular-small-apps/apps/recipes/src/app/shared/pipes/slugify.pipe.spec.ts
@@ -0,0 +1,8 @@
+import { SlugifyPipe } from './slugify.pipe';
+
+describe('SlugifyPipe', () => {
+ it('create an instance', () => {
+ const pipe = new SlugifyPipe();
+ expect(pipe).toBeTruthy();
+ });
+});
diff --git a/public/projects/angular-small-apps/apps/recipes/src/app/shared/pipes/slugify.pipe.ts b/public/projects/angular-small-apps/apps/recipes/src/app/shared/pipes/slugify.pipe.ts
new file mode 100644
index 0000000..1f44a03
--- /dev/null
+++ b/public/projects/angular-small-apps/apps/recipes/src/app/shared/pipes/slugify.pipe.ts
@@ -0,0 +1,19 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+@Pipe({
+ name: 'slugify',
+})
+export class SlugifyPipe implements PipeTransform {
+ transform(text: string): string {
+ return text
+ .toString()
+ .normalize('NFD')
+ .replace(/[\u0300-\u036f]/g, '')
+ .toLowerCase()
+ .trim()
+ .replace(/\s+/g, '-')
+ .replace(/[^\w-]+/g, '-')
+ .replace(/--+/g, '-')
+ .replace(/^-|-$/g, '');
+ }
+}
diff --git a/public/projects/angular-small-apps/apps/recipes/src/app/shared/recipes.ts b/public/projects/angular-small-apps/apps/recipes/src/app/shared/recipes.ts
new file mode 100644
index 0000000..c2fed84
--- /dev/null
+++ b/public/projects/angular-small-apps/apps/recipes/src/app/shared/recipes.ts
@@ -0,0 +1,49 @@
+export interface Recipes {
+ idMeal: number;
+ strMeal: string;
+ strCategory: string;
+ strInstructions: string;
+ strMealThumb: string;
+ strTags: string;
+ strIngredient1: string;
+ strIngredient2: string;
+ strIngredient3: string;
+ strIngredient4: string;
+ strIngredient5: string;
+ strIngredient6: string;
+ strIngredient7: string;
+ strIngredient8: string;
+ strIngredient9: string;
+ strIngredient10: string;
+ strIngredient11: string;
+ strIngredient12: string;
+ strIngredient13: string;
+ strIngredient14: string;
+ strIngredient15: string;
+ strIngredient16: string;
+ strIngredient17: string;
+ strIngredient18: string;
+ strIngredient19: string;
+ strIngredient20: string;
+ strMeasure1: string;
+ strMeasure2: string;
+ strMeasure3: string;
+ strMeasure4: string;
+ strMeasure5: string;
+ strMeasure6: string;
+ strMeasure7: string;
+ strMeasure8: string;
+ strMeasure9: string;
+ strMeasure10: string;
+ strMeasure11: string;
+ strMeasure12: string;
+ strMeasure13: string;
+ strMeasure14: string;
+ strMeasure15: string;
+ strMeasure16: string;
+ strMeasure17: string;
+ strMeasure18: string;
+ strMeasure19: string;
+ strMeasure20: string;
+ slug: string;
+}
diff --git a/public/projects/angular-small-apps/apps/recipes/src/app/shared/services/local-storage.service.spec.ts b/public/projects/angular-small-apps/apps/recipes/src/app/shared/services/local-storage.service.spec.ts
new file mode 100644
index 0000000..ba1dbd4
--- /dev/null
+++ b/public/projects/angular-small-apps/apps/recipes/src/app/shared/services/local-storage.service.spec.ts
@@ -0,0 +1,16 @@
+import { TestBed } from '@angular/core/testing';
+
+import { LocalStorageService } from './local-storage.service';
+
+describe('LocalStorageService', () => {
+ let service: LocalStorageService;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({});
+ service = TestBed.inject(LocalStorageService);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+});
diff --git a/public/projects/angular-small-apps/apps/recipes/src/app/shared/services/local-storage.service.ts b/public/projects/angular-small-apps/apps/recipes/src/app/shared/services/local-storage.service.ts
new file mode 100644
index 0000000..82a72e7
--- /dev/null
+++ b/public/projects/angular-small-apps/apps/recipes/src/app/shared/services/local-storage.service.ts
@@ -0,0 +1,39 @@
+import { Injectable } from '@angular/core';
+
+@Injectable({
+ providedIn: 'root',
+})
+export class LocalStorageService {
+ constructor() {}
+
+ get(key: string): any {
+ try {
+ const serialItem = localStorage.getItem(key);
+ if (serialItem) {
+ return JSON.parse(serialItem);
+ } else {
+ return undefined;
+ }
+ } catch (e) {
+ console.log(e);
+ return undefined;
+ }
+ }
+
+ set(key: string, value: any) {
+ try {
+ const serialItem = JSON.stringify(value);
+ localStorage.setItem(key, serialItem);
+ } catch (e) {
+ console.log(e);
+ }
+ }
+
+ remove(key: string) {
+ localStorage.removeItem(key);
+ }
+
+ clear() {
+ localStorage.clear;
+ }
+}
diff --git a/public/projects/angular-small-apps/apps/recipes/src/app/shared/services/recipes.service.spec.ts b/public/projects/angular-small-apps/apps/recipes/src/app/shared/services/recipes.service.spec.ts
new file mode 100644
index 0000000..e433b4e
--- /dev/null
+++ b/public/projects/angular-small-apps/apps/recipes/src/app/shared/services/recipes.service.spec.ts
@@ -0,0 +1,16 @@
+import { TestBed } from '@angular/core/testing';
+
+import { RecipesService } from './recipes.service';
+
+describe('RecipesService', () => {
+ let service: RecipesService;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({});
+ service = TestBed.inject(RecipesService);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+});
diff --git a/public/projects/angular-small-apps/apps/recipes/src/app/shared/services/recipes.service.ts b/public/projects/angular-small-apps/apps/recipes/src/app/shared/services/recipes.service.ts
new file mode 100644
index 0000000..a82e019
--- /dev/null
+++ b/public/projects/angular-small-apps/apps/recipes/src/app/shared/services/recipes.service.ts
@@ -0,0 +1,46 @@
+import { HttpClient, HttpHeaders } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+import { Observable } from 'rxjs';
+import { Recipes } from '../recipes';
+
+@Injectable({
+ providedIn: 'root',
+})
+export class RecipesService {
+ private allRecipesAPI =
+ 'https://www.themealdb.com/api/json/v1/1/search.php?f=a';
+ private recipeByIdAPI =
+ 'https://www.themealdb.com/api/json/v1/1/lookup.php?i=';
+ private recipeByNameAPI =
+ 'https://www.themealdb.com/api/json/v1/1/search.php?s=';
+ private recipeByIngredientAPI =
+ 'https://www.themealdb.com/api/json/v1/1/filter.php?i=';
+ private recipeByCategoryAPI =
+ 'https://www.themealdb.com/api/json/v1/1/filter.php?c=';
+
+ httpOptions = {
+ headers: new HttpHeaders({ 'Content-Type': 'application/json' }),
+ };
+
+ constructor(private http: HttpClient) {}
+
+ getAllRecipes(): Observable<Recipes[]> {
+ return this.http.get<Recipes[]>(this.allRecipesAPI);
+ }
+
+ getRecipeById(id: number): Observable<Recipes[]> {
+ return this.http.get<Recipes[]>(this.recipeByIdAPI + id);
+ }
+
+ getRecipeByName(name: string): Observable<Recipes[]> {
+ return this.http.get<Recipes[]>(this.recipeByNameAPI + name);
+ }
+
+ getRecipeByIngredient(ingredient: string): Observable<Recipes[]> {
+ return this.http.get<Recipes[]>(this.recipeByIngredientAPI + ingredient);
+ }
+
+ getRecipeByCategory(category: string): Observable<Recipes[]> {
+ return this.http.get<Recipes[]>(this.recipeByCategoryAPI + category);
+ }
+}
diff --git a/public/projects/angular-small-apps/apps/recipes/src/app/shared/services/search.service.spec.ts b/public/projects/angular-small-apps/apps/recipes/src/app/shared/services/search.service.spec.ts
new file mode 100644
index 0000000..23c42c7
--- /dev/null
+++ b/public/projects/angular-small-apps/apps/recipes/src/app/shared/services/search.service.spec.ts
@@ -0,0 +1,16 @@
+import { TestBed } from '@angular/core/testing';
+
+import { SearchService } from './search.service';
+
+describe('SearchService', () => {
+ let service: SearchService;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({});
+ service = TestBed.inject(SearchService);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+});
diff --git a/public/projects/angular-small-apps/apps/recipes/src/app/shared/services/search.service.ts b/public/projects/angular-small-apps/apps/recipes/src/app/shared/services/search.service.ts
new file mode 100644
index 0000000..6c16efd
--- /dev/null
+++ b/public/projects/angular-small-apps/apps/recipes/src/app/shared/services/search.service.ts
@@ -0,0 +1,39 @@
+import { Injectable } from '@angular/core';
+import { BehaviorSubject } from 'rxjs';
+import { Recipes } from '../recipes';
+import { RecipesService } from './recipes.service';
+
+@Injectable({
+ providedIn: 'root',
+})
+export class SearchService {
+ private results = new BehaviorSubject<Recipes[]>([]);
+
+ constructor(private recipes: RecipesService) {}
+
+ getResults() {
+ return this.results.asObservable();
+ }
+
+ findResults(query: string, by: string) {
+ switch (by) {
+ case 'name':
+ this.recipes
+ .getRecipeByName(query)
+ .subscribe((recipes: any) => this.results.next(recipes.meals));
+ break;
+ case 'ingredient':
+ this.recipes
+ .getRecipeByIngredient(query)
+ .subscribe((recipes: any) => this.results.next(recipes.meals));
+ break;
+ case 'category':
+ this.recipes
+ .getRecipeByCategory(query)
+ .subscribe((recipes: any) => this.results.next(recipes.meals));
+ break;
+ default:
+ break;
+ }
+ }
+}
diff --git a/public/projects/angular-small-apps/apps/recipes/src/app/shared/utilities/slugify.ts b/public/projects/angular-small-apps/apps/recipes/src/app/shared/utilities/slugify.ts
new file mode 100644
index 0000000..15de941
--- /dev/null
+++ b/public/projects/angular-small-apps/apps/recipes/src/app/shared/utilities/slugify.ts
@@ -0,0 +1,20 @@
+/**
+ * Convert a text into a slug or id.
+ * https://gist.github.com/codeguy/6684588#gistcomment-3332719
+ *
+ * @param {string} text Text to slugify.
+ */
+const slugify = (text: string) => {
+ return text
+ .toString()
+ .normalize('NFD')
+ .replace(/[\u0300-\u036f]/g, '')
+ .toLowerCase()
+ .trim()
+ .replace(/\s+/g, '-')
+ .replace(/[^\w-]+/g, '-')
+ .replace(/--+/g, '-')
+ .replace(/^-|-$/g, '');
+};
+
+export { slugify };
diff --git a/public/projects/angular-small-apps/apps/recipes/src/assets/.gitkeep b/public/projects/angular-small-apps/apps/recipes/src/assets/.gitkeep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/public/projects/angular-small-apps/apps/recipes/src/assets/.gitkeep
diff --git a/public/projects/angular-small-apps/apps/recipes/src/environments/environment.prod.ts b/public/projects/angular-small-apps/apps/recipes/src/environments/environment.prod.ts
new file mode 100644
index 0000000..3612073
--- /dev/null
+++ b/public/projects/angular-small-apps/apps/recipes/src/environments/environment.prod.ts
@@ -0,0 +1,3 @@
+export const environment = {
+ production: true
+};
diff --git a/public/projects/angular-small-apps/apps/recipes/src/environments/environment.ts b/public/projects/angular-small-apps/apps/recipes/src/environments/environment.ts
new file mode 100644
index 0000000..f56ff47
--- /dev/null
+++ b/public/projects/angular-small-apps/apps/recipes/src/environments/environment.ts
@@ -0,0 +1,16 @@
+// This file can be replaced during build by using the `fileReplacements` array.
+// `ng build` replaces `environment.ts` with `environment.prod.ts`.
+// The list of file replacements can be found in `angular.json`.
+
+export const environment = {
+ production: false
+};
+
+/*
+ * For easier debugging in development mode, you can import the following file
+ * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
+ *
+ * This import should be commented out in production mode because it will have a negative impact
+ * on performance if an error is thrown.
+ */
+// import 'zone.js/plugins/zone-error'; // Included with Angular CLI.
diff --git a/public/projects/angular-small-apps/apps/recipes/src/favicon.ico b/public/projects/angular-small-apps/apps/recipes/src/favicon.ico
new file mode 100644
index 0000000..997406a
--- /dev/null
+++ b/public/projects/angular-small-apps/apps/recipes/src/favicon.ico
Binary files differ
diff --git a/public/projects/angular-small-apps/apps/recipes/src/index.html b/public/projects/angular-small-apps/apps/recipes/src/index.html
new file mode 100644
index 0000000..9a723a5
--- /dev/null
+++ b/public/projects/angular-small-apps/apps/recipes/src/index.html
@@ -0,0 +1,13 @@
+<!doctype html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <title>Recipes</title>
+ <base href="/">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <link rel="icon" type="image/x-icon" href="favicon.ico">
+</head>
+<body>
+ <app-root></app-root>
+</body>
+</html>
diff --git a/public/projects/angular-small-apps/apps/recipes/src/main.ts b/public/projects/angular-small-apps/apps/recipes/src/main.ts
new file mode 100644
index 0000000..c7b673c
--- /dev/null
+++ b/public/projects/angular-small-apps/apps/recipes/src/main.ts
@@ -0,0 +1,12 @@
+import { enableProdMode } from '@angular/core';
+import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
+
+import { AppModule } from './app/app.module';
+import { environment } from './environments/environment';
+
+if (environment.production) {
+ enableProdMode();
+}
+
+platformBrowserDynamic().bootstrapModule(AppModule)
+ .catch(err => console.error(err));
diff --git a/public/projects/angular-small-apps/apps/recipes/src/polyfills.ts b/public/projects/angular-small-apps/apps/recipes/src/polyfills.ts
new file mode 100644
index 0000000..429bb9e
--- /dev/null
+++ b/public/projects/angular-small-apps/apps/recipes/src/polyfills.ts
@@ -0,0 +1,53 @@
+/**
+ * This file includes polyfills needed by Angular and is loaded before the app.
+ * You can add your own extra polyfills to this file.
+ *
+ * This file is divided into 2 sections:
+ * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
+ * 2. Application imports. Files imported after ZoneJS that should be loaded before your main
+ * file.
+ *
+ * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
+ * automatically update themselves. This includes recent versions of Safari, Chrome (including
+ * Opera), Edge on the desktop, and iOS and Chrome on mobile.
+ *
+ * Learn more in https://angular.io/guide/browser-support
+ */
+
+/***************************************************************************************************
+ * BROWSER POLYFILLS
+ */
+
+/**
+ * By default, zone.js will patch all possible macroTask and DomEvents
+ * user can disable parts of macroTask/DomEvents patch by setting following flags
+ * because those flags need to be set before `zone.js` being loaded, and webpack
+ * will put import in the top of bundle, so user need to create a separate file
+ * in this directory (for example: zone-flags.ts), and put the following flags
+ * into that file, and then add the following code before importing zone.js.
+ * import './zone-flags';
+ *
+ * The flags allowed in zone-flags.ts are listed here.
+ *
+ * The following flags will work for all browsers.
+ *
+ * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
+ * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
+ * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
+ *
+ * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
+ * with the following flag, it will bypass `zone.js` patch for IE/Edge
+ *
+ * (window as any).__Zone_enable_cross_context_check = true;
+ *
+ */
+
+/***************************************************************************************************
+ * Zone JS is required by default for Angular itself.
+ */
+import 'zone.js'; // Included with Angular CLI.
+
+
+/***************************************************************************************************
+ * APPLICATION IMPORTS
+ */
diff --git a/public/projects/angular-small-apps/apps/recipes/src/styles.scss b/public/projects/angular-small-apps/apps/recipes/src/styles.scss
new file mode 100644
index 0000000..968c0c4
--- /dev/null
+++ b/public/projects/angular-small-apps/apps/recipes/src/styles.scss
@@ -0,0 +1,26 @@
+/// This file gather all global styles.
+
+/**
+ * Vendors
+ */
+@use "modern-normalize";
+
+/**
+ * Base
+ */
+@use "styles/base/base";
+@use "styles/base/typography";
+
+/**
+ * Layout
+ */
+@use "styles/layout/grid";
+@use "styles/layout/footer";
+@use "styles/layout/main";
+
+/**
+ * Components
+ */
+@use "styles/components/buttons";
+@use "styles/components/card";
+@use "styles/components/meta";
diff --git a/public/projects/angular-small-apps/apps/recipes/src/styles/base/_base.scss b/public/projects/angular-small-apps/apps/recipes/src/styles/base/_base.scss
new file mode 100644
index 0000000..40658f5
--- /dev/null
+++ b/public/projects/angular-small-apps/apps/recipes/src/styles/base/_base.scss
@@ -0,0 +1,6 @@
+body {
+ background: #ededed;
+ font-family: Arial, Helvetica, sans-serif;
+ font-size: 16px;
+ line-height: 1.618;
+}
diff --git a/public/projects/angular-small-apps/apps/recipes/src/styles/base/_typography.scss b/public/projects/angular-small-apps/apps/recipes/src/styles/base/_typography.scss
new file mode 100644
index 0000000..daa7962
--- /dev/null
+++ b/public/projects/angular-small-apps/apps/recipes/src/styles/base/_typography.scss
@@ -0,0 +1,9 @@
+h1,
+h2,
+h3,
+h4,
+h5,
+h6,
+p {
+ margin: 0 0 1rem;
+}
diff --git a/public/projects/angular-small-apps/apps/recipes/src/styles/components/_buttons.scss b/public/projects/angular-small-apps/apps/recipes/src/styles/components/_buttons.scss
new file mode 100644
index 0000000..deecb1c
--- /dev/null
+++ b/public/projects/angular-small-apps/apps/recipes/src/styles/components/_buttons.scss
@@ -0,0 +1,38 @@
+.btn {
+ display: inline-flex;
+ align-items: center;
+ gap: 5px;
+ padding: 0.5rem;
+ background: #fff;
+ border: 3px solid #195881;
+ border-radius: 5px;
+ color: #195881;
+ font-weight: 600;
+ text-decoration: none;
+ cursor: pointer;
+
+ &:hover,
+ &:focus {
+ background: #195881;
+ color: #fff;
+ }
+
+ &__icon {
+ font-weight: 900;
+ transform: scale(1.2) translateY(-2px);
+ transition: all 0.3s ease-in-out 0s;
+ }
+
+ &:hover &,
+ &:focus & {
+ &__icon {
+ transform: scale(1.8) translateY(-2px);
+ }
+ }
+
+ &:active & {
+ &__icon {
+ transform: scale(1.2) translateY(-2px);
+ }
+ }
+}
diff --git a/public/projects/angular-small-apps/apps/recipes/src/styles/components/_card.scss b/public/projects/angular-small-apps/apps/recipes/src/styles/components/_card.scss
new file mode 100644
index 0000000..c288953
--- /dev/null
+++ b/public/projects/angular-small-apps/apps/recipes/src/styles/components/_card.scss
@@ -0,0 +1,47 @@
+.card {
+ background: #fff;
+ border: 1px solid #d7d7d7;
+ border-radius: 5px;
+ box-shadow: 0 0 5px -3px #000000a5;
+ display: flex;
+ flex-flow: column wrap;
+ height: 100%;
+ transition: all 0.3s ease-in-out 0s;
+
+ &:hover,
+ &:focus {
+ transform: scale(1.015);
+ box-shadow: 0 1px 5px -2px #000000a5;
+
+ .btn__icon {
+ transform: scale(1.8) translateY(-2px);
+ }
+ }
+
+ &__title,
+ &__body,
+ &__footer {
+ padding: 0 clamp(1rem, 3vw, 2rem);
+ }
+
+ &__body {
+ margin: auto 0 2rem;
+ }
+
+ &__footer {
+ display: flex;
+ flex-flow: row wrap;
+ align-items: center;
+ justify-content: space-between;
+ gap: 1rem;
+ padding-bottom: 2rem;
+ font-size: 0.9rem;
+ }
+
+ &__thumb {
+ max-height: 200px;
+ width: 100%;
+ margin-bottom: 1rem;
+ object-fit: cover;
+ }
+}
diff --git a/public/projects/angular-small-apps/apps/recipes/src/styles/components/_meta.scss b/public/projects/angular-small-apps/apps/recipes/src/styles/components/_meta.scss
new file mode 100644
index 0000000..ea4afac
--- /dev/null
+++ b/public/projects/angular-small-apps/apps/recipes/src/styles/components/_meta.scss
@@ -0,0 +1,9 @@
+.meta {
+ &__term {
+ font-weight: 600;
+ }
+
+ &__description {
+ margin: 0;
+ }
+}
diff --git a/public/projects/angular-small-apps/apps/recipes/src/styles/layout/_footer.scss b/public/projects/angular-small-apps/apps/recipes/src/styles/layout/_footer.scss
new file mode 100644
index 0000000..03227e5
--- /dev/null
+++ b/public/projects/angular-small-apps/apps/recipes/src/styles/layout/_footer.scss
@@ -0,0 +1,8 @@
+.footer {
+ margin-top: 3rem;
+ text-align: center;
+}
+
+.copyright {
+ font-size: 0.9rem;
+}
diff --git a/public/projects/angular-small-apps/apps/recipes/src/styles/layout/_grid.scss b/public/projects/angular-small-apps/apps/recipes/src/styles/layout/_grid.scss
new file mode 100644
index 0000000..4327ea4
--- /dev/null
+++ b/public/projects/angular-small-apps/apps/recipes/src/styles/layout/_grid.scss
@@ -0,0 +1,16 @@
+app-root {
+ display: flex;
+ flex-flow: column nowrap;
+ min-height: 100vh;
+}
+
+.header,
+.main,
+.footer {
+ width: min(calc(100vw - 2rem), 80ch);
+ margin: 1rem auto;
+}
+
+.main {
+ flex: 1;
+}
diff --git a/public/projects/angular-small-apps/apps/recipes/src/styles/layout/_main.scss b/public/projects/angular-small-apps/apps/recipes/src/styles/layout/_main.scss
new file mode 100644
index 0000000..16ff2a9
--- /dev/null
+++ b/public/projects/angular-small-apps/apps/recipes/src/styles/layout/_main.scss
@@ -0,0 +1,20 @@
+.recipes-list {
+ list-style-type: none;
+ padding: 0;
+ margin: 0 0 2rem;
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(0, 345px));
+ gap: 1rem;
+}
+
+.not-found {
+ background: #fff;
+ border: 1px solid #d7d7d7;
+ border-radius: 5px;
+ box-shadow: 0 0 5px -3px #000000a5;
+ padding: clamp(2rem, 3vw, 3rem) clamp(1rem, 3vw, 2rem) clamp(2rem, 3vw, 4rem);
+
+ &__result {
+ margin-bottom: 2rem;
+ }
+}
diff --git a/public/projects/angular-small-apps/apps/recipes/src/test.ts b/public/projects/angular-small-apps/apps/recipes/src/test.ts
new file mode 100644
index 0000000..598d11e
--- /dev/null
+++ b/public/projects/angular-small-apps/apps/recipes/src/test.ts
@@ -0,0 +1,26 @@
+// This file is required by karma.conf.js and loads recursively all the .spec and framework files
+
+import 'zone.js/testing';
+import { getTestBed } from '@angular/core/testing';
+import {
+ BrowserDynamicTestingModule,
+ platformBrowserDynamicTesting
+} from '@angular/platform-browser-dynamic/testing';
+
+declare const require: {
+ context(path: string, deep?: boolean, filter?: RegExp): {
+ keys(): string[];
+ <T>(id: string): T;
+ };
+};
+
+// First, initialize the Angular testing environment.
+getTestBed().initTestEnvironment(
+ BrowserDynamicTestingModule,
+ platformBrowserDynamicTesting(),
+);
+
+// Then we find all the tests.
+const context = require.context('./', true, /\.spec\.ts$/);
+// And load the modules.
+context.keys().map(context);
diff --git a/public/projects/angular-small-apps/apps/recipes/tsconfig.app.json b/public/projects/angular-small-apps/apps/recipes/tsconfig.app.json
new file mode 100644
index 0000000..82d91dc
--- /dev/null
+++ b/public/projects/angular-small-apps/apps/recipes/tsconfig.app.json
@@ -0,0 +1,15 @@
+/* To learn more about this file see: https://angular.io/config/tsconfig. */
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "./out-tsc/app",
+ "types": []
+ },
+ "files": [
+ "src/main.ts",
+ "src/polyfills.ts"
+ ],
+ "include": [
+ "src/**/*.d.ts"
+ ]
+}
diff --git a/public/projects/angular-small-apps/apps/recipes/tsconfig.json b/public/projects/angular-small-apps/apps/recipes/tsconfig.json
new file mode 100644
index 0000000..f531992
--- /dev/null
+++ b/public/projects/angular-small-apps/apps/recipes/tsconfig.json
@@ -0,0 +1,32 @@
+/* To learn more about this file see: https://angular.io/config/tsconfig. */
+{
+ "compileOnSave": false,
+ "compilerOptions": {
+ "baseUrl": "./",
+ "outDir": "./dist/out-tsc",
+ "forceConsistentCasingInFileNames": true,
+ "strict": true,
+ "noImplicitOverride": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true,
+ "sourceMap": true,
+ "declaration": false,
+ "downlevelIteration": true,
+ "experimentalDecorators": true,
+ "moduleResolution": "node",
+ "importHelpers": true,
+ "target": "es2017",
+ "module": "es2020",
+ "lib": [
+ "es2020",
+ "dom"
+ ]
+ },
+ "angularCompilerOptions": {
+ "enableI18nLegacyMessageIdFormat": false,
+ "strictInjectionParameters": true,
+ "strictInputAccessModifiers": true,
+ "strictTemplates": true
+ }
+}
diff --git a/public/projects/angular-small-apps/apps/recipes/tsconfig.spec.json b/public/projects/angular-small-apps/apps/recipes/tsconfig.spec.json
new file mode 100644
index 0000000..092345b
--- /dev/null
+++ b/public/projects/angular-small-apps/apps/recipes/tsconfig.spec.json
@@ -0,0 +1,18 @@
+/* To learn more about this file see: https://angular.io/config/tsconfig. */
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "./out-tsc/spec",
+ "types": [
+ "jasmine"
+ ]
+ },
+ "files": [
+ "src/test.ts",
+ "src/polyfills.ts"
+ ],
+ "include": [
+ "src/**/*.spec.ts",
+ "src/**/*.d.ts"
+ ]
+}