visit
1. Add a GitHub repository, come up with a name, description, specify the type, the license, and the .gitignore file. I called it FoodOrder:
git clone [email protected]:denismaster/FoodOrder.git
cd FoodOrder
npm install -g @angular/cli
ng new FoodOrder --directory. --force
We are using the –directory option to create a project in the existing directory and the –force option to rewrite the README and .gitignore files.
npm install bootstrap
1. Create a models.ts file with the contents .
The Product class contains basic information about the product. ProductCategory describes the category of the product. The final menu is just a ProductCategory[] massive.
2. Add some items. Create a new service, let’s call it MenuService with a list of our products :In our case, the service returns the list of predefined objects but it can be changed at any given time, for example, for it to make a backend request.3. Add components to display the menu, categories, and products. For this, use the following folder structure:The MenuComponent component displays the menu as a whole. This component is simple and is there just to show the complete list of items. Use the HTML code . And the Typescript .
4. Create the MenuCategoryComponent component. It is used to display the categories. Create a card with the category name and the list of products as a list of MenuCategoryItemComponent components. Use the HTML code for MenuCategoryComponent . And the Typescript .
5. Create the MenuCategoryItemComponent component. It is used to display the product itself, as well as the UI connected to it. In this component, we react to a click and display additional interface items for portion sizes.
6. You’ll need an additional UpdateOrderAction class:
export interface UpdateOrderAction {
action: "add" | "remove"
name: string;
category?: string;
price: number;
}
This class describes the changes that happen to the order itself. Use the HTML code for MenuCategoryItemComponent . And the Typescript .
7. Collect the entire code. For that, create the HomeComponent component. At this stage, it will only display MenuComponent:
<app-menu [menu]="menu$ | async"></app-menu>
@Component({
selector: 'app-home',
templateUrl: './home.component.html',
})
export class HomeComponent implements OnInit {
menu$: Observable<ProductCategory[]>;
constructor(private menuService: MenuService) {
}
ngOnInit() {
this.menu$ = this.menuService.getMenu().pipe(take(1));
}
}
npm install firebase @angular/fire --save
export interface OrderItem {
id: string,
name: string;
category: string,
price: number
}
export interface Order {
dishes: OrderItem[]
}
Inside the Firestore, we’ll be storing the order as an object with the dishes property as an OrderItem[] massive. Inside this massive, we store the menu item name required to delete the order.
1. Create a new OrderService service. We’ll be using it for the Firebase interaction. Creating an order:
import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';
import { Router } from '@angular/router';
import { AngularFirestore } from '@angular/fire/firestore';
import { v4 as uuid } from 'uuid';
import { Order } from './models';
@Injectable({
providedIn: 'root'
})
export class OrderService {
constructor(private router: Router, private afs: AngularFirestore, ) {
}
createOrder() {
let orderId = uuid();
sessionStorage.setItem('current_order', orderId)
this.afs.collection("orders").doc<Order>(orderId).set({ dishes: [] })
.then(_ => {
this.router.navigate(['/'], { queryParams: { 'order': orderId } });
})
.catch(err => {
alert(err);
})
}
}
import { Component, OnInit } from '@angular/core';
import { ProductCategory } from '../models';
import { Observable } from 'rxjs';
import { take } from 'rxjs/operators'
import { MenuService } from '../menu/menu.service';
import { ActivatedRoute } from '@angular/router';
import { OrderService } from '../order.service';
@Component({
selector: 'app-home',
templateUrl: './home.component.html',
})
export class HomeComponent implements OnInit {
menu$: Observable<ProductCategory[]>;
orderId: string;
constructor(private activatedRoute: ActivatedRoute, private menuService: MenuService, private orderService: OrderService) { }
ngOnInit() {
const order = this.activatedRoute.snapshot.queryParams.order;
if (!order) {
this.orderService.createOrder();
this.orderId = sessionStorage.getItem('current_order');
}
else {
this.orderId = order;
sessionStorage.setItem('current_order', order);
}
this.menu$ = this.menuService.getMenu().pipe(take(1));
}
}
3. Create a special component to display the selected items (the cart). Let’s call it OrderStatusComponent:
<div class="card mb-3">
<div class="card-body">
<div *ngFor="let category of dishCategories">
<strong>{{category}}:</strong>
<span *ngFor="let dish of categorizedDishes[category]">
<span *ngIf="dish.amount>1">
{{dish.name}} x {{dish.amount}}
</span>
<span *ngIf="dish.amount==1">
{{dish.name}}
</span>
</span>
</div>
<div>
<strong>Total: {{ totalPrice}} ₽</strong>
</div>
</div>
</div>
import { Order } from '../models';
import { Input, Component } from '@angular/core';
@Component({
selector: 'app-order-status',
templateUrl: './order-status.component.html',
})
export class OrderStatusComponent{
@Input()
order: Order = null;
get dishes() {
if(!this.order || !this.order.dishes) return [];
return this.order.dishes;
}
get categorizedDishes() {
return this.dishes.reduce((prev, current) => {
if (!prev[current.category]) {
prev[current.category] = [{
name: current.name,
amount: 1
}]
}
else {
let productsInCategory: { name: string, amount: number }[] = prev[current.category];
let currentDishIndex = productsInCategory.findIndex(p => p.name == current.name)
if (currentDishIndex !== -1) {
productsInCategory[currentDishIndex].amount++;
}
else {
productsInCategory.push({
name: current.name,
amount: 1
});
}
}
return prev;
}, {});
}
get dishCategories() {
return Object.keys(this.categorizedDishes);
}
get totalPrice() {
return this.dishes.reduce((p, c) => {
return p + c.price
}, 0)
}
}
4. Add the new getOrder() method in OrderService:
getOrder(orderId:string): Observable<Order> {
const orderDoc = this.afs.doc<Order>(`orders/${orderId}`);
return orderDoc.valueChanges();
}
5. Finally, add the updateOrder() method in OrderService, which will be updating the contents of the order in Firestore:
async updateOrder(orderId: string, update: UpdateOrderAction) {
const order = await this.afs.doc<Order>(`orders/${orderId}`).valueChanges().pipe(take(1)).toPromise();
if (update.action == "add") {
this.afs.doc<Order>(`orders/${orderId}`).update({
dishes: <any>firebase.firestore.FieldValue.arrayUnion({
id: uuid(),
name: update.name,
category: update.category,
price: update.price
})
})
}
else {
const dishIds = order.dishes.filter(d=>d.name==update.name).map(d=>d.id);
const idToRemove = dishIds[0];
if(!idToRemove) return;
this.afs.doc<Order>(`orders/${orderId}`).update({
dishes: <any>firebase.firestore.FieldValue.arrayRemove({
id: idToRemove,
name: update.name,
category: update.category,
price: update.price
})
})
}
}
6. Add the ability to clear the cart with this code in OrderService:
private _clear$ = new Subject<void>();
get clearOrder$() {
return this._clear$.asObservable();
}
clearOrder(orderId: string) {
this.afs.doc<Order>(`orders/${orderId}`).update({
dishes: []
})
this._clear$.next();
}
handleOrderClearing(){
this._clear$.next();
}
7. Modify MenuCategoryItemComponent:
constructor(private orderService: OrderService) {
this.orderService.clearOrder$.subscribe(()=>{
this.amount=0;
sessionStorage.removeItem(`${this.orderId}-${this.product.name}`)
})
}
this.order$.pipe(filter(order => !!order)).subscribe(order => {
if (order.dishes && !order.dishes.length) {
this.orderService.handleOrderClearing();
}
})
To set up CI/CD, use TravisCI. This product is really simple and sustainable, especially in the GitHub integration. For the setup, you’ll need the .travis.yml file in the root of the repository:
firebase login:ci