Compare commits

...

2 Commits

Author SHA1 Message Date
Clément FRÉVILLE 571df652d6 Add ESLint config
continuous-integration/drone/push Build is passing Details
1 year ago
Clément FRÉVILLE 729da35987 Add delivery and ingredients CRUD
1 year ago

@ -91,8 +91,22 @@
],
"scripts": []
}
},
"lint": {
"builder": "@angular-eslint/builder:lint",
"options": {
"lintFilePatterns": [
"src/**/*.ts",
"src/**/*.html"
]
}
}
}
}
},
"cli": {
"schematicCollections": [
"@angular-eslint/schematics"
]
}
}

@ -0,0 +1,45 @@
// @ts-check
const eslint = require('@eslint/js');
const tseslint = require('typescript-eslint');
const angular = require('angular-eslint');
module.exports = tseslint.config(
{
files: ['**/*.ts'],
extends: [
eslint.configs.recommended,
...tseslint.configs.recommended,
...tseslint.configs.stylistic,
...angular.configs.tsRecommended,
],
processor: angular.processInlineTemplates,
rules: {
'@angular-eslint/directive-selector': [
'error',
{
type: 'attribute',
prefix: 'app',
style: 'camelCase',
},
],
'@angular-eslint/component-selector': [
'error',
{
type: 'element',
prefix: 'app',
style: 'kebab-case',
},
],
'@typescript-eslint/no-inferrable-types': 'off',
'@typescript-eslint/consistent-type-definitions': 'off',
},
},
{
files: ['**/*.html'],
extends: [
...angular.configs.templateRecommended,
...angular.configs.templateAccessibility,
],
rules: {},
},
);

2647
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -6,7 +6,8 @@
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test"
"test": "ng test",
"lint": "ng lint"
},
"private": true,
"dependencies": {
@ -29,13 +30,16 @@
"@angular/cli": "^18.0.4",
"@angular/compiler-cli": "^18.0.0",
"@types/jasmine": "~5.1.0",
"angular-eslint": "18.0.1",
"dprint": "^0.46.2",
"eslint": "^9.3.0",
"jasmine-core": "~5.1.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"typescript": "~5.4.2"
"typescript": "~5.4.2",
"typescript-eslint": "8.0.0-alpha.36"
}
}
}

@ -1,7 +1,13 @@
Tiramisu
<button mat-button [routerLink]="['/recipes']">Les recettes</button>
<button mat-button [routerLink]="['/recipe/add']">Ajout recette</button>
<button mat-button routerLink="/recipes">Les recettes</button>
<button mat-button routerLink="/recipe/add">Ajout recette</button>
@if (login.isLoggedIn()) {
<button mat-button routerLink="/ingredients">Ingrédients</button>
<button mat-button routerLink="/ingredients/add">Ajout ingrédient</button>
<button mat-button routerLink="/logout">Déconnexion</button>
} @else {
<button mat-button routerLink="/login">Connexion</button>
}
<router-outlet></router-outlet>
<router-outlet></router-outlet>

