├── .editorconfig ├── .gitignore ├── CHANGELOG.md ├── Dockerfile.api ├── Dockerfile.nginx ├── LICENSE ├── README.md ├── backend ├── conf.json ├── index.js ├── mocks │ └── ormeau.mock.ts ├── nodemon.json ├── package.json ├── src │ ├── app.controller.ts │ ├── app.module.ts │ ├── command-line │ │ ├── command-line.component.ts │ │ ├── command-line.module.ts │ │ ├── command.class.ts │ │ └── commands │ │ │ ├── countdown.command.ts │ │ │ ├── provider.command.ts │ │ │ └── providers.command.ts │ ├── features │ │ ├── models │ │ │ ├── ingredients │ │ │ │ ├── ingredients.component.ts │ │ │ │ ├── ingredients.interface.ts │ │ │ │ └── ingredients.module.ts │ │ │ ├── normalized-model.class.ts │ │ │ ├── orders │ │ │ │ ├── orders.component.ts │ │ │ │ ├── orders.interface.ts │ │ │ │ └── orders.module.ts │ │ │ ├── pizzas-categories │ │ │ │ ├── pizzas-categories.component.ts │ │ │ │ ├── pizzas-categories.interface.ts │ │ │ │ └── pizzas-categories.module.ts │ │ │ ├── pizzas │ │ │ │ ├── pizzas.component.ts │ │ │ │ ├── pizzas.interface.ts │ │ │ │ └── pizzas.module.ts │ │ │ └── users │ │ │ │ ├── users.component.ts │ │ │ │ ├── users.interface.ts │ │ │ │ └── users.module.ts │ │ └── pizzas-providers │ │ │ ├── implementations │ │ │ ├── ormeau.class.ts │ │ │ ├── ormeau.mock.ts │ │ │ └── tutti.class.ts │ │ │ ├── pizzas-provider.class.ts │ │ │ ├── pizzas-providers.component.ts │ │ │ ├── pizzas-providers.interface.ts │ │ │ └── pizzas-providers.module.ts │ ├── helpers │ │ ├── file.helper.ts │ │ ├── http.helper.ts │ │ ├── normalize.helper.ts │ │ ├── object.helper.ts │ │ └── string.helper.ts │ └── server.ts ├── tsconfig.json ├── tslint.json └── yarn.lock ├── docker-compose.yml ├── frontend ├── angular.json ├── e2e │ ├── app.e2e-spec.ts │ ├── app.po.ts │ ├── tsconfig.e2e.json │ └── tsconfig.json ├── package.json ├── protractor.conf.js ├── src │ ├── app │ │ ├── app-routing.module.ts │ │ ├── app.component.html │ │ ├── app.component.scss │ │ ├── app.component.ts │ │ ├── app.module.ts │ │ ├── core │ │ │ ├── core.module.ts │ │ │ └── injection-tokens.ts │ │ ├── features │ │ │ ├── countdown │ │ │ │ ├── countdown.component.html │ │ │ │ ├── countdown.component.scss │ │ │ │ └── countdown.component.ts │ │ │ ├── features-routing.module.ts │ │ │ ├── features.component.html │ │ │ ├── features.component.scss │ │ │ ├── features.component.ts │ │ │ ├── features.module.ts │ │ │ ├── filter-ingredients │ │ │ │ ├── filter-ingredients.component.html │ │ │ │ ├── filter-ingredients.component.scss │ │ │ │ └── filter-ingredients.component.ts │ │ │ ├── footer │ │ │ │ ├── footer.component.html │ │ │ │ ├── footer.component.scss │ │ │ │ └── footer.component.ts │ │ │ ├── identification-dialog │ │ │ │ ├── identification-dialog.component.html │ │ │ │ ├── identification-dialog.component.scss │ │ │ │ └── identification-dialog.component.ts │ │ │ ├── order-summary-dialog │ │ │ │ ├── order-summary-dialog.component.html │ │ │ │ ├── order-summary-dialog.component.scss │ │ │ │ └── order-summary-dialog.component.ts │ │ │ ├── orders │ │ │ │ ├── orders.component.html │ │ │ │ ├── orders.component.scss │ │ │ │ ├── orders.component.ts │ │ │ │ ├── orders.module.ts │ │ │ │ └── user-order │ │ │ │ │ ├── user-order.component.html │ │ │ │ │ ├── user-order.component.scss │ │ │ │ │ └── user-order.component.ts │ │ │ ├── pizzas-search │ │ │ │ ├── pizzas-search.component.html │ │ │ │ ├── pizzas-search.component.scss │ │ │ │ └── pizzas-search.component.ts │ │ │ └── pizzas │ │ │ │ ├── pizzas.component.html │ │ │ │ ├── pizzas.component.scss │ │ │ │ ├── pizzas.component.ts │ │ │ │ ├── pizzas.effects.ts │ │ │ │ ├── pizzas.module.ts │ │ │ │ └── pizzas.service.ts │ │ └── shared │ │ │ ├── helpers │ │ │ ├── README.md │ │ │ ├── aot.helper.ts │ │ │ ├── date.helper.ts │ │ │ ├── mock.helper.ts │ │ │ └── time.helper.ts │ │ │ ├── interfaces │ │ │ ├── README.md │ │ │ └── store.interface.ts │ │ │ ├── pipes │ │ │ └── join.pipe.ts │ │ │ ├── services │ │ │ ├── countdown.service.ts │ │ │ ├── orders.service.ts │ │ │ ├── users.service.ts │ │ │ └── websocket.service.ts │ │ │ ├── shared.module.ts │ │ │ └── states │ │ │ ├── README.md │ │ │ ├── ingredients │ │ │ ├── ingredients.actions.ts │ │ │ ├── ingredients.interface.ts │ │ │ ├── ingredients.reducer.ts │ │ │ └── ingredients.selector.ts │ │ │ ├── orders │ │ │ ├── orders.actions.ts │ │ │ ├── orders.effects.ts │ │ │ ├── orders.initial-state.ts │ │ │ ├── orders.interface.ts │ │ │ ├── orders.reducer.ts │ │ │ └── orders.selector.ts │ │ │ ├── pizzas-categories │ │ │ ├── pizzas-categories.actions.ts │ │ │ ├── pizzas-categories.interface.ts │ │ │ ├── pizzas-categories.reducer.ts │ │ │ └── pizzas-categories.selector.ts │ │ │ ├── pizzas │ │ │ ├── pizzas.actions.ts │ │ │ ├── pizzas.interface.ts │ │ │ ├── pizzas.reducer.ts │ │ │ └── pizzas.selector.ts │ │ │ ├── root.reducer.ts │ │ │ ├── ui │ │ │ ├── ui.actions.ts │ │ │ ├── ui.initial-state.ts │ │ │ ├── ui.interface.ts │ │ │ ├── ui.reducer.ts │ │ │ └── ui.selector.ts │ │ │ └── users │ │ │ ├── users.actions.ts │ │ │ ├── users.effects.ts │ │ │ ├── users.interface.ts │ │ │ ├── users.reducer.ts │ │ │ └── users.selector.ts │ ├── assets │ │ ├── .gitkeep │ │ ├── i18n │ │ │ ├── README.md │ │ │ ├── en.json │ │ │ └── fr.json │ │ └── img │ │ │ ├── github-logo.svg │ │ │ ├── icon-person.png │ │ │ ├── icons │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-256x256.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── browserconfig.xml │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon.ico │ │ │ ├── manifest.json │ │ │ ├── mstile-150x150.png │ │ │ └── safari-pinned-tab.svg │ │ │ ├── pizza-logo.png │ │ │ └── pizzas-providers │ │ │ ├── l-ormeau │ │ │ ├── 4-fromages.png │ │ │ ├── basque.png │ │ │ ├── bolonaise.png │ │ │ ├── borneo.png │ │ │ ├── calzone-chausson.png │ │ │ ├── camembert.png │ │ │ ├── chef.png │ │ │ ├── chicago.png │ │ │ ├── chorizo.png │ │ │ ├── creole.png │ │ │ ├── delhi.png │ │ │ ├── florentine.png │ │ │ ├── forestiere.png │ │ │ ├── gersoise.png │ │ │ ├── izmir.png │ │ │ ├── madras.png │ │ │ ├── marguerite.png │ │ │ ├── milanaise.png │ │ │ ├── new-york.png │ │ │ ├── nicoise.png │ │ │ ├── norvegienne.png │ │ │ ├── ollegio.png │ │ │ ├── orientale.png │ │ │ ├── parmentier.png │ │ │ ├── pavie.png │ │ │ ├── poire-williams-chocolat.png │ │ │ ├── provencale.png │ │ │ ├── romaine.png │ │ │ ├── royale.png │ │ │ ├── san-sebastian.png │ │ │ ├── savoyarde.png │ │ │ ├── seville.png │ │ │ └── sicilienne.png │ │ │ └── pizza-default.png │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── index.html │ ├── main.ts │ ├── polyfills.ts │ ├── setup-jest.ts │ ├── styles.scss │ ├── styles │ │ ├── _index.scss │ │ ├── shared │ │ │ ├── _colors.scss │ │ │ ├── _shared.scss │ │ │ └── _utils.scss │ │ └── themes │ │ │ └── _light.theme.scss │ ├── tsconfig.app.json │ ├── tsconfig.json │ ├── tsconfig.spec.json │ └── typings.d.ts ├── tsconfig.json ├── tslint.json └── yarn.lock ├── logo ├── png │ ├── PS_icon_128.png │ ├── PS_icon_32.png │ ├── PS_icon_monochrome_128.png │ ├── PS_icon_monochrome_32.png │ ├── PS_logo_high_res.png │ ├── PS_logo_high_res_2.png │ ├── PS_logo_low_res.png │ ├── PS_logo_low_res_2.png │ ├── PS_logo_monochrome_high_res.png │ ├── PS_logo_monochrome_high_res_2.png │ ├── PS_logo_monochrome_low_res.png │ ├── PS_logo_monochrome_low_res_2.png │ ├── PS_logo_only_sign_ high_res.png │ ├── PS_logo_only_sign_low_res.png │ ├── PS_logo_only_sign_monochrome_high_res.png │ └── PS_logo_only_sign_monochrome_low_res.png ├── psd │ ├── PS_logo.psd │ ├── PS_logo_monochrome.psd │ ├── PS_logo_only_sign.psd │ └── PS_logo_only_sign_monochrome.psd ├── svg │ ├── PS_logo.svg │ ├── PS_logo_monochrome.svg │ ├── PS_logo_only_sign.svg │ └── PS_logo_only_sign_monochrome.svg └── vector │ ├── PS_icons.ai │ ├── PS_icons.eps │ ├── PS_logo.ai │ ├── PS_logo.eps │ ├── PS_logo_monochrome.ai │ ├── PS_logo_monochrome.eps │ ├── PS_logo_only_sign.ai │ ├── PS_logo_only_sign.eps │ ├── PS_logo_only_sign_monochrome.ai │ └── PS_logo_only_sign_monochrome.eps └── nginx.conf /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | # compiled output 3 | dist 4 | tmp 5 | out-tsc 6 | 7 | # dependencies 8 | node_modules 9 | 10 | # IDEs and editors 11 | .idea 12 | .project 13 | .classpath 14 | .c9/ 15 | *.launch 16 | .settings/ 17 | *.sublime-workspace 18 | 19 | # IDE - VSCode 20 | .vscode/* 21 | !.vscode/launch.json 22 | .history 23 | .angulardoc.json 24 | 25 | # misc 26 | .sass-cache 27 | connect.lock 28 | coverage 29 | libpeerconnection.log 30 | npm-debug.log 31 | testem.log 32 | typings 33 | 34 | # e2e 35 | e2e/*.js 36 | e2e/*.map 37 | 38 | # System Files 39 | .DS_Store 40 | Thumbs.db 41 | 42 | backend/public 43 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | # [v2.0.0](https://github.com/maxime1992/pizza-sync/compare/v1.1.0...v) (2017-12-27) 3 | 4 | 5 | ### Bug Fixes 6 | 7 | * **backend:** pizzas names are not cleaned correctly ([75b8d77](https://github.com/maxime1992/pizza-sync/commit/75b8d77)), closes [#49](https://github.com/maxime1992/pizza-sync/issues/49) 8 | * **frontend:** show order summary and CSV download ([bf86668](https://github.com/maxime1992/pizza-sync/commit/bf86668)) 9 | 10 | 11 | ### Features 12 | 13 | * **product:** dockerize pizza-sync ([36709c5](https://github.com/maxime1992/pizza-sync/commit/36709c5)) thanks to [@ppaysant](https://github.com/ppaysant) with the help of [@tbille](https://github.com/tbille) and [@victornoel](https://github.com/victornoel) 14 | 15 | 16 | ### Refactor 17 | 18 | * **backend:** complete refactor with [NestJs](https://github.com/nestjs/nest) [pull#48](https://github.com/maxime1992/pizza-sync/pull/48) 19 | 20 | 21 | 22 | # [v1.1.0](https://github.com/maxime1992/pizza-sync/compare/v1.0.0...v1.1.0) (2017-11-15) 23 | 24 | 25 | ### Bug Fixes 26 | 27 | * **frontend:** redirect wrong URLs to the app ([8dc3a99](https://github.com/maxime1992/pizza-sync/commit/8dc3a99)) 28 | * **frontend:** generation of CSV ([cc6a643](https://github.com/maxime1992/pizza-sync/commit/cc6a643)) 29 | * **backend:** remove accents on pizzas names when trying to find the corresponding pictures ([0f41662](https://github.com/maxime1992/pizza-sync/commit/0f41662)) 30 | 31 | 32 | ### Features 33 | 34 | * **frontend:** add a search bar to find pizza(s) by name ([345ea10](https://github.com/maxime1992/pizza-sync/commit/345ea10)) 35 | * add pizza images and a fallback image ([761cfcc](https://github.com/maxime1992/pizza-sync/commit/761cfcc)) 36 | * add pizzas-provider information ([5212a29](https://github.com/maxime1992/pizza-sync/commit/5212a29)), closes [#22](https://github.com/maxime1992/pizza-sync/issues/22) 37 | * **frontend:** click on image to display it ([c6b99e0](https://github.com/maxime1992/pizza-sync/commit/c6b99e0)) 38 | * **frontend:** disable easy order view button if there's no pizza order ATM ([ac7c914](https://github.com/maxime1992/pizza-sync/commit/ac7c914)) 39 | * **frontend:** display selected ingredients and filter the pizza view accordingly ([64b3773](https://github.com/maxime1992/pizza-sync/commit/64b3773)) 40 | * **frontend:** download a CSV to handle the $$$ ([5f574dc](https://github.com/maxime1992/pizza-sync/commit/5f574dc)) 41 | * **frontend:** put the searched pizza into url so it can persist on reload ([f104a36](https://github.com/maxime1992/pizza-sync/commit/f104a36)) 42 | * **frontend:** replace control-f binding by focusing on search input ([d28406c](https://github.com/maxime1992/pizza-sync/commit/d28406c)) 43 | * **frontend:** search pizza without taking accents into account ([0dcd5ca](https://github.com/maxime1992/pizza-sync/commit/0dcd5ca)) 44 | * **frontend:** when selecting ingredients, hide the ones that wouldn't give any result for currently selected pizzas ([daee72c](https://github.com/maxime1992/pizza-sync/commit/daee72c)) 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /Dockerfile.api: -------------------------------------------------------------------------------- 1 | FROM node:9.11.2-alpine 2 | 3 | WORKDIR /opt/pizza-sync/backend 4 | ADD ./backend . 5 | RUN yarn 6 | 7 | EXPOSE 3000 8 | 9 | CMD NODE_ENV=production node index.js 10 | -------------------------------------------------------------------------------- /Dockerfile.nginx: -------------------------------------------------------------------------------- 1 | FROM node:9.11.2-alpine as frontend-builder 2 | 3 | # needed to build (python needed for compiling node-sass, for instance...) 4 | RUN apk add --no-cache \ 5 | make \ 6 | gcc \ 7 | g++ \ 8 | python 9 | 10 | WORKDIR /opt/pizza-sync 11 | ADD ./frontend ./frontend 12 | RUN cd frontend && yarn && yarn run build:prod && rm -rf node_modules 13 | 14 | # -------------------------------------------------------------------------- 15 | 16 | FROM nginx:1.15.2-alpine 17 | 18 | COPY ./nginx.conf /etc/nginx/nginx.conf 19 | COPY --from=frontend-builder /opt/pizza-sync/frontend/dist /usr/share/nginx/html 20 | -------------------------------------------------------------------------------- /backend/conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultProvider": "Ormeau-Mock" 3 | } 4 | -------------------------------------------------------------------------------- /backend/index.js: -------------------------------------------------------------------------------- 1 | require('ts-node/register'); 2 | 3 | require('./src/server'); 4 | -------------------------------------------------------------------------------- /backend/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src"], 3 | "ext": "ts", 4 | "ignore": ["src/**/*.spec.ts"], 5 | "exec": "node ./index" 6 | } 7 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pizza-sync", 3 | "version": "2.0.0", 4 | "license": "AGPLv3", 5 | "private": true, 6 | "scripts": { 7 | "start": "node index.js", 8 | "start:watch": "nodemon", 9 | "prestart:prod": "tsc", 10 | "start:prod": "node dist/server.js", 11 | "lint:check": "tslint ./src/**/*", 12 | "lint:fix": "yarn-or-npm run lint:check -- --fix", 13 | "check": "yarn-or-npm run lint:check && yarn-or-npm run prettier:check", 14 | "check:fix": "yarn-or-npm run lint:fix; yarn-or-npm run prettier:fix", 15 | "prettier:base": 16 | "yarn-or-npm run prettier -- --single-quote --trailing-comma es5", 17 | "prettier:base-files": 18 | "yarn-or-npm run prettier:base -- './**/*.{js,ts,json}'", 19 | "prettier:fix": "yarn-or-npm run prettier:base-files -- --write", 20 | "prettier:check": "yarn-or-npm run prettier:base-files -- -l", 21 | "precommit": "lint-staged", 22 | "prepush": "yarn-or-npm run lint:check" 23 | }, 24 | "lint-staged": { 25 | "linters": { 26 | "*.{js,ts,json}": ["yarn-or-npm run prettier:base -- -l"] 27 | } 28 | }, 29 | "dependencies": { 30 | "@nestjs/common": "5.1.0", 31 | "@nestjs/core": "5.1.0", 32 | "@nestjs/microservices": "5.1.0", 33 | "@nestjs/testing": "5.1.0", 34 | "@nestjs/websockets": "5.1.0", 35 | "body-parser": "1.18.3", 36 | "cheerio": "1.0.0-rc.2", 37 | "cors": "2.8.4", 38 | "normalizr": "3.2.4", 39 | "redis": "2.8.0", 40 | "reflect-metadata": "0.1.12", 41 | "remove-accents": "0.4.2", 42 | "request": "2.88.0", 43 | "rxjs": "6.2.2", 44 | "rxjs-compat": "6.2.2", 45 | "typescript": "2.6.1", 46 | "uuid": "3.3.2", 47 | "vorpal": "1.12.0" 48 | }, 49 | "devDependencies": { 50 | "@types/cheerio": "0.22.8", 51 | "@types/cors": "2.8.4", 52 | "@types/node": "10.7.1", 53 | "@types/normalizr": "2.0.18", 54 | "@types/request": "2.47.1", 55 | "@types/uuid": "3.4.3", 56 | "husky": "0.14.3", 57 | "lint-staged": "5.0.0", 58 | "nodemon": "1.18.3", 59 | "prettier": "1.8.2", 60 | "ts-node": "3.3.0", 61 | "tslint": "5.8.0", 62 | "tslint-config-prettier": "1.6.0", 63 | "yarn-or-npm": "2.0.4" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /backend/src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | 3 | import { PizzasProvidersService } from './features/pizzas-providers/pizzas-providers.component'; 4 | import { UsersService } from './features/models/users/users.component'; 5 | import { OrdersService } from './features/models/orders/orders.component'; 6 | import { PizzasService } from './features/models/pizzas/pizzas.component'; 7 | import { PizzasCategoriesService } from './features/models/pizzas-categories/pizzas-categories.component'; 8 | import { IngredientsService } from './features/models/ingredients/ingredients.component'; 9 | 10 | @Controller() 11 | export class AppController { 12 | constructor( 13 | private pizzasProvidersService: PizzasProvidersService, 14 | private pizzasService: PizzasService, 15 | private pizzasCategoriesService: PizzasCategoriesService, 16 | private usersService: UsersService, 17 | private ordersService: OrdersService, 18 | private ingredientsService: IngredientsService 19 | ) {} 20 | 21 | @Get('initial-state') 22 | getInitialState() { 23 | const currentPizzaProviderInformation = this.pizzasProvidersService 24 | .getCurrentProvider() 25 | .getPizzeriaInformation(); 26 | 27 | return { 28 | pizzas: this.pizzasService.getNormalizedData(), 29 | pizzasCategories: this.pizzasCategoriesService.getNormalizedData(), 30 | users: this.usersService.getNormalizedData(), 31 | orders: this.ordersService.getNormalizedData(), 32 | ingredients: this.ingredientsService.getNormalizedData(), 33 | pizzeria: currentPizzaProviderInformation, 34 | }; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /backend/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { AppController } from './app.controller'; 4 | import { PizzasProvidersModule } from './features/pizzas-providers/pizzas-providers.module'; 5 | import { CommandLineModule } from './command-line/command-line.module'; 6 | import { IngredientsModule } from './features/models/ingredients/ingredients.module'; 7 | import { OrdersModule } from './features/models/orders/orders.module'; 8 | import { PizzasModule } from './features/models/pizzas/pizzas.module'; 9 | import { PizzasCategoriesModule } from './features/models/pizzas-categories/pizzas-categories.module'; 10 | import { UsersModule } from './features/models/users/users.module'; 11 | 12 | @Module({ 13 | modules: [ 14 | PizzasProvidersModule, 15 | CommandLineModule, 16 | IngredientsModule, 17 | OrdersModule, 18 | PizzasModule, 19 | PizzasCategoriesModule, 20 | UsersModule, 21 | ], 22 | controllers: [AppController], 23 | }) 24 | export class ApplicationModule {} 25 | -------------------------------------------------------------------------------- /backend/src/command-line/command-line.component.ts: -------------------------------------------------------------------------------- 1 | import * as _vorpal from 'vorpal'; 2 | 3 | import { OrdersService } from '../features/models/orders/orders.component'; 4 | import { UsersService } from '../features/models/users/users.component'; 5 | import { PizzasProvidersService } from '../features/pizzas-providers/pizzas-providers.component'; 6 | import { Command } from './command.class'; 7 | import { CountdownCommand } from './commands/countdown.command'; 8 | import { ProviderCommand } from './commands/provider.command'; 9 | import { ProvidersCommand } from './commands/providers.command'; 10 | 11 | export class CommandLineService { 12 | private vorpal = _vorpal(); 13 | private commands: Command[] = []; 14 | 15 | constructor( 16 | private pizzasProvidersService: PizzasProvidersService, 17 | private ordersService: OrdersService, 18 | private usersService: UsersService 19 | ) { 20 | // register all the command by pushing them into `commands` array 21 | this.commands.push( 22 | new ProviderCommand(this.vorpal, pizzasProvidersService, usersService), 23 | new ProvidersCommand(this.vorpal, pizzasProvidersService), 24 | new CountdownCommand(this.vorpal, ordersService) 25 | ); 26 | 27 | // register every commands 28 | this.commands.forEach(command => command.register()); 29 | 30 | this.vorpal.delimiter('pizza-sync$').show(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /backend/src/command-line/command-line.module.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Module } from '@nestjs/common'; 2 | 3 | import { OrdersService } from '../features/models/orders/orders.component'; 4 | import { OrdersModule } from '../features/models/orders/orders.module'; 5 | import { UsersService } from '../features/models/users/users.component'; 6 | import { UsersModule } from '../features/models/users/users.module'; 7 | import { PizzasProvidersService } from '../features/pizzas-providers/pizzas-providers.component'; 8 | import { PizzasProvidersModule } from '../features/pizzas-providers/pizzas-providers.module'; 9 | import { CommandLineService } from './command-line.component'; 10 | 11 | // before creating the `CommandLineService` we need to wait 12 | // for the default pizza provider to be set 13 | const CommandLineServiceFactory = { 14 | provide: 'CommandLineService', 15 | useFactory: async ( 16 | pizzasProvidersService: PizzasProvidersService, 17 | ordersService: OrdersService, 18 | usersService: UsersService 19 | ) => { 20 | try { 21 | await pizzasProvidersService.setDefaultProvider(); 22 | } catch (err) { 23 | // tslint:disable-next-line:no-console 24 | console.log('An error occured while setting the default provider:'); 25 | // tslint:disable-next-line:no-console 26 | console.log(err.message); 27 | // tslint:disable-next-line:no-console 28 | console.log('Skipping that step, please set one manually'); 29 | } 30 | 31 | return new CommandLineService( 32 | pizzasProvidersService, 33 | ordersService, 34 | usersService 35 | ); 36 | }, 37 | inject: [PizzasProvidersService, OrdersService, UsersService], 38 | }; 39 | 40 | @Module({ 41 | imports: [PizzasProvidersModule, OrdersModule, UsersModule], 42 | providers: [CommandLineServiceFactory], 43 | }) 44 | export class CommandLineModule { 45 | constructor(@Inject('CommandLineService') commandLineService) {} 46 | } 47 | -------------------------------------------------------------------------------- /backend/src/command-line/command.class.ts: -------------------------------------------------------------------------------- 1 | // this abstract class aims to help developers 2 | // to create a new vorpal command in isolation 3 | export abstract class Command { 4 | abstract titleWithParams: string; 5 | abstract description: string; 6 | options: { title: string; description: string }[] | false = false; 7 | 8 | constructor(private _vorpal: any) {} 9 | 10 | getAutocomplete(): string[] | false { 11 | return false; 12 | } 13 | 14 | abstract action( 15 | args: {}, 16 | callback: () => void, 17 | vorpalContext: { log: (msg: string) => void } 18 | ); 19 | 20 | register(): void { 21 | const self = this; 22 | 23 | let command = this._vorpal 24 | .command(this.titleWithParams) 25 | .description(this.description); 26 | 27 | if (this.getAutocomplete() !== false) { 28 | command.autocomplete((() => this.getAutocomplete())()); 29 | } 30 | 31 | if (this.options) { 32 | this.options.forEach(option => { 33 | command = command.option(option.title, option.description); 34 | }); 35 | } 36 | 37 | command.action(function(args, callback) { 38 | const vorpalContext = this; 39 | self.action(args, callback, vorpalContext); 40 | }); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /backend/src/command-line/commands/countdown.command.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '../command.class'; 2 | import { OrdersService } from '../../features/models/orders/orders.component'; 3 | 4 | export class CountdownCommand extends Command { 5 | titleWithParams = 'countdown'; 6 | description = 'Set or change the hour when to block the orders'; 7 | options = [ 8 | { 9 | title: '--hour ', 10 | description: 'Set or change the hour when to block the orders', 11 | }, 12 | { 13 | title: '--minute ', 14 | description: 'Set or change the minute when to block the orders', 15 | }, 16 | ]; 17 | 18 | constructor(private vorpal: any, private ordersService: OrdersService) { 19 | super(vorpal); 20 | } 21 | 22 | action( 23 | args: { options: { [key: string]: string } }, 24 | callback: () => void, 25 | vorpalContext: { log: (msg: string) => void } 26 | ) { 27 | if ( 28 | typeof args.options.hour === 'undefined' || 29 | typeof args.options.minute === 'undefined' 30 | ) { 31 | console.log('You need to define --hour and --minute'); 32 | return callback(); 33 | } 34 | 35 | if ( 36 | typeof args.options.hour !== 'number' || 37 | typeof args.options.minute !== 'number' 38 | ) { 39 | console.log('"hour" and "minute" should be integers'); 40 | return callback(); 41 | } 42 | 43 | const hourEnd = +args.options.hour; 44 | const minuteEnd = +args.options.minute; 45 | 46 | if (hourEnd < 0 || hourEnd > 23) { 47 | console.log('"hour" must be between 0 and 23'); 48 | return callback(); 49 | } 50 | 51 | if (minuteEnd < 0 || minuteEnd > 59) { 52 | console.log('"minute" must be between 0 and 59'); 53 | return callback(); 54 | } 55 | 56 | this.ordersService.setHourAndMinuteEnd(hourEnd, minuteEnd); 57 | callback(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /backend/src/command-line/commands/provider.command.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '../command.class'; 2 | import { PizzasProvidersService } from '../../features/pizzas-providers/pizzas-providers.component'; 3 | import { UsersService } from '../../features/models/users/users.component'; 4 | 5 | export class ProviderCommand extends Command { 6 | titleWithParams = 'provider '; 7 | description = 'Set the current provider'; 8 | 9 | constructor( 10 | private vorpal: any, 11 | private pizzasProvidersService: PizzasProvidersService, 12 | private usersService: UsersService 13 | ) { 14 | super(vorpal); 15 | } 16 | 17 | getAutocomplete(): string[] { 18 | return this.pizzasProvidersService.getProvidersShortNames(); 19 | } 20 | 21 | async action( 22 | args: { provider: string }, 23 | callback: () => void, 24 | vorpalContext: { log: (msg: string) => void } 25 | ) { 26 | const newProviderName = args.provider; 27 | 28 | if (this.usersService.getNbConnections()) { 29 | console.error( 30 | 'Some users are already connected, you shall stop server before' 31 | ); 32 | return callback(); 33 | } 34 | 35 | if (!newProviderName) { 36 | vorpalContext.log('You need to select a provider'); 37 | return callback(); 38 | } 39 | 40 | if (!this.pizzasProvidersService.includes(newProviderName)) { 41 | vorpalContext.log('Unknown provider'); 42 | return callback(); 43 | } 44 | 45 | const newProviderInstance = this.pizzasProvidersService.getProviderInstanceByName( 46 | newProviderName 47 | ); 48 | 49 | try { 50 | await this.pizzasProvidersService.setCurrentProvider(newProviderInstance); 51 | } catch (err) { 52 | console.log(err.message); 53 | } 54 | 55 | return callback(); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /backend/src/command-line/commands/providers.command.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '../command.class'; 2 | import { PizzasProvidersService } from '../../features/pizzas-providers/pizzas-providers.component'; 3 | 4 | export class ProvidersCommand extends Command { 5 | titleWithParams = 'providers'; 6 | description = 'List available pizzas providers'; 7 | 8 | constructor( 9 | private vorpal: any, 10 | private pizzasProvidersService: PizzasProvidersService 11 | ) { 12 | super(vorpal); 13 | } 14 | 15 | action( 16 | _, 17 | callback: () => void, 18 | vorpalContext: { log: (msg: string) => void } 19 | ) { 20 | this.displayFormattedProviders(vorpalContext); 21 | callback(); 22 | } 23 | 24 | private displayFormattedProviders(vorpalContext): void { 25 | vorpalContext.log('Available providers (current between curly brackets)'); 26 | 27 | this.getFormattedProviders().forEach(provider => 28 | vorpalContext.log(provider) 29 | ); 30 | } 31 | 32 | // returns an array of providers' name with the 33 | // currently selected between brackets 34 | private getFormattedProviders(): string[] { 35 | const providers = this.pizzasProvidersService.getProviders(); 36 | const currentProvider = this.pizzasProvidersService.getCurrentProvider(); 37 | 38 | return providers.map( 39 | provider => 40 | provider === currentProvider 41 | ? `- { ${provider.longCompanyName} }` 42 | : `- ${provider.longCompanyName}` 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /backend/src/features/models/ingredients/ingredients.component.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | import { NormalizedModel } from '../normalized-model.class'; 4 | import { IIngredientWithoutId } from './ingredients.interface'; 5 | 6 | @Injectable() 7 | export class IngredientsService extends NormalizedModel { 8 | protected sort = true; 9 | 10 | constructor() { 11 | super('ingredientId'); 12 | } 13 | 14 | // we want to order them by name, to display the list 15 | // of ingredients in the frontend and propose to filter by ingredients 16 | protected sortBy(ing1, ing2) { 17 | return ing1.name.localeCompare(ing2.name); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /backend/src/features/models/ingredients/ingredients.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IIngredientWithoutId { 2 | name: string; 3 | } 4 | 5 | export interface IIngredientWithId extends IIngredientWithoutId { 6 | id: string; 7 | } 8 | 9 | export interface IIngredientsNormalized { 10 | entities: { [key: string]: IIngredientWithId }; 11 | ids: string[]; 12 | } 13 | -------------------------------------------------------------------------------- /backend/src/features/models/ingredients/ingredients.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { IngredientsService } from './ingredients.component'; 4 | 5 | @Module({ 6 | providers: [IngredientsService], 7 | exports: [IngredientsService], 8 | }) 9 | export class IngredientsModule {} 10 | -------------------------------------------------------------------------------- /backend/src/features/models/normalized-model.class.ts: -------------------------------------------------------------------------------- 1 | type TypeWithId = T & { id: string }; 2 | 3 | export abstract class NormalizedModel { 4 | private id = 0; 5 | protected entities: { [key: string]: TypeWithId } = {}; 6 | protected ids: string[] = []; 7 | 8 | // set it to true will run the sortBy method 9 | // every time there's a call to `create` 10 | // or on every resource added with the `setNormalizedData` 11 | protected sort: boolean = false; 12 | 13 | constructor(private idPrefix: string) {} 14 | 15 | private getNewId() { 16 | return `${this.idPrefix}${this.id++}`; 17 | } 18 | 19 | protected sortBy( 20 | el1: TypeWithId, 21 | el2: TypeWithId 22 | ): number { 23 | return 0; 24 | } 25 | 26 | create(element: TypeWithoutId): TypeWithId { 27 | const newId = this.getNewId(); 28 | 29 | // `as any` syntax is a way to avoid the error of using 30 | // spread operator on a variable with a generic type 31 | this.entities[newId] = { ...(element as any), id: newId }; 32 | this.ids.push(newId); 33 | 34 | if (this.sort) { 35 | this.sortIds(); 36 | } 37 | 38 | return this.entities[newId]; 39 | } 40 | 41 | delete(id: string): boolean { 42 | if (!this.entities[id]) { 43 | return false; 44 | } 45 | 46 | delete this.entities[id]; 47 | 48 | this.ids = this.ids.filter(_id => _id !== id); 49 | return true; 50 | } 51 | 52 | setNormalizedData({ 53 | entities, 54 | ids, 55 | }: { 56 | entities: { [key: string]: TypeWithId }; 57 | ids: string[]; 58 | }) { 59 | this.entities = entities; 60 | this.ids = ids; 61 | 62 | if (this.sort) { 63 | this.sortIds(); 64 | } 65 | } 66 | 67 | getNormalizedData() { 68 | return { 69 | entities: this.entities, 70 | ids: this.ids, 71 | }; 72 | } 73 | 74 | private sortIds() { 75 | this.ids = this.ids 76 | .map(id => this.entities[id]) 77 | .sort((el1, el2) => this.sortBy(el1, el2)) 78 | .map(el => el.id); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /backend/src/features/models/orders/orders.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | WebSocketGateway, 3 | WebSocketServer, 4 | SubscribeMessage, 5 | OnGatewayConnection, 6 | } from '@nestjs/websockets'; 7 | 8 | import { NormalizedModel } from '../normalized-model.class'; 9 | import { IOrderWithoutId } from './orders.interface'; 10 | 11 | @WebSocketGateway() 12 | export class OrdersService extends NormalizedModel 13 | implements OnGatewayConnection { 14 | @WebSocketServer() server; 15 | 16 | // when the app should stop accepting orders 17 | private hourEnd: number; 18 | private minuteEnd: number; 19 | 20 | constructor() { 21 | super('orderId'); 22 | 23 | // by default, the app stop accepting orders after 1 hour 24 | const currentDate = new Date(); 25 | this.setHourAndMinuteEnd( 26 | currentDate.getHours() + 1, 27 | currentDate.getMinutes(), 28 | false 29 | ); 30 | } 31 | 32 | getHourEnd() { 33 | return this.hourEnd; 34 | } 35 | 36 | getMinuteEnd() { 37 | return this.minuteEnd; 38 | } 39 | 40 | // TODO: add a command line to change this 41 | setHourAndMinuteEnd(hourEnd, minuteEnd, broadcast = true) { 42 | this.hourEnd = hourEnd; 43 | this.minuteEnd = minuteEnd; 44 | 45 | if (broadcast) { 46 | this.server.sockets.emit('SET_COUNTDOWN', { 47 | hour: this.hourEnd, 48 | minute: this.minuteEnd, 49 | }); 50 | } 51 | } 52 | 53 | handleConnection(client: any) { 54 | client.emit('SET_COUNTDOWN', { 55 | hour: this.hourEnd, 56 | minute: this.minuteEnd, 57 | }); 58 | } 59 | 60 | @SubscribeMessage('ADD_ORDER') 61 | addOrder(client, orderWithoutId: IOrderWithoutId) { 62 | // TODO : block if current time >= hourEnd and minuteEnd 63 | const order = this.create(orderWithoutId); 64 | 65 | this.server.sockets.emit('ADD_ORDER_SUCCESS', order); 66 | } 67 | 68 | @SubscribeMessage('REMOVE_ORDER') 69 | removeOrder(client, orderId: string) { 70 | // TODO : block if current time >= hourEnd and minuteEnd 71 | const hasOrderBeenRemoved = this.delete(orderId); 72 | 73 | if (hasOrderBeenRemoved) { 74 | this.server.sockets.emit('REMOVE_ORDER_SUCCESS', orderId); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /backend/src/features/models/orders/orders.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IOrderWithoutId { 2 | pizzaId: string; 3 | priceIndex: number; 4 | userId: string; 5 | } 6 | 7 | export interface IOrderWithId extends IOrderWithoutId { 8 | id: string; 9 | } 10 | 11 | export interface IOrdersNormalized { 12 | entities: { [key: string]: IOrderWithId }; 13 | ids: string[]; 14 | } 15 | -------------------------------------------------------------------------------- /backend/src/features/models/orders/orders.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { OrdersService } from './orders.component'; 4 | 5 | @Module({ 6 | providers: [OrdersService], 7 | exports: [OrdersService], 8 | }) 9 | export class OrdersModule {} 10 | -------------------------------------------------------------------------------- /backend/src/features/models/pizzas-categories/pizzas-categories.component.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | import { NormalizedModel } from '../normalized-model.class'; 4 | import { IPizzaCategoryWithoutId } from './pizzas-categories.interface'; 5 | 6 | @Injectable() 7 | export class PizzasCategoriesService extends NormalizedModel< 8 | IPizzaCategoryWithoutId 9 | > { 10 | constructor() { 11 | super('pizzaCategoryId'); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /backend/src/features/models/pizzas-categories/pizzas-categories.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IPizzaCategoryWithoutId {} 2 | 3 | export interface IPizzaCategoryWithId extends IPizzaCategoryWithoutId { 4 | id: string; 5 | pizzasIds: string[]; 6 | } 7 | 8 | export interface IPizzasCategoriesNormalized { 9 | entities: { [key: string]: IPizzaCategoryWithId }; 10 | ids: string[]; 11 | } 12 | -------------------------------------------------------------------------------- /backend/src/features/models/pizzas-categories/pizzas-categories.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { PizzasCategoriesService } from './pizzas-categories.component'; 4 | 5 | @Module({ 6 | providers: [PizzasCategoriesService], 7 | exports: [PizzasCategoriesService], 8 | }) 9 | export class PizzasCategoriesModule {} 10 | -------------------------------------------------------------------------------- /backend/src/features/models/pizzas/pizzas.component.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | import { NormalizedModel } from '../normalized-model.class'; 4 | import { IPizzaWithoutId } from './pizzas.interface'; 5 | 6 | @Injectable() 7 | export class PizzasService extends NormalizedModel { 8 | constructor() { 9 | super('pizzaId'); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /backend/src/features/models/pizzas/pizzas.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IPizzaWithoutId { 2 | name: string; 3 | ingredientsIds: string[]; 4 | prices: number[]; 5 | imgUrl: string; 6 | } 7 | 8 | export interface IPizzaWithId extends IPizzaWithoutId { 9 | id: string; 10 | } 11 | 12 | export interface IPizzasNormalized { 13 | entities: { [key: string]: IPizzaWithId }; 14 | ids: string[]; 15 | } 16 | -------------------------------------------------------------------------------- /backend/src/features/models/pizzas/pizzas.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { PizzasService } from './pizzas.component'; 4 | 5 | @Module({ 6 | providers: [PizzasService], 7 | exports: [PizzasService], 8 | }) 9 | export class PizzasModule {} 10 | -------------------------------------------------------------------------------- /backend/src/features/models/users/users.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | WebSocketGateway, 3 | OnGatewayDisconnect, 4 | SubscribeMessage, 5 | WebSocketServer, 6 | } from '@nestjs/websockets'; 7 | import { get } from 'request'; 8 | 9 | import { NormalizedModel } from '../normalized-model.class'; 10 | import { requestOptions } from '../../../helpers/http.helper'; 11 | import { IUserWithId, IUserWithoutId } from './users.interface'; 12 | 13 | @WebSocketGateway() 14 | export class UsersService extends NormalizedModel 15 | implements OnGatewayDisconnect { 16 | @WebSocketServer() server; 17 | 18 | constructor() { 19 | super('userId'); 20 | } 21 | 22 | @SubscribeMessage('CONNECT_USER') 23 | async connectUser(client, username: string) { 24 | const user = this.getUser(username); 25 | 26 | if (!!user) { 27 | this.setUserOnline(user); 28 | 29 | client.user = user; 30 | 31 | this.server.sockets.emit('CONNECT_USER_SUCCESS', user); 32 | } else { 33 | const newUser = await this.addUser(username); 34 | 35 | client.user = newUser; 36 | 37 | this.setUserOnline(newUser); 38 | this.server.sockets.emit('CONNECT_USER_SUCCESS', newUser); 39 | } 40 | } 41 | 42 | handleDisconnect(client: any) { 43 | if (!client.user) { 44 | return; 45 | } 46 | 47 | this.setUserOffline(client.user); 48 | 49 | if (this.getNbConnectionsUser(client.user) === 0) { 50 | this.server.sockets.emit('DISCONNECT_USER_SUCCESS', client.user.id); 51 | } 52 | } 53 | 54 | getUser(username: string): IUserWithId { 55 | const user = this.ids 56 | .map(userId => this.entities[userId]) 57 | .find(userTmp => userTmp.username === username); 58 | 59 | return user ? user : null; 60 | } 61 | 62 | getNbConnectionsUser(user: IUserWithId): number { 63 | if (!!this.entities[user.id]) { 64 | return this.entities[user.id].nbConnections; 65 | } 66 | 67 | return 0; 68 | } 69 | 70 | getNbConnections(): number { 71 | return this.ids.length; 72 | } 73 | 74 | addUser(username: string): Promise { 75 | return new Promise((resolve, reject) => { 76 | get( 77 | `https://api.github.com/users/${username}`, 78 | requestOptions, 79 | (error, response, body) => { 80 | const user: IUserWithoutId = { 81 | username, 82 | thumbnail: '', 83 | nbConnections: 0, 84 | isOnline: false, 85 | }; 86 | 87 | if (!error) { 88 | try { 89 | body = JSON.parse(body); 90 | user.thumbnail = body.avatar_url || ''; 91 | } catch (err) {} 92 | } 93 | 94 | const newUser = this.create(user); 95 | 96 | resolve(newUser); 97 | } 98 | ); 99 | }); 100 | } 101 | 102 | setUserOnline(user: IUserWithId): void { 103 | const userRef = this.entities[user.id]; 104 | 105 | if (!userRef) { 106 | return; 107 | } 108 | 109 | userRef.isOnline = true; 110 | userRef.nbConnections++; 111 | } 112 | 113 | setUserOffline(user: IUserWithId): void { 114 | const userRef = this.entities[user.id]; 115 | 116 | if (!userRef) { 117 | return; 118 | } 119 | 120 | userRef.nbConnections--; 121 | userRef.isOnline = !!userRef.nbConnections; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /backend/src/features/models/users/users.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IUserWithoutId { 2 | username: string; 3 | nbConnections: number; 4 | thumbnail: string; 5 | isOnline: boolean; 6 | } 7 | 8 | export interface IUserWithId extends IUserWithoutId { 9 | id: string; 10 | } 11 | 12 | export interface IUsersNormalized { 13 | entities: { [key: string]: IUserWithId }; 14 | ids: string[]; 15 | } 16 | -------------------------------------------------------------------------------- /backend/src/features/models/users/users.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { UsersService } from './users.component'; 4 | 5 | @Module({ 6 | providers: [UsersService], 7 | exports: [UsersService], 8 | }) 9 | export class UsersModule {} 10 | -------------------------------------------------------------------------------- /backend/src/features/pizzas-providers/implementations/ormeau.class.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@nestjs/common'; 2 | import { v4 as uuid } from 'uuid'; 3 | 4 | import { PizzasProvider } from '../pizzas-provider.class'; 5 | 6 | export class OrmeauProvider extends PizzasProvider { 7 | readonly longCompanyName = `L'Ormeau`; 8 | readonly shortCompanyName = `Ormeau`; 9 | 10 | protected phone = '05 61 34 86 23'; 11 | protected url = 'http://www.pizzadelormeau.com'; 12 | protected urlsPizzasPages = [this.url]; 13 | 14 | getPhone(): string { 15 | return this.phone; 16 | } 17 | 18 | getPizzasCategoriesWrapper($: CheerioStatic): Cheerio { 19 | return $('.section_wrapper').find('.one'); 20 | } 21 | 22 | getPizzaCategoryName(pizzaCategoryWrapper: Cheerio): string { 23 | return pizzaCategoryWrapper.find('h2').text(); 24 | } 25 | 26 | getPizzasWrappers(pizzaCategoryWrapper: Cheerio): Cheerio { 27 | return pizzaCategoryWrapper.nextUntil('.one, .pad50'); 28 | } 29 | 30 | getPizzaName(pizzaWrapper: Cheerio): string { 31 | return pizzaWrapper 32 | .find('.big') 33 | .text() 34 | .replace(/ +\(.*\)/, ''); 35 | } 36 | 37 | getPizzaIngredients(pizzaWrapper: Cheerio): string[] { 38 | const pizzaIngredientsText = pizzaWrapper 39 | .find('.midbig') 40 | .text() 41 | .replace('.', '') 42 | .replace(', ', ',') 43 | .trim(); 44 | 45 | return pizzaIngredientsText.split(','); 46 | } 47 | 48 | getPrices(pizzaWrapper: Cheerio, $: CheerioStatic): number[] { 49 | return pizzaWrapper 50 | .find('.pizzap') 51 | .text() 52 | .replace(/,/g, '.') 53 | .match(/\d+(\.\d+)?/g) 54 | .map(Number); 55 | } 56 | 57 | getPizzaImage(): { localFolderName: string } | { distantUrl: string } { 58 | return { localFolderName: 'l-ormeau' }; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /backend/src/features/pizzas-providers/implementations/ormeau.mock.ts: -------------------------------------------------------------------------------- 1 | import { PizzaProviderMock } from '../pizzas-provider.class'; 2 | import { ormeauMock } from '../../../../mocks/ormeau.mock'; 3 | 4 | export class OrmeauMockProvider extends PizzaProviderMock { 5 | shortCompanyName = `Ormeau-Mock`; 6 | 7 | constructor() { 8 | super(ormeauMock.pizzeria); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /backend/src/features/pizzas-providers/implementations/tutti.class.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@nestjs/common'; 2 | import { v4 as uuid } from 'uuid'; 3 | 4 | import { PizzasProvider } from '../pizzas-provider.class'; 5 | 6 | export class TuttiProvider extends PizzasProvider { 7 | readonly longCompanyName = `Tutti Pizza`; 8 | readonly shortCompanyName = `Tutti`; 9 | 10 | protected phone = ''; 11 | protected url = 'https://www.tutti-pizza.com'; 12 | protected urlsPizzasPages = [ 13 | 'https://www.tutti-pizza.com/fr/boutique/pizzas.php', 14 | ]; 15 | 16 | getPhone(): string { 17 | return this.phone; 18 | } 19 | 20 | getPizzasCategoriesWrapper($: CheerioStatic): Cheerio { 21 | return $('#listProduct .row'); 22 | } 23 | 24 | getPizzaCategoryName(pizzaCategoryWrapper: Cheerio): string { 25 | return pizzaCategoryWrapper.prev().text(); 26 | } 27 | 28 | getPizzasWrappers(pizzaCategoryWrapper: Cheerio): Cheerio { 29 | return pizzaCategoryWrapper.find('.item'); 30 | } 31 | 32 | getPizzaName(pizzaWrapper: Cheerio): string { 33 | return pizzaWrapper.find('.item-name').text(); 34 | } 35 | 36 | getPizzaIngredients(pizzaWrapper: Cheerio): string[] { 37 | const pizzaIngredientsText = pizzaWrapper.find('.item-ingredients').text(); 38 | 39 | return pizzaIngredientsText.split(','); 40 | } 41 | 42 | getPrices(pizzaWrapper: Cheerio, $: CheerioStatic): number[] { 43 | const pizzaPricesDom = pizzaWrapper 44 | .find('.item-price') 45 | // tutti sometimes add "promotion" in `.item-price` and 2 prices 46 | // are displayed here except that one is striked through 47 | // so we just keep the one which is not into the `del` component 48 | .children('del') 49 | .remove() 50 | .end(); 51 | 52 | return pizzaPricesDom.toArray().map(pizzaPriceDom => { 53 | const [price] = $(pizzaPriceDom) 54 | .text() 55 | .replace(',', '.') 56 | .split(' '); 57 | 58 | return +price; 59 | }); 60 | } 61 | 62 | getPizzaImage( 63 | pizzaWrapper: Cheerio 64 | ): { localFolderName: string } | { distantUrl: string } { 65 | const distantUrl = pizzaWrapper.find('.item-img img').attr('src'); 66 | 67 | return { distantUrl }; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /backend/src/features/pizzas-providers/pizzas-providers.interface.ts: -------------------------------------------------------------------------------- 1 | import { IIngredientsNormalized } from './../models/ingredients/ingredients.interface'; 2 | import { IPizzasNormalized } from '../models/pizzas/pizzas.interface'; 3 | import { IPizzasCategoriesNormalized } from '../models/pizzas-categories/pizzas-categories.interface'; 4 | 5 | export interface INormalizedInformation { 6 | pizzeria: { name: string; phone: string; url: string }; 7 | pizzas: IPizzasNormalized; 8 | pizzasCategories: IPizzasCategoriesNormalized; 9 | ingredients: IIngredientsNormalized; 10 | } 11 | 12 | // -------------------------- 13 | // NESTED WITHOUT ID 14 | // -------------------------- 15 | // pizzeria 16 | export interface IPizzeriaNestedCommonWithoutId { 17 | name: string; 18 | phone: string; 19 | url: string; 20 | } 21 | export interface IPizzeriaNestedFkWithoutId 22 | extends IPizzeriaNestedCommonWithoutId { 23 | pizzasCategories: IPizzaCategoryFkWithoutId[]; 24 | } 25 | 26 | // category 27 | interface IPizzaCategoryCommonWithoutId { 28 | name: string; 29 | } 30 | export interface IPizzaCategoryFkWithoutId 31 | extends IPizzaCategoryCommonWithoutId { 32 | pizzas: IPizzaFkWithoutId[]; 33 | } 34 | 35 | // pizza 36 | interface IPizzaCommonWithoutId { 37 | name: string; 38 | imgUrl: string; 39 | prices: number[]; 40 | } 41 | export interface IPizzaFkWithoutId extends IPizzaCommonWithoutId { 42 | ingredients: { name: string }[]; 43 | } 44 | 45 | // -------------------------- 46 | // NESTED WITH ID 47 | // -------------------------- 48 | // pizzeria 49 | export interface IPizzeriaNestedFkWithId 50 | extends IPizzeriaNestedCommonWithoutId { 51 | id: string; 52 | pizzasCategories: IPizzaCategoryFkWithId[]; 53 | } 54 | 55 | // category 56 | export interface IPizzaCategoryFkWithId extends IPizzaCategoryCommonWithoutId { 57 | id: string; 58 | pizzas: IPizzaFkWithId[]; 59 | } 60 | 61 | // pizza 62 | export interface IPizzaFkWithId extends IPizzaFkWithoutId { 63 | id: string; 64 | } 65 | -------------------------------------------------------------------------------- /backend/src/features/pizzas-providers/pizzas-providers.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { PizzasProvidersService } from './pizzas-providers.component'; 4 | import { PizzasModule } from '../models/pizzas/pizzas.module'; 5 | import { PizzasCategoriesModule } from '../models/pizzas-categories/pizzas-categories.module'; 6 | import { IngredientsModule } from '../models/ingredients/ingredients.module'; 7 | 8 | @Module({ 9 | imports: [PizzasModule, PizzasCategoriesModule, IngredientsModule], 10 | providers: [PizzasProvidersService], 11 | exports: [PizzasProvidersService], 12 | }) 13 | export class PizzasProvidersModule {} 14 | -------------------------------------------------------------------------------- /backend/src/helpers/file.helper.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | 3 | import { cleanPizzaName, removeLocalPath } from './string.helper'; 4 | 5 | /** 6 | * check if the image of a pizza is available 7 | * if available : it returns the path of the image 8 | * if not available: it returns the path of the default image 9 | */ 10 | export function getPathImgPizza(pizzaName: string, folderPath: string): string { 11 | const pizzaImgPath = `${folderPath}/${cleanPizzaName(pizzaName)}.png`; 12 | 13 | if (fs.existsSync(pizzaImgPath)) { 14 | return removeLocalPath(pizzaImgPath); 15 | } 16 | 17 | return 'assets/img/pizzas-providers/pizza-default.png'; 18 | } 19 | -------------------------------------------------------------------------------- /backend/src/helpers/http.helper.ts: -------------------------------------------------------------------------------- 1 | export const requestOptions = { 2 | headers: { 3 | 'User-Agent': 4 | 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/55.0.2883.87 Chrome/55.0.2883.87 Safari/537.36', 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /backend/src/helpers/normalize.helper.ts: -------------------------------------------------------------------------------- 1 | export function normalizeArray( 2 | array: { id: string }[] 3 | ): { entities: {}; ids: string[] } { 4 | if (!array) { 5 | return { entities: {}, ids: [] }; 6 | } 7 | 8 | return array.reduce( 9 | (acc, next) => { 10 | acc.entities[next.id] = next; 11 | acc.ids.push(next.id); 12 | 13 | return acc; 14 | }, 15 | { entities: {}, ids: [] } 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /backend/src/helpers/object.helper.ts: -------------------------------------------------------------------------------- 1 | export const renameKeyInObject = ( 2 | obj: { [key: string]: any }, 3 | keyToRename: string, 4 | newKey: string 5 | ): { [key: string]: any } => { 6 | const { [keyToRename]: omit, ...objWithoutKey } = obj; 7 | 8 | return { 9 | ...objWithoutKey, 10 | [newKey]: obj[keyToRename], 11 | }; 12 | }; 13 | 14 | export const renameKeysInObject = ( 15 | obj: { [key: string]: any }, 16 | ids: string[] = [], 17 | keyToRename: string, 18 | newKey: string 19 | ) => 20 | ids.reduce((acc, itemId) => { 21 | acc[itemId] = renameKeyInObject(obj[itemId], keyToRename, newKey); 22 | 23 | return acc; 24 | }, {}); 25 | -------------------------------------------------------------------------------- /backend/src/helpers/string.helper.ts: -------------------------------------------------------------------------------- 1 | import * as removeAccents from 'remove-accents'; 2 | 3 | export function cleanIngredientName(ingredientName: string): string { 4 | return ingredientName.trim().toLowerCase(); 5 | } 6 | 7 | export function cleanIngredientNameAsId(ingredientName: string): string { 8 | return removeAccents(ingredientName) 9 | .trim() 10 | .replace(/[-\/ ]+/g, '-') 11 | .toLowerCase(); 12 | } 13 | 14 | export function cleanPizzaName(name: string): string { 15 | return removeAccents(name) 16 | .trim() 17 | .replace(/[-\/ ]+/g, '-') 18 | .toLowerCase(); 19 | } 20 | 21 | /** 22 | * pizzas images' path contains the whole path from / on the system 23 | * the frontend is not aware of that (hopefully), and thus we should remove 24 | * everything before assets/img/pizzas-providers... 25 | */ 26 | export function removeLocalPath(path: string): string { 27 | const re = /.*\/(assets\/img\/pizzas-providers\/.*)/; 28 | 29 | if (re.test(path)) { 30 | return re.exec(path)[1]; 31 | } 32 | 33 | return path; 34 | } 35 | -------------------------------------------------------------------------------- /backend/src/server.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import * as bodyParser from 'body-parser'; 3 | import * as cors from 'cors'; 4 | 5 | import { ApplicationModule } from './app.module'; 6 | 7 | async function bootstrap() { 8 | const app = await NestFactory.create(ApplicationModule); 9 | 10 | app.use(bodyParser.json()); 11 | 12 | app.use( 13 | cors({ 14 | credentials: false, 15 | }) 16 | ); 17 | 18 | await app.listen(3000); 19 | } 20 | 21 | bootstrap(); 22 | -------------------------------------------------------------------------------- /backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": false, 5 | "noImplicitAny": false, 6 | "removeComments": true, 7 | "noLib": false, 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "target": "es6", 11 | "sourceMap": true, 12 | "allowJs": true, 13 | "outDir": "./dist" 14 | }, 15 | "include": ["src/**/*.ts"], 16 | "exclude": ["node_modules", "**/*.spec.ts"] 17 | } 18 | -------------------------------------------------------------------------------- /backend/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": ["tslint:recommended", "tslint-config-prettier"], 4 | "jsRules": { 5 | "no-unused-expression": true 6 | }, 7 | "rules": { 8 | "eofline": false, 9 | "quotemark": [true, "single"], 10 | "member-access": [false], 11 | "max-line-length": [150], 12 | "member-ordering": [false], 13 | "curly": false, 14 | "interface-name": [false], 15 | "array-type": [false], 16 | "no-empty-interface": false, 17 | "no-empty": false, 18 | "arrow-parens": false, 19 | "object-literal-sort-keys": false, 20 | "no-unused-expression": false, 21 | "max-classes-per-file": [false], 22 | "variable-name": [false], 23 | "one-line": [false], 24 | "one-variable-per-declaration": [false], 25 | "no-unused-variable": true, 26 | "ordered-imports": true 27 | }, 28 | "rulesDirectory": [] 29 | } 30 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | pizza-sync-api: 5 | build: 6 | context: . 7 | dockerfile: Dockerfile.api 8 | image: pizza-sync-api 9 | hostname: pizza-sync-api 10 | stdin_open: true 11 | tty: true 12 | 13 | pizza-sync-nginx: 14 | build: 15 | context: . 16 | dockerfile: Dockerfile.nginx 17 | image: pizza-sync-nginx 18 | ports: 19 | - 3000:80 20 | -------------------------------------------------------------------------------- /frontend/angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "pizza-sync": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "architect": { 11 | "build": { 12 | "builder": "@angular-devkit/build-angular:browser", 13 | "options": { 14 | "outputPath": "dist", 15 | "index": "src/index.html", 16 | "main": "src/main.ts", 17 | "tsConfig": "src/tsconfig.app.json", 18 | "polyfills": "src/polyfills.ts", 19 | "assets": [ 20 | "src/assets" 21 | ], 22 | "styles": [ 23 | "node_modules/material-design-icons-iconfont/dist/material-design-icons.css", 24 | "src/styles.scss" 25 | ], 26 | "scripts": [] 27 | }, 28 | "configurations": { 29 | "production": { 30 | "optimization": true, 31 | "outputHashing": "all", 32 | "sourceMap": false, 33 | "extractCss": true, 34 | "namedChunks": false, 35 | "aot": true, 36 | "extractLicenses": true, 37 | "vendorChunk": false, 38 | "buildOptimizer": true, 39 | "fileReplacements": [ 40 | { 41 | "replace": "src/environments/environment.ts", 42 | "with": "src/environments/environment.prod.ts" 43 | } 44 | ] 45 | } 46 | } 47 | }, 48 | "serve": { 49 | "builder": "@angular-devkit/build-angular:dev-server", 50 | "options": { 51 | "browserTarget": "pizza-sync:build" 52 | }, 53 | "configurations": { 54 | "production": { 55 | "browserTarget": "pizza-sync:build:production" 56 | } 57 | } 58 | }, 59 | "extract-i18n": { 60 | "builder": "@angular-devkit/build-angular:extract-i18n", 61 | "options": { 62 | "browserTarget": "pizza-sync:build" 63 | } 64 | }, 65 | "lint": { 66 | "builder": "@angular-devkit/build-angular:tslint", 67 | "options": { 68 | "tsConfig": [ 69 | "src/tsconfig.app.json", 70 | "src/tsconfig.spec.json" 71 | ], 72 | "exclude": [ 73 | "**/node_modules/**" 74 | ] 75 | } 76 | } 77 | } 78 | }, 79 | "pizza-sync-e2e": { 80 | "root": "e2e", 81 | "sourceRoot": "e2e", 82 | "projectType": "application", 83 | "architect": { 84 | "e2e": { 85 | "builder": "@angular-devkit/build-angular:protractor", 86 | "options": { 87 | "protractorConfig": "./protractor.conf.js", 88 | "devServerTarget": "pizza-sync:serve" 89 | } 90 | }, 91 | "lint": { 92 | "builder": "@angular-devkit/build-angular:tslint", 93 | "options": { 94 | "tsConfig": [ 95 | "e2e/tsconfig.e2e.json" 96 | ], 97 | "exclude": [ 98 | "**/node_modules/**" 99 | ] 100 | } 101 | } 102 | } 103 | } 104 | }, 105 | "defaultProject": "pizza-sync", 106 | "schematics": { 107 | "@schematics/angular:component": { 108 | "prefix": "app", 109 | "styleext": "scss" 110 | }, 111 | "@schematics/angular:directive": { 112 | "prefix": "app" 113 | } 114 | } 115 | } -------------------------------------------------------------------------------- /frontend/e2e/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { PizzaSyncPage } from './app.po'; 2 | 3 | describe('pizza-sync App', () => { 4 | let page: PizzaSyncPage; 5 | 6 | beforeEach(() => { 7 | page = new PizzaSyncPage(); 8 | }); 9 | 10 | it('should display message saying app works', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toEqual('app works!'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /frontend/e2e/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, element, by } from 'protractor'; 2 | 3 | export class PizzaSyncPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getParagraphText() { 9 | return element(by.css('app-root h1')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /frontend/e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/e2e", 5 | "baseUrl": "./", 6 | "module": "commonjs", 7 | "target": "es5", 8 | "types": [ 9 | "jasmine", 10 | "jasminewd2", 11 | "node" 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /frontend/e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.e2e.json" 3 | } 4 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pizza-sync", 3 | "version": "2.0.0", 4 | "license": "AGPLv3", 5 | "private": true, 6 | "scripts": { 7 | "ng": "ng", 8 | "yarn-or-npm": "yarn-or-npm", 9 | "start": "yarn-or-npm ng serve", 10 | "test": "yarn-or-npm ng test pizza-sync", 11 | "test:ci": "yarn-or-npm run test pizza-sync --single-run --progress=false", 12 | "e2e": "yarn-or-npm ng e2e", 13 | "e2e:ci": "yarn-or-npm ng e2e --prod --progress=false", 14 | "build:prod": "yarn-or-npm ng build --prod", 15 | "lint:check": "yarn-or-npm ng lint pizza-sync", 16 | "lint:fix": "yarn-or-npm run lint:check --fix", 17 | "check": "yarn-or-npm run lint:check && yarn-or-npm run prettier:check", 18 | "check:fix": "yarn-or-npm run lint:fix; yarn-or-npm run prettier:fix", 19 | "prettier:base": "yarn-or-npm run prettier --single-quote --trailing-comma es5", 20 | "prettier:base-files": "yarn-or-npm run prettier:base \"./{e2e,src}/**/*.{scss,ts}\"", 21 | "prettier:fix": "yarn-or-npm run prettier:base-files --write", 22 | "prettier:check": "yarn-or-npm run prettier:base-files -l", 23 | "precommit": "lint-staged", 24 | "prepush": "yarn-or-npm run lint:check", 25 | "postbuild": "gulp compress", 26 | "postinstall": "replace \"fs = require\\(\\'fs\\'\\);\" \"\" ./node_modules/csv-file-creator/index.js" 27 | }, 28 | "lint-staged": { 29 | "linters": { 30 | "*.{ts,scss}": [ 31 | "yarn-or-npm run prettier:base -l" 32 | ] 33 | } 34 | }, 35 | "dependencies": { 36 | "@angular/animations": "6.1.1", 37 | "@angular/cdk": "6.4.2", 38 | "@angular/common": "6.1.1", 39 | "@angular/compiler": "6.1.1", 40 | "@angular/core": "6.1.1", 41 | "@angular/flex-layout": "6.0.0-beta.17", 42 | "@angular/forms": "6.1.1", 43 | "@angular/http": "6.1.1", 44 | "@angular/material": "6.4.2", 45 | "@angular/platform-browser": "6.1.1", 46 | "@angular/platform-browser-dynamic": "6.1.1", 47 | "@angular/router": "6.1.1", 48 | "@ngrx/effects": "6.1.0", 49 | "@ngrx/entity": "6.1.0", 50 | "@ngrx/store": "6.1.0", 51 | "@ngrx/store-devtools": "6.1.0", 52 | "@ngx-translate/core": "10.0.2", 53 | "@ngx-translate/http-loader": "3.0.1", 54 | "buffer": "5.2.0", 55 | "core-js": "2.5.7", 56 | "countdown": "2.6.0", 57 | "csv-file-creator": "1.0.7", 58 | "hammerjs": "2.0.8", 59 | "material-design-icons-iconfont": "3.0.3", 60 | "ng2-webstorage": "2.0.0", 61 | "ngrx-store-freeze": "0.2.4", 62 | "redux": "4.0.0", 63 | "redux-batched-actions": "0.3.0", 64 | "remove-accents": "0.4.2", 65 | "rxjs": "6.2.2", 66 | "rxjs-compat": "6.2.2", 67 | "socket.io-client": "2.1.1", 68 | "zone.js": "0.8.26" 69 | }, 70 | "devDependencies": { 71 | "@angular-devkit/build-angular": "0.7.2", 72 | "@angular/cli": "6.1.2", 73 | "@angular/compiler-cli": "6.1.1", 74 | "@types/jasmine": "2.8.8", 75 | "@types/jest": "23.3.1", 76 | "@types/node": "10.5.6", 77 | "@types/socket.io-client": "1.4.32", 78 | "codelyzer": "4.4.2", 79 | "gulp": "3.9.1", 80 | "gulp-gzip": "1.4.2", 81 | "husky": "0.14.3", 82 | "jasmine-core": "2.8.0", 83 | "jasmine-spec-reporter": "4.2.1", 84 | "jest": "22.0.4", 85 | "jest-preset-angular": "5.0.0", 86 | "lint-staged": "6.0.0", 87 | "prettier": "1.9.2", 88 | "protractor": "5.4.0", 89 | "replace": "1.0.0", 90 | "ts-node": "7.0.0", 91 | "tslint": "5.11.0", 92 | "typescript": "2.9.2", 93 | "yarn-or-npm": "2.0.4" 94 | }, 95 | "jest": { 96 | "preset": "jest-preset-angular", 97 | "setupTestFrameworkScriptFile": "/src/setup-jest.ts", 98 | "collectCoverageFrom": [ 99 | "src/**/*.ts", 100 | "!src/**/*.spec.ts", 101 | "!src/environment/**", 102 | "!src/**/*.mock.ts", 103 | "!src/mocks/**", 104 | "!src/**/*.d.ts" 105 | ], 106 | "coveragePathIgnorePatterns": [ 107 | "/src/environment", 108 | "/src/mocks" 109 | ] 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /frontend/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: [ 9 | './e2e/**/*.e2e-spec.ts' 10 | ], 11 | capabilities: { 12 | 'browserName': 'chrome' 13 | }, 14 | directConnect: true, 15 | baseUrl: 'http://localhost:4200/', 16 | framework: 'jasmine', 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000, 20 | print: function() {} 21 | }, 22 | onPrepare() { 23 | require('ts-node').register({ 24 | project: 'e2e/tsconfig.e2e.json' 25 | }); 26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /frontend/src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | 4 | // if you don't want to lazy load the features module, 5 | // simply put the loadFeaturesModule as value of loadChildren 6 | // import { FeaturesModule } from 'app/features/features.module'; 7 | 8 | // export function loadFeaturesModule() { 9 | // return FeaturesModule; 10 | // } 11 | 12 | const routes: Routes = [ 13 | { 14 | path: '', 15 | loadChildren: 'app/features/features.module#FeaturesModule', 16 | }, 17 | { 18 | path: '**', 19 | redirectTo: '/', 20 | }, 21 | ]; 22 | 23 | @NgModule({ 24 | imports: [RouterModule.forRoot(routes)], 25 | }) 26 | export class AppRoutingModule {} 27 | -------------------------------------------------------------------------------- /frontend/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /frontend/src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | .logo-toolbar { 2 | max-height: 45px; 3 | margin-right: 15px; 4 | } 5 | 6 | mat-sidenav { 7 | width: 320px; 8 | } 9 | -------------------------------------------------------------------------------- /frontend/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Inject, OnDestroy, OnInit } from '@angular/core'; 2 | import { MatIconRegistry } from '@angular/material'; 3 | import { DomSanitizer } from '@angular/platform-browser'; 4 | import { Store } from '@ngrx/store'; 5 | import { TranslateService } from '@ngx-translate/core'; 6 | import { Subject } from 'rxjs'; 7 | import { filter, takeUntil, tap } from 'rxjs/operators'; 8 | 9 | import { LANGUAGES } from 'app/core/injection-tokens'; 10 | import { IStore } from 'app/shared/interfaces/store.interface'; 11 | import * as UiActions from 'app/shared/states/ui/ui.actions'; 12 | 13 | @Component({ 14 | selector: 'app-root', 15 | templateUrl: './app.component.html', 16 | styleUrls: ['./app.component.scss'], 17 | }) 18 | export class AppComponent implements OnInit, OnDestroy { 19 | private onDestroy$ = new Subject(); 20 | 21 | constructor( 22 | private translate: TranslateService, 23 | @Inject(LANGUAGES) public languages, 24 | private store$: Store, 25 | private matIconRegistry: MatIconRegistry, 26 | private sanitizer: DomSanitizer, 27 | ) {} 28 | 29 | ngOnInit() { 30 | // default and fallback language 31 | // if a translation isn't found in a language, 32 | // it'll try to get it on the default language 33 | // by default here, we take the first of the array 34 | this.translate.setDefaultLang(this.languages[0]); 35 | this.store$.dispatch( 36 | new UiActions.SetLanguage({ language: this.languages[0] }) 37 | ); 38 | 39 | // when the language changes in store, 40 | // change it in translate provider 41 | this.store$ 42 | .select(state => state.ui.language) 43 | .pipe( 44 | takeUntil(this.onDestroy$), 45 | filter(language => !!language), 46 | tap(language => this.translate.use(language)) 47 | ) 48 | .subscribe(); 49 | 50 | const safeLogo = this.sanitizer.bypassSecurityTrustResourceUrl( 51 | '/assets/img/github-logo.svg' 52 | ); 53 | this.matIconRegistry.addSvgIcon('github', safeLogo); 54 | } 55 | 56 | ngOnDestroy() { 57 | this.onDestroy$.next(); 58 | this.onDestroy$.complete(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /frontend/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | 4 | import { AppRoutingModule } from 'app/app-routing.module'; 5 | import { AppComponent } from 'app/app.component'; 6 | import { CoreModule } from 'app/core/core.module'; 7 | import { SharedModule } from 'app/shared/shared.module'; 8 | 9 | /** 10 | * this module should be kept as small as possible and shouldn't be modified 11 | * if you feel like you want to add something here, you should take a look into SharedModule or CoreModule 12 | */ 13 | @NgModule({ 14 | declarations: [AppComponent], 15 | imports: [BrowserModule, CoreModule, SharedModule, AppRoutingModule], 16 | bootstrap: [AppComponent], 17 | }) 18 | export class AppModule {} 19 | -------------------------------------------------------------------------------- /frontend/src/app/core/core.module.ts: -------------------------------------------------------------------------------- 1 | import { 2 | HashLocationStrategy, 3 | LocationStrategy, 4 | PathLocationStrategy, 5 | } from '@angular/common'; 6 | import { HttpClient } from '@angular/common/http'; 7 | import { NgModule } from '@angular/core'; 8 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 9 | import { EffectsModule } from '@ngrx/effects'; 10 | import { StoreModule } from '@ngrx/store'; 11 | import { StoreDevtoolsModule } from '@ngrx/store-devtools'; 12 | import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; 13 | import { Ng2Webstorage } from 'ng2-webstorage'; 14 | // import hammerjs only if needed : 15 | // From https://material.angular.io/guide/getting-started#additional-setup-for-gestures 16 | // Some components (md-slide-toggle, md-slider, mdTooltip) rely on HammerJS for gestures 17 | // In order to get the full feature-set of these components, HammerJS must be loaded into the application 18 | // import 'hammerjs'; 19 | 20 | import { LANGUAGES } from 'app/core/injection-tokens'; 21 | import { PizzasEffects } from 'app/features/pizzas/pizzas.effects'; 22 | import { PizzasService } from 'app/features/pizzas/pizzas.service'; 23 | import { createTranslateLoader } from 'app/shared/helpers/aot.helper'; 24 | import { CountdownService } from 'app/shared/services/countdown.service'; 25 | import { OrdersService } from 'app/shared/services/orders.service'; 26 | import { UsersService } from 'app/shared/services/users.service'; 27 | import { WebsocketService } from 'app/shared/services/websocket.service'; 28 | import { OrdersEffects } from 'app/shared/states/orders/orders.effects'; 29 | import { metaReducers, reducers } from 'app/shared/states/root.reducer'; 30 | import { UsersEffects } from 'app/shared/states/users/users.effects'; 31 | import { environment } from 'environments/environment'; 32 | 33 | /** 34 | * this module will be imported only once, in AppModule and shouldn't be imported from anywhere else 35 | * you can define here the modules and providers that you only want to import once 36 | */ 37 | @NgModule({ 38 | imports: [ 39 | // START : Do not add your libs here 40 | BrowserAnimationsModule, 41 | // TODO : lazy loaded reducers? 42 | StoreModule.forRoot(reducers, { metaReducers }), 43 | // TODO it's not clear if the module is enabled when the extension is not present... 44 | StoreDevtoolsModule.instrument({ maxAge: 50 }), 45 | TranslateModule.forRoot({ 46 | loader: { 47 | provide: TranslateLoader, 48 | useFactory: createTranslateLoader, 49 | deps: [HttpClient], 50 | }, 51 | }), 52 | // END : Do not add your libs here 53 | 54 | Ng2Webstorage, 55 | 56 | // TODO batched actions are not taken into accounts by effects 57 | EffectsModule.forRoot([PizzasEffects, OrdersEffects, UsersEffects]), 58 | ], 59 | providers: [ 60 | PizzasService, 61 | OrdersService, 62 | UsersService, 63 | WebsocketService, 64 | CountdownService, 65 | { 66 | provide: LANGUAGES, 67 | // order matters : The first one will be used by default 68 | useValue: ['en', 'fr'], 69 | }, 70 | // use hash location strategy or not based on env 71 | { 72 | provide: LocationStrategy, 73 | useClass: environment.hashLocationStrategy 74 | ? HashLocationStrategy 75 | : PathLocationStrategy, 76 | }, 77 | ], 78 | }) 79 | export class CoreModule {} 80 | -------------------------------------------------------------------------------- /frontend/src/app/core/injection-tokens.ts: -------------------------------------------------------------------------------- 1 | import { InjectionToken } from '@angular/core'; 2 | 3 | export const LANGUAGES: InjectionToken = new InjectionToken( 4 | 'available languages' 5 | ); 6 | -------------------------------------------------------------------------------- /frontend/src/app/features/countdown/countdown.component.html: -------------------------------------------------------------------------------- 1 | {{ countdown }} 2 | -------------------------------------------------------------------------------- /frontend/src/app/features/countdown/countdown.component.scss: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /frontend/src/app/features/countdown/countdown.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | EventEmitter, 4 | Input, 5 | OnChanges, 6 | OnDestroy, 7 | OnInit, 8 | Output, 9 | } from '@angular/core'; 10 | import { Subscription } from 'rxjs'; 11 | import { tap } from 'rxjs/operators'; 12 | 13 | import { CountdownService } from 'app/shared/services/countdown.service'; 14 | 15 | @Component({ 16 | selector: 'app-countdown', 17 | templateUrl: './countdown.component.html', 18 | styleUrls: ['./countdown.component.scss'], 19 | }) 20 | export class CountdownComponent implements OnInit, OnDestroy, OnChanges { 21 | @Input() hour: number; 22 | @Input() minute: number; 23 | @Output() onCountdownStart: EventEmitter = new EventEmitter(); 24 | @Output() onCountdownEnd: EventEmitter = new EventEmitter(); 25 | 26 | public countdown: string; 27 | private countdownSub: Subscription; 28 | 29 | constructor(private countdownService: CountdownService) {} 30 | 31 | ngOnInit() {} 32 | 33 | ngOnChanges() { 34 | if (this.countdownSub) { 35 | this.countdownSub.unsubscribe(); 36 | } 37 | 38 | let hasEmittedCountdownStart = false; 39 | 40 | this.countdownSub = this.countdownService 41 | .getCountdownTo(this.hour, this.minute) 42 | .pipe( 43 | tap(countdown => { 44 | if (!hasEmittedCountdownStart) { 45 | hasEmittedCountdownStart = true; 46 | this.onCountdownStart.next(); 47 | } 48 | 49 | this.countdown = countdown; 50 | }) 51 | ) 52 | .subscribe( 53 | () => {}, 54 | () => {}, 55 | () => { 56 | this.countdown = ''; 57 | this.onCountdownEnd.next(); 58 | } 59 | ); 60 | } 61 | 62 | ngOnDestroy() { 63 | this.onCountdownEnd.next(); 64 | this.countdownSub.unsubscribe(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /frontend/src/app/features/features-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | 4 | import { FeaturesComponent } from 'app/features/features.component'; 5 | 6 | const routes: Routes = [ 7 | { 8 | path: '', 9 | component: FeaturesComponent, 10 | }, 11 | ]; 12 | 13 | @NgModule({ 14 | imports: [RouterModule.forChild(routes)], 15 | }) 16 | export class FeaturesRoutingModule {} 17 | -------------------------------------------------------------------------------- /frontend/src/app/features/features.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 6 | 7 | 8 | 9 | 17 | 18 | 22 | 23 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 47 | 48 | 49 |
50 |
51 |
Selected ingredient{{ ingredientsSelected.length > 1 ? 's': '' }}:
52 | 53 | 54 | 59 | {{ ingredient.name }} 60 | 61 | 62 |
63 |
64 | 65 | 66 |
67 | 68 | 71 | 72 | 75 | 76 | 77 |
78 | -------------------------------------------------------------------------------- /frontend/src/app/features/features.component.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/shared/_colors'; 2 | 3 | mat-sidenav { 4 | width: 300px; 5 | } 6 | 7 | mat-toolbar.header { 8 | .logo-toolbar { 9 | height: 40px; 10 | margin-left: 10px; 11 | margin-right: 10px; 12 | } 13 | 14 | button { 15 | margin-right: 5px; 16 | } 17 | } 18 | 19 | .order-summary { 20 | color: white; 21 | position: absolute; 22 | right: 20px; 23 | bottom: 80px; 24 | } 25 | 26 | .csv-download { 27 | color: white; 28 | position: absolute; 29 | right: 20px; 30 | bottom: 136px; 31 | } 32 | 33 | .csv-download, 34 | .order-summary { 35 | z-index: 1; 36 | } 37 | 38 | .nb-filters-ingredients { 39 | position: absolute; 40 | margin-left: -7px; 41 | margin-top: 3px; 42 | font-size: 12px; 43 | color: mat-color($accent); 44 | } 45 | 46 | .ingredients-selected { 47 | margin-left: 16px; 48 | margin-right: 16px; 49 | padding-top: 10px; 50 | padding-bottom: 3px; 51 | border-bottom: 1px solid mat-color($accent); 52 | 53 | &.border-top { 54 | border-top: 1px solid mat-color($accent); 55 | } 56 | 57 | .title { 58 | margin-bottom: 7px; 59 | font-weight: 600; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /frontend/src/app/features/features.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | import { CountdownComponent } from 'app/features/countdown/countdown.component'; 4 | import { FeaturesRoutingModule } from 'app/features/features-routing.module'; 5 | import { FeaturesComponent } from 'app/features/features.component'; 6 | import { IdentificationDialogComponent } from 'app/features/identification-dialog/identification-dialog.component'; 7 | import { OrderSummaryDialogComponent } from 'app/features/order-summary-dialog/order-summary-dialog.component'; 8 | import { OrdersModule } from 'app/features/orders/orders.module'; 9 | import { PizzasModule } from 'app/features/pizzas/pizzas.module'; 10 | import { SharedModule } from 'app/shared/shared.module'; 11 | import { FilterIngredientsComponent } from './filter-ingredients/filter-ingredients.component'; 12 | import { FooterComponent } from './footer/footer.component'; 13 | import { PizzasSearchComponent } from './pizzas-search/pizzas-search.component'; 14 | 15 | @NgModule({ 16 | imports: [SharedModule, FeaturesRoutingModule, PizzasModule, OrdersModule], 17 | declarations: [ 18 | FeaturesComponent, 19 | IdentificationDialogComponent, 20 | CountdownComponent, 21 | OrderSummaryDialogComponent, 22 | PizzasSearchComponent, 23 | FilterIngredientsComponent, 24 | FooterComponent, 25 | ], 26 | entryComponents: [IdentificationDialogComponent, OrderSummaryDialogComponent], 27 | }) 28 | export class FeaturesModule {} 29 | -------------------------------------------------------------------------------- /frontend/src/app/features/filter-ingredients/filter-ingredients.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 10 | {{ ingredient.name }} 11 | 12 | 13 |
14 | -------------------------------------------------------------------------------- /frontend/src/app/features/filter-ingredients/filter-ingredients.component.scss: -------------------------------------------------------------------------------- 1 | .filter-ingredients { 2 | margin: 16px; 3 | } 4 | 5 | // since beta 12, chips have margin... 6 | .mat-chip { 7 | margin: 0 !important; 8 | margin-right: 3px !important; 9 | margin-bottom: 7px !important; 10 | } 11 | -------------------------------------------------------------------------------- /frontend/src/app/features/filter-ingredients/filter-ingredients.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ChangeDetectionStrategy, 3 | Component, 4 | EventEmitter, 5 | Input, 6 | Output, 7 | } from '@angular/core'; 8 | 9 | import { IIngredientCommon } from 'app/shared/states/ingredients/ingredients.interface'; 10 | 11 | @Component({ 12 | selector: 'app-filter-ingredients', 13 | templateUrl: './filter-ingredients.component.html', 14 | styleUrls: ['./filter-ingredients.component.scss'], 15 | changeDetection: ChangeDetectionStrategy.OnPush, 16 | }) 17 | export class FilterIngredientsComponent { 18 | @Input() ingredients: IIngredientCommon[]; 19 | 20 | @Output() onIngredientSelected = new EventEmitter(); 21 | @Output() onIngredientUnselected = new EventEmitter(); 22 | 23 | handleClick(ingredient: IIngredientCommon) { 24 | // disabled property doesn't seem to work on an `md-chip` 25 | // so this is a temporary fix 26 | if (!ingredient.isSelectable) { 27 | return; 28 | } 29 | 30 | if (ingredient.isSelected) { 31 | this.onIngredientUnselected.emit(ingredient.id); 32 | } else { 33 | this.onIngredientSelected.emit(ingredient.id); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /frontend/src/app/features/footer/footer.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | {{ pizzeria.name }} 5 | 6 | | 7 | 8 | phone {{ pizzeria.phone }} 9 | 10 | | 11 | 12 | 13 | public 14 | 15 |
16 |
17 |
18 | -------------------------------------------------------------------------------- /frontend/src/app/features/footer/footer.component.scss: -------------------------------------------------------------------------------- 1 | mat-toolbar.footer { 2 | .pizzeria-name { 3 | font-weight: bold; 4 | } 5 | 6 | a:link, 7 | a:visited, 8 | a:hover, 9 | a:focus, 10 | a:active { 11 | color: white; 12 | text-decoration: none; 13 | } 14 | 15 | .split { 16 | margin-left: 15px; 17 | margin-right: 15px; 18 | } 19 | 20 | mat-icon { 21 | margin-right: 5px; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /frontend/src/app/features/footer/footer.component.ts: -------------------------------------------------------------------------------- 1 | import { animate, style, transition, trigger } from '@angular/animations'; 2 | import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; 3 | import { IPizzeria } from '../../shared/states/ui/ui.interface'; 4 | 5 | @Component({ 6 | selector: 'app-footer', 7 | templateUrl: './footer.component.html', 8 | styleUrls: ['./footer.component.scss'], 9 | animations: [ 10 | trigger('enterAnimation', [ 11 | transition(':enter', [ 12 | style({ opacity: 0 }), 13 | animate('1000ms', style({ opacity: 1 })), 14 | ]), 15 | ]), 16 | ], 17 | changeDetection: ChangeDetectionStrategy.OnPush, 18 | }) 19 | export class FooterComponent { 20 | @Input() pizzeria: IPizzeria; 21 | } 22 | -------------------------------------------------------------------------------- /frontend/src/app/features/identification-dialog/identification-dialog.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 13 | 14 | 15 | 16 | 17 | 18 | for picture, otherwise any username 19 | 20 | 21 | 41 |
42 | -------------------------------------------------------------------------------- /frontend/src/app/features/identification-dialog/identification-dialog.component.scss: -------------------------------------------------------------------------------- 1 | mat-form-field { 2 | width: 100%; 3 | 4 | .mat-icon { 5 | width: 20px; 6 | margin-left: 8px; 7 | margin-right: 8px; 8 | } 9 | } 10 | 11 | button { 12 | margin-top: 15px; 13 | 14 | mat-progress-spinner { 15 | display: inline-block; 16 | margin-left: 8px; 17 | } 18 | } 19 | 20 | mat-hint { 21 | color: grey; 22 | } 23 | 24 | button { 25 | margin-top: 10px; 26 | } 27 | -------------------------------------------------------------------------------- /frontend/src/app/features/identification-dialog/identification-dialog.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { FormBuilder, FormGroup, Validators } from '@angular/forms'; 3 | import { Store } from '@ngrx/store'; 4 | import { LocalStorageService } from 'ng2-webstorage'; 5 | import { Observable } from 'rxjs'; 6 | 7 | import { IStore } from 'app/shared/interfaces/store.interface'; 8 | import * as UsersActions from 'app/shared/states/users/users.actions'; 9 | 10 | @Component({ 11 | selector: 'app-identification-dialog', 12 | templateUrl: './identification-dialog.component.html', 13 | styleUrls: ['./identification-dialog.component.scss'], 14 | }) 15 | export class IdentificationDialogComponent implements OnInit { 16 | public identificationForm: FormGroup; 17 | public isIdentifying$: Observable; 18 | 19 | constructor( 20 | private store$: Store, 21 | private fb: FormBuilder, 22 | private storage: LocalStorageService 23 | ) {} 24 | 25 | ngOnInit() { 26 | this.isIdentifying$ = this.store$.select( 27 | state => state.users.isIdentifying 28 | ); 29 | 30 | const username = this.storage.retrieve('username') || ''; 31 | 32 | this.identificationForm = this.fb.group({ 33 | username: [username, Validators.required], 34 | }); 35 | } 36 | 37 | onSubmit({ value }: FormGroup) { 38 | this.store$.dispatch(new UsersActions.Identification(value.username)); 39 | 40 | this.identificationForm.controls['username'].disable(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /frontend/src/app/features/order-summary-dialog/order-summary-dialog.component.html: -------------------------------------------------------------------------------- 1 | 2 |

3 | Easy order view 4 | 5 |
6 | : 7 | 1 pizza only 8 | {{ nbOfPizzas }} pizzas 9 | 10 |
11 |

12 | 13 | 14 |

{{ pizza.pizzaName }}

15 |

{{ howManyPerSize(pizza) }}

16 |
17 |
18 | -------------------------------------------------------------------------------- /frontend/src/app/features/order-summary-dialog/order-summary-dialog.component.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/shared/_colors'; 2 | 3 | mat-list { 4 | min-width: 300px; 5 | max-height: 400px; 6 | } 7 | 8 | .pizza-name { 9 | color: mat-color($primary); 10 | font-weight: bold; 11 | } 12 | 13 | .how-many-per-size { 14 | color: grey; 15 | } 16 | -------------------------------------------------------------------------------- /frontend/src/app/features/order-summary-dialog/order-summary-dialog.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Store } from '@ngrx/store'; 3 | import { Observable } from 'rxjs'; 4 | 5 | import { IStore } from 'app/shared/interfaces/store.interface'; 6 | import { 7 | IOrdersSummary, 8 | IPizzaOrderSummary, 9 | } from 'app/shared/states/orders/orders.interface'; 10 | import { 11 | getOrderSummary, 12 | selectOrdersTotal, 13 | } from 'app/shared/states/orders/orders.selector'; 14 | 15 | @Component({ 16 | selector: 'app-order-summary-dialog', 17 | templateUrl: './order-summary-dialog.component.html', 18 | styleUrls: ['./order-summary-dialog.component.scss'], 19 | }) 20 | export class OrderSummaryDialogComponent implements OnInit { 21 | public orderSummary$: Observable; 22 | public nbOfPizzas$: Observable; 23 | 24 | constructor(private store$: Store) {} 25 | 26 | ngOnInit() { 27 | this.orderSummary$ = this.store$.select(getOrderSummary); 28 | this.nbOfPizzas$ = this.store$.select(selectOrdersTotal); 29 | } 30 | 31 | howManyPerSize(pizzaOrderSummary: IPizzaOrderSummary) { 32 | const nbS = pizzaOrderSummary.howManyPerSize['S'].howMany; 33 | const nbM = pizzaOrderSummary.howManyPerSize['M'].howMany; 34 | const nbL = pizzaOrderSummary.howManyPerSize['L'].howMany; 35 | const nbXl = pizzaOrderSummary.howManyPerSize['XL'].howMany; 36 | 37 | const howManyPerSizeStr = [ 38 | nbS > 0 ? `Small x${nbS}` : '', 39 | nbM > 0 ? `Medium x${nbM}` : '', 40 | nbL > 0 ? `Large x${nbL}` : '', 41 | nbXl > 0 ? `Extra large x${nbXl}` : '', 42 | ] 43 | .filter(s => s !== '') 44 | .join(', '); 45 | 46 | return howManyPerSizeStr; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /frontend/src/app/features/orders/orders.component.html: -------------------------------------------------------------------------------- 1 |

{{ (fullOrder$ | async).totalPrice | number:'1.2-2' }}€

2 | 3 | 4 |
5 | 6 |
7 |
8 | -------------------------------------------------------------------------------- /frontend/src/app/features/orders/orders.component.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/shared/_colors'; 2 | 3 | .sum { 4 | background-color: mat-color($accent); 5 | margin: 0; 6 | padding: 10px; 7 | text-align: center; 8 | } 9 | -------------------------------------------------------------------------------- /frontend/src/app/features/orders/orders.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnInit } from '@angular/core'; 2 | import { Store } from '@ngrx/store'; 3 | import { Observable } from 'rxjs'; 4 | import { filter } from 'rxjs/operators'; 5 | 6 | import { IStore } from 'app/shared/interfaces/store.interface'; 7 | import * as OrdersActions from 'app/shared/states/orders/orders.actions'; 8 | import { IUserWithPizzas } from 'app/shared/states/users/users.interface'; 9 | import { 10 | getCurrentUser, 11 | getFullOrder, 12 | } from 'app/shared/states/users/users.selector'; 13 | 14 | @Component({ 15 | selector: 'app-orders', 16 | templateUrl: './orders.component.html', 17 | styleUrls: ['./orders.component.scss'], 18 | }) 19 | export class OrdersComponent implements OnInit { 20 | @Input() locked: boolean; 21 | public fullOrder$: Observable<{ 22 | users: IUserWithPizzas[]; 23 | totalPrice: number; 24 | }> = this.store$.select(getFullOrder); 25 | 26 | public idCurrentUser$: Observable = this.store$ 27 | .select(getCurrentUser) 28 | .pipe(filter(idCurrentUser => !!idCurrentUser)); 29 | 30 | constructor(private store$: Store) {} 31 | 32 | ngOnInit() {} 33 | 34 | removeOrder(id: string) { 35 | this.store$.dispatch(new OrdersActions.RemoveOrder({ id })); 36 | } 37 | 38 | trackById(index, item) { 39 | return item.id; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /frontend/src/app/features/orders/orders.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | import { OrdersComponent } from 'app/features/orders/orders.component'; 4 | import { UserOrderComponent } from 'app/features/orders/user-order/user-order.component'; 5 | import { SharedModule } from 'app/shared/shared.module'; 6 | 7 | @NgModule({ 8 | imports: [SharedModule], 9 | declarations: [OrdersComponent, UserOrderComponent], 10 | exports: [OrdersComponent], 11 | }) 12 | export class OrdersModule {} 13 | -------------------------------------------------------------------------------- /frontend/src/app/features/orders/user-order/user-order.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |

6 | {{ user.username }} - {{ user.totalPrice | number:'1.2-2' }}€ 7 |

8 | 9 |

10 | {{ pizza.name }} ({{ pizza.size }}) 11 | X 12 |

13 |
14 | -------------------------------------------------------------------------------- /frontend/src/app/features/orders/user-order/user-order.component.scss: -------------------------------------------------------------------------------- 1 | .being-removed { 2 | color: grey; 3 | text-decoration: line-through; 4 | } 5 | 6 | .btn-remove-order { 7 | cursor: pointer; 8 | 9 | &:hover { 10 | color: red; 11 | } 12 | } 13 | 14 | .green-led { 15 | position: absolute; 16 | margin-left: 30px; 17 | margin-top: 12px; 18 | } 19 | -------------------------------------------------------------------------------- /frontend/src/app/features/orders/user-order/user-order.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ChangeDetectionStrategy, 3 | Component, 4 | EventEmitter, 5 | Input, 6 | Output, 7 | } from '@angular/core'; 8 | 9 | import { IUserWithPizzas } from 'app/shared/states/users/users.interface'; 10 | 11 | @Component({ 12 | selector: 'app-user-order', 13 | templateUrl: './user-order.component.html', 14 | styleUrls: ['./user-order.component.scss'], 15 | changeDetection: ChangeDetectionStrategy.OnPush, 16 | }) 17 | export class UserOrderComponent { 18 | @Input() user: IUserWithPizzas; 19 | @Input() idCurrentUser: string; 20 | @Input() locked: boolean; 21 | @Output() onRemoveOrder = new EventEmitter(); 22 | 23 | constructor() {} 24 | 25 | getThumbnail(user: IUserWithPizzas) { 26 | return user.thumbnail || 'assets/img/icon-person.png'; 27 | } 28 | 29 | trackById(index, item) { 30 | return item.id; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /frontend/src/app/features/pizzas-search/pizzas-search.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | search 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /frontend/src/app/features/pizzas-search/pizzas-search.component.scss: -------------------------------------------------------------------------------- 1 | .search-icon { 2 | font-size: 18px; 3 | } 4 | -------------------------------------------------------------------------------- /frontend/src/app/features/pizzas-search/pizzas-search.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | EventEmitter, 4 | HostListener, 5 | Input, 6 | OnChanges, 7 | OnDestroy, 8 | OnInit, 9 | Output, 10 | SimpleChanges, 11 | ViewChild, 12 | } from '@angular/core'; 13 | import { FormControl } from '@angular/forms'; 14 | import { MatInput } from '@angular/material'; 15 | import { Subject } from 'rxjs'; 16 | import { debounceTime, takeUntil, tap } from 'rxjs/operators'; 17 | 18 | @Component({ 19 | selector: 'app-pizzas-search', 20 | templateUrl: './pizzas-search.component.html', 21 | styleUrls: ['./pizzas-search.component.scss'], 22 | }) 23 | export class PizzasSearchComponent implements OnInit, OnChanges, OnDestroy { 24 | private onDestroy$ = new Subject(); 25 | 26 | @Input() searchedText?: string; 27 | @ViewChild(MatInput) searchInput: MatInput; 28 | @Output() onSearch = new EventEmitter(); 29 | public search = new FormControl(); 30 | 31 | @HostListener('document:keydown', ['$event']) 32 | handleKeyboardEvent(event: KeyboardEvent) { 33 | if (event.ctrlKey && event.key === 'f') { 34 | event.preventDefault(); 35 | this.searchInput.focus(); 36 | } 37 | } 38 | 39 | constructor() {} 40 | 41 | ngOnInit() { 42 | this.search.valueChanges 43 | .pipe( 44 | takeUntil(this.onDestroy$.asObservable()), 45 | debounceTime(200), 46 | tap(search => this.onSearch.emit(search)) 47 | ) 48 | .subscribe(); 49 | } 50 | 51 | ngOnChanges(changes: SimpleChanges) { 52 | if (changes['searchedText']) { 53 | this.search.patchValue(this.searchedText); 54 | } 55 | } 56 | 57 | ngOnDestroy() { 58 | this.onDestroy$.next(); 59 | this.onDestroy$.complete(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /frontend/src/app/features/pizzas/pizzas.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |

{{ pizzaCategorie.name }}

6 | 7 |
8 | 9 | 10 | 11 | 19 | {{ pizza.name }} 20 | 21 |

{{ pizza.ingredients | join:'name' }}

22 | 23 | 24 |
25 | 26 | 30 | 31 |
32 |
33 |
34 |
35 | 36 | 37 |
38 | 39 |

4local_pizza4

40 |

Pizza "{{ search }}" not found

41 |
42 |
43 |
44 |
45 |
46 | -------------------------------------------------------------------------------- /frontend/src/app/features/pizzas/pizzas.component.scss: -------------------------------------------------------------------------------- 1 | .pizzas { 2 | margin: 16px; 3 | } 4 | 5 | button[mat-mini-fab] { 6 | font-size: 12px; 7 | margin-right: 5px; 8 | margin-bottom: 10px; 9 | } 10 | 11 | mat-card { 12 | margin-bottom: 15px; 13 | 14 | mat-card-content { 15 | .pizza-img { 16 | cursor: pointer; 17 | } 18 | } 19 | 20 | mat-card-actions { 21 | padding-left: 16px; 22 | } 23 | } 24 | 25 | .pizza-not-found-404 { 26 | font-size: 25px; 27 | color: grey; 28 | text-align: center; 29 | } 30 | -------------------------------------------------------------------------------- /frontend/src/app/features/pizzas/pizzas.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Inject, Input, OnInit } from '@angular/core'; 2 | import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material'; 3 | import { Store } from '@ngrx/store'; 4 | import { Observable } from 'rxjs'; 5 | 6 | import { IStore } from 'app/shared/interfaces/store.interface'; 7 | import * as OrdersActions from 'app/shared/states/orders/orders.actions'; 8 | import { IPizzaCategoryWithPizzas } from 'app/shared/states/pizzas-categories/pizzas-categories.interface'; 9 | import * as PizzasActions from 'app/shared/states/pizzas/pizzas.actions'; 10 | import { 11 | IPizzaCommon, 12 | IPizzaWithIngredients, 13 | } from 'app/shared/states/pizzas/pizzas.interface'; 14 | import { tap } from 'rxjs/operators'; 15 | import { 16 | getCategoriesAndPizzas, 17 | getPizzaSearch, 18 | } from '../../shared/states/ui/ui.selector'; 19 | 20 | @Component({ 21 | selector: 'app-pizza-details-dialog', 22 | template: ` 23 |
28 | `, 29 | styles: [`div { background-size: cover; }`], 30 | }) 31 | export class PizzaDetailsDialogComponent { 32 | constructor( 33 | public dialogRef: MatDialogRef, 34 | @Inject(MAT_DIALOG_DATA) public data: { pizza: IPizzaWithIngredients } 35 | ) {} 36 | 37 | close() { 38 | this.dialogRef.close(); 39 | } 40 | } 41 | 42 | @Component({ 43 | selector: 'app-pizzas', 44 | templateUrl: './pizzas.component.html', 45 | styleUrls: ['./pizzas.component.scss'], 46 | }) 47 | export class PizzasComponent implements OnInit { 48 | @Input() locked: boolean; 49 | 50 | public pizzasCategories$: Observable< 51 | IPizzaCategoryWithPizzas[] 52 | > = this.store$.select(getCategoriesAndPizzas).pipe(tap(console.log)); 53 | 54 | public search$: Observable = this.store$.select(getPizzaSearch); 55 | 56 | constructor(private store$: Store, public dialog: MatDialog) {} 57 | 58 | ngOnInit() { 59 | this.store$.dispatch(new PizzasActions.LoadPizzas()); 60 | } 61 | 62 | addOrder(pizza: IPizzaCommon, priceIndex: number) { 63 | this.store$.dispatch( 64 | new OrdersActions.AddOrder({ 65 | pizzaId: pizza.id, 66 | priceIndex, 67 | }) 68 | ); 69 | } 70 | 71 | openPizzaDialog(pizza: IPizzaWithIngredients) { 72 | this.dialog.open(PizzaDetailsDialogComponent, { 73 | width: '550px', 74 | height: '550px', 75 | panelClass: 'dialog-with-transparent-background', 76 | data: { pizza }, 77 | }); 78 | } 79 | 80 | trackById(index, item) { 81 | return item.id; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /frontend/src/app/features/pizzas/pizzas.effects.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Actions, Effect } from '@ngrx/effects'; 3 | import { Action } from '@ngrx/store'; 4 | import { BatchAction, batchActions } from 'redux-batched-actions'; 5 | import { Observable } from 'rxjs'; 6 | import { map, switchMap } from 'rxjs/operators'; 7 | 8 | import { PizzasService } from 'app/features/pizzas/pizzas.service'; 9 | import * as IngredientsActions from 'app/shared/states/ingredients/ingredients.actions'; 10 | import * as OrdersActions from 'app/shared/states/orders/orders.actions'; 11 | import * as PizzasCategoriesActions from 'app/shared/states/pizzas-categories/pizzas-categories.actions'; 12 | import * as PizzasActions from 'app/shared/states/pizzas/pizzas.actions'; 13 | import * as UiActions from 'app/shared/states/ui/ui.actions'; 14 | import * as UsersActions from 'app/shared/states/users/users.actions'; 15 | 16 | @Injectable() 17 | export class PizzasEffects { 18 | constructor(private actions$: Actions, private pizzaService: PizzasService) {} 19 | 20 | // tslint:disable-next-line:member-ordering 21 | @Effect({ dispatch: true }) 22 | initialLoad$: Observable = this.actions$ 23 | .ofType(PizzasActions.LOAD_PIZZAS) 24 | .pipe( 25 | switchMap((action: Action) => 26 | this.pizzaService.getPizzas().pipe( 27 | map(res => { 28 | return batchActions([ 29 | new PizzasActions.LoadPizzasSuccess(res.pizzas), 30 | new PizzasCategoriesActions.LoadPizzasCategoriesSuccess( 31 | res.pizzasCategories 32 | ), 33 | new UsersActions.LoadUsersSuccess(res.users), 34 | new OrdersActions.LoadOrdersSuccess(res.orders), 35 | new UiActions.UpdatePizzeriaInformation(res.pizzeria), 36 | new IngredientsActions.LoadIngredientsSuccess(res.ingredients), 37 | ]); 38 | }) 39 | ) 40 | ) 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /frontend/src/app/features/pizzas/pizzas.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | import { 4 | PizzaDetailsDialogComponent, 5 | PizzasComponent, 6 | } from 'app/features/pizzas/pizzas.component'; 7 | import { SharedModule } from 'app/shared/shared.module'; 8 | 9 | @NgModule({ 10 | imports: [SharedModule], 11 | declarations: [PizzasComponent, PizzaDetailsDialogComponent], 12 | exports: [PizzasComponent], 13 | entryComponents: [PizzaDetailsDialogComponent], 14 | }) 15 | export class PizzasModule {} 16 | -------------------------------------------------------------------------------- /frontend/src/app/features/pizzas/pizzas.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { Observable } from 'rxjs'; 4 | 5 | import { IIngredientsTable } from 'app/shared/states/ingredients/ingredients.interface'; 6 | import { IOrdersTable } from 'app/shared/states/orders/orders.interface'; 7 | import { IPizzasCategoriesTable } from 'app/shared/states/pizzas-categories/pizzas-categories.interface'; 8 | import { IPizzasTable } from 'app/shared/states/pizzas/pizzas.interface'; 9 | import { IUsersTable } from 'app/shared/states/users/users.interface'; 10 | import { environment } from 'environments/environment'; 11 | 12 | @Injectable() 13 | export class PizzasService { 14 | constructor(private http: HttpClient) {} 15 | 16 | // TODO : As we're now calling initial-state 17 | // we should move this call into another service 18 | getPizzas(): Observable<{ 19 | pizzeria: { 20 | name: string; 21 | phone: string; 22 | url: string; 23 | }; 24 | pizzas: IPizzasTable; 25 | pizzasCategories: IPizzasCategoriesTable; 26 | users: IUsersTable; 27 | orders: IOrdersTable; 28 | ingredients: IIngredientsTable; 29 | }> { 30 | return this.http.get(`${environment.urlBackend}/initial-state`); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /frontend/src/app/shared/helpers/README.md: -------------------------------------------------------------------------------- 1 | # Helpers 2 | 3 | If you need to create utility function(s), that you can re-use all over the app, create a new file `[your-utility-group-name].helper.ts` or simply add it to an existing one. 4 | 5 | Every function should be exported for `AOT` support : 6 | 7 | ``` 8 | export function yourFunction() { 9 | // ... 10 | } 11 | ``` 12 | -------------------------------------------------------------------------------- /frontend/src/app/shared/helpers/aot.helper.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { TranslateHttpLoader } from '@ngx-translate/http-loader'; 3 | 4 | export function createTranslateLoader(http: HttpClient) { 5 | return new TranslateHttpLoader(http, './assets/i18n/', '.json'); 6 | } 7 | -------------------------------------------------------------------------------- /frontend/src/app/shared/helpers/date.helper.ts: -------------------------------------------------------------------------------- 1 | // adapted from this stackoverflow answer https://stackoverflow.com/a/3552493/2398593 2 | export function getCurrentDateFormatted() { 3 | const monthNames = [ 4 | 'January', 5 | 'February', 6 | 'March', 7 | 'April', 8 | 'May', 9 | 'June', 10 | 'July', 11 | 'August', 12 | 'September', 13 | 'October', 14 | 'November', 15 | 'December', 16 | ]; 17 | 18 | const date = new Date(); 19 | const day = date.getDate(); 20 | const monthIndex = date.getMonth(); 21 | const year = date.getFullYear(); 22 | 23 | return `${day}-${monthNames[monthIndex]}-${year}`; 24 | } 25 | -------------------------------------------------------------------------------- /frontend/src/app/shared/helpers/mock.helper.ts: -------------------------------------------------------------------------------- 1 | import { HttpErrorResponse } from '@angular/common/http'; 2 | import { Observable, of, throwError as _throw } from 'rxjs'; 3 | import { delay, dematerialize, materialize } from 'rxjs/operators'; 4 | 5 | import { environment } from 'environments/environment'; 6 | 7 | /** 8 | * this simulates the behaviour of Angular's http module: 9 | * if the status code is not a 2XX, it will return a failing Observable 10 | */ 11 | export function response(status: number): Observable { 12 | return responseBody(undefined, status); 13 | } 14 | 15 | /** 16 | * this simulates the behaviour of Angular's http module: 17 | * if the status code is not a 2XX, it will return a failing Observable 18 | */ 19 | export function responseBody( 20 | body: T, 21 | status = 200, 22 | error?: { code: number; message: string } 23 | ): Observable { 24 | if (status >= 200 && status < 300) { 25 | return of(body).pipe(delay(environment.httpDelay)); 26 | } else { 27 | return _throw(new HttpErrorResponse({ status, error })).pipe( 28 | materialize(), 29 | delay(environment.httpDelay), 30 | dematerialize() 31 | ); 32 | } 33 | } 34 | 35 | /** 36 | * the backend answers errors like this 37 | */ 38 | export function errorBackend( 39 | message: string, 40 | code: number 41 | ): Observable { 42 | return responseBody(undefined, code, { code, message }); 43 | } 44 | -------------------------------------------------------------------------------- /frontend/src/app/shared/helpers/time.helper.ts: -------------------------------------------------------------------------------- 1 | export function getFormatedTime(ts: { 2 | hours: number; 3 | minutes: number; 4 | seconds: number; 5 | }): string { 6 | // if hours, minutes or seconds are < 10 add a 0 before 7 | const hours = ('0' + ts.hours).toString().slice(-2); 8 | const minutes = ('0' + ts.minutes).toString().slice(-2); 9 | const seconds = ('0' + ts.seconds).toString().slice(-2); 10 | 11 | return `${hours}:${minutes}:${seconds}`; 12 | } 13 | -------------------------------------------------------------------------------- /frontend/src/app/shared/interfaces/README.md: -------------------------------------------------------------------------------- 1 | # Interfaces 2 | 3 | Create a file `your-interface-name.interface.ts` per (shared) interface needed. 4 | -------------------------------------------------------------------------------- /frontend/src/app/shared/interfaces/store.interface.ts: -------------------------------------------------------------------------------- 1 | import { IIngredientsTable } from 'app/shared/states/ingredients/ingredients.interface'; 2 | import { IOrdersTable } from 'app/shared/states/orders/orders.interface'; 3 | import { IPizzasCategoriesTable } from 'app/shared/states/pizzas-categories/pizzas-categories.interface'; 4 | import { IPizzasTable } from 'app/shared/states/pizzas/pizzas.interface'; 5 | import { IUi } from 'app/shared/states/ui/ui.interface'; 6 | import { IUsersTable } from 'app/shared/states/users/users.interface'; 7 | 8 | export interface IStore { 9 | ui: IUi; 10 | pizzas: IPizzasTable; 11 | pizzasCategories: IPizzasCategoriesTable; 12 | users: IUsersTable; 13 | orders: IOrdersTable; 14 | ingredients: IIngredientsTable; 15 | } 16 | -------------------------------------------------------------------------------- /frontend/src/app/shared/pipes/join.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | 3 | @Pipe({ 4 | name: 'join', 5 | }) 6 | export class JoinPipe implements PipeTransform { 7 | transform(arr: any[], nameToPluck: string): any { 8 | if (!arr) { 9 | return ''; 10 | } 11 | 12 | return arr 13 | .filter(x => typeof x !== 'undefined') 14 | .map(x => x[nameToPluck]) 15 | .join(', '); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /frontend/src/app/shared/services/countdown.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import * as countdown from 'countdown'; 3 | import { Observable, Observer } from 'rxjs'; 4 | 5 | import { getFormatedTime } from 'app/shared/helpers/time.helper'; 6 | 7 | @Injectable() 8 | export class CountdownService { 9 | constructor() {} 10 | 11 | getCountdownTo(hour: number, minute: number): Observable { 12 | const now = new Date(); 13 | let previousSecond; 14 | 15 | return Observable.create((observer: Observer) => { 16 | const timerId = countdown( 17 | new Date( 18 | now.getFullYear(), 19 | now.getMonth(), 20 | now.getDate(), 21 | hour, 22 | minute, 23 | 0 24 | ), 25 | (ts: { hours: number; minutes: number; seconds: number }) => { 26 | const time = getFormatedTime(ts); 27 | 28 | // tell listeners that a new value is available 29 | if (previousSecond && previousSecond > ts.seconds) { 30 | observer.next(time); 31 | } 32 | 33 | // stop the countdown if 0:0 or if already finished 34 | if ( 35 | (ts.hours === 0 && ts.minutes === 0 && ts.seconds === 0) || 36 | (previousSecond && previousSecond < ts.seconds) 37 | ) { 38 | window.clearInterval(timerId); 39 | observer.complete(); 40 | } 41 | 42 | previousSecond = ts.seconds; 43 | }, 44 | // tslint:disable-next-line:no-bitwise 45 | countdown.HOURS | countdown.MINUTES | countdown.SECONDS 46 | ); 47 | 48 | // this will be called if unsubscribe's called 49 | return () => { 50 | // if nobody's listening, clean the countdown 51 | window.clearInterval(timerId); 52 | }; 53 | }); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /frontend/src/app/shared/services/orders.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | 4 | @Injectable() 5 | export class OrdersService { 6 | // TODO : Search in code for 'TODO(SPLIT_SOCKET)' 7 | // constructor(private websocketService: WebsocketService) {} 8 | } 9 | -------------------------------------------------------------------------------- /frontend/src/app/shared/services/users.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | 4 | @Injectable() 5 | export class UsersService { 6 | // TODO : Search in code for 'TODO(SPLIT_SOCKET)' 7 | // constructor(private websocketService: WebsocketService) {} 8 | } 9 | -------------------------------------------------------------------------------- /frontend/src/app/shared/services/websocket.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Store } from '@ngrx/store'; 3 | import { LocalStorageService } from 'ng2-webstorage'; 4 | import * as io from 'socket.io-client'; 5 | 6 | import { IStore } from 'app/shared/interfaces/store.interface'; 7 | import * as OrdersActions from 'app/shared/states/orders/orders.actions'; 8 | import { INewOrder, IOrder } from 'app/shared/states/orders/orders.interface'; 9 | import * as UiActions from 'app/shared/states/ui/ui.actions'; 10 | import * as UsersActions from 'app/shared/states/users/users.actions'; 11 | import { environment } from 'environments/environment'; 12 | 13 | @Injectable() 14 | export class WebsocketService { 15 | private socket: SocketIOClient.Socket = io(environment.urlBackend); 16 | 17 | constructor( 18 | private store$: Store, 19 | private storage: LocalStorageService 20 | ) { 21 | // TODO(SPLIT_SOCKET) : Instead of handling every socket from here, we should handle them from separate services 22 | this.socket.on('CONNECT_USER_SUCCESS', user => { 23 | this.connectUserSuccess(user); 24 | }); 25 | 26 | this.socket.on('DISCONNECT_USER_SUCCESS', userId => 27 | this.onDisconnectUserSuccess(userId) 28 | ); 29 | 30 | this.socket.on('ADD_ORDER_SUCCESS', order => this.onAddOrderSuccess(order)); 31 | 32 | this.socket.on('REMOVE_ORDER_SUCCESS', orderId => 33 | this.onRemoveOrderSuccess(orderId) 34 | ); 35 | 36 | this.socket.on( 37 | 'SET_COUNTDOWN', 38 | ({ hour, minute }: { hour: number; minute: number }) => { 39 | this.onSetCountdown(hour, minute); 40 | } 41 | ); 42 | } 43 | 44 | public connectUser(username: string) { 45 | this.storage.store('username', username); 46 | this.socket.emit('CONNECT_USER', username); 47 | } 48 | 49 | private connectUserSuccess(user) { 50 | if (this.storage.retrieve('username') === user.username) { 51 | this.store$.dispatch(new UsersActions.IdentificationSuccess(user.id)); 52 | this.store$.dispatch(new UiActions.CloseDialogIdentification()); 53 | } 54 | 55 | this.store$.dispatch(new UsersActions.AddUserSuccess(user)); 56 | } 57 | 58 | public addOrder(order: INewOrder & { userId: string }) { 59 | this.socket.emit('ADD_ORDER', order); 60 | } 61 | 62 | private onAddOrderSuccess(order: IOrder) { 63 | this.store$.dispatch(new OrdersActions.AddOrderSuccess(order)); 64 | } 65 | 66 | public removeOrder(orderId: string) { 67 | this.socket.emit('REMOVE_ORDER', orderId); 68 | } 69 | 70 | private onRemoveOrderSuccess(id: string) { 71 | this.store$.dispatch(new OrdersActions.RemoveOrderSuccess({ id })); 72 | } 73 | 74 | private onDisconnectUserSuccess(id: string) { 75 | this.store$.dispatch(new UsersActions.SetUserOffline({ id })); 76 | } 77 | 78 | private onSetCountdown(hour: number, minute: number) { 79 | this.store$.dispatch(new OrdersActions.SetCountdown({ hour, minute })); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /frontend/src/app/shared/shared.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { HttpClientModule } from '@angular/common/http'; 3 | import { NgModule } from '@angular/core'; 4 | import { FlexLayoutModule } from '@angular/flex-layout'; 5 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 6 | // we now have to import every sub modules of material we want to use 7 | import { 8 | MatButtonModule, 9 | MatCardModule, 10 | MatChipsModule, 11 | MatDialogModule, 12 | MatIconModule, 13 | MatInputModule, 14 | MatListModule, 15 | MatProgressSpinnerModule, 16 | MatRippleModule, 17 | MatSelectModule, 18 | MatSidenavModule, 19 | MatTabsModule, 20 | MatToolbarModule, 21 | MatTooltipModule, 22 | } from '@angular/material'; 23 | import { RouterModule } from '@angular/router'; 24 | import { StoreModule } from '@ngrx/store'; 25 | import { TranslateModule } from '@ngx-translate/core'; 26 | import { JoinPipe } from './pipes/join.pipe'; 27 | 28 | const MaterialModules = [ 29 | MatButtonModule, 30 | MatCardModule, 31 | MatDialogModule, 32 | MatIconModule, 33 | MatInputModule, 34 | MatListModule, 35 | MatProgressSpinnerModule, 36 | MatRippleModule, 37 | MatSidenavModule, 38 | MatTabsModule, 39 | MatToolbarModule, 40 | MatTooltipModule, 41 | MatSelectModule, 42 | MatChipsModule, 43 | ]; 44 | 45 | /** 46 | * this module should be imported in every sub-modules 47 | * you can define here the modules, components, pipes that you want to re-use all over your app 48 | */ 49 | export const modules = [ 50 | CommonModule, 51 | FormsModule, 52 | ReactiveFormsModule, 53 | HttpClientModule, 54 | RouterModule, 55 | FlexLayoutModule, 56 | StoreModule, 57 | TranslateModule, 58 | ...MaterialModules, 59 | ]; 60 | 61 | export const declarations = [JoinPipe]; 62 | 63 | @NgModule({ 64 | imports: modules, 65 | exports: [...modules, ...declarations], 66 | declarations, 67 | }) 68 | export class SharedModule {} 69 | -------------------------------------------------------------------------------- /frontend/src/app/shared/states/README.md: -------------------------------------------------------------------------------- 1 | # States 2 | 3 | **A state folder should contain :** 4 | your-state-name.actions.ts 5 | your-state-name.initial-state.ts 6 | your-state-name.interfaces.ts 7 | your-state-name.reducer.ts 8 | your-state-name.reducer.spec.ts 9 | your-state-name.selectors.ts 10 | your-state-name.selectors.spec.ts 11 | 12 | The `ui` state is a special one. It's a very simple one where we do not have normalized data for example. 13 | 14 | In you application, you'll probably have more complex states and this file intends to help you design them. 15 | 16 | A built-in model has been made into the "pizzas" folder. 17 | You may want to copy the whole folder when creating a new state and update the names to have a scaffolding. 18 | -------------------------------------------------------------------------------- /frontend/src/app/shared/states/ingredients/ingredients.actions.ts: -------------------------------------------------------------------------------- 1 | import { Action } from '@ngrx/store'; 2 | 3 | import { IIngredientsTable } from 'app/shared/states/ingredients/ingredients.interface'; 4 | 5 | export const LOAD_INGREDIENTS_SUCCESS = 6 | '[Ingredients] Load ingredients success'; 7 | export class LoadIngredientsSuccess implements Action { 8 | readonly type = LOAD_INGREDIENTS_SUCCESS; 9 | 10 | constructor(public payload: IIngredientsTable) {} 11 | } 12 | 13 | export const SELECT_INGREDIENT = '[Ingredients] Select ingredient'; 14 | export class SelectIngredient implements Action { 15 | readonly type = SELECT_INGREDIENT; 16 | 17 | constructor(public payload: { id: string }) {} 18 | } 19 | 20 | export const UNSELECT_INGREDIENT = '[Ingredients] Unselect ingredient'; 21 | export class UnselectIngredient implements Action { 22 | readonly type = UNSELECT_INGREDIENT; 23 | 24 | constructor(public payload: { id: string }) {} 25 | } 26 | 27 | export type All = 28 | | LoadIngredientsSuccess 29 | | SelectIngredient 30 | | UnselectIngredient; 31 | -------------------------------------------------------------------------------- /frontend/src/app/shared/states/ingredients/ingredients.interface.ts: -------------------------------------------------------------------------------- 1 | import { EntityState } from '@ngrx/entity'; 2 | 3 | export interface IIngredientCommon { 4 | id: string; 5 | name: string; 6 | isSelected: boolean; 7 | isSelectable: boolean; 8 | } 9 | 10 | export interface IIngredientsTable extends EntityState {} 11 | -------------------------------------------------------------------------------- /frontend/src/app/shared/states/ingredients/ingredients.reducer.ts: -------------------------------------------------------------------------------- 1 | import { createEntityAdapter, EntityAdapter } from '@ngrx/entity'; 2 | import * as IngredientsActions from 'app/shared/states/ingredients/ingredients.actions'; 3 | import { 4 | IIngredientCommon, 5 | IIngredientsTable, 6 | } from 'app/shared/states/ingredients/ingredients.interface'; 7 | 8 | export const ingredientsAdapter: EntityAdapter< 9 | IIngredientCommon 10 | > = createEntityAdapter(); 11 | 12 | export function ingredientsInitState(): IIngredientsTable { 13 | return ingredientsAdapter.getInitialState(); 14 | } 15 | 16 | export function ingredientsReducer( 17 | ingredientsTbl = ingredientsInitState(), 18 | action: IngredientsActions.All 19 | ): IIngredientsTable { 20 | switch (action.type) { 21 | case IngredientsActions.LOAD_INGREDIENTS_SUCCESS: { 22 | return { 23 | ...ingredientsTbl, 24 | ...action.payload, 25 | }; 26 | } 27 | 28 | case IngredientsActions.SELECT_INGREDIENT: { 29 | return ingredientsAdapter.updateOne( 30 | { 31 | id: action.payload.id, 32 | changes: { isSelected: true }, 33 | }, 34 | ingredientsTbl 35 | ); 36 | } 37 | 38 | case IngredientsActions.UNSELECT_INGREDIENT: { 39 | return ingredientsAdapter.updateOne( 40 | { 41 | id: action.payload.id, 42 | changes: { isSelected: false }, 43 | }, 44 | ingredientsTbl 45 | ); 46 | } 47 | 48 | default: 49 | return ingredientsTbl; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /frontend/src/app/shared/states/ingredients/ingredients.selector.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createFeatureSelector, 3 | createSelector, 4 | MemoizedSelector, 5 | } from '@ngrx/store'; 6 | 7 | import { 8 | IIngredientCommon, 9 | IIngredientsTable, 10 | } from 'app/shared/states/ingredients/ingredients.interface'; 11 | import { ingredientsAdapter } from './ingredients.reducer'; 12 | 13 | const { 14 | selectIds: _selectIngredientsIds, 15 | selectEntities: _selectIngredientsEntities, 16 | selectAll: _selectIngredientsAll, 17 | selectTotal: _selectIngredientsTotal, 18 | } = ingredientsAdapter.getSelectors(); 19 | 20 | export const selectIngredientsState = createFeatureSelector( 21 | 'ingredients' 22 | ); 23 | 24 | export const selectIngredientsIds = createSelector( 25 | selectIngredientsState, 26 | _selectIngredientsIds 27 | ); 28 | export const selectIngredientsEntities = createSelector( 29 | selectIngredientsState, 30 | _selectIngredientsEntities 31 | ); 32 | export const selectIngredientsAll = createSelector( 33 | selectIngredientsState, 34 | _selectIngredientsAll 35 | ); 36 | export const selectIngredientsTotal = createSelector( 37 | selectIngredientsState, 38 | _selectIngredientsTotal 39 | ); 40 | 41 | export const getSelectedIngredientsIds: MemoizedSelector< 42 | object, 43 | string[] 44 | > = createSelector(selectIngredientsAll, ingredients => 45 | ingredients 46 | .filter(ingredient => ingredient.isSelected) 47 | .map(ingredient => ingredient.id) 48 | ); 49 | 50 | export const getSelectedIngredients: MemoizedSelector< 51 | object, 52 | IIngredientCommon[] 53 | > = createSelector( 54 | selectIngredientsEntities, 55 | getSelectedIngredientsIds, 56 | (ingredientsEntities, selectedIngredientsIds) => 57 | selectedIngredientsIds.map( 58 | ingredientId => ingredientsEntities[ingredientId] 59 | ) 60 | ); 61 | 62 | export const getNbIngredientsSelected = createSelector( 63 | getSelectedIngredientsIds, 64 | selectedIngredientsIds => selectedIngredientsIds.length 65 | ); 66 | -------------------------------------------------------------------------------- /frontend/src/app/shared/states/orders/orders.actions.ts: -------------------------------------------------------------------------------- 1 | import { Action } from '@ngrx/store'; 2 | 3 | import { 4 | INewOrder, 5 | IOrder, 6 | IOrdersTable, 7 | } from 'app/shared/states/orders/orders.interface'; 8 | 9 | export const LOAD_ORDERS_SUCCESS = '[Orders] Load orders success'; 10 | export class LoadOrdersSuccess implements Action { 11 | readonly type = LOAD_ORDERS_SUCCESS; 12 | 13 | constructor(public payload: IOrdersTable) {} 14 | } 15 | 16 | export const ADD_ORDER = '[Orders] Add order'; 17 | export class AddOrder implements Action { 18 | readonly type = ADD_ORDER; 19 | 20 | constructor(public payload: INewOrder) {} 21 | } 22 | 23 | export const ADD_ORDER_SUCCESS = '[Orders] Add order success'; 24 | export class AddOrderSuccess implements Action { 25 | readonly type = ADD_ORDER_SUCCESS; 26 | 27 | constructor(public payload: IOrder) {} 28 | } 29 | 30 | export const REMOVE_ORDER = '[Orders] Remove order'; 31 | export class RemoveOrder implements Action { 32 | readonly type = REMOVE_ORDER; 33 | 34 | constructor(public payload: { id: string }) {} 35 | } 36 | 37 | export const REMOVE_ORDER_SUCCESS = '[Orders] Remove order success'; 38 | export class RemoveOrderSuccess implements Action { 39 | readonly type = REMOVE_ORDER_SUCCESS; 40 | 41 | constructor(public payload: { id: string }) {} 42 | } 43 | 44 | export const SET_COUNTDOWN = '[Orders] Set countdown'; 45 | export class SetCountdown implements Action { 46 | readonly type = SET_COUNTDOWN; 47 | 48 | constructor(public payload: { hour: number; minute: number }) {} 49 | } 50 | 51 | export type All = 52 | | LoadOrdersSuccess 53 | | AddOrder 54 | | AddOrderSuccess 55 | | RemoveOrder 56 | | RemoveOrderSuccess 57 | | SetCountdown; 58 | -------------------------------------------------------------------------------- /frontend/src/app/shared/states/orders/orders.effects.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Actions, Effect } from '@ngrx/effects'; 3 | import { Store } from '@ngrx/store'; 4 | import { tap, withLatestFrom } from 'rxjs/operators'; 5 | 6 | import { IStore } from 'app/shared/interfaces/store.interface'; 7 | import { WebsocketService } from 'app/shared/services/websocket.service'; 8 | import * as OrdersActions from 'app/shared/states/orders/orders.actions'; 9 | 10 | @Injectable() 11 | export class OrdersEffects { 12 | constructor( 13 | private store$: Store, 14 | private actions$: Actions, 15 | private webSocketService: WebsocketService 16 | ) {} 17 | 18 | // tslint:disable-next-line:member-ordering 19 | @Effect({ dispatch: false }) 20 | addOrder$ = this.actions$ 21 | .ofType(OrdersActions.ADD_ORDER) 22 | .pipe( 23 | withLatestFrom(this.store$.select(state => state.users.idCurrentUser)), 24 | tap(([action, idCurrentUser]) => 25 | this.webSocketService.addOrder({ 26 | ...action.payload, 27 | userId: idCurrentUser, 28 | }) 29 | ) 30 | ); 31 | 32 | // tslint:disable-next-line:member-ordering 33 | @Effect({ dispatch: false }) 34 | removeOrder$ = this.actions$ 35 | .ofType(OrdersActions.REMOVE_ORDER) 36 | .pipe(tap(action => this.webSocketService.removeOrder(action.payload.id))); 37 | } 38 | -------------------------------------------------------------------------------- /frontend/src/app/shared/states/orders/orders.initial-state.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/frontend/src/app/shared/states/orders/orders.initial-state.ts -------------------------------------------------------------------------------- /frontend/src/app/shared/states/orders/orders.interface.ts: -------------------------------------------------------------------------------- 1 | import { EntityState } from '@ngrx/entity'; 2 | import { IPizzaCommon } from 'app/shared/states/pizzas/pizzas.interface'; 3 | 4 | export interface IOrderCommon { 5 | pizzaId: string; 6 | priceIndex: number; 7 | } 8 | 9 | export interface IOrder extends IOrderCommon { 10 | userId: string; 11 | id: string; 12 | isBeingRemoved: boolean; 13 | } 14 | 15 | // tslint:disable-next-line:no-empty-interface 16 | export interface INewOrder extends IOrderCommon {} 17 | 18 | export interface IPizzaOrderSummary { 19 | pizzaName: string; 20 | howManyPerSize: { [size: string]: { size: string; howMany: number } }; 21 | } 22 | 23 | // tslint:disable:no-empty-interface 24 | export interface IOrdersSummary extends Array {} 25 | 26 | export interface IOrdersTable extends EntityState { 27 | // when should we lock the orders 28 | hourEnd: number; 29 | minuteEnd: number; 30 | } 31 | 32 | export interface IOrderWithPizzas extends IOrder { 33 | pizzas: IPizzaCommon[]; 34 | } 35 | -------------------------------------------------------------------------------- /frontend/src/app/shared/states/orders/orders.reducer.ts: -------------------------------------------------------------------------------- 1 | import { createEntityAdapter, EntityAdapter } from '@ngrx/entity'; 2 | import * as OrdersActions from 'app/shared/states/orders/orders.actions'; 3 | import { 4 | IOrder, 5 | IOrdersTable, 6 | } from 'app/shared/states/orders/orders.interface'; 7 | 8 | export const ordersAdapter: EntityAdapter = createEntityAdapter< 9 | IOrder 10 | >(); 11 | 12 | export function ordersInitState(): IOrdersTable { 13 | return ordersAdapter.getInitialState({ 14 | hourEnd: null, 15 | minuteEnd: null, 16 | }); 17 | } 18 | 19 | export function ordersReducer( 20 | ordersTbl = ordersInitState(), 21 | action: OrdersActions.All 22 | ): IOrdersTable { 23 | switch (action.type) { 24 | case OrdersActions.LOAD_ORDERS_SUCCESS: { 25 | return { 26 | ...ordersTbl, 27 | ...action.payload, 28 | }; 29 | } 30 | 31 | case OrdersActions.ADD_ORDER_SUCCESS: { 32 | return ordersAdapter.addOne(action.payload, ordersTbl); 33 | } 34 | 35 | case OrdersActions.REMOVE_ORDER: { 36 | return ordersAdapter.updateOne( 37 | { id: action.payload.id, changes: { isBeingRemoved: true } }, 38 | ordersTbl 39 | ); 40 | } 41 | 42 | case OrdersActions.REMOVE_ORDER_SUCCESS: { 43 | return ordersAdapter.removeOne(action.payload.id, ordersTbl); 44 | } 45 | 46 | case OrdersActions.SET_COUNTDOWN: { 47 | return { 48 | ...ordersTbl, 49 | hourEnd: action.payload.hour, 50 | minuteEnd: action.payload.minute, 51 | }; 52 | } 53 | 54 | default: 55 | return ordersTbl; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /frontend/src/app/shared/states/orders/orders.selector.ts: -------------------------------------------------------------------------------- 1 | import { createFeatureSelector, createSelector } from '@ngrx/store'; 2 | 3 | import { 4 | IOrder, 5 | IOrdersTable, 6 | IPizzaOrderSummary, 7 | } from 'app/shared/states/orders/orders.interface'; 8 | import { selectPizzasEntities } from '../pizzas/pizzas.selector'; 9 | import { ordersAdapter } from './orders.reducer'; 10 | 11 | const pizzaPriceIndexToSize = { 12 | 0: 'S', 13 | 1: 'M', 14 | 2: 'L', 15 | 3: 'XL', 16 | }; 17 | 18 | const { 19 | selectIds: _selectOrdersIds, 20 | selectEntities: _selectOrdersEntities, 21 | selectAll: _selectOrdersAll, 22 | selectTotal: _selectOrdersTotal, 23 | } = ordersAdapter.getSelectors(); 24 | 25 | export const selectOrdersState = createFeatureSelector('orders'); 26 | 27 | export const selectOrdersIds = createSelector( 28 | selectOrdersState, 29 | _selectOrdersIds 30 | ); 31 | export const selectOrdersEntities = createSelector( 32 | selectOrdersState, 33 | _selectOrdersEntities 34 | ); 35 | export const selectOrdersAll = createSelector( 36 | selectOrdersState, 37 | _selectOrdersAll 38 | ); 39 | export const selectOrdersTotal = createSelector( 40 | selectOrdersState, 41 | _selectOrdersTotal 42 | ); 43 | 44 | export const getOrderSummary = createSelector( 45 | selectOrdersAll, 46 | selectPizzasEntities, 47 | (ordersAll, pizzasEntities) => { 48 | const ordersSummaryMap = ordersAll.reduce( 49 | (acc: { [key: string]: IPizzaOrderSummary }, order: IOrder) => { 50 | const pizza = pizzasEntities[order.pizzaId]; 51 | const pizzaSize = pizzaPriceIndexToSize[order.priceIndex]; 52 | 53 | if (!acc[pizza.id]) { 54 | acc[pizza.id] = { 55 | pizzaName: pizza.name, 56 | howManyPerSize: { 57 | S: { size: 'S', howMany: 0 }, 58 | M: { size: 'M', howMany: 0 }, 59 | L: { size: 'L', howMany: 0 }, 60 | XL: { size: 'XL', howMany: 0 }, 61 | }, 62 | }; 63 | } 64 | 65 | acc[pizza.id].howManyPerSize[pizzaSize].howMany++; 66 | 67 | return acc; 68 | }, 69 | {} 70 | ); 71 | 72 | const pizzasIds = Object.keys(ordersSummaryMap); 73 | 74 | return pizzasIds.map(pizzaId => ordersSummaryMap[pizzaId]); 75 | } 76 | ); 77 | 78 | export const getTimeEnd = createSelector(selectOrdersState, orderState => ({ 79 | hour: orderState.hourEnd, 80 | minute: orderState.minuteEnd, 81 | })); 82 | -------------------------------------------------------------------------------- /frontend/src/app/shared/states/pizzas-categories/pizzas-categories.actions.ts: -------------------------------------------------------------------------------- 1 | import { Action } from '@ngrx/store'; 2 | 3 | import { IPizzasCategoriesTable } from 'app/shared/states/pizzas-categories/pizzas-categories.interface'; 4 | 5 | export const LOAD_PIZZAS_CATEGORIES_SUCCESS = 6 | '[Pizzas categ] Load pizzas categories success'; 7 | export class LoadPizzasCategoriesSuccess implements Action { 8 | readonly type = LOAD_PIZZAS_CATEGORIES_SUCCESS; 9 | 10 | constructor(public payload: IPizzasCategoriesTable) {} 11 | } 12 | 13 | export type All = LoadPizzasCategoriesSuccess; 14 | -------------------------------------------------------------------------------- /frontend/src/app/shared/states/pizzas-categories/pizzas-categories.interface.ts: -------------------------------------------------------------------------------- 1 | import { EntityState } from '@ngrx/entity'; 2 | import { IPizzaWithIngredients } from 'app/shared/states/pizzas/pizzas.interface'; 3 | 4 | export interface IPizzaCategoryCommon { 5 | id: string; 6 | name: string; 7 | pizzasIds: string[]; 8 | } 9 | 10 | export interface IPizzasCategoriesTable 11 | extends EntityState {} 12 | 13 | export interface IPizzaCategoryWithPizzas extends IPizzaCategoryCommon { 14 | pizzas: IPizzaWithIngredients[]; 15 | } 16 | -------------------------------------------------------------------------------- /frontend/src/app/shared/states/pizzas-categories/pizzas-categories.reducer.ts: -------------------------------------------------------------------------------- 1 | import { createEntityAdapter, EntityAdapter } from '@ngrx/entity'; 2 | import * as PizzasCategoriesActions from 'app/shared/states/pizzas-categories/pizzas-categories.actions'; 3 | import { 4 | IPizzaCategoryCommon, 5 | IPizzasCategoriesTable, 6 | } from 'app/shared/states/pizzas-categories/pizzas-categories.interface'; 7 | 8 | export const pizzasCategoriesAdapter: EntityAdapter< 9 | IPizzaCategoryCommon 10 | > = createEntityAdapter(); 11 | 12 | export function pizzasCategoriesInitState(): IPizzasCategoriesTable { 13 | return pizzasCategoriesAdapter.getInitialState(); 14 | } 15 | 16 | export function pizzasCategoriesReducer( 17 | pizzasCategoriesTbl = pizzasCategoriesInitState(), 18 | action: PizzasCategoriesActions.All 19 | ): IPizzasCategoriesTable { 20 | switch (action.type) { 21 | case PizzasCategoriesActions.LOAD_PIZZAS_CATEGORIES_SUCCESS: { 22 | return { 23 | ...pizzasCategoriesTbl, 24 | ...action.payload, 25 | }; 26 | } 27 | 28 | default: 29 | return pizzasCategoriesTbl; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /frontend/src/app/shared/states/pizzas-categories/pizzas-categories.selector.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createFeatureSelector, 3 | createSelector, 4 | } from '@ngrx/store'; 5 | import { 6 | IPizzasCategoriesTable, 7 | } from 'app/shared/states/pizzas-categories/pizzas-categories.interface'; 8 | import { pizzasCategoriesAdapter } from './pizzas-categories.reducer'; 9 | 10 | const { 11 | selectIds: _selectPizzasCategoriesIds, 12 | selectEntities: _selectPizzasCategoriesEntities, 13 | selectAll: _selectPizzasCategoriesAll, 14 | selectTotal: _selectPizzasCategoriesTotal, 15 | } = pizzasCategoriesAdapter.getSelectors(); 16 | 17 | export const selectPizzasCategoriesState = createFeatureSelector< 18 | IPizzasCategoriesTable 19 | >('pizzasCategories'); 20 | 21 | export const selectPizzasCategoriesIds = createSelector( 22 | selectPizzasCategoriesState, 23 | _selectPizzasCategoriesIds 24 | ); 25 | export const selectPizzasCategoriesEntities = createSelector( 26 | selectPizzasCategoriesState, 27 | _selectPizzasCategoriesEntities 28 | ); 29 | export const selectPizzasCategoriesAll = createSelector( 30 | selectPizzasCategoriesState, 31 | _selectPizzasCategoriesAll 32 | ); 33 | export const selectPizzasCategoriesTotal = createSelector( 34 | selectPizzasCategoriesState, 35 | _selectPizzasCategoriesTotal 36 | ); 37 | -------------------------------------------------------------------------------- /frontend/src/app/shared/states/pizzas/pizzas.actions.ts: -------------------------------------------------------------------------------- 1 | import { Action } from '@ngrx/store'; 2 | 3 | import { IPizzasTable } from 'app/shared/states/pizzas/pizzas.interface'; 4 | 5 | export const LOAD_PIZZAS = '[Pizzas] Load pizzas'; 6 | export class LoadPizzas implements Action { 7 | readonly type = LOAD_PIZZAS; 8 | 9 | constructor() {} 10 | } 11 | 12 | export const LOAD_PIZZAS_SUCCESS = '[Pizzas] Load pizzas success'; 13 | export class LoadPizzasSuccess implements Action { 14 | readonly type = LOAD_PIZZAS_SUCCESS; 15 | 16 | constructor(public payload: IPizzasTable) {} 17 | } 18 | 19 | export type All = LoadPizzas | LoadPizzasSuccess; 20 | -------------------------------------------------------------------------------- /frontend/src/app/shared/states/pizzas/pizzas.interface.ts: -------------------------------------------------------------------------------- 1 | import { EntityState } from '@ngrx/entity'; 2 | import { IIngredientCommon } from 'app/shared/states/ingredients/ingredients.interface'; 3 | 4 | export interface IPizzaCommon { 5 | id: string; 6 | name: string; 7 | ingredientsIds: string[]; 8 | prices: number[]; 9 | imgUrl: string; 10 | } 11 | 12 | export interface IPizzasTable extends EntityState {} 13 | 14 | export interface IPizzaWithIngredients extends IPizzaCommon { 15 | ingredients: IIngredientCommon[]; 16 | } 17 | 18 | export interface IPizzaWithPrice extends IPizzaCommon { 19 | orderId: string; 20 | price: number; 21 | size: string; 22 | } 23 | -------------------------------------------------------------------------------- /frontend/src/app/shared/states/pizzas/pizzas.reducer.ts: -------------------------------------------------------------------------------- 1 | import { createEntityAdapter, EntityAdapter } from '@ngrx/entity'; 2 | import * as PizzasActions from 'app/shared/states/pizzas//pizzas.actions'; 3 | import { 4 | IPizzaCommon, 5 | IPizzasTable, 6 | } from 'app/shared/states/pizzas/pizzas.interface'; 7 | 8 | export const pizzasAdapter: EntityAdapter = createEntityAdapter< 9 | IPizzaCommon 10 | >(); 11 | 12 | export function pizzasInitState(): IPizzasTable { 13 | return pizzasAdapter.getInitialState(); 14 | } 15 | 16 | export function pizzasReducer( 17 | pizzasTbl = pizzasInitState(), 18 | action: PizzasActions.All 19 | ): IPizzasTable { 20 | switch (action.type) { 21 | case PizzasActions.LOAD_PIZZAS_SUCCESS: { 22 | return { 23 | ...pizzasTbl, 24 | ...action.payload, 25 | }; 26 | } 27 | 28 | default: 29 | return pizzasTbl; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /frontend/src/app/shared/states/pizzas/pizzas.selector.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createFeatureSelector, 3 | createSelector, 4 | } from '@ngrx/store'; 5 | import { IPizzasTable } from './pizzas.interface'; 6 | import { pizzasAdapter } from './pizzas.reducer'; 7 | // import { selectIngredientsEntities } from '../ingredients/ingredients.selector'; 8 | 9 | const { 10 | selectIds: _selectPizzasIds, 11 | selectEntities: _selectPizzasEntities, 12 | selectAll: _selectPizzasAll, 13 | selectTotal: _selectPizzasTotal, 14 | } = pizzasAdapter.getSelectors(); 15 | 16 | export const selectPizzasSelector = createFeatureSelector( 17 | 'pizzas' 18 | ); 19 | 20 | export const selectPizzasIds = createSelector( 21 | selectPizzasSelector, 22 | _selectPizzasIds 23 | ); 24 | export const selectPizzasEntities = createSelector( 25 | selectPizzasSelector, 26 | _selectPizzasEntities 27 | ); 28 | export const selectPizzasAll = createSelector( 29 | selectPizzasSelector, 30 | _selectPizzasAll 31 | ); 32 | export const selectPizzasTota = createSelector( 33 | selectPizzasSelector, 34 | _selectPizzasTotal 35 | ); 36 | -------------------------------------------------------------------------------- /frontend/src/app/shared/states/root.reducer.ts: -------------------------------------------------------------------------------- 1 | import { storeFreeze } from 'ngrx-store-freeze'; 2 | import { enableBatching } from 'redux-batched-actions'; 3 | 4 | import { ingredientsReducer } from 'app/shared/states/ingredients/ingredients.reducer'; 5 | import { ordersReducer } from 'app/shared/states/orders/orders.reducer'; 6 | import { pizzasCategoriesReducer } from 'app/shared/states/pizzas-categories/pizzas-categories.reducer'; 7 | import { pizzasReducer } from 'app/shared/states/pizzas/pizzas.reducer'; 8 | import { uiReducer } from 'app/shared/states/ui/ui.reducer'; 9 | import { usersReducer } from 'app/shared/states/users/users.reducer'; 10 | import { environment } from 'environments/environment'; 11 | 12 | // ------------------------------------------------------------------------------ 13 | 14 | export const reducers = { 15 | // pass your reducers here 16 | ui: uiReducer, 17 | pizzas: pizzasReducer, 18 | pizzasCategories: pizzasCategoriesReducer, 19 | users: usersReducer, 20 | orders: ordersReducer, 21 | ingredients: ingredientsReducer, 22 | }; 23 | 24 | // ------------------------------------------------------------------------------ 25 | 26 | // enableBatching allows us to dispatch multiple actions 27 | // without letting the subscribers being warned between the actions 28 | // only at the end : https://github.com/tshelburne/redux-batched-actions 29 | // can be very handy when normalizing HTTP response 30 | const metaReducersDev = [storeFreeze, enableBatching]; 31 | 32 | const metaReducersProd = [enableBatching]; 33 | 34 | // if environment is != from production 35 | // use storeFreeze to avoid state mutation 36 | export const metaReducers = environment.production 37 | ? metaReducersProd 38 | : metaReducersDev; 39 | -------------------------------------------------------------------------------- /frontend/src/app/shared/states/ui/ui.actions.ts: -------------------------------------------------------------------------------- 1 | import { Action } from '@ngrx/store'; 2 | 3 | export const SET_LANGUAGE = 'Set language'; 4 | export class SetLanguage implements Action { 5 | readonly type = SET_LANGUAGE; 6 | 7 | constructor(public payload: { language: string }) {} 8 | } 9 | 10 | export const TOGGLE_SIDENAV = 'Toggle sidenav'; 11 | export class ToggleSidenav implements Action { 12 | readonly type = TOGGLE_SIDENAV; 13 | 14 | constructor() {} 15 | } 16 | 17 | export const OPEN_SIDENAV = 'Open sidenav'; 18 | export class OpenSidenav implements Action { 19 | readonly type = OPEN_SIDENAV; 20 | 21 | constructor() {} 22 | } 23 | 24 | export const CLOSE_SIDENAV = 'Close sidenav'; 25 | export class CloseSidenav implements Action { 26 | readonly type = CLOSE_SIDENAV; 27 | 28 | constructor() {} 29 | } 30 | 31 | export const OPEN_DIALOG_IDENTIFICATION = 'Open dialog identification'; 32 | export class OpenDialogIdentification implements Action { 33 | readonly type = OPEN_DIALOG_IDENTIFICATION; 34 | 35 | constructor() {} 36 | } 37 | 38 | export const CLOSE_DIALOG_IDENTIFICATION = 'Close dialog identification'; 39 | export class CloseDialogIdentification implements Action { 40 | readonly type = CLOSE_DIALOG_IDENTIFICATION; 41 | 42 | constructor() {} 43 | } 44 | 45 | export const OPEN_DIALOG_ORDER_SUMMARY = 'Open dialog order summary'; 46 | export class OpenDialogOrderSummary implements Action { 47 | readonly type = OPEN_DIALOG_ORDER_SUMMARY; 48 | 49 | constructor() {} 50 | } 51 | 52 | export const CLOSE_DIALOG_ORDER_SUMMARY = 'Close dialog order summary'; 53 | export class CloseDialogOrderSummary implements Action { 54 | readonly type = CLOSE_DIALOG_ORDER_SUMMARY; 55 | 56 | constructor() {} 57 | } 58 | 59 | export const UPDATE_PIZZERIA_INFORMATION = 'Update pizzeria information'; 60 | export class UpdatePizzeriaInformation implements Action { 61 | readonly type = UPDATE_PIZZERIA_INFORMATION; 62 | 63 | constructor(public payload: { name: string; phone: string; url: string }) {} 64 | } 65 | 66 | export const UPDATE_PIZZA_SEARCH = 'Update pizza search'; 67 | export class UpdatePizzaSearch implements Action { 68 | readonly type = UPDATE_PIZZA_SEARCH; 69 | 70 | constructor(public payload: { search: string }) {} 71 | } 72 | 73 | export const TOGGLE_VISIBILITY_FILTER_INGREDIENT = 74 | 'Toggle visibility filter ingredient'; 75 | export class ToggleVisibilityFilterIngredient implements Action { 76 | readonly type = TOGGLE_VISIBILITY_FILTER_INGREDIENT; 77 | 78 | constructor() {} 79 | } 80 | 81 | export type All = 82 | | SetLanguage 83 | | ToggleSidenav 84 | | OpenSidenav 85 | | CloseSidenav 86 | | OpenDialogIdentification 87 | | CloseDialogIdentification 88 | | OpenDialogOrderSummary 89 | | CloseDialogOrderSummary 90 | | UpdatePizzeriaInformation 91 | | UpdatePizzaSearch 92 | | ToggleVisibilityFilterIngredient; 93 | -------------------------------------------------------------------------------- /frontend/src/app/shared/states/ui/ui.initial-state.ts: -------------------------------------------------------------------------------- 1 | import { IUi } from 'app/shared/states/ui/ui.interface'; 2 | 3 | export function uiInitialState(): IUi { 4 | return { 5 | language: '', 6 | isSidenavVisible: true, 7 | isDialogIdentificationOpen: true, 8 | isDialogOrderSummaryOpen: false, 9 | isFilterIngredientVisible: false, 10 | pizzaSearch: '', 11 | 12 | // pizzeria information 13 | pizzeria: { 14 | name: '', 15 | phone: '', 16 | url: '', 17 | }, 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /frontend/src/app/shared/states/ui/ui.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IPizzeria { 2 | readonly name: string; 3 | readonly phone: string; 4 | readonly url: string; 5 | } 6 | 7 | export interface IUi { 8 | readonly language: string; 9 | readonly isSidenavVisible: boolean; 10 | readonly isDialogIdentificationOpen: boolean; 11 | readonly isDialogOrderSummaryOpen: boolean; 12 | readonly isFilterIngredientVisible: boolean; 13 | readonly pizzaSearch: string; 14 | 15 | // pizzeria information 16 | readonly pizzeria: IPizzeria; 17 | } 18 | -------------------------------------------------------------------------------- /frontend/src/app/shared/states/ui/ui.reducer.ts: -------------------------------------------------------------------------------- 1 | import * as UiActions from 'app/shared/states/ui/ui.actions'; 2 | import { uiInitialState } from 'app/shared/states/ui/ui.initial-state'; 3 | import { IUi } from 'app/shared/states/ui/ui.interface'; 4 | 5 | export function uiReducer( 6 | ui: IUi = uiInitialState(), 7 | action: UiActions.All 8 | ): IUi { 9 | switch (action.type) { 10 | case UiActions.SET_LANGUAGE: { 11 | return { 12 | ...ui, 13 | language: action.payload.language, 14 | }; 15 | } 16 | 17 | case UiActions.TOGGLE_SIDENAV: { 18 | return { 19 | ...ui, 20 | isSidenavVisible: !ui.isSidenavVisible, 21 | }; 22 | } 23 | 24 | case UiActions.OPEN_SIDENAV: { 25 | return { 26 | ...ui, 27 | isSidenavVisible: true, 28 | }; 29 | } 30 | 31 | case UiActions.CLOSE_SIDENAV: { 32 | return { 33 | ...ui, 34 | isSidenavVisible: false, 35 | }; 36 | } 37 | 38 | case UiActions.OPEN_DIALOG_IDENTIFICATION: { 39 | return { 40 | ...ui, 41 | isDialogIdentificationOpen: true, 42 | }; 43 | } 44 | 45 | case UiActions.CLOSE_DIALOG_IDENTIFICATION: { 46 | return { 47 | ...ui, 48 | isDialogIdentificationOpen: false, 49 | }; 50 | } 51 | 52 | case UiActions.OPEN_DIALOG_ORDER_SUMMARY: { 53 | return { 54 | ...ui, 55 | isDialogOrderSummaryOpen: true, 56 | }; 57 | } 58 | 59 | case UiActions.CLOSE_DIALOG_ORDER_SUMMARY: { 60 | return { 61 | ...ui, 62 | isDialogOrderSummaryOpen: false, 63 | }; 64 | } 65 | 66 | case UiActions.UPDATE_PIZZERIA_INFORMATION: { 67 | return { 68 | ...ui, 69 | pizzeria: { ...action.payload }, 70 | }; 71 | } 72 | 73 | case UiActions.UPDATE_PIZZA_SEARCH: { 74 | return { 75 | ...ui, 76 | pizzaSearch: action.payload.search, 77 | }; 78 | } 79 | 80 | case UiActions.TOGGLE_VISIBILITY_FILTER_INGREDIENT: { 81 | return { 82 | ...ui, 83 | isFilterIngredientVisible: !ui.isFilterIngredientVisible, 84 | }; 85 | } 86 | 87 | default: 88 | return ui; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /frontend/src/app/shared/states/ui/ui.selector.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createFeatureSelector, 3 | createSelector, 4 | MemoizedSelector, 5 | } from '@ngrx/store'; 6 | import * as removeAccents from 'remove-accents'; 7 | import { IIngredientCommon } from '../ingredients/ingredients.interface'; 8 | import { 9 | getSelectedIngredientsIds, 10 | selectIngredientsAll, 11 | selectIngredientsEntities, 12 | } from '../ingredients/ingredients.selector'; 13 | import { IPizzaCategoryWithPizzas } from '../pizzas-categories/pizzas-categories.interface'; 14 | import { selectPizzasCategoriesAll } from '../pizzas-categories/pizzas-categories.selector'; 15 | import { IPizzaWithIngredients } from '../pizzas/pizzas.interface'; 16 | import { selectPizzasEntities } from '../pizzas/pizzas.selector'; 17 | import { IUi } from './ui.interface'; 18 | 19 | export const selectUiState = createFeatureSelector('ui'); 20 | 21 | export const getPizzaSearch = createSelector( 22 | selectUiState, 23 | ui => ui.pizzaSearch 24 | ); 25 | 26 | export const getIsFilterIngredientVisible = createSelector( 27 | selectUiState, 28 | ui => ui.isFilterIngredientVisible 29 | ); 30 | 31 | export const getIsDialogIdentificationOpen = createSelector( 32 | selectUiState, 33 | ui => ui.isDialogIdentificationOpen 34 | ); 35 | 36 | export const getIsDialogOrderSummaryOpen = createSelector( 37 | selectUiState, 38 | ui => ui.isDialogOrderSummaryOpen 39 | ); 40 | 41 | function doesPizzaContainsAllSelectedIngredients( 42 | selectedIngredientsIds: string[], 43 | pizza: IPizzaWithIngredients 44 | ): boolean { 45 | return selectedIngredientsIds.every(ingredientId => 46 | pizza.ingredientsIds.includes(ingredientId) 47 | ); 48 | } 49 | 50 | export const getCategoriesAndPizzas: MemoizedSelector< 51 | object, 52 | IPizzaCategoryWithPizzas[] 53 | > = createSelector( 54 | getPizzaSearch, 55 | selectPizzasEntities, 56 | selectPizzasCategoriesAll, 57 | getSelectedIngredientsIds, 58 | selectIngredientsEntities, 59 | ( 60 | pizzasSearch, 61 | pizzasEntities, 62 | pizzasCategoriesAll, 63 | selectedIngredientsIds, 64 | ingredientsEntities 65 | ) => { 66 | pizzasSearch = removeAccents(pizzasSearch.toLowerCase()); 67 | 68 | return pizzasCategoriesAll 69 | .map(pizzasCategory => { 70 | const pizzasCategorie: IPizzaCategoryWithPizzas = { 71 | ...pizzasCategory, 72 | pizzas: pizzasCategory.pizzasIds 73 | .map(pizzaId => ({ 74 | ...pizzasEntities[pizzaId], 75 | ingredients: pizzasEntities[pizzaId].ingredientsIds.map( 76 | ingredientId => ingredientsEntities[ingredientId] 77 | ), 78 | })) 79 | .filter( 80 | p => 81 | removeAccents(p.name.toLowerCase()).includes(pizzasSearch) && 82 | doesPizzaContainsAllSelectedIngredients( 83 | selectedIngredientsIds, 84 | p 85 | ) 86 | ), 87 | }; 88 | 89 | return pizzasCategorie; 90 | }) 91 | .filter(pizzasCategorie => pizzasCategorie.pizzas.length); 92 | } 93 | ); 94 | 95 | const getIngredientsOfFilteredPizzas: MemoizedSelector< 96 | object, 97 | string[] 98 | > = createSelector(getCategoriesAndPizzas, categoriesAndPizzas => { 99 | const ingredientsOfFilteredPizzas: Set = new Set(); 100 | 101 | categoriesAndPizzas.forEach(categorieAndPizzas => { 102 | categorieAndPizzas.pizzas.forEach(pizza => { 103 | pizza.ingredientsIds.forEach(ingredientId => 104 | ingredientsOfFilteredPizzas.add(ingredientId) 105 | ); 106 | }); 107 | }); 108 | 109 | return Array.from(ingredientsOfFilteredPizzas); 110 | }); 111 | 112 | export const getIngredients: MemoizedSelector< 113 | object, 114 | IIngredientCommon[] 115 | > = createSelector( 116 | selectIngredientsAll, 117 | getIngredientsOfFilteredPizzas, 118 | (ingredients, ingredientsOfFilteredPizzas) => { 119 | return ingredients.map(ingredient => ({ 120 | ...ingredient, 121 | isSelectable: ingredientsOfFilteredPizzas.includes(ingredient.id), 122 | })); 123 | } 124 | ); 125 | -------------------------------------------------------------------------------- /frontend/src/app/shared/states/users/users.actions.ts: -------------------------------------------------------------------------------- 1 | import { Action } from '@ngrx/store'; 2 | 3 | import { 4 | IUserCommon, 5 | IUsersTable, 6 | } from 'app/shared/states/users/users.interface'; 7 | 8 | export const LOAD_USERS_SUCCESS = '[Users] Load users success'; 9 | export class LoadUsersSuccess implements Action { 10 | readonly type = LOAD_USERS_SUCCESS; 11 | 12 | constructor(public payload: IUsersTable) {} 13 | } 14 | 15 | export const IDENTIFICATION = '[Users] Identification'; 16 | export class Identification implements Action { 17 | readonly type = IDENTIFICATION; 18 | 19 | constructor(public payload: string) {} 20 | } 21 | 22 | export const IDENTIFICATION_SUCCESS = '[Users] Identification success'; 23 | export class IdentificationSuccess implements Action { 24 | readonly type = IDENTIFICATION_SUCCESS; 25 | 26 | constructor(public payload: string) {} 27 | } 28 | 29 | export const ADD_USER_SUCCESS = '[Users] Add user success'; 30 | export class AddUserSuccess implements Action { 31 | readonly type = ADD_USER_SUCCESS; 32 | 33 | constructor(public payload: IUserCommon) {} 34 | } 35 | 36 | export const SET_USER_ONLINE = '[Users] Set user online'; 37 | export class SetUserOnline implements Action { 38 | readonly type = SET_USER_ONLINE; 39 | 40 | constructor(public payload: { id: string }) {} 41 | } 42 | 43 | export const SET_USER_OFFLINE = '[Users] Set user offline'; 44 | export class SetUserOffline implements Action { 45 | readonly type = SET_USER_OFFLINE; 46 | 47 | constructor(public payload: { id: string }) {} 48 | } 49 | 50 | export type All = 51 | | LoadUsersSuccess 52 | | Identification 53 | | IdentificationSuccess 54 | | AddUserSuccess 55 | | SetUserOnline 56 | | SetUserOffline; 57 | -------------------------------------------------------------------------------- /frontend/src/app/shared/states/users/users.effects.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Actions, Effect } from '@ngrx/effects'; 3 | import { tap } from 'rxjs/operators'; 4 | 5 | import { WebsocketService } from 'app/shared/services/websocket.service'; 6 | import * as UsersActions from 'app/shared/states/users/users.actions'; 7 | 8 | @Injectable() 9 | export class UsersEffects { 10 | constructor( 11 | private actions$: Actions, 12 | private websocketService: WebsocketService 13 | ) {} 14 | 15 | // tslint:disable-next-line:member-ordering 16 | @Effect({ dispatch: false }) 17 | identification$ = this.actions$ 18 | .ofType(UsersActions.IDENTIFICATION) 19 | .pipe(tap(action => this.websocketService.connectUser(action.payload))); 20 | } 21 | -------------------------------------------------------------------------------- /frontend/src/app/shared/states/users/users.interface.ts: -------------------------------------------------------------------------------- 1 | import { EntityState } from '@ngrx/entity'; 2 | import { IPizzaWithPrice } from 'app/shared/states/pizzas/pizzas.interface'; 3 | 4 | export interface IUserCommon { 5 | id: string; 6 | name: string; 7 | username: string; 8 | thumbnail: string; 9 | thumnail: string; 10 | isOnline: boolean; 11 | } 12 | 13 | export interface IUsersTable extends EntityState { 14 | isIdentifying: boolean; 15 | idCurrentUser: string; 16 | } 17 | 18 | export interface IUserWithPizzas extends IUserCommon { 19 | totalPrice: number; 20 | pizzas: IPizzaWithPrice[]; 21 | } 22 | -------------------------------------------------------------------------------- /frontend/src/app/shared/states/users/users.reducer.ts: -------------------------------------------------------------------------------- 1 | import { createEntityAdapter, EntityAdapter } from '@ngrx/entity'; 2 | import * as UsersActions from 'app/shared/states/users/users.actions'; 3 | import { 4 | IUserCommon, 5 | IUsersTable, 6 | } from 'app/shared/states/users/users.interface'; 7 | 8 | export const usersAdapter: EntityAdapter = createEntityAdapter< 9 | IUserCommon 10 | >(); 11 | 12 | export function usersInitState(): IUsersTable { 13 | return usersAdapter.getInitialState({ 14 | isIdentifying: false, 15 | idCurrentUser: '', 16 | }); 17 | } 18 | 19 | export function usersReducer( 20 | usersTbl = usersInitState(), 21 | action: UsersActions.All 22 | ): IUsersTable { 23 | switch (action.type) { 24 | case UsersActions.LOAD_USERS_SUCCESS: { 25 | return { 26 | ...usersTbl, 27 | ...action.payload, 28 | }; 29 | } 30 | 31 | case UsersActions.IDENTIFICATION: { 32 | return { 33 | ...usersTbl, 34 | isIdentifying: true, 35 | }; 36 | } 37 | 38 | case UsersActions.IDENTIFICATION_SUCCESS: { 39 | return { 40 | ...usersTbl, 41 | isIdentifying: false, 42 | idCurrentUser: action.payload, 43 | }; 44 | } 45 | 46 | case UsersActions.ADD_USER_SUCCESS: { 47 | return usersAdapter.upsertOne( 48 | { 49 | ...action.payload, 50 | isOnline: true, 51 | }, 52 | usersTbl 53 | ); 54 | } 55 | 56 | case UsersActions.SET_USER_ONLINE: { 57 | return usersAdapter.updateOne( 58 | { 59 | id: action.payload.id, 60 | changes: { 61 | isOnline: true, 62 | }, 63 | }, 64 | usersTbl 65 | ); 66 | } 67 | 68 | case UsersActions.SET_USER_OFFLINE: { 69 | return usersAdapter.updateOne( 70 | { 71 | id: action.payload.id, 72 | changes: { 73 | isOnline: false, 74 | }, 75 | }, 76 | usersTbl 77 | ); 78 | } 79 | 80 | default: 81 | return usersTbl; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /frontend/src/app/shared/states/users/users.selector.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createFeatureSelector, 3 | createSelector, 4 | MemoizedSelector, 5 | } from '@ngrx/store'; 6 | import { 7 | IPizzaWithPrice, 8 | } from 'app/shared/states/pizzas/pizzas.interface'; 9 | import { 10 | IUsersTable, 11 | IUserWithPizzas, 12 | } from 'app/shared/states/users/users.interface'; 13 | import { selectOrdersAll } from '../orders/orders.selector'; 14 | import { selectPizzasEntities } from '../pizzas/pizzas.selector'; 15 | import { usersAdapter } from './users.reducer'; 16 | 17 | const pizzaSizeByIndex = ['S', 'M', 'L', 'XL']; 18 | 19 | const { 20 | selectIds: _selectUsersIds, 21 | selectEntities: _selectUsersEntities, 22 | selectAll: _selectUsersAll, 23 | selectTotal: _selectUsersTotal, 24 | } = usersAdapter.getSelectors(); 25 | 26 | export const selectUsersState = createFeatureSelector('users'); 27 | 28 | export const selectUsersIds = createSelector(selectUsersState, _selectUsersIds); 29 | 30 | export const selectUsersEntities = createSelector( 31 | selectUsersState, 32 | _selectUsersEntities 33 | ); 34 | 35 | export const selectUsersAll = createSelector(selectUsersState, _selectUsersAll); 36 | 37 | export const selectUsersTotal = createSelector( 38 | selectUsersState, 39 | _selectUsersTotal 40 | ); 41 | 42 | export const getCurrentUser = createSelector( 43 | selectUsersState, 44 | userState => userState.idCurrentUser 45 | ); 46 | 47 | export const getFullOrder: MemoizedSelector< 48 | object, 49 | { 50 | users: IUserWithPizzas[]; 51 | totalPrice: number; 52 | } 53 | > = createSelector( 54 | selectUsersAll, 55 | selectPizzasEntities, 56 | selectOrdersAll, 57 | (users, pizzasEntities, orders) => { 58 | const usersWithPizzas = users.map(user => { 59 | const ordersOfUser = orders.filter(order => order.userId === user.id); 60 | 61 | const pizzasOfUser = ordersOfUser.map(order => { 62 | const pizza = pizzasEntities[order.pizzaId]; 63 | const pizzaPrice = pizza.prices[order.priceIndex]; 64 | 65 | return { 66 | ...pizzasEntities[order.pizzaId], 67 | 68 | orderId: order.id, 69 | isBeingRemoved: order.isBeingRemoved, 70 | price: pizzaPrice, 71 | size: pizzaSizeByIndex[order.priceIndex], 72 | }; 73 | }); 74 | 75 | const totalPriceOfUser = pizzasOfUser.reduce( 76 | (acc, pizza) => acc + pizza.price, 77 | 0 78 | ); 79 | 80 | return { 81 | ...user, 82 | ...({ 83 | totalPrice: totalPriceOfUser, 84 | pizzas: pizzasOfUser, 85 | }), 86 | }; 87 | }); 88 | 89 | const totalPrice = usersWithPizzas.reduce( 90 | (acc, user) => acc + user.totalPrice, 91 | 0 92 | ); 93 | 94 | return { 95 | users: usersWithPizzas, 96 | totalPrice, 97 | }; 98 | } 99 | ); 100 | 101 | export const getFullOrderCsvFormat = createSelector( 102 | getFullOrder, 103 | ({ users }) => { 104 | const header = [ 105 | 'Person name', 106 | 'Pizza', 107 | 'Size', 108 | 'Price', 109 | 'Pay', 110 | 'Change', 111 | 'Done ✓ or not yet ✕', 112 | ]; 113 | 114 | // the row index starts at 2 because in a speadsheet, it starts at 1 and we'll have the header 115 | let i = 2; 116 | 117 | const data = users.reduce((acc, user) => { 118 | user.pizzas.forEach(pizza => { 119 | acc.push([ 120 | user.username, 121 | pizza.name, 122 | pizza.size, 123 | pizza.price, 124 | '0', 125 | `=E${i}-D${i}`, 126 | '✕', 127 | ]); 128 | 129 | i++; 130 | }); 131 | 132 | return acc; 133 | }, []); 134 | 135 | return [header, ...data]; 136 | } 137 | ); 138 | -------------------------------------------------------------------------------- /frontend/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/frontend/src/assets/.gitkeep -------------------------------------------------------------------------------- /frontend/src/assets/i18n/README.md: -------------------------------------------------------------------------------- 1 | # i18n 2 | 3 | When adding a language to your app, you should create a new file `[language].json` in this folder and list the new language in CoreModule, LANGUAGES provider. It'll then automatically be taken into account within your app. 4 | -------------------------------------------------------------------------------- /frontend/src/assets/i18n/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "OPEN_LEFT_SIDENAV": "Open left sidenav", 3 | "OPEN_RIGHT_SIDENAV": "Open right sidenav" 4 | } 5 | -------------------------------------------------------------------------------- /frontend/src/assets/i18n/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "OPEN_LEFT_SIDENAV": "Ouvrir la sidenav de gauche", 3 | "OPEN_RIGHT_SIDENAV": "Ouvrir la sidenav de droite" 4 | } 5 | -------------------------------------------------------------------------------- /frontend/src/assets/img/github-logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/assets/img/icon-person.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/frontend/src/assets/img/icon-person.png -------------------------------------------------------------------------------- /frontend/src/assets/img/icons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/frontend/src/assets/img/icons/android-chrome-192x192.png -------------------------------------------------------------------------------- /frontend/src/assets/img/icons/android-chrome-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/frontend/src/assets/img/icons/android-chrome-256x256.png -------------------------------------------------------------------------------- /frontend/src/assets/img/icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/frontend/src/assets/img/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /frontend/src/assets/img/icons/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #da532c 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /frontend/src/assets/img/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/frontend/src/assets/img/icons/favicon-16x16.png -------------------------------------------------------------------------------- /frontend/src/assets/img/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/frontend/src/assets/img/icons/favicon-32x32.png -------------------------------------------------------------------------------- /frontend/src/assets/img/icons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/frontend/src/assets/img/icons/favicon.ico -------------------------------------------------------------------------------- /frontend/src/assets/img/icons/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "icons": [ 4 | { 5 | "src": "/android-chrome-192x192.png", 6 | "sizes": "192x192", 7 | "type": "image/png" 8 | }, 9 | { 10 | "src": "/android-chrome-256x256.png", 11 | "sizes": "256x256", 12 | "type": "image/png" 13 | } 14 | ], 15 | "theme_color": "#ffffff", 16 | "background_color": "#ffffff", 17 | "display": "standalone" 18 | } -------------------------------------------------------------------------------- /frontend/src/assets/img/icons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/frontend/src/assets/img/icons/mstile-150x150.png -------------------------------------------------------------------------------- /frontend/src/assets/img/pizza-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/frontend/src/assets/img/pizza-logo.png -------------------------------------------------------------------------------- /frontend/src/assets/img/pizzas-providers/l-ormeau/4-fromages.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/frontend/src/assets/img/pizzas-providers/l-ormeau/4-fromages.png -------------------------------------------------------------------------------- /frontend/src/assets/img/pizzas-providers/l-ormeau/basque.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/frontend/src/assets/img/pizzas-providers/l-ormeau/basque.png -------------------------------------------------------------------------------- /frontend/src/assets/img/pizzas-providers/l-ormeau/bolonaise.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/frontend/src/assets/img/pizzas-providers/l-ormeau/bolonaise.png -------------------------------------------------------------------------------- /frontend/src/assets/img/pizzas-providers/l-ormeau/borneo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/frontend/src/assets/img/pizzas-providers/l-ormeau/borneo.png -------------------------------------------------------------------------------- /frontend/src/assets/img/pizzas-providers/l-ormeau/calzone-chausson.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/frontend/src/assets/img/pizzas-providers/l-ormeau/calzone-chausson.png -------------------------------------------------------------------------------- /frontend/src/assets/img/pizzas-providers/l-ormeau/camembert.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/frontend/src/assets/img/pizzas-providers/l-ormeau/camembert.png -------------------------------------------------------------------------------- /frontend/src/assets/img/pizzas-providers/l-ormeau/chef.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/frontend/src/assets/img/pizzas-providers/l-ormeau/chef.png -------------------------------------------------------------------------------- /frontend/src/assets/img/pizzas-providers/l-ormeau/chicago.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/frontend/src/assets/img/pizzas-providers/l-ormeau/chicago.png -------------------------------------------------------------------------------- /frontend/src/assets/img/pizzas-providers/l-ormeau/chorizo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/frontend/src/assets/img/pizzas-providers/l-ormeau/chorizo.png -------------------------------------------------------------------------------- /frontend/src/assets/img/pizzas-providers/l-ormeau/creole.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/frontend/src/assets/img/pizzas-providers/l-ormeau/creole.png -------------------------------------------------------------------------------- /frontend/src/assets/img/pizzas-providers/l-ormeau/delhi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/frontend/src/assets/img/pizzas-providers/l-ormeau/delhi.png -------------------------------------------------------------------------------- /frontend/src/assets/img/pizzas-providers/l-ormeau/florentine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/frontend/src/assets/img/pizzas-providers/l-ormeau/florentine.png -------------------------------------------------------------------------------- /frontend/src/assets/img/pizzas-providers/l-ormeau/forestiere.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/frontend/src/assets/img/pizzas-providers/l-ormeau/forestiere.png -------------------------------------------------------------------------------- /frontend/src/assets/img/pizzas-providers/l-ormeau/gersoise.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/frontend/src/assets/img/pizzas-providers/l-ormeau/gersoise.png -------------------------------------------------------------------------------- /frontend/src/assets/img/pizzas-providers/l-ormeau/izmir.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/frontend/src/assets/img/pizzas-providers/l-ormeau/izmir.png -------------------------------------------------------------------------------- /frontend/src/assets/img/pizzas-providers/l-ormeau/madras.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/frontend/src/assets/img/pizzas-providers/l-ormeau/madras.png -------------------------------------------------------------------------------- /frontend/src/assets/img/pizzas-providers/l-ormeau/marguerite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/frontend/src/assets/img/pizzas-providers/l-ormeau/marguerite.png -------------------------------------------------------------------------------- /frontend/src/assets/img/pizzas-providers/l-ormeau/milanaise.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/frontend/src/assets/img/pizzas-providers/l-ormeau/milanaise.png -------------------------------------------------------------------------------- /frontend/src/assets/img/pizzas-providers/l-ormeau/new-york.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/frontend/src/assets/img/pizzas-providers/l-ormeau/new-york.png -------------------------------------------------------------------------------- /frontend/src/assets/img/pizzas-providers/l-ormeau/nicoise.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/frontend/src/assets/img/pizzas-providers/l-ormeau/nicoise.png -------------------------------------------------------------------------------- /frontend/src/assets/img/pizzas-providers/l-ormeau/norvegienne.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/frontend/src/assets/img/pizzas-providers/l-ormeau/norvegienne.png -------------------------------------------------------------------------------- /frontend/src/assets/img/pizzas-providers/l-ormeau/ollegio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/frontend/src/assets/img/pizzas-providers/l-ormeau/ollegio.png -------------------------------------------------------------------------------- /frontend/src/assets/img/pizzas-providers/l-ormeau/orientale.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/frontend/src/assets/img/pizzas-providers/l-ormeau/orientale.png -------------------------------------------------------------------------------- /frontend/src/assets/img/pizzas-providers/l-ormeau/parmentier.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/frontend/src/assets/img/pizzas-providers/l-ormeau/parmentier.png -------------------------------------------------------------------------------- /frontend/src/assets/img/pizzas-providers/l-ormeau/pavie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/frontend/src/assets/img/pizzas-providers/l-ormeau/pavie.png -------------------------------------------------------------------------------- /frontend/src/assets/img/pizzas-providers/l-ormeau/poire-williams-chocolat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/frontend/src/assets/img/pizzas-providers/l-ormeau/poire-williams-chocolat.png -------------------------------------------------------------------------------- /frontend/src/assets/img/pizzas-providers/l-ormeau/provencale.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/frontend/src/assets/img/pizzas-providers/l-ormeau/provencale.png -------------------------------------------------------------------------------- /frontend/src/assets/img/pizzas-providers/l-ormeau/romaine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/frontend/src/assets/img/pizzas-providers/l-ormeau/romaine.png -------------------------------------------------------------------------------- /frontend/src/assets/img/pizzas-providers/l-ormeau/royale.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/frontend/src/assets/img/pizzas-providers/l-ormeau/royale.png -------------------------------------------------------------------------------- /frontend/src/assets/img/pizzas-providers/l-ormeau/san-sebastian.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/frontend/src/assets/img/pizzas-providers/l-ormeau/san-sebastian.png -------------------------------------------------------------------------------- /frontend/src/assets/img/pizzas-providers/l-ormeau/savoyarde.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/frontend/src/assets/img/pizzas-providers/l-ormeau/savoyarde.png -------------------------------------------------------------------------------- /frontend/src/assets/img/pizzas-providers/l-ormeau/seville.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/frontend/src/assets/img/pizzas-providers/l-ormeau/seville.png -------------------------------------------------------------------------------- /frontend/src/assets/img/pizzas-providers/l-ormeau/sicilienne.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/frontend/src/assets/img/pizzas-providers/l-ormeau/sicilienne.png -------------------------------------------------------------------------------- /frontend/src/assets/img/pizzas-providers/pizza-default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/frontend/src/assets/img/pizzas-providers/pizza-default.png -------------------------------------------------------------------------------- /frontend/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | // PRODUCTION 3 | // angular can optimize some part of his code 4 | // (make more or less checks) according to an environment 5 | production: true, 6 | 7 | // URLBACKEND 8 | // your backend URL 9 | // you can then use it for example in a service 10 | // `${environment.urlBackend}/some/resource` 11 | urlBackend: '', 12 | 13 | // MOCK 14 | // should you keep mocks when building the app 15 | // or hit the real API 16 | mock: false, 17 | 18 | // HTTPDELAY 19 | // when using mocked data, you can use that 20 | // variable with `.delay` to simulate a network latency 21 | httpDelay: 0, 22 | 23 | // HASHLOCATIONSTRATEGY 24 | // should the URL be 25 | // http://some-domain#/your/app/routes (true) 26 | // or 27 | // http://some-domain/your/app/routes (false) 28 | hashLocationStrategy: false, 29 | 30 | // DEBUG 31 | // wether to display debug informations or not 32 | // TIP : Use console.debug, console.warn and console.error 33 | // console.log should be used only in dev and never commited 34 | // this way you can find every console.log very easily 35 | debug: true, 36 | }; 37 | -------------------------------------------------------------------------------- /frontend/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // The file contents for the current environment will overwrite these during build. 2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do 3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead. 4 | // The list of which env maps to which file can be found in `.angular-cli.json`. 5 | 6 | export const environment = { 7 | // PRODUCTION 8 | // angular can optimize some part of his code 9 | // (make more or less checks) according to an environment 10 | production: false, 11 | 12 | // URLBACKEND 13 | // your backend URL 14 | // you can then use it for example in a service 15 | // `${environment.urlBackend}/some/resource` 16 | urlBackend: 'http://localhost:3000', 17 | 18 | // MOCK 19 | // should you keep mocks when building the app 20 | // or hit the real API 21 | mock: true, 22 | 23 | // HTTPDELAY (ms) 24 | // when using mocked data, you can use that 25 | // variable with `.delay` to simulate a network latency 26 | httpDelay: 500, 27 | 28 | // HASHLOCATIONSTRATEGY 29 | // should the URL be 30 | // http://some-domain#/your/app/routes (true) 31 | // or 32 | // http://some-domain/your/app/routes (false) 33 | hashLocationStrategy: false, 34 | 35 | // DEBUG 36 | // wether to display debug informations or not 37 | // TIP : Use console.debug, console.warn and console.error 38 | // console.log should be used only in dev and never commited 39 | // this way you can find every console.log very easily 40 | debug: true, 41 | }; 42 | -------------------------------------------------------------------------------- /frontend/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Pizza Sync 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 20 | 21 | 22 | 23 | Loading... 24 | 25 | 26 | -------------------------------------------------------------------------------- /frontend/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from 'app/app.module'; 5 | import { environment } from 'environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule); 12 | -------------------------------------------------------------------------------- /frontend/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/ 22 | // import 'core-js/es6/symbol'; 23 | // import 'core-js/es6/object'; 24 | // import 'core-js/es6/function'; 25 | // import 'core-js/es6/parse-int'; 26 | // import 'core-js/es6/parse-float'; 27 | // import 'core-js/es6/number'; 28 | // import 'core-js/es6/math'; 29 | // import 'core-js/es6/string'; 30 | // import 'core-js/es6/date'; 31 | // import 'core-js/es6/array'; 32 | // import 'core-js/es6/regexp'; 33 | // import 'core-js/es6/map'; 34 | // import 'core-js/es6/weak-map'; 35 | // import 'core-js/es6/set'; 36 | 37 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 38 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 39 | 40 | /** Evergreen browsers require these. **/ 41 | import 'core-js/es6/reflect'; 42 | import 'core-js/es7/reflect'; 43 | 44 | /** 45 | * Required to support Web Animations `@angular/platform-browser/animations`. 46 | * Needed for: All but Chrome, Firefox and Opera. http://caniuse.com/#feat=web-animation 47 | **/ 48 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 49 | 50 | /*************************************************************************************************** 51 | * Zone JS is required by Angular itself. 52 | */ 53 | import 'zone.js/dist/zone'; // Included with Angular CLI. 54 | 55 | /*************************************************************************************************** 56 | * APPLICATION IMPORTS 57 | */ 58 | 59 | /** 60 | * Date, currency, decimal and percent pipes. 61 | * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10 62 | */ 63 | // import 'intl'; // Run `npm install --save intl`. 64 | /** 65 | * Need to import at least one locale-data with intl. 66 | */ 67 | // import 'intl/locale-data/jsonp/en'; 68 | 69 | // https://stackoverflow.com/a/50377270/2398593 70 | import { Buffer } from 'buffer'; 71 | const global: any = window; 72 | 73 | global.Buffer = global.Buffer || Buffer; 74 | -------------------------------------------------------------------------------- /frontend/src/setup-jest.ts: -------------------------------------------------------------------------------- 1 | import 'jest-preset-angular'; 2 | 3 | // basic mocks for some of the browser features 4 | const mock = () => { 5 | let storage = {}; 6 | return { 7 | getItem: key => (key in storage ? storage[key] : null), 8 | setItem: (key, value) => (storage[key] = value || ''), 9 | removeItem: key => delete storage[key], 10 | clear: () => (storage = {}), 11 | }; 12 | }; 13 | 14 | Object.defineProperty(window, 'localStorage', { value: mock() }); 15 | Object.defineProperty(window, 'sessionStorage', { value: mock() }); 16 | Object.defineProperty(window, 'getComputedStyle', { 17 | value: () => ['-webkit-appearance'], 18 | }); 19 | -------------------------------------------------------------------------------- /frontend/src/styles.scss: -------------------------------------------------------------------------------- 1 | // you can add global styles to this file, and also import other style files 2 | // mainly, with the index barel you probably won't need to import anything 3 | 4 | @import './styles/_index'; 5 | -------------------------------------------------------------------------------- /frontend/src/styles/_index.scss: -------------------------------------------------------------------------------- 1 | // shared 2 | @import './shared/_shared'; 3 | 4 | // themes 5 | // we do not have a barrel for the themes folder (_themes.scss) because we want to import only one theme here 6 | // if you want to compile multiple themes to allow the user to switch from one to another, 7 | @import './styles/themes/_light.theme'; 8 | -------------------------------------------------------------------------------- /frontend/src/styles/shared/_colors.scss: -------------------------------------------------------------------------------- 1 | @import '~@angular/material/theming'; 2 | @include mat-core(); 3 | 4 | $primary: mat-palette($mat-deep-purple); 5 | $accent: mat-palette($mat-amber, A400, A100, A600); 6 | 7 | // will generate .color-{colorName}-x1, .color-{colorName}-x2, etc (same for background and others) 8 | $color-declination: 10; 9 | 10 | // picked from http://www.cssauthor.com/wp-content/uploads/2015/01/Material-Color-Picker.jpg 11 | $red: #f54337; 12 | $pink: #ea1e63; 13 | $purple: #9c28b1; 14 | $deep-purple: #6937b7; 15 | $indigo: #3f51b5; 16 | $blue: #2295f4; 17 | $light-blue: #03a9f5; 18 | $cyan: #00bcd5; 19 | $teal: #00968a; 20 | $green: #4caf50; 21 | $light-green: #8cc34b; 22 | $lime: #cddc39; 23 | $yellow: #ffeb3c; 24 | $amber: #fec107; 25 | $orange: #ff9801; 26 | $deep-orange: #fe5522; 27 | $brown: #775545; 28 | $grey: #9d9e9f; 29 | $white: #ffffff; 30 | $black: #000000; 31 | 32 | $color-palette: ( 33 | 'red': $red, 34 | 'pink': $pink, 35 | 'purple': $purple, 36 | 'deep-purple': $deep-purple, 37 | 'indigo': $indigo, 38 | 'blue': $blue, 39 | 'light-blue': $light-blue, 40 | 'cyan': $cyan, 41 | 'teal': $teal, 42 | 'green': $green, 43 | 'light-green': $light-green, 44 | 'lime': $lime, 45 | 'yellow': $yellow, 46 | 'amber': $amber, 47 | 'orange': $orange, 48 | 'deep-orange': $deep-orange, 49 | 'brown': $brown, 50 | 'grey': $grey, 51 | 'white': $white, 52 | 'black': $black 53 | ); 54 | -------------------------------------------------------------------------------- /frontend/src/styles/shared/_shared.scss: -------------------------------------------------------------------------------- 1 | @import './_utils'; 2 | 3 | html, 4 | body, 5 | app-root { 6 | height: 100%; 7 | margin: 0; 8 | padding: 0; 9 | } 10 | 11 | @font-face { 12 | font-family: Material Icons; 13 | font-style: normal; 14 | font-weight: 400; 15 | src: local(Material Icons), local(MaterialIcons-Regular); 16 | } 17 | 18 | .material-icons { 19 | font-family: 'Material Icons'; 20 | font-weight: normal; 21 | font-style: normal; 22 | font-size: 24px; 23 | line-height: 1; 24 | letter-spacing: normal; 25 | text-transform: none; 26 | display: inline-block; 27 | white-space: nowrap; 28 | word-wrap: normal; 29 | direction: ltr; 30 | font-feature-settings: 'liga'; 31 | -webkit-font-feature-settings: 'liga'; 32 | -webkit-font-smoothing: antialiased; 33 | } 34 | 35 | .green-led { 36 | margin: 0; 37 | width: 10px; 38 | height: 10px; 39 | background-color: #abff00; 40 | border-radius: 50%; 41 | box-shadow: rgba(0, 0, 0, 0.2) 0 -1px 3px 1px, inset #304701 0 -1px 3px, 42 | #89ff00 0 1px 3px; 43 | } 44 | 45 | .cursor-pointer { 46 | cursor: pointer; 47 | } 48 | 49 | .dialog-with-transparent-background { 50 | mat-dialog-container { 51 | background: none; 52 | box-shadow: none; 53 | padding: 0; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /frontend/src/styles/shared/_utils.scss: -------------------------------------------------------------------------------- 1 | @import './_colors'; 2 | 3 | .full-width { 4 | width: 100%; 5 | } 6 | 7 | .text-center { 8 | text-align: center; 9 | } 10 | 11 | .no-border-radius { 12 | border-radius: 0; 13 | } 14 | 15 | .color-primary { 16 | color: mat-color($primary); 17 | } 18 | 19 | .color-accent { 20 | color: mat-color($accent); 21 | } 22 | 23 | .bold { 24 | font-weight: bold; 25 | } 26 | 27 | .light-bold { 28 | font-weight: 500; 29 | } 30 | 31 | .hidden { 32 | display: none; 33 | } 34 | 35 | .color-primary-light-bold { 36 | @extend .color-primary; 37 | @extend .light-bold; 38 | } 39 | 40 | .color-accent-light-bold { 41 | @extend .color-accent; 42 | @extend .light-bold; 43 | } 44 | 45 | .color-primary-bold { 46 | @extend .color-primary; 47 | @extend .bold; 48 | } 49 | 50 | .color-accent-bold { 51 | @extend .color-accent; 52 | @extend .bold; 53 | } 54 | 55 | // --------------------------------------------------------------- 56 | 57 | // generate padding and margin according to directions + px 58 | $sizes: ( 59 | x0: 0, 60 | x1: 10, 61 | x2: 20, 62 | x3: 30, 63 | x4: 40 64 | ); 65 | $directions: '', '-left', '-right', '-top', '-bottom'; 66 | 67 | @each $keySize, $valueSize in $sizes { 68 | @each $dir in $directions { 69 | .padding#{$dir}-#{$keySize} { 70 | padding#{$dir}: #{$valueSize}px !important; 71 | } 72 | 73 | .margin#{$dir}-#{$keySize} { 74 | margin#{$dir}: #{$valueSize}px !important; 75 | } 76 | 77 | .margin#{$dir}-#{$keySize}-no-last:not(:last-child) { 78 | margin#{$dir}: #{$valueSize}px !important; 79 | } 80 | } 81 | } 82 | 83 | // --------------------------------------------------------------- 84 | 85 | // generate the color palette 86 | @mixin generate-colors($name, $colorHexa, $index, $lightOrDark) { 87 | .color-#{$name}-x#{$index} { 88 | color: lighten($colorHexa, 10 * $index); 89 | } 90 | 91 | .background-color-#{$name}-x#{$index} { 92 | background-color: lighten($colorHexa, 10 * $index); 93 | } 94 | 95 | .border-color-#{$name}-x#{$index} { 96 | border-color: lighten($colorHexa, 10 * $index); 97 | } 98 | 99 | .background-color-hover-#{$name}-x#{$index}:hover { 100 | background-color: lighten($colorHexa, 10 * $index); 101 | } 102 | } 103 | 104 | @each $colorName, $colorHexa in $color-palette { 105 | @for $indexColor from 1 through $color-declination { 106 | @include generate-colors($colorName, $colorHexa, $indexColor, ''); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /frontend/src/styles/themes/_light.theme.scss: -------------------------------------------------------------------------------- 1 | @import '../shared/_colors'; 2 | 3 | $theme: mat-light-theme($primary, $accent); 4 | 5 | @include angular-material-theme($theme); 6 | -------------------------------------------------------------------------------- /frontend/src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "baseUrl": "./", 6 | "module": "es2015", 7 | "types": [] 8 | }, 9 | "exclude": [ 10 | "test.ts", 11 | "**/*.spec.ts" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /frontend/src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "baseUrl": "", 5 | "types": [ 6 | "jest", 7 | "node" 8 | ] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /frontend/src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "baseUrl": "./", 6 | "module": "commonjs", 7 | "target": "es5", 8 | "types": [ 9 | "jest", 10 | "node" 11 | ] 12 | }, 13 | "include": [ 14 | "**/*.spec.ts", 15 | "**/*.d.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /frontend/src/typings.d.ts: -------------------------------------------------------------------------------- 1 | /* SystemJS module definition */ 2 | declare var module: NodeModule; 3 | interface NodeModule { 4 | id: string; 5 | } 6 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "outDir": "./dist/out-tsc", 5 | "sourceMap": true, 6 | "declaration": false, 7 | "moduleResolution": "node", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "target": "es5", 11 | "typeRoots": [ 12 | "node_modules/@types" 13 | ], 14 | "lib": [ 15 | "es2017", 16 | "dom" 17 | ], 18 | "module": "es2015", 19 | "baseUrl": "./" 20 | } 21 | } -------------------------------------------------------------------------------- /frontend/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "arrow-return-shorthand": true, 7 | "callable-types": true, 8 | "class-name": true, 9 | "comment-format": [ 10 | true, 11 | "check-space" 12 | ], 13 | "curly": true, 14 | "eofline": true, 15 | "forin": true, 16 | "import-blacklist": [ 17 | true 18 | ], 19 | "import-spacing": true, 20 | "indent": [ 21 | true, 22 | "spaces" 23 | ], 24 | "interface-over-type-literal": true, 25 | "label-position": true, 26 | "max-line-length": [ 27 | true, 28 | 140 29 | ], 30 | "member-access": false, 31 | "member-ordering": [ 32 | true, 33 | { 34 | "order": [ 35 | "static-field", 36 | "instance-field", 37 | "static-method", 38 | "instance-method" 39 | ] 40 | } 41 | ], 42 | "no-arg": true, 43 | "no-bitwise": true, 44 | "no-console": [ 45 | true, 46 | "debug", 47 | "info", 48 | "time", 49 | "timeEnd", 50 | "trace" 51 | ], 52 | "no-construct": true, 53 | "no-debugger": true, 54 | "no-duplicate-super": true, 55 | "no-empty": false, 56 | "no-empty-interface": true, 57 | "no-eval": true, 58 | "no-inferrable-types": [ 59 | true, 60 | "ignore-params" 61 | ], 62 | "no-misused-new": true, 63 | "no-non-null-assertion": true, 64 | "no-shadowed-variable": true, 65 | "no-string-literal": false, 66 | "no-string-throw": true, 67 | "no-switch-case-fall-through": true, 68 | "no-trailing-whitespace": true, 69 | "no-unnecessary-initializer": true, 70 | "no-unused-expression": true, 71 | "no-use-before-declare": true, 72 | "no-var-keyword": true, 73 | "object-literal-sort-keys": false, 74 | "one-line": [ 75 | true, 76 | "check-open-brace", 77 | "check-catch", 78 | "check-else", 79 | "check-whitespace" 80 | ], 81 | "prefer-const": true, 82 | "quotemark": [ 83 | true, 84 | "single" 85 | ], 86 | "radix": true, 87 | "semicolon": [ 88 | true, 89 | "always" 90 | ], 91 | "triple-equals": [ 92 | true, 93 | "allow-null-check" 94 | ], 95 | "typedef-whitespace": [ 96 | true, 97 | { 98 | "call-signature": "nospace", 99 | "index-signature": "nospace", 100 | "parameter": "nospace", 101 | "property-declaration": "nospace", 102 | "variable-declaration": "nospace" 103 | } 104 | ], 105 | "unified-signatures": true, 106 | "variable-name": false, 107 | "whitespace": [ 108 | true, 109 | "check-branch", 110 | "check-decl", 111 | "check-operator", 112 | "check-separator", 113 | "check-type" 114 | ], 115 | "directive-selector": [ 116 | true, 117 | "attribute", 118 | "app", 119 | "camelCase" 120 | ], 121 | "component-selector": [ 122 | true, 123 | "element", 124 | "app", 125 | "kebab-case" 126 | ], 127 | "use-input-property-decorator": true, 128 | "use-output-property-decorator": true, 129 | "use-host-property-decorator": true, 130 | "no-input-rename": true, 131 | "no-output-rename": true, 132 | "use-life-cycle-interface": true, 133 | "use-pipe-transform-interface": true, 134 | "component-class-suffix": true, 135 | "directive-class-suffix": true, 136 | "no-unused-variable": true, 137 | "ordered-imports": true 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /logo/png/PS_icon_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/logo/png/PS_icon_128.png -------------------------------------------------------------------------------- /logo/png/PS_icon_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/logo/png/PS_icon_32.png -------------------------------------------------------------------------------- /logo/png/PS_icon_monochrome_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/logo/png/PS_icon_monochrome_128.png -------------------------------------------------------------------------------- /logo/png/PS_icon_monochrome_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/logo/png/PS_icon_monochrome_32.png -------------------------------------------------------------------------------- /logo/png/PS_logo_high_res.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/logo/png/PS_logo_high_res.png -------------------------------------------------------------------------------- /logo/png/PS_logo_high_res_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/logo/png/PS_logo_high_res_2.png -------------------------------------------------------------------------------- /logo/png/PS_logo_low_res.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/logo/png/PS_logo_low_res.png -------------------------------------------------------------------------------- /logo/png/PS_logo_low_res_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/logo/png/PS_logo_low_res_2.png -------------------------------------------------------------------------------- /logo/png/PS_logo_monochrome_high_res.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/logo/png/PS_logo_monochrome_high_res.png -------------------------------------------------------------------------------- /logo/png/PS_logo_monochrome_high_res_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/logo/png/PS_logo_monochrome_high_res_2.png -------------------------------------------------------------------------------- /logo/png/PS_logo_monochrome_low_res.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/logo/png/PS_logo_monochrome_low_res.png -------------------------------------------------------------------------------- /logo/png/PS_logo_monochrome_low_res_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/logo/png/PS_logo_monochrome_low_res_2.png -------------------------------------------------------------------------------- /logo/png/PS_logo_only_sign_ high_res.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/logo/png/PS_logo_only_sign_ high_res.png -------------------------------------------------------------------------------- /logo/png/PS_logo_only_sign_low_res.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/logo/png/PS_logo_only_sign_low_res.png -------------------------------------------------------------------------------- /logo/png/PS_logo_only_sign_monochrome_high_res.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/logo/png/PS_logo_only_sign_monochrome_high_res.png -------------------------------------------------------------------------------- /logo/png/PS_logo_only_sign_monochrome_low_res.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/logo/png/PS_logo_only_sign_monochrome_low_res.png -------------------------------------------------------------------------------- /logo/psd/PS_logo.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/logo/psd/PS_logo.psd -------------------------------------------------------------------------------- /logo/psd/PS_logo_monochrome.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/logo/psd/PS_logo_monochrome.psd -------------------------------------------------------------------------------- /logo/psd/PS_logo_only_sign.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/logo/psd/PS_logo_only_sign.psd -------------------------------------------------------------------------------- /logo/psd/PS_logo_only_sign_monochrome.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/logo/psd/PS_logo_only_sign_monochrome.psd -------------------------------------------------------------------------------- /logo/svg/PS_logo_monochrome.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 18 | 19 | 20 | 21 | 29 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 57 | 59 | 60 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /logo/svg/PS_logo_only_sign.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 11 | 12 | 13 | 14 | 16 | 24 | 31 | 34 | 36 | 37 | 38 | 39 | 41 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /logo/svg/PS_logo_only_sign_monochrome.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 18 | 19 | 20 | 21 | 29 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /logo/vector/PS_icons.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/logo/vector/PS_icons.ai -------------------------------------------------------------------------------- /logo/vector/PS_icons.eps: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/logo/vector/PS_icons.eps -------------------------------------------------------------------------------- /logo/vector/PS_logo.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/logo/vector/PS_logo.ai -------------------------------------------------------------------------------- /logo/vector/PS_logo.eps: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/logo/vector/PS_logo.eps -------------------------------------------------------------------------------- /logo/vector/PS_logo_monochrome.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/logo/vector/PS_logo_monochrome.ai -------------------------------------------------------------------------------- /logo/vector/PS_logo_monochrome.eps: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/logo/vector/PS_logo_monochrome.eps -------------------------------------------------------------------------------- /logo/vector/PS_logo_only_sign.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/logo/vector/PS_logo_only_sign.ai -------------------------------------------------------------------------------- /logo/vector/PS_logo_only_sign.eps: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/logo/vector/PS_logo_only_sign.eps -------------------------------------------------------------------------------- /logo/vector/PS_logo_only_sign_monochrome.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/logo/vector/PS_logo_only_sign_monochrome.ai -------------------------------------------------------------------------------- /logo/vector/PS_logo_only_sign_monochrome.eps: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime1992/pizza-sync/52cb2d0d87eba5ca38b92b4ffe6a6d8964f11ca3/logo/vector/PS_logo_only_sign_monochrome.eps -------------------------------------------------------------------------------- /nginx.conf: -------------------------------------------------------------------------------- 1 | events { 2 | worker_connections 4096; 3 | } 4 | 5 | http { 6 | include /etc/nginx/mime.types; 7 | 8 | server { 9 | listen 80; 10 | server_name pizza-sync.com; 11 | 12 | root /usr/share/nginx/html; 13 | index index.html; 14 | 15 | location / { 16 | try_files $uri /index.html; 17 | } 18 | 19 | location ~ \.css { 20 | add_header Content-Type text/css; 21 | } 22 | 23 | location /initial-state { 24 | proxy_pass http://pizza-sync-api:3000; 25 | proxy_http_version 1.1; 26 | proxy_set_header Upgrade $http_upgrade; 27 | proxy_set_header Connection 'upgrade'; 28 | proxy_set_header Host $host; 29 | proxy_cache_bypass $http_upgrade; 30 | proxy_set_header X-Real-IP $remote_addr; 31 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 32 | } 33 | 34 | location /socket.io/ { 35 | proxy_pass http://pizza-sync-api:3000; 36 | proxy_http_version 1.1; 37 | proxy_set_header Upgrade $http_upgrade; 38 | proxy_set_header Connection "Upgrade"; 39 | } 40 | 41 | location ~* .(jpg|jpeg|png|gif|ico|css|js)$ { 42 | expires 1d; 43 | } 44 | } 45 | } 46 | --------------------------------------------------------------------------------