@ -1,8 +1,10 @@
import { Component } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatMenuModule } from '@angular/material/menu';
import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
import { DeliveryService } from './delivery.service';
import { LoginService } from './login.service';
import { RecipeService } from './recipe.service';
import { MatMenuModule } from '@angular/material/menu';
import { MatButtonModule } from '@angular/material/button';
@Component({
selector: 'app-root',
@ -10,11 +12,11 @@ import { MatButtonModule } from '@angular/material/button';
imports: [RouterOutlet, RouterLink, RouterLinkActive, MatMenuModule, MatButtonModule],
templateUrl: './app.component.html',
styleUrl: './app.component.css',
providers: [RecipeService],
providers: [RecipeService, LoginService, DeliveryService],
})
export class AppComponent {
title = 'tiramisu';
constructor(protected recipes: RecipeService) {
constructor(protected recipes: RecipeService, protected login: LoginService) {
}
}

@ -1,6 +1,9 @@
import { inject } from '@angular/core';
import { CanActivateFn, Routes } from '@angular/router';
import { AuthComponent } from './auth/auth.component';
import { DeliveryComponent } from './delivery/delivery.component';
import { IngredientAddComponent } from './ingredient-add/ingredient-add.component';
import { IngredientsComponent } from './ingredients/ingredients.component';
import { LoginService } from './login.service';
import { RecipeAddComponent } from './recipe-add/recipe-add.component';
import { RecipeComponent } from './recipe/recipe.component';
@ -8,11 +11,14 @@ import { RecipesComponent } from './recipes/recipes.component';
const LoggedGuard: CanActivateFn = () => inject(LoginService).isLoggedIn();
export const routes: Routes = [
{ path: '', component: DeliveryComponent, pathMatch: 'full' },
{ path: 'recipes', component: RecipesComponent },
{ path: 'recipe/add', component: RecipeAddComponent },
{ path: 'recipe/:id', component: RecipeComponent },
{ path: 'recipe/:id/edit', component: RecipeAddComponent },
{ path: 'ingredients', component: RecipesComponent, canActivate: [LoggedGuard] },
{ path: 'ingredients', component: IngredientsComponent, canActivate: [LoggedGuard] },
{ path: 'ingredients/add', component: IngredientAddComponent, canActivate: [LoggedGuard] },
{ path: 'ingredients/:id/edit', component: IngredientAddComponent, canActivate: [LoggedGuard] },
{ path: 'login', component: AuthComponent },
{ path: 'logout', component: AuthComponent, data: { registering: false }, canActivate: [LoggedGuard] },
];

@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { DeliveryService } from './delivery.service';
describe('DeliveryService', () => {
let service: DeliveryService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(DeliveryService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

@ -0,0 +1,22 @@
import { Injectable } from '@angular/core';
import { Recipe } from '../cookbook/type';
@Injectable({
providedIn: 'root',
})
export class DeliveryService {
readonly #shoppingCart: Recipe[];
constructor() {
this.#shoppingCart = JSON.parse(localStorage.getItem('shoppingCart') || '[]');
}
addToCart(recipe: Recipe): void {
this.#shoppingCart.push(recipe);
localStorage.setItem('shoppingCart', JSON.stringify(this.#shoppingCart));
}
get shoppingCart(): readonly Recipe[] {
return this.#shoppingCart;
}
}

@ -0,0 +1,13 @@
<h3>Commande en cours</h3>
@for (item of delivery.shoppingCart; track item.id) {
<div class="container">
<img [src]="item.image || 'https://placehold.co/200x200'" [alt]="item.image ? item.name : ''" width="200" height="200">
<div class="container_text">
<h1>{{item.name}}</h1>
<p>
{{item.description}}
</p>
</div>
</div>
}

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

@ -0,0 +1,12 @@
import { Component } from '@angular/core';
import { DeliveryService } from '../delivery.service';
@Component({
selector: 'app-delivery',
standalone: true,
imports: [],
templateUrl: './delivery.component.html',
})
export class DeliveryComponent {
constructor(protected delivery: DeliveryService) {}
}

@ -0,0 +1,15 @@
<form [formGroup]="createForm" (ngSubmit)="onSubmit()">
<div>
<mat-form-field>
<label for="name">Nom</label>
<input matInput id="name" type="text" formControlName="name" required>
@if (createForm.controls.name.errors?.['minlength']) {
<mat-error>Le nom doit contenir au moins {{ createForm.controls.name.errors!['minlength'].requiredLength }} caractères.</mat-error>
}
@if (createForm.controls.name.errors?.['maxlength']) {
<mat-error>Le nom ne peut dépasser {{ createForm.controls.name.errors!['maxlength'].requiredLength }} caractères.</mat-error>
}
</mat-form-field>
</div>
<button mat-flat-button class="button" type="submit">{{ ingredientId === -1 ? 'Ajouter' : 'Éditer' }}</button>
</form>

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

@ -0,0 +1,63 @@
import { Component, Input } from '@angular/core';
import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
import { MatButton } from '@angular/material/button';
import { MatOption } from '@angular/material/core';
import { MatError, MatFormField } from '@angular/material/form-field';
import { MatInput } from '@angular/material/input';
import { MatSelect } from '@angular/material/select';
import { Router } from '@angular/router';
import { RecipeService } from '../recipe.service';
@Component({
selector: 'app-ingredient-add',
standalone: true,
imports: [
MatButton,
MatError,
MatFormField,
MatInput,
MatOption,
MatSelect,
ReactiveFormsModule,
],
templateUrl: './ingredient-add.component.html',
})
export class IngredientAddComponent {
createForm = this.formBuilder.group({
name: '',
});
#ingredientId: number = -1;
@Input()
set id(recipeId: string) {
if (recipeId === undefined) return;
this.#ingredientId = parseInt(recipeId);
const ingredient = this.recipes.getIngredientById(this.#ingredientId);
if (ingredient === null) {
this.router.navigateByUrl('404');
return;
}
this.createForm.patchValue({
name: ingredient.name,
});
}
get ingredientId() {
return this.#ingredientId;
}
constructor(private formBuilder: FormBuilder, private recipes: RecipeService, private router: Router) {
}
onSubmit() {
if (this.createForm.invalid) {
return;
}
const value = this.createForm.value;
if (this.ingredientId !== -1) {
this.recipes.editIngredient({ id: this.ingredientId, name: value.name! });
} else {
this.recipes.addIngredient({ name: value.name! });
}
this.router.navigateByUrl('/ingredients');
}
}

@ -0,0 +1,34 @@
<div class="mat-elevation-z8">
<table mat-table [dataSource]="dataSource">
<ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef> id </th>
<td mat-cell *matCellDef="let element">
{{element.id}}
</td>
</ng-container>
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef> name </th>
<td mat-cell *matCellDef="let element">
{{element.name}}
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef> actions </th>
<td mat-cell *matCellDef="let element">
<button mat-button [routerLink]="['/ingredients', element.id, 'edit']">Modifier</button>
<button mat-button (click)="delete(element)" color="warn">Supprimer</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let element; columns: displayedColumns"></tr>
</table>
<mat-paginator [pageSizeOptions]="[5, 10, 20]"
showFirstLastButtons
aria-label="Select page of recipes">
</mat-paginator>
</div>

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

@ -0,0 +1,52 @@
import { Component } from '@angular/core';
import { MatButton } from '@angular/material/button';
import { MatPaginator } from '@angular/material/paginator';
import {
MatCell,
MatCellDef,
MatColumnDef,
MatHeaderCell,
MatHeaderCellDef,
MatHeaderRow,
MatHeaderRowDef,
MatRow,
MatRowDef,
MatTable,
MatTableDataSource,
} from '@angular/material/table';
import { RouterLink } from '@angular/router';
import { Ingredient } from '../../cookbook/type';
import { RecipeService } from '../recipe.service';
@Component({
selector: 'app-ingredients',
standalone: true,
imports: [
MatButton,
MatCell,
MatCellDef,
MatColumnDef,
MatHeaderCell,
MatHeaderRow,
MatHeaderRowDef,
MatPaginator,
MatRow,
MatRowDef,
MatTable,
MatHeaderCellDef,
RouterLink,
],
templateUrl: './ingredients.component.html',
})
export class IngredientsComponent {
displayedColumns: string[] = ['id', 'name', 'actions'];
dataSource = new MatTableDataSource<Ingredient>();
constructor(protected recipes: RecipeService) {
this.dataSource = new MatTableDataSource<Ingredient>(recipes.getAllIngredients());
}
delete(ingredient: Ingredient): void {
this.recipes.deleteIngredient(ingredient);
}
}

@ -67,4 +67,28 @@ export class RecipeService {
}
}
}
addIngredient(ingredient: Omit<Ingredient, 'id'>) {
const id = this.#ingredients.length ? Math.max(...this.#ingredients.map((ingredient) => ingredient.id)) + 1 : 1;
this.#ingredients.push({
id,
...ingredient,
});
}
editIngredient(ingredient: Ingredient) {
for (let i = 0; i < this.#ingredients.length; ++i) {
if (this.#ingredients[i].id === ingredient.id) {
this.#ingredients[i] = ingredient;
}
}
}
deleteIngredient(ingredient: Ingredient) {
const index = this.#ingredients.findIndex((v) => v.id === ingredient.id);
if (index === -1) {
return;
}
this.#ingredients.splice(index, 1);
}
}

@ -1,10 +1,10 @@
import { CommonModule } from '@angular/common';
import { Component, Input } from '@angular/core';
import { Recipe } from '../../cookbook/type';
import { RecipeService } from '../recipe.service';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { RouterLink } from '@angular/router';
import { Recipe } from '../../cookbook/type';
import { RecipeService } from '../recipe.service';
@Component({
selector: 'app-recipe',

@ -1,10 +1,10 @@
<div class="mat-elevation-z8">
<table mat-table [dataSource]="dataSource">
<!-- Position Column -->
<ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef> id </th>
<td mat-cell *matCellDef="let element">
<td mat-cell *matCellDef="let element">
{{element.id}}
</td>
</ng-container>
@ -30,13 +30,18 @@
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef> actions </th>
<td mat-cell *matCellDef="let element">
<button mat-button (click)="deliver(element)">Commander</button>
<button mat-button [routerLink]="['/recipe', element.id, 'edit']">Modifier</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let element; columns: displayedColumns" [routerLink]="['/recipe/', element.id]"></tr>
</table>
<mat-paginator [pageSizeOptions]="[5, 10, 20]"
showFirstLastButtons
aria-label="Select page of recipes">

@ -1,21 +1,23 @@
import { AfterViewInit, Component, ViewChild } from '@angular/core';
import { MatButton } from '@angular/material/button';
import { MatPaginator, MatPaginatorModule } from '@angular/material/paginator';
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
import { RouterLink } from '@angular/router';
import { Recipe } from '../../cookbook/type';
import { DeliveryService } from '../delivery.service';
import { RecipeService } from '../recipe.service';
@Component({
selector: 'app-recipes',
standalone: true,
imports: [MatTableModule, MatPaginatorModule, RouterLink],
imports: [MatTableModule, MatPaginatorModule, RouterLink, MatButton],
templateUrl: './recipes.component.html',
})
export class RecipesComponent implements AfterViewInit {
displayedColumns: string[] = ['id', 'name', 'description', 'image'];
displayedColumns: string[] = ['id', 'name', 'description', 'image', 'actions'];
dataSource = new MatTableDataSource<Recipe>();
constructor(protected recipes: RecipeService) {
constructor(protected recipes: RecipeService, private delivery: DeliveryService) {
this.dataSource = new MatTableDataSource<Recipe>(recipes.getAll());
}
@ -25,4 +27,8 @@ export class RecipesComponent implements AfterViewInit {
ngAfterViewInit() {
this.dataSource.paginator = this.paginator;
}
deliver(recipe: Recipe): void {
this.delivery.addToCart(recipe);
}
}

Loading…
Cancel
Save