├── src ├── assets │ ├── .gitkeep │ └── images │ │ ├── macbook.jpg │ │ ├── package.svg │ │ └── world.svg ├── app │ ├── app.component.css │ ├── auth │ │ ├── index.component.scss │ │ ├── login │ │ │ ├── login.component.css │ │ │ ├── login.component.html │ │ │ └── login.component.ts │ │ ├── profile │ │ │ ├── profile.component.css │ │ │ ├── profile.component.ts │ │ │ ├── profile.component.spec.ts │ │ │ └── profile.component.html │ │ ├── register │ │ │ ├── register.component.css │ │ │ ├── register.component.spec.ts │ │ │ ├── register.component.html │ │ │ └── register.component.ts │ │ ├── index.component.ts │ │ ├── auth.routing.ts │ │ ├── auth.module.ts │ │ └── index.component.html │ ├── components │ │ ├── home │ │ │ ├── home.component.css │ │ │ ├── home.component.spec.ts │ │ │ ├── home.component.ts │ │ │ └── home.component.html │ │ ├── about │ │ │ ├── about.component.css │ │ │ ├── about.component.html │ │ │ ├── about.component.ts │ │ │ └── about.component.spec.ts │ │ └── page-not-found │ │ │ ├── page-not-found.component.ts │ │ │ ├── page-not-found.component.html │ │ │ └── page-not-found.component.scss │ ├── cart │ │ ├── cart-summary │ │ │ ├── cart-summary.component.css │ │ │ ├── cart-summary.component.html │ │ │ └── cart-summary.component.ts │ │ ├── cart-summary-actions │ │ │ ├── cart-summary-actions.component.css │ │ │ ├── cart-summary-actions.component.html │ │ │ └── cart-summary-actions.component.ts │ │ ├── my-cart │ │ │ ├── my-cart.component.css │ │ │ ├── my-cart.component.ts │ │ │ └── my-cart.component.html │ │ ├── cart.routing.ts │ │ └── cart.module.ts │ ├── orders │ │ ├── order-create │ │ │ ├── checkout.component.css │ │ │ ├── checkout.component.spec.ts │ │ │ ├── checkout.component.ts │ │ │ └── checkout.component.html │ │ ├── order-list │ │ │ ├── order-list.component.css │ │ │ ├── order-list.component.html │ │ │ ├── order-list.component.spec.ts │ │ │ └── order-list.component.ts │ │ ├── order-details │ │ │ ├── order-details.component.css │ │ │ ├── order-details.component.html │ │ │ ├── order-details.component.spec.ts │ │ │ └── order-details.component.ts │ │ ├── orders.routing.ts │ │ └── orders.module.ts │ ├── shared │ │ ├── components │ │ │ ├── footer │ │ │ │ ├── footer.component.css │ │ │ │ ├── footer.component.html │ │ │ │ ├── footer.component.ts │ │ │ │ └── footer.component.spec.ts │ │ │ ├── header │ │ │ │ ├── header.component.css │ │ │ │ ├── header.component.spec.ts │ │ │ │ ├── header.component.ts │ │ │ │ └── header.component.html │ │ │ ├── carousel │ │ │ │ ├── carousel.component.css │ │ │ │ ├── carousel.component.ts │ │ │ │ ├── carousel.component.spec.ts │ │ │ │ └── carousel.component.html │ │ │ └── pagination │ │ │ │ ├── pagination.component.css │ │ │ │ ├── pagination.component.spec.ts │ │ │ │ ├── pagination.component.ts │ │ │ │ └── pagination.component.html │ │ ├── dtos │ │ │ ├── requests │ │ │ │ ├── base.dto.ts │ │ │ │ ├── create_order.dto.ts │ │ │ │ ├── login.dto.ts │ │ │ │ └── register.dto.ts │ │ │ ├── local │ │ │ │ ├── products.dto.ts │ │ │ │ ├── notifications.dto.ts │ │ │ │ └── base.ts │ │ │ └── responses │ │ │ │ ├── auth │ │ │ │ ├── auth-info.dto.ts │ │ │ │ └── login-success.dto.ts │ │ │ │ ├── order_items │ │ │ │ └── order-item.dto.ts │ │ │ │ ├── orders │ │ │ │ ├── order-list.dto.ts │ │ │ │ └── order-details.response.ts │ │ │ │ ├── users │ │ │ │ └── auth.dto.ts │ │ │ │ ├── pages │ │ │ │ └── home.dto.ts │ │ │ │ ├── shared │ │ │ │ ├── base.dto.ts │ │ │ │ └── page-meta.dto.ts │ │ │ │ ├── comments │ │ │ │ └── comment-submitted.response.ts │ │ │ │ ├── addresses │ │ │ │ └── addresses.dto.ts │ │ │ │ └── products │ │ │ │ └── products.dto.ts │ │ ├── models │ │ │ ├── cart-item.model.ts │ │ │ ├── category.model.ts │ │ │ ├── tag.model.ts │ │ │ ├── wishlist.model.ts │ │ │ ├── address.model.ts │ │ │ ├── order.model.ts │ │ │ ├── shopping-cart.model.ts │ │ │ ├── comment.model.ts │ │ │ ├── contact_info.model.ts │ │ │ ├── user.ts │ │ │ └── product.ts │ │ ├── services │ │ │ ├── interfaces │ │ │ │ ├── IStorageService.ts │ │ │ │ └── IAuthService.ts │ │ │ ├── addresses.service.spec.ts │ │ │ ├── local-storage.service.ts │ │ │ ├── notification.service.ts │ │ │ ├── addresses.service.ts │ │ │ ├── pages.service.ts │ │ │ ├── orders.service.ts │ │ │ ├── users.service.ts │ │ │ ├── shopping-cart.service.ts │ │ │ └── products.service.ts │ │ ├── guards │ │ │ ├── admin.guard.ts │ │ │ └── authentication.guard.ts │ │ ├── utils │ │ │ └── net.utils.ts │ │ ├── shared.module.ts │ │ └── interceptors │ │ │ └── jwt-http.interceptor.ts │ ├── addresses │ │ ├── address-list │ │ │ ├── address-list.component.css │ │ │ ├── address-list.component.html │ │ │ ├── address-list.component.spec.ts │ │ │ └── address-list.component.ts │ │ ├── address-create │ │ │ ├── address-create.component.css │ │ │ ├── address-create.component.html │ │ │ ├── address-create.component.ts │ │ │ └── address-create.component.spec.ts │ │ ├── address-details │ │ │ ├── address-details.component.css │ │ │ ├── address-details.component.html │ │ │ ├── address-details.component.ts │ │ │ └── address-details.component.spec.ts │ │ └── addresses.module.ts │ ├── products │ │ ├── product-list │ │ │ ├── product-list.component.css │ │ │ ├── product-list.component.spec.ts │ │ │ ├── product-list.component.ts │ │ │ └── product-list.component.html │ │ ├── product-create │ │ │ ├── product-create.component.css │ │ │ ├── product-create.component.spec.ts │ │ │ ├── product-create.component.html │ │ │ └── product-create.component.ts │ │ ├── product-details │ │ │ ├── product-details.component.css │ │ │ ├── product-details.component.spec.ts │ │ │ ├── product-details.component.html │ │ │ └── product-details.component.ts │ │ ├── products.routing.ts │ │ └── products.module.ts │ ├── app.component.html │ ├── app.component.ts │ ├── app-routing.module.ts │ ├── app.component.spec.ts │ └── app.module.ts ├── favicon.ico ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── tsconfig.app.json ├── tsconfig.spec.json ├── tslint.json ├── main.ts ├── browserslist ├── test.ts ├── karma.conf.js ├── index.html ├── polyfills.ts └── styles.css ├── e2e ├── src │ ├── app.po.ts │ └── app.e2e-spec.ts ├── tsconfig.e2e.json └── protractor.conf.js ├── .editorconfig ├── tsconfig.json ├── .gitignore ├── package.json ├── tslint.json ├── angular.json └── README.md /src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/app.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/auth/index.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/auth/login/login.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/auth/profile/profile.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/components/home/home.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/auth/register/register.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/components/about/about.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/cart/cart-summary/cart-summary.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/orders/order-create/checkout.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/orders/order-list/order-list.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/shared/components/footer/footer.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/shared/components/header/header.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/addresses/address-list/address-list.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/orders/order-details/order-details.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/products/product-list/product-list.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/shared/components/carousel/carousel.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/shared/components/footer/footer.component.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/addresses/address-create/address-create.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/addresses/address-details/address-details.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/products/product-create/product-create.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/products/product-details/product-details.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/shared/components/pagination/pagination.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/cart/cart-summary-actions/cart-summary-actions.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/addresses/address-create/address-create.component.html: -------------------------------------------------------------------------------- 1 | images 2 | -------------------------------------------------------------------------------- /src/app/components/about/about.component.html: -------------------------------------------------------------------------------- 1 |

2 | about works! 3 |

4 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melardev/AngularEcommerceRestApi/HEAD/src/favicon.ico -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/app/addresses/address-details/address-details.component.html: -------------------------------------------------------------------------------- 1 |

2 | address-details works! 3 |

4 | -------------------------------------------------------------------------------- /src/assets/images/macbook.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melardev/AngularEcommerceRestApi/HEAD/src/assets/images/macbook.jpg -------------------------------------------------------------------------------- /src/app/shared/dtos/requests/base.dto.ts: -------------------------------------------------------------------------------- 1 | export class PaginatedRequestDto { 2 | page: number; 3 | pageSize: number; 4 | } 5 | -------------------------------------------------------------------------------- /src/app/shared/models/cart-item.model.ts: -------------------------------------------------------------------------------- 1 | import {Product} from './product'; 2 | 3 | export class CartItem extends Product { 4 | isInCart = true; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/shared/models/category.model.ts: -------------------------------------------------------------------------------- 1 | export class Category { 2 | id: string; 3 | name: string; 4 | slug: string; 5 | descritpion: string; 6 | } 7 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 |
6 | -------------------------------------------------------------------------------- /src/app/shared/models/tag.model.ts: -------------------------------------------------------------------------------- 1 | export class Tag { 2 | id: string; 3 | name: string; 4 | slug: string; 5 | descritpion: string; 6 | image_urls: string[]; 7 | } 8 | -------------------------------------------------------------------------------- /src/app/shared/models/wishlist.model.ts: -------------------------------------------------------------------------------- 1 | import {Product} from './product'; 2 | 3 | export class WishList { 4 | name: string; 5 | products: Product[]; 6 | createdAt: Date; 7 | } 8 | -------------------------------------------------------------------------------- /src/app/shared/services/interfaces/IStorageService.ts: -------------------------------------------------------------------------------- 1 | interface IStorageService { 2 | get(key: string); 3 | 4 | set(key: string, value: string); 5 | 6 | clear(key: string); 7 | } 8 | -------------------------------------------------------------------------------- /src/app/shared/dtos/local/products.dto.ts: -------------------------------------------------------------------------------- 1 | import {ProductDto} from '../responses/products/products.dto'; 2 | 3 | export class ProductLocalDto extends ProductDto { 4 | isInCart: boolean; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/shared/services/interfaces/IAuthService.ts: -------------------------------------------------------------------------------- 1 | export interface IAuthService { 2 | register(key: string); 3 | 4 | login(key: string, value: string); 5 | 6 | logout(key: string); 7 | } 8 | -------------------------------------------------------------------------------- /src/app/shared/dtos/responses/auth/auth-info.dto.ts: -------------------------------------------------------------------------------- 1 | // Not implemented yet neither on the server, nor the client application 2 | export class AuthInfo { 3 | canDelete: boolean; 4 | canEdit: boolean; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/shared/dtos/responses/order_items/order-item.dto.ts: -------------------------------------------------------------------------------- 1 | export class OrderItemDto { 2 | id: string; 3 | name: string; 4 | slug: string; 5 | price: number; 6 | image_urls: string[]; 7 | quantity: number; 8 | } 9 | -------------------------------------------------------------------------------- /src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "types": [] 6 | }, 7 | "exclude": [ 8 | "test.ts", 9 | "**/*.spec.ts" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /src/app/shared/models/address.model.ts: -------------------------------------------------------------------------------- 1 | export class Address { 2 | id: number; 3 | first_name: string; 4 | last_name: string; 5 | street_address: string; 6 | city: string; 7 | country: string; 8 | zip_code: string; 9 | } 10 | -------------------------------------------------------------------------------- /src/app/shared/dtos/requests/create_order.dto.ts: -------------------------------------------------------------------------------- 1 | import {Product} from '../../models/product'; 2 | import {ContactInfo} from '../../models/contact_info.model'; 3 | 4 | export class CreateOrderDto { 5 | products: Product[]; 6 | contactInfo: ContactInfo; 7 | } 8 | -------------------------------------------------------------------------------- /src/app/shared/dtos/responses/orders/order-list.dto.ts: -------------------------------------------------------------------------------- 1 | import {Order} from '../../../models/order.model'; 2 | import {PagedResponseDto} from '../shared/page-meta.dto'; 3 | 4 | 5 | export class OrderListDto extends PagedResponseDto { 6 | orders: Order[]; 7 | } 8 | -------------------------------------------------------------------------------- /src/app/shared/dtos/requests/login.dto.ts: -------------------------------------------------------------------------------- 1 | export class LoginRequestDto { 2 | username: string; 3 | password: string; 4 | 5 | constructor(username: string, password: string) { 6 | this.username = username; 7 | this.password = password; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getParagraphText() { 9 | return element(by.css('app-root h1')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/app/shared/dtos/responses/users/auth.dto.ts: -------------------------------------------------------------------------------- 1 | import {BaseAppDtoResponse} from '../shared/base.dto'; 2 | import {User} from '../../../models/user'; 3 | 4 | export class LoginDtoResponse extends BaseAppDtoResponse { 5 | token: string; 6 | scheme: string; 7 | user: User; 8 | } 9 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | templateUrl: './app.component.html', 6 | styleUrls: ['./app.component.css'] 7 | }) 8 | export class AppComponent { 9 | title = 'AngularShopApp'; 10 | } 11 | -------------------------------------------------------------------------------- /e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } -------------------------------------------------------------------------------- /src/app/cart/cart-summary-actions/cart-summary-actions.component.html: -------------------------------------------------------------------------------- 1 | Continue Shipping 2 | Checkout 3 | Clear cart 4 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /src/app/cart/my-cart/my-cart.component.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | .remove-product { 4 | width: 25px; 5 | height: 25px; 6 | border-radius: 50%; 7 | border: 0; 8 | background-color: #E0E0E0; 9 | color: #fff; 10 | cursor: pointer; 11 | } 12 | 13 | .remove-product:hover { 14 | background-color: #e02820; 15 | } 16 | -------------------------------------------------------------------------------- /src/app/cart/cart.routing.ts: -------------------------------------------------------------------------------- 1 | import {RouterModule, Routes} from '@angular/router'; 2 | import {MyCartComponent} from './my-cart/my-cart.component'; 3 | 4 | const routes: Routes = [ 5 | { 6 | path: '', component: MyCartComponent 7 | }, 8 | ]; 9 | 10 | export const CartRouter = RouterModule.forChild(routes); 11 | -------------------------------------------------------------------------------- /src/app/shared/models/order.model.ts: -------------------------------------------------------------------------------- 1 | import {Address} from './address.model'; 2 | 3 | export class Order { 4 | id: number; 5 | order_status: string; 6 | tracking_number: string; 7 | order_items_count: number; 8 | total: number; 9 | created_at: string; 10 | updated_at: string; 11 | address?: Address; 12 | } 13 | -------------------------------------------------------------------------------- /src/app/shared/dtos/responses/pages/home.dto.ts: -------------------------------------------------------------------------------- 1 | import {BaseAppDtoResponse} from '../shared/base.dto'; 2 | import {Tag} from '../../../models/tag.model'; 3 | import {Category} from '../../../models/category.model'; 4 | 5 | export class HomeResponseDto extends BaseAppDtoResponse { 6 | tags: Tag[]; 7 | categories: Category[]; 8 | } 9 | -------------------------------------------------------------------------------- /src/app/auth/index.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-index', 5 | templateUrl: './index.component.html', 6 | styleUrls: ['./index.component.scss'] 7 | }) 8 | export class IndexComponent implements OnInit { 9 | constructor() {} 10 | 11 | ngOnInit() {} 12 | } 13 | -------------------------------------------------------------------------------- /src/app/shared/models/shopping-cart.model.ts: -------------------------------------------------------------------------------- 1 | import {Product} from './product'; 2 | 3 | export class ShoppingCart { 4 | cartItems: Product[]; 5 | created: number; 6 | lastUpdated: number; 7 | 8 | constructor() { 9 | this.cartItems = []; 10 | this.created = Date.now(); 11 | this.lastUpdated = Date.now(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/app/shared/dtos/responses/shared/base.dto.ts: -------------------------------------------------------------------------------- 1 | export class BaseAppDtoResponse { 2 | success: boolean; 3 | full_messages: string[]; 4 | } 5 | 6 | export class ErrorAppDtoResponse extends BaseAppDtoResponse { 7 | success = false; 8 | } 9 | 10 | export class SuccessAppDtoResponse extends BaseAppDtoResponse { 11 | success = true; 12 | } 13 | -------------------------------------------------------------------------------- /src/app/orders/order-details/order-details.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | Name: {{cartItem.name}} 4 | SubTotal: {{cartItem.quantity}} x {{cartItem.price}} $ 5 | Total: {{order.total}} 6 |
7 | Product 8 |
9 | -------------------------------------------------------------------------------- /src/app/shared/dtos/local/notifications.dto.ts: -------------------------------------------------------------------------------- 1 | export class NotificationsDto { 2 | constructor(public type: string, public message: string) { 3 | } 4 | } 5 | 6 | export class NotificationTypes { 7 | public static SUCCESS_TYPE = 'success'; 8 | public static ERROR_TYPE = 'error'; 9 | } 10 | 11 | // export type NotificationType = 'success' | 'error'; 12 | -------------------------------------------------------------------------------- /src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "test.ts", 12 | "polyfills.ts" 13 | ], 14 | "include": [ 15 | "**/*.spec.ts", 16 | "**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /src/app/components/about/about.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-about', 5 | templateUrl: './about.component.html', 6 | styleUrls: ['./about.component.css'] 7 | }) 8 | export class AboutComponent implements OnInit { 9 | 10 | constructor() { } 11 | 12 | ngOnInit() { 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/app/shared/dtos/responses/comments/comment-submitted.response.ts: -------------------------------------------------------------------------------- 1 | 2 | import {User} from '../../../models/user'; 3 | import {BaseAppDtoResponse} from '../shared/base.dto'; 4 | 5 | 6 | export class CommentSubmittedResponse extends BaseAppDtoResponse { 7 | id: number; 8 | username: string; 9 | created_at: string; 10 | content: string; 11 | user?: User; 12 | } 13 | -------------------------------------------------------------------------------- /src/app/shared/components/footer/footer.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-footer', 5 | templateUrl: './footer.component.html', 6 | styleUrls: ['./footer.component.css'] 7 | }) 8 | export class FooterComponent implements OnInit { 9 | 10 | constructor() { } 11 | 12 | ngOnInit() { 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | 3 | describe('workspace-project App', () => { 4 | let page: AppPage; 5 | 6 | beforeEach(() => { 7 | page = new AppPage(); 8 | }); 9 | 10 | it('should display welcome message', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toEqual('Welcome to AngularShopApp!'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /src/app/shared/dtos/requests/register.dto.ts: -------------------------------------------------------------------------------- 1 | export class RegisterDto { 2 | name: string; 3 | username: string; 4 | email: string; 5 | password: string; 6 | 7 | constructor(name: string, username: string, email: string, password: string) { 8 | this.name = name; 9 | this.username = username; 10 | this.email = email; 11 | this.password = password; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/app/shared/components/carousel/carousel.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-carousel', 5 | templateUrl: './carousel.component.html', 6 | styleUrls: ['./carousel.component.css'] 7 | }) 8 | export class CarouselComponent implements OnInit { 9 | 10 | constructor() { } 11 | 12 | ngOnInit() { 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/app/shared/dtos/local/base.ts: -------------------------------------------------------------------------------- 1 | import {BaseAppDtoResponse, ErrorAppDtoResponse, SuccessAppDtoResponse} from '../responses/shared/base.dto'; 2 | 3 | // Created only for readability 4 | export class BaseAppResult extends BaseAppDtoResponse { 5 | } 6 | 7 | export class ErrorResult extends ErrorAppDtoResponse { 8 | } 9 | 10 | export class SuccessResult extends SuccessAppDtoResponse { 11 | } 12 | 13 | -------------------------------------------------------------------------------- /src/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "app", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "app", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/app/orders/order-list/order-list.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | Status: {{order.order_status}}
4 | Tracking Number: {{order.tracking_number}}
5 | Products in this order: {{order.order_items_count}}
6 | Total: {{order.total}} $
7 |
8 | Details 9 |
10 | -------------------------------------------------------------------------------- /src/app/addresses/address-create/address-create.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-address-create', 5 | templateUrl: './address-create.component.html', 6 | styleUrls: ['./address-create.component.css'] 7 | }) 8 | export class AddressCreateComponent implements OnInit { 9 | 10 | constructor() { } 11 | 12 | ngOnInit() { 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/app/components/page-not-found/page-not-found.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-page-not-found', 5 | templateUrl: './page-not-found.component.html', 6 | styleUrls: ['./page-not-found.component.scss'] 7 | }) 8 | export class PageNotFoundComponent implements OnInit { 9 | constructor() { 10 | 11 | } 12 | 13 | ngOnInit() { 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/app/addresses/address-details/address-details.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-address-details', 5 | templateUrl: './address-details.component.html', 6 | styleUrls: ['./address-details.component.css'] 7 | }) 8 | export class AddressDetailsComponent implements OnInit { 9 | 10 | constructor() { } 11 | 12 | ngOnInit() { 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /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 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /src/app/shared/dtos/responses/addresses/addresses.dto.ts: -------------------------------------------------------------------------------- 1 | import {PagedResponseDto} from '../shared/page-meta.dto'; 2 | 3 | export class AddressDto { 4 | id: string; 5 | first_name: string; 6 | last_name: string; 7 | street_address: string; 8 | city: string; 9 | country: string; 10 | zip_code: string; 11 | } 12 | 13 | export class AddressListResponseDto extends PagedResponseDto { 14 | addresses: AddressDto[]; 15 | } 16 | -------------------------------------------------------------------------------- /src/app/shared/dtos/responses/auth/login-success.dto.ts: -------------------------------------------------------------------------------- 1 | import {BaseAppDtoResponse} from '../shared/base.dto'; 2 | 3 | 4 | export class UserData { 5 | username: string; 6 | email: string; 7 | first_name: string; 8 | last_name: string; 9 | roles: string[]; 10 | expiry: number; 11 | } 12 | 13 | export class LoginSuccessDto extends BaseAppDtoResponse { 14 | token: string; 15 | scheme: string; 16 | user: UserData; 17 | } 18 | -------------------------------------------------------------------------------- /src/app/shared/services/addresses.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { AddressesService } from './addresses.service'; 4 | 5 | describe('AddressesService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: AddressesService = TestBed.get(AddressesService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/browserslist: -------------------------------------------------------------------------------- 1 | # This file is currently used by autoprefixer to adjust CSS to support the below specified browsers 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | # 5 | # For IE 9-11 support, please remove 'not' from the last line of the file and adjust as needed 6 | 7 | > 0.5% 8 | last 2 versions 9 | Firefox ESR 10 | not dead 11 | not IE 9-11 -------------------------------------------------------------------------------- /src/app/addresses/address-list/address-list.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | First Name: {{address.first_name}}
4 | Last Name: {{address.last_name}}
5 | Street address: {{address.street_address}}
6 | City: {{address.city}}
7 | Country: {{address.city}}
8 | Zip Code: {{address.zip_code}}
9 |
10 | Details 11 |
12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "module": "es2015", 9 | "moduleResolution": "node", 10 | "emitDecoratorMetadata": true, 11 | "experimentalDecorators": true, 12 | "target": "es5", 13 | "typeRoots": [ 14 | "node_modules/@types" 15 | ], 16 | "lib": [ 17 | "es2018", 18 | "dom" 19 | ] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/app/shared/models/comment.model.ts: -------------------------------------------------------------------------------- 1 | import {User} from './user'; 2 | 3 | export class Comment { 4 | constructor(params: any = {}) { 5 | this.id = params.id; 6 | this.user = params.user; 7 | this.username = params.username; 8 | this.productId = params.product_id; 9 | this.content = params.content; 10 | this.createdAt = params.created_at; 11 | } 12 | 13 | id: number; 14 | user?: User; 15 | username: string; 16 | productId?: number; 17 | content: string; 18 | createdAt: string; 19 | } 20 | -------------------------------------------------------------------------------- /src/app/shared/guards/admin.guard.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {CanActivate, Router} from '@angular/router'; 3 | import {UsersService} from '../services/users.service'; 4 | 5 | 6 | @Injectable() 7 | export class AdminGuard implements CanActivate { 8 | constructor(private router: Router, private authService: UsersService) { 9 | } 10 | 11 | canActivate() { 12 | if (this.authService.isLoggedInSync() && this.authService.isAdminSync()) { 13 | return true; 14 | } 15 | this.router.navigate(['home']); 16 | return false; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/app/auth/auth.routing.ts: -------------------------------------------------------------------------------- 1 | import {LoginComponent} from './login/login.component'; 2 | import {Routes} from '@angular/router'; 3 | import {RegisterComponent} from './register/register.component'; 4 | import {ProfileComponent} from './profile/profile.component'; 5 | import {PageNotFoundComponent} from '../components/page-not-found/page-not-found.component'; 6 | 7 | export const AuthRoutes: Routes = [ 8 | { 9 | path: 'login', 10 | component: LoginComponent 11 | }, 12 | { 13 | path: 'register', 14 | component: RegisterComponent 15 | }, 16 | { 17 | path: 'profile', 18 | component: ProfileComponent 19 | } 20 | ]; 21 | -------------------------------------------------------------------------------- /src/app/shared/dtos/responses/shared/page-meta.dto.ts: -------------------------------------------------------------------------------- 1 | import {BaseAppDtoResponse} from './base.dto'; 2 | 3 | 4 | export class PageMeta { 5 | has_prev_page: boolean; 6 | has_next_page: boolean; 7 | current_page_number: number; 8 | 9 | next_page_number: number; 10 | prev_page_number: number; 11 | 12 | next_page_url: string; 13 | prev_page_url: string; 14 | 15 | offset: number; 16 | requested_page_size: number; 17 | current_items_count: number; 18 | 19 | number_of_pages: number; 20 | total_items_count: number; 21 | } 22 | 23 | export class PagedResponseDto extends BaseAppDtoResponse { 24 | page_meta: PageMeta; 25 | } 26 | -------------------------------------------------------------------------------- /src/app/shared/dtos/responses/orders/order-details.response.ts: -------------------------------------------------------------------------------- 1 | 2 | import {Address} from '../../../models/address.model'; 3 | import {OrderItemDto} from '../order_items/order-item.dto'; 4 | import {BaseAppDtoResponse} from '../shared/base.dto'; 5 | 6 | export enum OrderStatus { 7 | Processing, Processed, Delivered, Shipped 8 | } 9 | 10 | export class OrderDetailsDto extends BaseAppDtoResponse { 11 | id: number; 12 | order_status: string; 13 | tracking_number: string; 14 | order_items: OrderItemDto[]; 15 | address: Address; 16 | 17 | total: number; 18 | totlalAmount: number; 19 | createdAt: string; 20 | updatedAt: string; 21 | } 22 | -------------------------------------------------------------------------------- /src/app/cart/cart-summary-actions/cart-summary-actions.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import {ShoppingCartService} from '../../shared/services/shopping-cart.service'; 3 | 4 | @Component({ 5 | selector: 'app-cart-summary-actions', 6 | templateUrl: './cart-summary-actions.component.html', 7 | styleUrls: ['./cart-summary-actions.component.css'] 8 | }) 9 | export class CartSummaryActionsComponent implements OnInit { 10 | 11 | constructor(private shoppingCartService: ShoppingCartService) { 12 | } 13 | 14 | ngOnInit() { 15 | } 16 | 17 | clearCart() { 18 | this.shoppingCartService.clearCart(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/app/orders/orders.routing.ts: -------------------------------------------------------------------------------- 1 | import {Routes} from '@angular/router'; 2 | import {CheckoutComponent} from './order-create/checkout.component'; 3 | import {OrderListComponent} from './order-list/order-list.component'; 4 | import {OrderDetailsComponent} from './order-details/order-details.component'; 5 | 6 | 7 | export const orderRoutes: Routes = [ 8 | { 9 | path: 'create', 10 | component: CheckoutComponent 11 | }, 12 | { 13 | path: '', redirectTo: '/orders/list', pathMatch: 'full' 14 | }, 15 | { 16 | path: 'list', component: OrderListComponent 17 | } 18 | , 19 | { 20 | path: ':id', component: OrderDetailsComponent 21 | } 22 | ]; 23 | -------------------------------------------------------------------------------- /src/app/shared/models/contact_info.model.ts: -------------------------------------------------------------------------------- 1 | export class ContactInfo { 2 | constructor(obj: any) { 3 | if (typeof obj === 'object') { 4 | this.firstName = obj.first_name; 5 | this.lastName = obj.last_name; 6 | this.email = obj.email; 7 | this.address = obj.address; 8 | this.country = obj.country; 9 | this.zipCode = obj.zip_code; 10 | this.city = obj.city; 11 | this.cardNumber = obj.cardNumber; 12 | } 13 | } 14 | 15 | firstName: string; 16 | lastName: string; 17 | email: string; 18 | address: string; 19 | country: string; 20 | city: string; 21 | zipCode: string; 22 | cardNumber: string; 23 | } 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | 8 | # dependencies 9 | /node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | 20 | # IDE - VSCode 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/tasks.json 24 | !.vscode/launch.json 25 | !.vscode/extensions.json 26 | 27 | # misc 28 | /.sass-cache 29 | /connect.lock 30 | /coverage 31 | /libpeerconnection.log 32 | npm-debug.log 33 | yarn-error.log 34 | testem.log 35 | /typings 36 | 37 | # System Files 38 | .DS_Store 39 | Thumbs.db 40 | -------------------------------------------------------------------------------- /src/app/auth/profile/profile.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import {User} from '../../shared/models/user'; 3 | import {UsersService} from '../../shared/services/users.service'; 4 | import {Router} from '@angular/router'; 5 | 6 | @Component({ 7 | selector: 'app-profile', 8 | templateUrl: './profile.component.html', 9 | styleUrls: ['./profile.component.css'] 10 | }) 11 | export class ProfileComponent implements OnInit { 12 | private user: User; 13 | 14 | constructor(private usersService: UsersService, private router: Router) { 15 | this.usersService.getUser().subscribe(user => { 16 | this.user = user; 17 | }); 18 | } 19 | 20 | ngOnInit() { 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting() 16 | ); 17 | // Then we find all the tests. 18 | const context = require.context('./', true, /\.spec\.ts$/); 19 | // And load the modules. 20 | context.keys().map(context); 21 | -------------------------------------------------------------------------------- /src/app/shared/services/local-storage.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | 3 | let CREATED = false; 4 | @Injectable({ 5 | providedIn: 'root' 6 | }) 7 | export class LocalStorageService implements IStorageService { 8 | 9 | constructor() { 10 | console.log('Jwt Service constructed'); 11 | 12 | if (CREATED) { 13 | alert('Two instances of the same LocalStorageService'); 14 | return; 15 | } 16 | CREATED = true; 17 | } 18 | 19 | get(key: string) { 20 | return window.localStorage[key]; 21 | } 22 | 23 | set(key: string, value: string) { 24 | window.localStorage[key] = value; 25 | } 26 | 27 | clear(key: string) { 28 | window.localStorage.removeItem(key); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/app/components/home/home.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { HomeComponent } from './home.component'; 4 | 5 | describe('HomeComponent', () => { 6 | let component: HomeComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ HomeComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(HomeComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/components/about/about.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { AboutComponent } from './about.component'; 4 | 5 | describe('AboutComponent', () => { 6 | let component: AboutComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ AboutComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(AboutComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/auth/profile/profile.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ProfileComponent } from './profile.component'; 4 | 5 | describe('ProfileComponent', () => { 6 | let component: ProfileComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ProfileComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ProfileComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/cart/cart-summary/cart-summary.component.html: -------------------------------------------------------------------------------- 1 |

2 | Your cart 3 | {{cartItems.length}} products 4 |

5 |
    6 |
  • 7 |
    8 |
    {{cartItem.name}} x{{cartItem.quantity}}
    9 |
    10 | ${{cartItem.price * cartItem.quantity}} 11 |
  • 12 |
    13 |
  • 14 | Total (USD) 15 | ${{totalValue}} 16 |
  • 17 | 18 |
19 | -------------------------------------------------------------------------------- /src/app/shared/components/footer/footer.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { FooterComponent } from './footer.component'; 4 | 5 | describe('FooterComponent', () => { 6 | let component: FooterComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ FooterComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(FooterComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/shared/components/header/header.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { HeaderComponent } from './header.component'; 4 | 5 | describe('HeaderComponent', () => { 6 | let component: HeaderComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ HeaderComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(HeaderComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/auth/register/register.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { RegisterComponent } from './register.component'; 4 | 5 | describe('RegisterComponent', () => { 6 | let component: RegisterComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ RegisterComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(RegisterComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/orders/order-create/checkout.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { CheckoutComponent } from './checkout.component'; 4 | 5 | describe('CheckoutComponent', () => { 6 | let component: CheckoutComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ CheckoutComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(CheckoutComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/products/products.routing.ts: -------------------------------------------------------------------------------- 1 | import {RouterModule, Routes} from '@angular/router'; 2 | import {ProductListComponent} from './product-list/product-list.component'; 3 | import {ProductDetailsComponent} from './product-details/product-details.component'; 4 | import {ProductCreateComponent} from './product-create/product-create.component'; 5 | import {AdminGuard} from '../shared/guards/admin.guard'; 6 | 7 | const routes: Routes = [ 8 | { 9 | path: '', 10 | component: ProductListComponent 11 | }, 12 | { 13 | path: 'create', 14 | component: ProductCreateComponent, 15 | canActivate: [AdminGuard] 16 | }, 17 | { 18 | path: ':slug', 19 | component: ProductDetailsComponent 20 | }, 21 | 22 | ]; 23 | 24 | export const ProductsRouter = RouterModule.forChild(routes); 25 | -------------------------------------------------------------------------------- /src/app/orders/order-list/order-list.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { OrderListComponent } from './order-list.component'; 4 | 5 | describe('OrderListComponent', () => { 6 | let component: OrderListComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ OrderListComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(OrderListComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/shared/components/carousel/carousel.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { CarouselComponent } from './carousel.component'; 4 | 5 | describe('CarouselComponent', () => { 6 | let component: CarouselComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ CarouselComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(CarouselComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/shared/components/pagination/pagination.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { PaginationComponent } from './pagination.component'; 4 | 5 | describe('PaginationComponent', () => { 6 | let component: PaginationComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ PaginationComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(PaginationComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/addresses/address-list/address-list.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { AddressListComponent } from './address-list.component'; 4 | 5 | describe('AddressListComponent', () => { 6 | let component: AddressListComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ AddressListComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(AddressListComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/components/page-not-found/page-not-found.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |

6 | Oops!

7 |

8 | 404 Page Not Found

9 |
10 | Sorry, an error has occured, Requested page not found! 11 |
12 | 19 |
20 |
21 |
22 |
23 | -------------------------------------------------------------------------------- /src/app/products/product-list/product-list.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ProductListComponent } from './product-list.component'; 4 | 5 | describe('ProductListComponent', () => { 6 | let component: ProductListComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ProductListComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ProductListComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/orders/order-details/order-details.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { OrderDetailsComponent } from './order-details.component'; 4 | 5 | describe('OrderDetailsComponent', () => { 6 | let component: OrderDetailsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ OrderDetailsComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(OrderDetailsComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/products/product-create/product-create.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ProductCreateComponent } from './product-create.component'; 4 | 5 | describe('ProductCreateComponent', () => { 6 | let component: ProductCreateComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ProductCreateComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ProductCreateComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/addresses/address-create/address-create.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { AddressCreateComponent } from './address-create.component'; 4 | 5 | describe('AddressCreateComponent', () => { 6 | let component: AddressCreateComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ AddressCreateComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(AddressCreateComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/addresses/address-details/address-details.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { AddressDetailsComponent } from './address-details.component'; 4 | 5 | describe('AddressDetailsComponent', () => { 6 | let component: AddressDetailsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ AddressDetailsComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(AddressDetailsComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/products/product-details/product-details.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ProductDetailsComponent } from './product-details.component'; 4 | 5 | describe('ProductDetailsComponent', () => { 6 | let component: ProductDetailsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ProductDetailsComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ProductDetailsComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/auth/login/login.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Login

4 |
5 |
6 | 7 |
8 |
9 | 11 |
12 | 13 |
14 | Need an Account? 15 |
16 |
17 | -------------------------------------------------------------------------------- /src/app/shared/dtos/responses/products/products.dto.ts: -------------------------------------------------------------------------------- 1 | import {Tag} from '../../../models/tag.model'; 2 | import {Category} from '../../../models/category.model'; 3 | 4 | import {PagedResponseDto} from '../shared/page-meta.dto'; 5 | import {Product} from '../../../models/product'; 6 | import {BaseAppDtoResponse} from '../shared/base.dto'; 7 | 8 | export class ProductDto extends BaseAppDtoResponse { 9 | // product: Product; 10 | // authInfo: AuthInfo; 11 | 12 | id: string; 13 | name: string; 14 | slug: string; 15 | description: string; 16 | 17 | price: number; 18 | stock: number; 19 | image_urls: string[]; 20 | 21 | created_at: string; 22 | 23 | categories?: Category[]; 24 | tags?: Tag[]; 25 | comments?: Comment[]; 26 | } 27 | 28 | 29 | export class ProductListResponseDto extends PagedResponseDto { 30 | products: Product[]; 31 | } 32 | -------------------------------------------------------------------------------- /src/app/cart/cart.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {CommonModule} from '@angular/common'; 3 | import {CartRouter} from './cart.routing'; 4 | import {RouterModule} from '@angular/router'; 5 | 6 | import {FormsModule, ReactiveFormsModule} from '@angular/forms'; 7 | import {MyCartComponent} from './my-cart/my-cart.component'; 8 | import {CartSummaryComponent} from './cart-summary/cart-summary.component'; 9 | import {CartSummaryActionsComponent} from './cart-summary-actions/cart-summary-actions.component'; 10 | 11 | @NgModule({ 12 | declarations: [MyCartComponent, CartSummaryComponent, CartSummaryActionsComponent], 13 | imports: [ 14 | CommonModule, 15 | RouterModule, 16 | FormsModule, 17 | ReactiveFormsModule, 18 | CartRouter 19 | ], 20 | exports: [CartSummaryComponent] 21 | }) 22 | export class CartModule { 23 | } 24 | -------------------------------------------------------------------------------- /e2e/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 | './src/**/*.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: require('path').join(__dirname, './tsconfig.e2e.json') 25 | }); 26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 27 | } 28 | }; -------------------------------------------------------------------------------- /src/app/orders/orders.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {CommonModule} from '@angular/common'; 3 | import {CheckoutComponent} from './order-create/checkout.component'; 4 | import {OrderListComponent} from './order-list/order-list.component'; 5 | import {RouterModule} from '@angular/router'; 6 | import {orderRoutes} from './orders.routing'; 7 | import {FormsModule, ReactiveFormsModule} from '@angular/forms'; 8 | import {CartModule} from '../cart/cart.module'; 9 | import { OrderDetailsComponent } from './order-details/order-details.component'; 10 | 11 | @NgModule({ 12 | declarations: [CheckoutComponent, OrderListComponent, OrderDetailsComponent], 13 | imports: [ 14 | CommonModule, 15 | FormsModule, 16 | RouterModule.forChild(orderRoutes), 17 | ReactiveFormsModule, 18 | CartModule, 19 | ] 20 | }) 21 | export class OrdersModule { 22 | } 23 | -------------------------------------------------------------------------------- /src/app/shared/models/user.ts: -------------------------------------------------------------------------------- 1 | export class User { 2 | 3 | constructor(param: any = {}) { 4 | this.id = param.id; 5 | this.username = param.username; 6 | this.first_name = param.first_name; 7 | this.last_name = param.last_name; 8 | this.email = param.email; 9 | this.roles = param.roles; 10 | } 11 | 12 | id: string; 13 | username: string; 14 | first_name: string; 15 | last_name: string; 16 | email: string; 17 | roles: string[]; 18 | address: string; 19 | address2: string; 20 | country: string; 21 | city: string; 22 | zipCode: string; 23 | token: string; 24 | } 25 | 26 | export class UserDetail { 27 | $key: string; 28 | firstName: string; 29 | lastName: string; 30 | userName: string; 31 | emailId: string; 32 | address1: string; 33 | address2: string; 34 | country: string; 35 | state: string; 36 | zip: number; 37 | } 38 | -------------------------------------------------------------------------------- /src/app/addresses/address-list/address-list.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import {AddressesService} from '../../shared/services/addresses.service'; 3 | import {AddressDto, AddressListResponseDto} from '../../shared/dtos/responses/addresses/addresses.dto'; 4 | 5 | @Component({ 6 | selector: 'app-address-list', 7 | templateUrl: './address-list.component.html', 8 | styleUrls: ['./address-list.component.css'] 9 | }) 10 | export class AddressListComponent implements OnInit { 11 | private addresses: AddressDto[]; 12 | 13 | constructor(private addressesService: AddressesService) { 14 | } 15 | 16 | ngOnInit() { 17 | this.addressesService.fetchAll().subscribe(res => { 18 | if (res.success) { 19 | this.addresses = (res as AddressListResponseDto).addresses; 20 | } 21 | }, err => { 22 | 23 | }); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/app/orders/order-list/order-list.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import {OrdersService} from '../../shared/services/orders.service'; 3 | import {OrderListDto} from '../../shared/dtos/responses/orders/order-list.dto'; 4 | import {Order} from '../../shared/models/order.model'; 5 | 6 | @Component({ 7 | selector: 'app-order-list', 8 | templateUrl: './order-list.component.html', 9 | styleUrls: ['./order-list.component.css'] 10 | }) 11 | export class OrderListComponent implements OnInit { 12 | private orders: Order[] = []; 13 | 14 | constructor(private ordersService: OrdersService) { 15 | } 16 | 17 | ngOnInit() { 18 | 19 | this.ordersService.getMyOrders().subscribe(res => { 20 | if (res.success) { 21 | console.log(res.orders); 22 | const response = res as OrderListDto; 23 | this.orders = response.orders; 24 | } 25 | }); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/app/components/page-not-found/page-not-found.component.scss: -------------------------------------------------------------------------------- 1 | body { 2 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABoAAAAaCAYAAACpSkzOAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAABZ0RVh0Q3JlYXRpb24gVGltZQAxMC8yOS8xMiKqq3kAAAAcdEVYdFNvZnR3YXJlAEFkb2JlIEZpcmV3b3JrcyBDUzVxteM2AAABHklEQVRIib2Vyw6EIAxFW5idr///Qx9sfG3pLEyJ3tAwi5EmBqRo7vHawiEEERHS6x7MTMxMVv6+z3tPMUYSkfTM/R0fEaG2bbMv+Gc4nZzn+dN4HAcREa3r+hi3bcuu68jLskhVIlW073tWaYlQ9+F9IpqmSfq+fwskhdO/AwmUTJXrOuaRQNeRkOd5lq7rXmS5InmERKoER/QMvUAPlZDHcZRhGN4CSeGY+aHMqgcks5RrHv/eeh455x5KrMq2yHQdibDO6ncG/KZWL7M8xDyS1/MIO0NJqdULLS81X6/X6aR0nqBSJcPeZnlZrzN477NKURn2Nus8sjzmEII0TfMiyxUuxphVWjpJkbx0btUnshRihVv70Bv8ItXq6Asoi/ZiCbU6YgAAAABJRU5ErkJggg==); 3 | } 4 | .error-template { 5 | padding: 40px 15px; 6 | text-align: center; 7 | } 8 | .error-actions { 9 | margin-top: 15px; 10 | margin-bottom: 15px; 11 | } 12 | .error-actions .btn { 13 | margin-right: 10px; 14 | } 15 | -------------------------------------------------------------------------------- /src/app/shared/guards/authentication.guard.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {CanActivate, Router, RouterStateSnapshot} from '@angular/router'; 3 | import {UsersService} from '../services/users.service'; 4 | 5 | let CREATED = false; 6 | 7 | @Injectable() 8 | export class AuthenticationGuard implements CanActivate { 9 | constructor(private router: Router, private authService: UsersService) { 10 | if (CREATED) { 11 | alert('Two instances of the same AuthenticationGuard'); 12 | return; 13 | } 14 | CREATED = true; 15 | this.authService.isLoggedInAsync().subscribe(isLoggedIn => { 16 | 17 | }); 18 | } 19 | 20 | canActivate(route, state: RouterStateSnapshot) { 21 | if (this.authService.isLoggedInSync()) { 22 | return true; 23 | } 24 | this.router.navigate(['/index/login'], { 25 | queryParams: {returnUrl: state.url} 26 | }); 27 | return false; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/app/components/home/home.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import {PagesService} from '../../shared/services/pages.service'; 3 | import {Category} from '../../shared/models/category.model'; 4 | import {Tag} from '../../shared/models/tag.model'; 5 | import {HomeResponseDto} from '../../shared/dtos/responses/pages/home.dto'; 6 | 7 | @Component({ 8 | selector: 'app-home', 9 | templateUrl: './home.component.html', 10 | styleUrls: ['./home.component.css'] 11 | }) 12 | export class HomeComponent implements OnInit { 13 | private categories: Category[]; 14 | private tags: Tag[]; 15 | 16 | constructor(private pageService: PagesService) { 17 | } 18 | 19 | ngOnInit() { 20 | this.pageService.fetchHome().subscribe(res => { 21 | if (res.success) { 22 | this.tags = (res as HomeResponseDto).tags; 23 | this.categories = (res as HomeResponseDto).categories; 24 | } 25 | }); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/app/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | // Core Dependencies 2 | import {RouterModule} from '@angular/router'; 3 | import {NgModule, NO_ERRORS_SCHEMA} from '@angular/core'; 4 | import {CommonModule} from '@angular/common'; 5 | 6 | import {AuthRoutes} from './auth.routing'; 7 | // Components 8 | import {LoginComponent} from './login/login.component'; 9 | import {SharedModule} from '../shared/shared.module'; 10 | import {RegisterComponent} from './register/register.component'; 11 | import {ReactiveFormsModule} from '@angular/forms'; 12 | import {ProfileComponent} from './profile/profile.component'; 13 | 14 | @NgModule({ 15 | imports: [ 16 | CommonModule, 17 | SharedModule, 18 | ReactiveFormsModule, 19 | RouterModule.forChild(AuthRoutes) 20 | ], 21 | declarations: [ 22 | // IndexComponent, 23 | LoginComponent, 24 | RegisterComponent, 25 | ProfileComponent 26 | ], 27 | schemas: [NO_ERRORS_SCHEMA], 28 | providers: [] 29 | }) 30 | export class AuthModule { 31 | } 32 | -------------------------------------------------------------------------------- /src/app/shared/utils/net.utils.ts: -------------------------------------------------------------------------------- 1 | import {HttpErrorResponse} from '@angular/common/http'; 2 | import {Observable, of, throwError} from 'rxjs'; 3 | import {ErrorResult} from '../dtos/local/base'; 4 | 5 | export function buildErrorObservable(err: HttpErrorResponse | string | String): Observable { 6 | if (err instanceof HttpErrorResponse || err instanceof String) { 7 | return of(buildError(err)); 8 | } else { 9 | debugger; 10 | return throwError(err); 11 | } 12 | } 13 | 14 | export function buildError(err: HttpErrorResponse | string | String): ErrorResult { 15 | if (err instanceof HttpErrorResponse) { 16 | const response = new ErrorResult(); 17 | response.full_messages = [`Local error, details: ${err.message} `]; 18 | return response; 19 | } else if (typeof err === 'string') { 20 | const response = new ErrorResult(); 21 | response.full_messages = [`Local error, details: ${err} `]; 22 | return response; 23 | } else { 24 | debugger; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/app/cart/cart-summary/cart-summary.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input, OnChanges, OnInit, SimpleChange, SimpleChanges} from '@angular/core'; 2 | import {Product} from '../../shared/models/product'; 3 | 4 | 5 | @Component({ 6 | selector: 'app-cart-summary', 7 | templateUrl: './cart-summary.component.html', 8 | styleUrls: ['./cart-summary.component.css'] 9 | }) 10 | export class CartSummaryComponent implements OnInit, OnChanges { 11 | 12 | @Input() cartItems: Product[]; 13 | 14 | totalValue = 0; 15 | 16 | constructor() { 17 | } 18 | 19 | ngOnChanges(changes: SimpleChanges) { 20 | const dataChanges: SimpleChange = changes.cartItems; 21 | 22 | const cartItems: Product[] = dataChanges.currentValue; 23 | this.totalValue = 0; 24 | cartItems.forEach(product => { 25 | console.log( 26 | 'Adding: ' + product.name + ' $ ' + product.price 27 | ); 28 | this.totalValue += product.price * product.quantity; 29 | }); 30 | } 31 | 32 | ngOnInit() { 33 | } 34 | 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/app/addresses/addresses.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {CommonModule} from '@angular/common'; 3 | import {AddressListComponent} from './address-list/address-list.component'; 4 | import {AddressCreateComponent} from './address-create/address-create.component'; 5 | import {AddressDetailsComponent} from './address-details/address-details.component'; 6 | import {RouterModule, Routes} from '@angular/router'; 7 | 8 | const orderRoutes: Routes = [ 9 | { 10 | path: 'create', 11 | component: AddressCreateComponent 12 | }, 13 | { 14 | path: '', redirectTo: '/addresses/list', pathMatch: 'full' 15 | }, 16 | { 17 | path: 'list', component: AddressListComponent 18 | } 19 | , 20 | { 21 | path: ':id', component: AddressDetailsComponent 22 | } 23 | ]; 24 | 25 | @NgModule({ 26 | declarations: [AddressListComponent, AddressCreateComponent, AddressDetailsComponent], 27 | imports: [ 28 | CommonModule, 29 | RouterModule.forChild(orderRoutes), 30 | ] 31 | }) 32 | export class AddressesModule { 33 | } 34 | -------------------------------------------------------------------------------- /src/app/shared/services/notification.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {BehaviorSubject, Observable} from 'rxjs'; 3 | import {NotificationsDto, NotificationTypes} from '../dtos/local/notifications.dto'; 4 | 5 | let CREATED = false; 6 | 7 | @Injectable({ 8 | providedIn: 'root' 9 | }) 10 | export class NotificationService { 11 | constructor() { 12 | if (CREATED) { 13 | alert('Two instances of the same NotificationService'); 14 | return; 15 | } 16 | CREATED = true; 17 | this.messages = new BehaviorSubject(null); 18 | } 19 | 20 | 21 | private messages: BehaviorSubject; 22 | 23 | getNotifications(): Observable { 24 | return this.messages.asObservable(); 25 | } 26 | 27 | dispatchSuccessMessage(message: string) { 28 | this.messages.next(new NotificationsDto(NotificationTypes.SUCCESS_TYPE, message)); 29 | } 30 | 31 | dispatchErrorMessage(message: string) { 32 | this.messages.next(new NotificationsDto(NotificationTypes.ERROR_TYPE, message)); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/app/shared/models/product.ts: -------------------------------------------------------------------------------- 1 | import {Category} from './category.model'; 2 | import {Tag} from './tag.model'; 3 | import {Comment} from './comment.model'; 4 | 5 | export class Product { 6 | constructor(params: any = {}) { 7 | this.id = params.id; 8 | this.name = params.name; 9 | this.slug = params.slug; 10 | this.price = params.price; 11 | this.description = params.description; 12 | this.image_urls = params.image_urls; 13 | this.quantity = params.stock; 14 | this.isInCart = params.isInCart; 15 | this.tags = params.tags; 16 | this.categories = params.categories; 17 | this.comments = params.comments; 18 | } 19 | 20 | id: string; 21 | name: string; 22 | slug: string; 23 | // productCategory: string; 24 | price: number; 25 | description: string; 26 | stock: number; 27 | image_urls: string[]; 28 | isInCart: boolean; 29 | quantity: number; 30 | 31 | productSeller: string; 32 | created_at: string; 33 | publish_on: string; 34 | categories?: Category[]; 35 | tags?: Tag[]; 36 | comments?: Comment[]; 37 | } 38 | -------------------------------------------------------------------------------- /src/app/shared/shared.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {CommonModule} from '@angular/common'; 3 | import {FormsModule} from '@angular/forms'; 4 | 5 | import {RouterModule} from '@angular/router'; 6 | import {HttpClientModule} from '@angular/common/http'; 7 | import {CarouselComponent} from './components/carousel/carousel.component'; 8 | import {HeaderComponent} from './components/header/header.component'; 9 | import {FooterComponent} from './components/footer/footer.component'; 10 | import { PaginationComponent } from './components/pagination/pagination.component'; 11 | 12 | @NgModule({ 13 | imports: [ 14 | CommonModule, 15 | FormsModule, 16 | HttpClientModule, 17 | RouterModule, 18 | ], 19 | declarations: [ 20 | CarouselComponent, 21 | HeaderComponent, 22 | FooterComponent, 23 | PaginationComponent 24 | ], 25 | exports: [ 26 | FormsModule, 27 | RouterModule, 28 | HeaderComponent, 29 | FooterComponent, 30 | PaginationComponent, 31 | ], 32 | providers: [] 33 | }) 34 | export class SharedModule { 35 | } 36 | -------------------------------------------------------------------------------- /src/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, '../coverage'), 20 | reports: ['html', 'lcovonly'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false 30 | }); 31 | }; -------------------------------------------------------------------------------- /src/app/shared/interceptors/jwt-http.interceptor.ts: -------------------------------------------------------------------------------- 1 | import {HttpEvent, HttpHandler, HttpInterceptor, HttpRequest} from '@angular/common/http'; 2 | import {Observable} from 'rxjs'; 3 | import {Injectable} from '@angular/core'; 4 | import {UsersService} from '../services/users.service'; 5 | 6 | @Injectable() 7 | export class JwtHttpInterceptor implements HttpInterceptor { 8 | private token: string; 9 | 10 | constructor(private usersService: UsersService) { 11 | this.usersService.getUser().subscribe(user => { 12 | if (user && user.token) { 13 | this.token = user.token; 14 | } else { 15 | this.token = null; 16 | } 17 | }); 18 | } 19 | 20 | intercept(req: HttpRequest, next: HttpHandler): Observable> { 21 | const headersConfig = { 22 | // 'Content-Type': 'application/json', 23 | 'Accept': 'application/json' 24 | }; 25 | 26 | if (this.token) { 27 | headersConfig['Authorization'] = `Bearer ${this.token}`; 28 | } 29 | 30 | const request = req.clone({setHeaders: headersConfig}); 31 | return next.handle(request); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/app/products/products.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {CommonModule} from '@angular/common'; 3 | import {ProductListComponent} from './product-list/product-list.component'; 4 | import {ProductsRouter} from './products.routing'; 5 | import {ProductDetailsComponent} from './product-details/product-details.component'; 6 | import {RouterModule} from '@angular/router'; 7 | import {FormsModule, ReactiveFormsModule} from '@angular/forms'; 8 | import {SharedModule} from '../shared/shared.module'; 9 | import {ProductCreateComponent} from './product-create/product-create.component'; 10 | import {FroalaEditorModule, FroalaViewModule} from 'angular-froala-wysiwyg'; 11 | import {AdminGuard} from '../shared/guards/admin.guard'; 12 | 13 | @NgModule({ 14 | declarations: [ProductListComponent, ProductDetailsComponent, ProductCreateComponent], 15 | imports: [ 16 | CommonModule, 17 | RouterModule, 18 | FormsModule, 19 | ReactiveFormsModule, 20 | FroalaEditorModule.forRoot(), 21 | FroalaViewModule.forRoot(), 22 | SharedModule, 23 | ProductsRouter 24 | ], 25 | providers: [AdminGuard] 26 | }) 27 | export class ProductsModule { 28 | } 29 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | const BASE_URL = 'http://localhost:8080/api'; 6 | export const environment = { 7 | production: false, 8 | urls: { 9 | base: BASE_URL, 10 | pages: { 11 | home: `${BASE_URL}/home`, 12 | about: `${BASE_URL}/about` 13 | }, 14 | products: `${BASE_URL}/products`, 15 | comments: `${BASE_URL}/comments`, 16 | addresses: `${BASE_URL}/addresses`, 17 | cart: `${BASE_URL}/cart`, 18 | users: `${BASE_URL}/users`, 19 | orders: `${BASE_URL}/orders`, 20 | } 21 | }; 22 | 23 | /* 24 | * For easier debugging in development mode, you can import the following file 25 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 26 | * 27 | * This import should be commented out in production mode because it will have a negative impact 28 | * on performance if an error is thrown. 29 | */ 30 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 31 | -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {RouterModule, Routes} from '@angular/router'; 3 | import {HomeComponent} from './components/home/home.component'; 4 | import {PageNotFoundComponent} from './components/page-not-found/page-not-found.component'; 5 | 6 | const routes: Routes = [ 7 | { 8 | path: '', redirectTo: 'home', pathMatch: 'full' 9 | }, 10 | { 11 | path: 'home', component: HomeComponent 12 | }, 13 | { 14 | path: 'auth', 15 | loadChildren: './auth/auth.module#AuthModule' 16 | }, 17 | { 18 | path: 'cart', 19 | loadChildren: './cart/cart.module#CartModule', 20 | }, 21 | { 22 | path: 'products', loadChildren: './products/products.module#ProductsModule' 23 | }, 24 | { 25 | path: 'orders', loadChildren: './orders/orders.module#OrdersModule' 26 | }, 27 | { 28 | path: 'addresses', loadChildren: './addresses/addresses.module#AddressesModule' 29 | }, 30 | { 31 | path: '**', 32 | component: PageNotFoundComponent 33 | } 34 | ]; 35 | 36 | @NgModule({ 37 | imports: [RouterModule.forRoot(routes)], 38 | exports: [RouterModule] 39 | }) 40 | export class AppRoutingModule { 41 | } 42 | -------------------------------------------------------------------------------- /src/app/shared/components/pagination/pagination.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges} from '@angular/core'; 2 | import {PaginatedRequestDto} from '../../dtos/requests/base.dto'; 3 | import {PageMeta} from '../../dtos/responses/shared/page-meta.dto'; 4 | 5 | @Component({ 6 | selector: 'app-pagination', 7 | templateUrl: './pagination.component.html', 8 | styleUrls: ['./pagination.component.css'] 9 | }) 10 | export class PaginationComponent implements OnInit, OnChanges { 11 | @Input() pageMeta: PageMeta; 12 | @Output() loadMore: EventEmitter = new EventEmitter(); 13 | 14 | private totalItemsCount: number; 15 | private lastRecord: number; 16 | private firstRecord: number; 17 | 18 | constructor() { 19 | 20 | } 21 | 22 | ngOnInit() { 23 | 24 | } 25 | 26 | ngOnChanges(changes: SimpleChanges): void { 27 | if (this.pageMeta) { 28 | this.lastRecord = this.pageMeta.current_items_count + this.pageMeta.offset; 29 | this.firstRecord = this.pageMeta.offset + 1; 30 | this.totalItemsCount = this.pageMeta.total_items_count; 31 | } 32 | } 33 | 34 | fetchMore(page: number, pageSize: number) { 35 | this.loadMore.emit({page, pageSize}); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | import { AppComponent } from './app.component'; 4 | 5 | describe('AppComponent', () => { 6 | beforeEach(async(() => { 7 | TestBed.configureTestingModule({ 8 | imports: [ 9 | RouterTestingModule 10 | ], 11 | declarations: [ 12 | AppComponent 13 | ], 14 | }).compileComponents(); 15 | })); 16 | 17 | it('should create the app', () => { 18 | const fixture = TestBed.createComponent(AppComponent); 19 | const app = fixture.debugElement.componentInstance; 20 | expect(app).toBeTruthy(); 21 | }); 22 | 23 | it(`should have as title 'AngularShopApp'`, () => { 24 | const fixture = TestBed.createComponent(AppComponent); 25 | const app = fixture.debugElement.componentInstance; 26 | expect(app.title).toEqual('AngularShopApp'); 27 | }); 28 | 29 | it('should render title in a h1 tag', () => { 30 | const fixture = TestBed.createComponent(AppComponent); 31 | fixture.detectChanges(); 32 | const compiled = fixture.debugElement.nativeElement; 33 | expect(compiled.querySelector('h1').textContent).toContain('Welcome to AngularShopApp!'); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /src/app/orders/order-details/order-details.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import {OrdersService} from '../../shared/services/orders.service'; 3 | import {ActivatedRoute} from '@angular/router'; 4 | import {OrderDetailsDto} from '../../shared/dtos/responses/orders/order-details.response'; 5 | import {OrderItemDto} from '../../shared/dtos/responses/order_items/order-item.dto'; 6 | 7 | @Component({ 8 | selector: 'app-order-details', 9 | templateUrl: './order-details.component.html', 10 | styleUrls: ['./order-details.component.css'] 11 | }) 12 | export class OrderDetailsComponent implements OnInit { 13 | private orderItems: OrderItemDto[] = []; 14 | private order: OrderDetailsDto; 15 | 16 | constructor(private ordersService: OrdersService, private route: ActivatedRoute) { 17 | } 18 | 19 | ngOnInit() { 20 | this.route.params.subscribe(params => { 21 | const id = params['id']; 22 | 23 | this.ordersService.getOrder(id).subscribe(res => { 24 | if (res.success) { 25 | console.log(res); 26 | const response = res as OrderDetailsDto; 27 | this.order = response; 28 | this.orderItems = this.order.order_items; 29 | console.log(this.order); 30 | console.log(this.orderItems); 31 | } 32 | }); 33 | }); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/app/shared/services/addresses.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {Observable} from 'rxjs'; 3 | 4 | import {NotificationService} from './notification.service'; 5 | import {AddressListResponseDto} from '../dtos/responses/addresses/addresses.dto'; 6 | import {catchError, map} from 'rxjs/operators'; 7 | 8 | import {HttpClient} from '@angular/common/http'; 9 | import {environment} from '../../../environments/environment'; 10 | import {ErrorResult} from '../dtos/local/base'; 11 | import {buildErrorObservable} from '../utils/net.utils'; 12 | 13 | @Injectable({ 14 | providedIn: 'root' 15 | }) 16 | export class AddressesService { 17 | 18 | private readonly baseUrl: string; 19 | 20 | constructor(private httpClient: HttpClient, private notificationService: NotificationService) { 21 | this.baseUrl = environment.urls.addresses; 22 | } 23 | 24 | fetchAll(): Observable { 25 | return this.httpClient.get(this.baseUrl) 26 | .pipe( 27 | map(res => { 28 | if (res.success && res.addresses) { 29 | console.log('[+] Received ' + res.addresses.length + ' addresses'); 30 | } 31 | return res as AddressListResponseDto; 32 | }), catchError(err => { 33 | return buildErrorObservable(err); 34 | })); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/app/shared/services/pages.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {Observable} from 'rxjs'; 3 | import {HttpClient, HttpErrorResponse} from '@angular/common/http'; 4 | import {catchError, map} from 'rxjs/operators'; 5 | 6 | import {environment} from '../../../environments/environment'; 7 | import {buildErrorObservable} from '../utils/net.utils'; 8 | import {ErrorResult} from '../dtos/local/base'; 9 | import {HomeResponseDto} from '../dtos/responses/pages/home.dto'; 10 | import {NotificationService} from './notification.service'; 11 | 12 | 13 | @Injectable({ 14 | providedIn: 'root' 15 | }) 16 | export class PagesService { 17 | private baseUrl: { about: string; home: string }; 18 | 19 | constructor(private httpClient: HttpClient, private notificationService: NotificationService) { 20 | this.baseUrl = environment.urls.pages; 21 | } 22 | 23 | fetchHome(): Observable { 24 | return this.httpClient.get(this.baseUrl.home).pipe( 25 | map(res => { 26 | if (res.success) { 27 | console.log('[+] Fetched home successfully'); 28 | } 29 | return res; 30 | }), 31 | catchError((err: HttpErrorResponse) => { 32 | this.notificationService.dispatchErrorMessage(err.message); 33 | return buildErrorObservable(err); 34 | }) 35 | ); 36 | } 37 | 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/app/auth/profile/profile.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | Card image cap 5 |
6 |
Addresses
7 |

Make sure your addresses are up to date

8 |

9 | 10 | Addresses 11 | 12 |

13 |
14 |
15 |   16 |
17 | Card image cap 18 |
19 |
My Orders
20 |

Check all your orders

21 |

22 | 23 | Orders 24 | 25 |

26 |
27 |
28 |
29 |
30 | 31 |
32 | Username {{user.username}} 33 | 34 | {{role}} 35 | 36 |
37 | -------------------------------------------------------------------------------- /src/app/auth/register/register.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |

Sign Up

5 |
6 |
7 |
8 | 9 |
10 |
11 | 12 |
13 |
14 |
15 | 16 |
17 |
18 | 19 |
20 |
21 | 22 |
23 | 24 |
25 | Already have an Account? 26 |
27 |
28 | -------------------------------------------------------------------------------- /src/app/products/product-create/product-create.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 | 7 |
8 |
9 | 10 |
11 |
14 |
15 |
16 | 17 |
18 |
19 | 20 | 22 |
23 |
24 | 25 |
26 |
27 | 28 | 30 |
31 |
32 |
33 | 35 | 36 |
37 | 38 |
39 |
40 | 41 |
42 | 43 | {{description}} 44 | 45 | 46 |
47 |
48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-shop-app", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "test": "ng test", 9 | "lint": "ng lint", 10 | "e2e": "ng e2e" 11 | }, 12 | "private": true, 13 | "dependencies": { 14 | "@angular/animations": "~7.0.0", 15 | "@angular/common": "~7.0.0", 16 | "@angular/compiler": "~7.0.0", 17 | "@angular/core": "~7.0.0", 18 | "@angular/forms": "~7.0.0", 19 | "@angular/http": "~7.0.0", 20 | "@angular/platform-browser": "~7.0.0", 21 | "@angular/platform-browser-dynamic": "~7.0.0", 22 | "@angular/router": "~7.0.0", 23 | "angular-froala-wysiwyg": "^3.0.2", 24 | "bootstrap": "^4.1.3", 25 | "core-js": "^2.5.4", 26 | "font-awesome": "^4.7.0", 27 | "lodash": "^4.17.11", 28 | "ng2-toasty": "^4.0.3", 29 | "rxjs": "~6.3.3", 30 | "zone.js": "~0.8.26" 31 | }, 32 | "devDependencies": { 33 | "@angular-devkit/build-angular": "~0.10.0", 34 | "@angular/cli": "~7.0.2", 35 | "@angular/compiler-cli": "~7.0.0", 36 | "@angular/language-service": "~7.0.0", 37 | "@types/jasmine": "~2.8.8", 38 | "@types/jasminewd2": "~2.0.3", 39 | "@types/node": "~8.9.4", 40 | "codelyzer": "~4.5.0", 41 | "jasmine-core": "~2.99.1", 42 | "jasmine-spec-reporter": "~4.2.1", 43 | "karma": "~3.0.0", 44 | "karma-chrome-launcher": "~2.2.0", 45 | "karma-coverage-istanbul-reporter": "~2.0.1", 46 | "karma-jasmine": "~1.1.2", 47 | "karma-jasmine-html-reporter": "^0.2.2", 48 | "node-sass": "^4.10.0", 49 | "protractor": "~5.4.0", 50 | "ts-node": "~7.0.0", 51 | "tslint": "~5.11.0", 52 | "typescript": "~3.1.1" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/app/shared/components/carousel/carousel.component.html: -------------------------------------------------------------------------------- 1 |
2 | 37 |
38 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import {BrowserModule} from '@angular/platform-browser'; 2 | import {NgModule} from '@angular/core'; 3 | import {AppRoutingModule} from './app-routing.module'; 4 | import {AppComponent} from './app.component'; 5 | import {HomeComponent} from './components/home/home.component'; 6 | import {AboutComponent} from './components/about/about.component'; 7 | import {SharedModule} from './shared/shared.module'; 8 | import {PageNotFoundComponent} from './components/page-not-found/page-not-found.component'; 9 | import {ProductsService} from './shared/services/products.service'; 10 | import {UsersService} from './shared/services/users.service'; 11 | import {LocalStorageService} from './shared/services/local-storage.service'; 12 | import {ShoppingCartService} from './shared/services/shopping-cart.service'; 13 | import {AuthenticationGuard} from './shared/guards/authentication.guard'; 14 | import {HTTP_INTERCEPTORS} from '@angular/common/http'; 15 | import {JwtHttpInterceptor} from './shared/interceptors/jwt-http.interceptor'; 16 | import {FroalaEditorModule, FroalaViewModule} from 'angular-froala-wysiwyg'; 17 | 18 | 19 | @NgModule({ 20 | declarations: [ 21 | AppComponent, 22 | HomeComponent, 23 | AboutComponent, 24 | PageNotFoundComponent, 25 | ], 26 | imports: [ 27 | // Angular Modules 28 | BrowserModule, 29 | 30 | // 3party 31 | 32 | 33 | // My Modules 34 | AppRoutingModule, 35 | SharedModule, 36 | ], 37 | exports: [PageNotFoundComponent], 38 | providers: [ 39 | ProductsService, 40 | UsersService, 41 | LocalStorageService, 42 | ShoppingCartService, 43 | 44 | // TODO: place it in shared module and check if still singleton 45 | AuthenticationGuard, 46 | {provide: HTTP_INTERCEPTORS, useClass: JwtHttpInterceptor, multi: true} 47 | ], 48 | bootstrap: [AppComponent] 49 | }) 50 | export class AppModule { 51 | } 52 | -------------------------------------------------------------------------------- /src/app/shared/components/pagination/pagination.component.html: -------------------------------------------------------------------------------- 1 |
2 | {{firstRecord}}-{{lastRecord}}/{{totalItemsCount}}
3 | 49 |
50 | -------------------------------------------------------------------------------- /src/app/shared/components/header/header.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnDestroy, OnInit} from '@angular/core'; 2 | import {ShoppingCartService} from '../../services/shopping-cart.service'; 3 | import {UsersService} from '../../services/users.service'; 4 | import {Observable, Subscription} from 'rxjs'; 5 | import {ShoppingCart} from '../../models/shopping-cart.model'; 6 | import {NotificationService} from '../../services/notification.service'; 7 | 8 | @Component({ 9 | selector: 'app-header', 10 | templateUrl: './header.component.html', 11 | styleUrls: ['./header.component.css'] 12 | }) 13 | export class HeaderComponent implements OnInit, OnDestroy { 14 | private cart: Observable; 15 | private message: string; 16 | private cartItemsLength: number; 17 | private isLoggedIn = false; 18 | private subscriptions: Subscription[] = []; 19 | private className: string; 20 | 21 | constructor(private usersService: UsersService, private cartService: ShoppingCartService, 22 | private notificationService: NotificationService) { 23 | 24 | this.cart = this.cartService.getCart(); 25 | 26 | 27 | this.subscriptions.push(this.usersService.isLoggedInAsync().subscribe(isLoggedIn => { 28 | this.isLoggedIn = isLoggedIn; 29 | // this.isLoggedIn = !!(user && user.username && user.token); 30 | })); 31 | this.cart.subscribe(cart => { 32 | this.cartItemsLength = cart.cartItems.length; 33 | }); 34 | this.notificationService.getNotifications().subscribe(notification => { 35 | if (notification == null) { 36 | return; 37 | } 38 | this.className = notification.type === 'success' ? 'alert alert-success' : 'alert alert-danger'; 39 | this.message = notification.message; 40 | }); 41 | } 42 | 43 | ngOnInit() { 44 | } 45 | 46 | logout() { 47 | this.usersService.logout(); 48 | } 49 | 50 | ngOnDestroy(): void { 51 | // TODO: Unsubscribe 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/app/products/product-create/product-create.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import {ProductsService} from '../../shared/services/products.service'; 3 | import {FormBuilder, FormGroup, Validators} from '@angular/forms'; 4 | import {Product} from '../../shared/models/product'; 5 | import {Router} from '@angular/router'; 6 | import {ProductDto} from '../../shared/dtos/responses/products/products.dto'; 7 | 8 | @Component({ 9 | selector: 'app-product-create', 10 | templateUrl: './product-create.component.html', 11 | styleUrls: ['./product-create.component.css'] 12 | }) 13 | export class ProductCreateComponent implements OnInit { 14 | // https://github.com/froala/angular-froala-wysiwyg#installation-instructions 15 | public editorContent: string; 16 | private description: string; 17 | 18 | showCode = false; 19 | 20 | // images: Array = []; 21 | images: FileList; 22 | 23 | public options: Object = { 24 | placeholderText: 'Description', 25 | charCounterCount: false, 26 | imageUpload: false, 27 | quickInsertTags: ['p', 'div', 'h1'], 28 | }; 29 | 30 | private productForm: FormGroup; 31 | 32 | constructor(private productsService: ProductsService, private fb: FormBuilder, 33 | private router: Router) { 34 | } 35 | 36 | ngOnInit() { 37 | this.productForm = this.fb.group({ 38 | name: ['', [Validators.required, Validators.minLength(2)]], 39 | price: [1, [Validators.required, Validators.min(1)]], 40 | stock: [1, [Validators.required, Validators.min(1)]], 41 | }); 42 | } 43 | 44 | onDataChanged(content) { 45 | this.description = content; 46 | } 47 | 48 | addFiles(e: any) { 49 | this.images = e.target.files; 50 | } 51 | 52 | submitForm() { 53 | 54 | const productInfo = this.productForm.value as Product; 55 | productInfo.description = this.description; 56 | 57 | this.productsService.createProduct(productInfo, this.images) 58 | .subscribe( 59 | data => { 60 | if (data && data.success) { 61 | this.router.navigate(['/', 'products', (data as ProductDto).id]); 62 | } else { 63 | this.router.navigate(['/', 'products']); 64 | } 65 | }, 66 | err => { 67 | console.log(err); 68 | } 69 | ); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/app/cart/my-cart/my-cart.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import {Observable} from 'rxjs'; 3 | import {ShoppingCart} from '../../shared/models/shopping-cart.model'; 4 | import {Product} from '../../shared/models/product'; 5 | import {ProductsService} from '../../shared/services/products.service'; 6 | import {ShoppingCartService} from '../../shared/services/shopping-cart.service'; 7 | import {AbstractControl, FormControl, FormGroup, Validators} from '@angular/forms'; 8 | 9 | 10 | @Component({ 11 | selector: 'app-my-cart', 12 | templateUrl: './my-cart.component.html', 13 | styleUrls: ['./my-cart.component.css'] 14 | }) 15 | export class MyCartComponent implements OnInit { 16 | cart: Observable; 17 | cartItems: Product[]; 18 | showDataNotFound = true; 19 | 20 | // Not Found Message 21 | messageTitle = 'No Products Found in Cart'; 22 | messageDescription = 'Please, Add Products to Cart'; 23 | private myCartProductsFrom: FormGroup; 24 | private totalAmount: number; 25 | 26 | constructor(private productService: ProductsService, private shoppingCartService: ShoppingCartService) { 27 | this.cart = this.shoppingCartService.getCart(); 28 | this.cart.subscribe(cart => { 29 | this.cartItems = cart.cartItems; 30 | this.totalAmount = cart.cartItems.reduce((accumulator, cartItem) => accumulator + cartItem.quantity * cartItem.price, 0); 31 | }); 32 | 33 | this.createForm(); 34 | } 35 | 36 | 37 | ngOnInit() { 38 | 39 | } 40 | 41 | removeCartProduct(product: Product) { 42 | this.shoppingCartService.removeFromCart(product); 43 | } 44 | 45 | updateQuantities($event) { 46 | $event.preventDefault(); 47 | console.log(this.myCartProductsFrom.value); 48 | 49 | // ES6 syntax same as for(var key in form){form[key]} 50 | for (const [id, quantity] of Object.entries(this.myCartProductsFrom.value)) { 51 | const cartItem = this.cartItems.find(ci => ci.id === id); 52 | this.shoppingCartService.updateQuantity(cartItem, Number(quantity)); 53 | } 54 | } 55 | 56 | private createForm() { 57 | const group: { [key: string]: AbstractControl } = {}; 58 | this.cartItems.forEach(product => { 59 | group[product.id] = new FormControl(product.quantity, [Validators.required, Validators.min(0)]); 60 | }); 61 | this.myCartProductsFrom = new FormGroup(group); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/app/auth/login/login.component.ts: -------------------------------------------------------------------------------- 1 | import {FormBuilder, FormGroup, Validators} from '@angular/forms'; 2 | import {Component, OnDestroy, OnInit} from '@angular/core'; 3 | 4 | import {ActivatedRoute, Router} from '@angular/router'; 5 | import {User} from '../../shared/models/user'; 6 | import {UsersService} from '../../shared/services/users.service'; 7 | import {NotificationService} from '../../shared/services/notification.service'; 8 | import {Subscription} from 'rxjs'; 9 | 10 | declare var $: any; 11 | 12 | @Component({ 13 | selector: 'app-login', 14 | templateUrl: './login.component.html', 15 | styleUrls: ['./login.component.css'], 16 | }) 17 | export class LoginComponent implements OnInit, OnDestroy { 18 | 19 | private user: User; 20 | public loginForm: FormGroup; 21 | private subscriptions: Subscription[] = []; 22 | private returnUrl: string; 23 | 24 | constructor( 25 | private fb: FormBuilder, 26 | private router: Router, 27 | private route: ActivatedRoute, 28 | private usersService: UsersService, 29 | private notificationService: NotificationService, 30 | ) { 31 | this.createForm(); 32 | 33 | 34 | } 35 | 36 | ngOnInit() { 37 | this.returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/home'; 38 | 39 | this.subscriptions.push( 40 | this.usersService.getUser().subscribe(user => { 41 | this.user = user; 42 | if (!!user) { 43 | this.router.navigateByUrl('/home'); 44 | } 45 | }) 46 | ); 47 | } 48 | 49 | private createForm(): void { 50 | this.loginForm = this.fb.group({ 51 | username: ['', [Validators.required, /* Validators.email*/]], 52 | password: ['', [Validators.required, /*Validators.minLength(8) */]] 53 | }); 54 | } 55 | 56 | public submit(): void { 57 | 58 | if (this.loginForm.valid) { 59 | const {username, password} = this.loginForm.value; 60 | 61 | this.subscriptions.push(this.usersService.login({ 62 | username, 63 | password 64 | }).subscribe(result => { // in OnInit we are listening to user events, if the user logs in, we redirect to home 65 | }, err => { 66 | debugger; 67 | })); 68 | } else { 69 | this.notificationService.dispatchErrorMessage('Invalid form'); 70 | } 71 | } 72 | 73 | ngOnDestroy() { 74 | this.subscriptions.forEach(sub => 75 | sub.unsubscribe()); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/assets/images/package.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/app/products/product-list/product-list.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import {Observable} from 'rxjs'; 3 | 4 | import {Router} from '@angular/router'; 5 | import {Product} from '../../shared/models/product'; 6 | import {Category} from '../../shared/models/category.model'; 7 | import {ProductsService} from '../../shared/services/products.service'; 8 | import {UsersService} from '../../shared/services/users.service'; 9 | import {ProductListResponseDto} from '../../shared/dtos/responses/products/products.dto'; 10 | import {PaginatedRequestDto} from '../../shared/dtos/requests/base.dto'; 11 | 12 | 13 | @Component({ 14 | selector: 'app-product-list', 15 | templateUrl: './product-list.component.html', 16 | styleUrls: ['./product-list.component.css'] 17 | }) 18 | export class ProductListComponent implements OnInit { 19 | 20 | 21 | products: Observable; 22 | productList: Product[]; 23 | categories: Category[]; 24 | selectedCategory: string; 25 | private isAdmin: boolean; 26 | private errors: any; 27 | 28 | constructor(private productsService: ProductsService, public usersService: UsersService, 29 | private router: Router) { 30 | this.usersService.isAdminAsync().subscribe(isAdmin => { 31 | this.isAdmin = isAdmin; 32 | }); 33 | } 34 | 35 | ngOnInit() { 36 | this.products = this.productsService.getAllProducts(); 37 | 38 | this.products.subscribe(res => { 39 | console.log('I got something'); 40 | if (res && res.success) { 41 | if (res.products) { 42 | this.productList = res.products; 43 | } 44 | if (res.categories) { 45 | this.categories = res.categories; 46 | } 47 | } 48 | // this.tags = res.tags; 49 | }, err => { 50 | console.error(err); 51 | }); 52 | 53 | } 54 | 55 | 56 | addOrUpdateCart(product: Product) { 57 | 58 | } 59 | 60 | edit(id: string) { 61 | this.productsService.getById(id).subscribe(product => { 62 | console.log(product); 63 | }); 64 | } 65 | 66 | getDetails(product: Product) { 67 | this.productsService.getById(product.id).subscribe(res => { 68 | if (res.success) { 69 | this.router.navigate(['simple_todos_api/', res.id]); 70 | } 71 | console.log('fetched ' + res.id); 72 | }); 73 | } 74 | 75 | onLoadMore(query: PaginatedRequestDto) { 76 | this.productsService.getAllProducts(query); // no need to subscribe, we render using observables so angular takes care for us 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/app/auth/index.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 60 | 61 |
62 | 63 |
-------------------------------------------------------------------------------- /src/app/auth/register/register.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import {FormBuilder, FormGroup, Validators} from '@angular/forms'; 3 | import {Router} from '@angular/router'; 4 | import {UsersService} from '../../shared/services/users.service'; 5 | import {catchError, map} from 'rxjs/operators'; 6 | 7 | import {NotificationService} from '../../shared/services/notification.service'; 8 | import {BaseAppDtoResponse} from '../../shared/dtos/responses/shared/base.dto'; 9 | 10 | @Component({ 11 | selector: 'app-register', 12 | templateUrl: './register.component.html', 13 | styleUrls: ['./register.component.css'] 14 | }) 15 | export class RegisterComponent implements OnInit { 16 | private registerForm: FormGroup; 17 | 18 | constructor( 19 | private fb: FormBuilder, 20 | // private alertService: AlertService, 21 | private auth: UsersService, 22 | // private loadingService: LoadingService, 23 | private router: Router, 24 | private notificationService: NotificationService 25 | ) { 26 | this.createForm(); 27 | } 28 | 29 | ngOnInit() { 30 | } 31 | 32 | private createForm(): void { 33 | this.registerForm = this.fb.group({ 34 | firstName: ['', [Validators.required]], 35 | lastName: ['', [Validators.required]], 36 | email: ['', [Validators.required, Validators.email]], 37 | username: ['', [Validators.required]], 38 | password: ['', [Validators.required, Validators.minLength(8)]] 39 | }); 40 | } 41 | 42 | private submit(): void { 43 | if (this.registerForm.valid) { 44 | this.notificationService.dispatchSuccessMessage('submitting register Form'); 45 | const {firstName, lastName, email, password, username} = this.registerForm.value; 46 | 47 | // TODO call the auth service 48 | 49 | this.auth.register({firstName, lastName, email, password, username}).pipe(map(res => { 50 | if (res && res.success) { 51 | this.notificationService.dispatchSuccessMessage('register successful'); 52 | console.log(res); 53 | if (res.full_messages) { 54 | // alert(res.full_messages[0]); 55 | this.notificationService.dispatchSuccessMessage(res.full_messages[0]); 56 | } 57 | this.router.navigate(['/auth/login']); 58 | } else { 59 | this.notificationService.dispatchErrorMessage('Failed registration process'); 60 | } 61 | // this.loadingService.isLoading.next(false); 62 | }), catchError(err => { 63 | if (err.error) { 64 | const error = err.error as BaseAppDtoResponse; 65 | error.full_messages = err.error.full_messages; 66 | this.notificationService.dispatchErrorMessage(error.full_messages[0]); 67 | } 68 | return [err]; 69 | })).subscribe(res => { 70 | console.log(res); 71 | }); 72 | 73 | } else { 74 | alert('Invalid form'); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/app/products/product-details/product-details.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 7 |
8 | 9 |
10 |
11 |

{{product.name}}

12 |
13 |

{{product.description}}

14 |
15 | Quantity 16 | 21 | 22 |

Price: ${{product.price ? product.price.toFixed(2) : 0}}

23 | 24 | 27 |   28 | 31 |   32 | 33 | Checkout 34 | 35 |
36 |
37 |
38 | 39 |
40 | 41 |
42 |

Comments

43 |
44 | {{comment.user?.username}} 45 |

{{comment.content}}

46 | 48 |
49 |
50 | 51 |
52 |
53 |
Comment
54 |
55 |
56 |
57 | 59 |
60 |
61 | 62 |
63 |
64 |
65 |
66 | Login If you want to comment 67 | 68 | Login 69 | 70 | 71 | Register 72 | 73 |
74 | 75 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | AngularShopApp 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /src/app/shared/components/header/header.component.html: -------------------------------------------------------------------------------- 1 | 77 | 78 |
79 | 80 | Id: {{item.id}} Qty: {{item.quantity}} 81 | 82 |
83 | 84 |
{{message}}
85 | -------------------------------------------------------------------------------- /src/app/cart/my-cart/my-cart.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 30 | 31 | 37 | 38 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 55 | 56 | 58 | 67 | 68 | 69 |
ProductPrice QuantitySubtotal
18 |
19 | 24 |
25 |

{{cartItem.name}}

26 |

{{cartItem.description}}

27 |
28 |
29 |
${{cartItem.price}} 32 | 36 | $ {{cartItem.quantity * cartItem.price}} 39 | 42 |
Total {{totalAmount}}
51 | 52 | Continue Shopping 53 | 54 | 59 | 60 | Checkout 61 | 62 | 63 | 66 |
70 |
71 |
72 | 73 | 74 |
75 |
76 |
77 |
78 | 79 | No products added yet ;( 80 |
81 | 82 | -------------------------------------------------------------------------------- /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 | "deprecation": { 15 | "severity": "warn" 16 | }, 17 | "eofline": true, 18 | "forin": true, 19 | "import-blacklist": [ 20 | true, 21 | "rxjs/Rx" 22 | ], 23 | "import-spacing": true, 24 | "indent": [ 25 | true, 26 | "spaces" 27 | ], 28 | "interface-over-type-literal": true, 29 | "label-position": true, 30 | "max-line-length": [ 31 | true, 32 | 140 33 | ], 34 | "member-access": false, 35 | "member-ordering": [ 36 | true, 37 | { 38 | "order": [ 39 | "static-field", 40 | "instance-field", 41 | "static-method", 42 | "instance-method" 43 | ] 44 | } 45 | ], 46 | "no-arg": true, 47 | "no-bitwise": true, 48 | "no-console": [ 49 | true, 50 | "debug", 51 | "info", 52 | "time", 53 | "timeEnd", 54 | "trace" 55 | ], 56 | "no-construct": true, 57 | "no-debugger": false, 58 | "no-duplicate-super": true, 59 | "no-empty": false, 60 | "no-empty-interface": true, 61 | "no-eval": true, 62 | "no-inferrable-types": [ 63 | true, 64 | "ignore-params" 65 | ], 66 | "no-misused-new": true, 67 | "no-non-null-assertion": true, 68 | "no-redundant-jsdoc": true, 69 | "no-shadowed-variable": true, 70 | "no-string-literal": false, 71 | "no-string-throw": true, 72 | "no-switch-case-fall-through": true, 73 | "no-trailing-whitespace": true, 74 | "no-unnecessary-initializer": true, 75 | "no-unused-expression": true, 76 | "no-use-before-declare": true, 77 | "no-var-keyword": true, 78 | "object-literal-sort-keys": false, 79 | "one-line": [ 80 | true, 81 | "check-open-brace", 82 | "check-catch", 83 | "check-else", 84 | "check-whitespace" 85 | ], 86 | "prefer-const": true, 87 | "quotemark": [ 88 | true, 89 | "single" 90 | ], 91 | "radix": true, 92 | "semicolon": [ 93 | true, 94 | "always" 95 | ], 96 | "triple-equals": [ 97 | true, 98 | "allow-null-check" 99 | ], 100 | "typedef-whitespace": [ 101 | true, 102 | { 103 | "call-signature": "nospace", 104 | "index-signature": "nospace", 105 | "parameter": "nospace", 106 | "property-declaration": "nospace", 107 | "variable-declaration": "nospace" 108 | } 109 | ], 110 | "unified-signatures": true, 111 | "variable-name": false, 112 | "whitespace": [ 113 | true, 114 | "check-branch", 115 | "check-decl", 116 | "check-operator", 117 | "check-separator", 118 | "check-type" 119 | ], 120 | "no-output-on-prefix": true, 121 | "use-input-property-decorator": true, 122 | "use-output-property-decorator": true, 123 | "use-host-property-decorator": true, 124 | "no-input-rename": true, 125 | "no-output-rename": true, 126 | "use-life-cycle-interface": true, 127 | "use-pipe-transform-interface": true, 128 | "component-class-suffix": true, 129 | "directive-class-suffix": true 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /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/guide/browser-support 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 | /** 38 | * If the application will be indexed by Google Search, the following is required. 39 | * Googlebot uses a renderer based on Chrome 41. 40 | * https://developers.google.com/search/docs/guides/rendering 41 | **/ 42 | // import 'core-js/es6/array'; 43 | 44 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 45 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 46 | 47 | /** IE10 and IE11 requires the following for the Reflect API. */ 48 | // import 'core-js/es6/reflect'; 49 | 50 | /** 51 | * Web Animations `@angular/platform-browser/animations` 52 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 53 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 54 | **/ 55 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 56 | 57 | /** 58 | * By default, zone.js will patch all possible macroTask and DomEvents 59 | * user can disable parts of macroTask/DomEvents patch by setting following flags 60 | */ 61 | 62 | // (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 63 | // (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 64 | // (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 65 | 66 | /* 67 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 68 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 69 | */ 70 | // (window as any).__Zone_enable_cross_context_check = true; 71 | 72 | /*************************************************************************************************** 73 | * Zone JS is required by default for Angular itself. 74 | */ 75 | import 'zone.js/dist/zone'; // Included with Angular CLI. 76 | 77 | 78 | /*************************************************************************************************** 79 | * APPLICATION IMPORTS 80 | */ 81 | -------------------------------------------------------------------------------- /src/app/shared/services/orders.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {Product} from '../models/product'; 3 | import {ContactInfo} from '../models/contact_info.model'; 4 | import {HttpClient} from '@angular/common/http'; 5 | import {environment} from '../../../environments/environment'; 6 | import {Observable} from 'rxjs'; 7 | 8 | import {catchError, map} from 'rxjs/operators'; 9 | import {NotificationService} from './notification.service'; 10 | 11 | import {OrderListDto} from '../dtos/responses/orders/order-list.dto'; 12 | 13 | import {OrderDetailsDto} from '../dtos/responses/orders/order-details.response'; 14 | import {ErrorResult} from '../dtos/local/base'; 15 | import {buildErrorObservable} from '../utils/net.utils'; 16 | import {ShoppingCartService} from './shopping-cart.service'; 17 | 18 | let CREATED = false; 19 | 20 | @Injectable({ 21 | providedIn: 'root' 22 | }) 23 | export class OrdersService { 24 | 25 | constructor(private httpClient: HttpClient, private cartService: ShoppingCartService, private notificationService: NotificationService) { 26 | if (CREATED) { 27 | alert('Two instances of the same OrdersService'); 28 | return; 29 | } 30 | CREATED = true; 31 | } 32 | 33 | createOrderwithNewAddress(products: Product[], contactInfo: ContactInfo): Observable { 34 | const contactData = { 35 | // TODO: we are placing redundant fields, fix. 36 | ...contactInfo, 37 | first_name: contactInfo.firstName, 38 | last_name: contactInfo.lastName, 39 | zip_code: contactInfo.zipCode, 40 | card_number: contactInfo.cardNumber, 41 | }; 42 | 43 | return this.handleCreateOrderPromise(this.httpClient.post(`${environment.urls.orders}`, { 44 | cart_items: products, 45 | ...contactData 46 | })); 47 | } 48 | 49 | createOrderReusingAddress(cartItems: Product[], addressId: string): Observable { 50 | return this.handleCreateOrderPromise(this.httpClient.post(`${environment.urls.orders}`, { 51 | cart_items: cartItems, 52 | address_id: addressId, 53 | })); 54 | } 55 | 56 | private handleCreateOrderPromise(orderPromise: Observable): Observable { 57 | return orderPromise.pipe(map(res => { 58 | if (res.success) { 59 | this.notificationService.dispatchSuccessMessage('Order placed successfully'); 60 | this.cartService.clearCart(); 61 | } 62 | return res; 63 | }), catchError(err => { 64 | this.notificationService.dispatchErrorMessage(err.message); 65 | return buildErrorObservable(err); 66 | })); 67 | } 68 | 69 | getMyOrders(): Observable { 70 | return this.httpClient.get(environment.urls.orders).pipe(map(res => { 71 | if (res.success) { 72 | const response = res as OrderListDto; 73 | console.log(res); 74 | this.notificationService.dispatchSuccessMessage('Retrieved ' + response.page_meta.current_items_count); 75 | } 76 | return res; 77 | }, catchError(err => { 78 | this.notificationService.dispatchErrorMessage(err); 79 | return []; 80 | }))); 81 | } 82 | 83 | getOrder(id: number): Observable { 84 | return this.httpClient.get(`${environment.urls.orders}/${id}`) 85 | .pipe(map(res => { 86 | if (res.success) { 87 | const response = res as OrderDetailsDto; 88 | } 89 | return res; 90 | }), catchError(err => { 91 | this.notificationService.dispatchErrorMessage(err); 92 | return []; 93 | })); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/app/products/product-list/product-list.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 | 9 |
10 |
11 |
12 |
13 | 14 |
15 | 16 |
17 |
18 | 19 |
21 |
22 |
23 | 24 |
25 | 26 | 27 | 37 | 38 | 39 | 40 |
41 | 42 | 43 |
{{ product.name }}
44 |
45 |

46 | 47 | {{ 48 | product.name }} 49 | 50 |

51 | 52 |

{{ product.description }} 53 |

54 | 55 | 71 | 72 |
73 | 74 | 75 |
76 | 77 | 78 |
79 |
80 |
81 |
82 | 83 |
84 | 85 |
86 |
87 | 88 |
89 |
90 |
91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /src/app/components/home/home.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
6 |
7 |
8 | 9 |
10 | 11 | 12 | 22 | 23 | 24 | 25 |
26 | 27 | 28 |
{{ category.name }}
29 |
30 |

31 | 32 | {{ 33 | category.name }} 34 | 35 |

36 | 37 |

{{ category.descritpion }} 38 |

39 | 40 | 45 | 46 |
47 | 48 | 49 |
50 | 51 | 52 |
53 |
54 |
55 |
56 |
57 | 58 |
60 |
61 |
62 | 63 |
64 | 65 | 66 | 76 | 77 | 78 | 79 |
80 | 81 | 82 |
{{ tag.name }}
83 |
84 |

85 | 86 | {{ 87 | tag.name }} 88 | 89 |

90 | 91 |

{{ tag.description }} 92 |

93 | 94 | 99 | 100 |
101 | 102 | 103 |
104 | 105 | 106 |
107 |
108 |
109 |
110 | 111 |
112 | 113 |
114 |
115 | -------------------------------------------------------------------------------- /src/assets/images/world.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/app/shared/services/users.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {BehaviorSubject, Observable} from 'rxjs'; 3 | import {HttpClient} from '@angular/common/http'; 4 | import {environment} from '../../../environments/environment'; 5 | import {User} from '../models/user'; 6 | import {catchError, map, retry} from 'rxjs/operators'; 7 | import {ErrorResult} from '../dtos/local/base'; 8 | import {NotificationService} from './notification.service'; 9 | import {buildError, buildErrorObservable} from '../utils/net.utils'; 10 | import {LoginDtoResponse} from '../dtos/responses/users/auth.dto'; 11 | import {LoginRequestDto} from '../dtos/requests/login.dto'; 12 | import {LocalStorageService} from './local-storage.service'; 13 | 14 | 15 | let CREATED = false; 16 | 17 | @Injectable({ 18 | providedIn: 'root' 19 | }) 20 | export class UsersService { 21 | 22 | private isLoggedIn: boolean; 23 | private cachedUser: User; 24 | private user: BehaviorSubject; 25 | private USER_KEY = 'auth-user'; 26 | 27 | constructor(private storageService: LocalStorageService, private notificationService: NotificationService, private http: HttpClient) { 28 | if (CREATED) { 29 | alert('Two instances of the same UsersService'); 30 | return; 31 | } 32 | 33 | CREATED = true; 34 | 35 | this.cachedUser = this.getUserSync(); 36 | this.isLoggedIn = !!this.cachedUser; 37 | this.user = new BehaviorSubject(this.cachedUser); 38 | } 39 | 40 | login(credentials: LoginRequestDto): Observable { 41 | return this.http.post(`${environment.urls.users}/login`, credentials) 42 | .pipe(retry(2), map(res => { 43 | 44 | if (res && res.success) { 45 | if (res.full_messages && Array.isArray(res.full_messages) 46 | && res.full_messages.length > 0) { 47 | this.notificationService.dispatchSuccessMessage(res.full_messages[0]); 48 | } else { 49 | this.notificationService.dispatchSuccessMessage('Logged in successfully'); 50 | } 51 | 52 | const user: User = res.user; 53 | user.token = res.token; 54 | this.saveUser(res.user); 55 | return user; 56 | } 57 | 58 | return buildError('Unknown error while trying to login'); 59 | } 60 | ), catchError(err => { 61 | this.notificationService.dispatchErrorMessage(err.message); 62 | return buildErrorObservable(err); 63 | })); 64 | } 65 | 66 | logout(): void { 67 | this.clearUser(); 68 | } 69 | 70 | isLoggedInSync(): boolean { 71 | // return this.cachedUser != null && this.cachedUser.username !== null; 72 | return this.user.getValue() != null && this.user.getValue().username !== null; 73 | } 74 | 75 | isLoggedInAsync(): Observable { 76 | return this.user.pipe(map(user => { 77 | if (user == null || user.username == null) { 78 | return false; 79 | } 80 | return true; 81 | })); 82 | } 83 | 84 | isAdminSync(): boolean { 85 | return this.user.getValue().roles.indexOf('ROLE_ADMIN') !== -1; 86 | } 87 | 88 | 89 | getUser(): Observable { 90 | return this.user.asObservable(); 91 | } 92 | 93 | register(userInfo: Object): Observable { 94 | return this.http.post(`${environment.urls.users}`, userInfo); 95 | } 96 | 97 | isAdminAsync(): Observable { 98 | return this.user.pipe( 99 | map(user => { 100 | if (user == null) { 101 | return false; 102 | } 103 | const rolesIntersection = user.roles.filter(role => -1 !== ['ROLE_ADMIN'].indexOf(role)); 104 | return rolesIntersection.length >= 1; 105 | }) 106 | ); 107 | } 108 | 109 | public saveUser(user: User): void { 110 | this.storageService.clear(this.USER_KEY); 111 | this.cachedUser = user; 112 | this.storageService.set(this.USER_KEY, JSON.stringify(user)); 113 | this.user.next(user); 114 | } 115 | 116 | 117 | private getUserSync(): User { 118 | const user = this.storageService.get(this.USER_KEY); 119 | if (user) { 120 | return JSON.parse(user) as User; 121 | } 122 | return null; 123 | } 124 | 125 | clearUser(): void { 126 | this.signOut(); 127 | } 128 | 129 | signOut(): void { 130 | this.storageService.clear(this.USER_KEY); 131 | this.user.next(null); 132 | } 133 | 134 | public getRolesSync(): string[] { 135 | const roles = []; 136 | if (this.storageService.get(this.USER_KEY)) { 137 | JSON.parse(sessionStorage.getItem(this.USER_KEY)).roles.forEach(role => { 138 | roles.push(role); 139 | }); 140 | } 141 | return roles; 142 | } 143 | 144 | } 145 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "AngularShopApp": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "prefix": "app", 11 | "schematics": {}, 12 | "architect": { 13 | "build": { 14 | "builder": "@angular-devkit/build-angular:browser", 15 | "options": { 16 | "outputPath": "dist/AngularShopApp", 17 | "index": "src/index.html", 18 | "main": "src/main.ts", 19 | "polyfills": "src/polyfills.ts", 20 | "tsConfig": "src/tsconfig.app.json", 21 | "assets": [ 22 | "src/favicon.ico", 23 | "src/assets" 24 | ], 25 | "styles": [ 26 | "src/styles.css", 27 | "node_modules/bootstrap/dist/css/bootstrap.min.css", 28 | "node_modules/froala-editor/css/froala_editor.pkgd.min.css", 29 | "node_modules/froala-editor/css/froala_style.min.css" 30 | ], 31 | "scripts": [ 32 | "./node_modules/froala-editor/js/froala_editor.pkgd.min.js" 33 | ] 34 | }, 35 | "configurations": { 36 | "production": { 37 | "fileReplacements": [ 38 | { 39 | "replace": "src/environments/environment.ts", 40 | "with": "src/environments/environment.prod.ts" 41 | } 42 | ], 43 | "optimization": true, 44 | "outputHashing": "all", 45 | "sourceMap": false, 46 | "extractCss": true, 47 | "namedChunks": false, 48 | "aot": true, 49 | "extractLicenses": true, 50 | "vendorChunk": false, 51 | "buildOptimizer": true, 52 | "budgets": [ 53 | { 54 | "type": "initial", 55 | "maximumWarning": "2mb", 56 | "maximumError": "5mb" 57 | } 58 | ] 59 | } 60 | } 61 | }, 62 | "serve": { 63 | "builder": "@angular-devkit/build-angular:dev-server", 64 | "options": { 65 | "browserTarget": "AngularShopApp:build" 66 | }, 67 | "configurations": { 68 | "production": { 69 | "browserTarget": "AngularShopApp:build:production" 70 | } 71 | } 72 | }, 73 | "extract-i18n": { 74 | "builder": "@angular-devkit/build-angular:extract-i18n", 75 | "options": { 76 | "browserTarget": "AngularShopApp:build" 77 | } 78 | }, 79 | "test": { 80 | "builder": "@angular-devkit/build-angular:karma", 81 | "options": { 82 | "main": "src/test.ts", 83 | "polyfills": "src/polyfills.ts", 84 | "tsConfig": "src/tsconfig.spec.json", 85 | "karmaConfig": "src/karma.conf.js", 86 | "styles": [ 87 | "src/styles.css", 88 | "node_modules/bootstrap/dist/css/bootstrap.min.css", 89 | "node_modules/froala-editor/css/froala_editor.pkgd.min.css", 90 | "node_modules/froala-editor/css/froala_style.min.css" 91 | ], 92 | "scripts": [ 93 | "./node_modules/froala-editor/js/froala_editor.pkgd.min.js" 94 | ], 95 | "assets": [ 96 | "src/favicon.ico", 97 | "src/assets" 98 | ] 99 | } 100 | }, 101 | "lint": { 102 | "builder": "@angular-devkit/build-angular:tslint", 103 | "options": { 104 | "tsConfig": [ 105 | "src/tsconfig.app.json", 106 | "src/tsconfig.spec.json" 107 | ], 108 | "exclude": [ 109 | "**/node_modules/**" 110 | ] 111 | } 112 | } 113 | } 114 | }, 115 | "AngularShopApp-e2e": { 116 | "root": "e2e/", 117 | "projectType": "application", 118 | "prefix": "", 119 | "architect": { 120 | "e2e": { 121 | "builder": "@angular-devkit/build-angular:protractor", 122 | "options": { 123 | "protractorConfig": "e2e/protractor.conf.js", 124 | "devServerTarget": "AngularShopApp:serve" 125 | }, 126 | "configurations": { 127 | "production": { 128 | "devServerTarget": "AngularShopApp:serve:production" 129 | } 130 | } 131 | }, 132 | "lint": { 133 | "builder": "@angular-devkit/build-angular:tslint", 134 | "options": { 135 | "tsConfig": "e2e/tsconfig.e2e.json", 136 | "exclude": [ 137 | "**/node_modules/**" 138 | ] 139 | } 140 | } 141 | } 142 | } 143 | }, 144 | "defaultProject": "AngularShopApp" 145 | } 146 | -------------------------------------------------------------------------------- /src/app/shared/services/shopping-cart.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {HttpClient} from '@angular/common/http'; 3 | import {environment} from '../../../environments/environment'; 4 | import {BehaviorSubject, Observable, throwError} from 'rxjs'; 5 | 6 | import {Product} from '../models/product'; 7 | import {ShoppingCart} from '../models/shopping-cart.model'; 8 | import {catchError} from 'rxjs/operators'; 9 | import {LocalStorageService} from './local-storage.service'; 10 | 11 | 12 | let CREATED = false; 13 | 14 | @Injectable({ 15 | providedIn: 'root' 16 | }) 17 | export class ShoppingCartService { 18 | BASE_URL = `${environment.urls.cart}`; 19 | 20 | private cartModel: ShoppingCart; 21 | private cartBehaviorSubject: BehaviorSubject; 22 | 23 | private subscribers: any; 24 | private readonly cartKey = 'app_cart'; 25 | 26 | constructor(private http: HttpClient, private storageService: LocalStorageService) { 27 | if (CREATED) { 28 | alert('Two instances of the same ShoppingCartService'); 29 | return; 30 | } 31 | CREATED = true; 32 | this.cartBehaviorSubject = new BehaviorSubject(this.cartModel); 33 | this.getCartFromStorage(); 34 | 35 | } 36 | 37 | private getCartFromStorage(): ShoppingCart { 38 | this.cartModel = (JSON.parse(window.localStorage.getItem(this.cartKey)) as ShoppingCart); 39 | if (!this.cartModel) { 40 | this.cartModel = new ShoppingCart(); 41 | } else { 42 | // populate the isInCart flag which is not stored but it is required and used by other functions in this app 43 | this.cartModel.cartItems.forEach(ci => { 44 | ci.id = String(ci.id); // we have to do this because somehow the id is stored as an integer 45 | ci.isInCart = true; 46 | }); 47 | } 48 | this.notifyDataSetChanged(); 49 | return this.cartModel; 50 | } 51 | 52 | private formatErrors(error: any) { 53 | return throwError(error.error); 54 | } 55 | 56 | getCart(): Observable { 57 | // return this.http.get(this.BASE_URL).pipe(catchError(this.formatErrors)); 58 | return this.cartBehaviorSubject.asObservable(); 59 | } 60 | 61 | updateCart(body: Object = {}): Observable { 62 | alert('updateCart'); 63 | return this.http.put(this.BASE_URL, JSON.stringify(body)).pipe(catchError(this.formatErrors)); 64 | } 65 | 66 | 67 | addToCart(product: Product, quantity: number): Product { 68 | if (this.cartModel == null) { 69 | this.cartModel = this.getCartFromStorage(); 70 | } 71 | 72 | let item = this.cartModel.cartItems.find((pr) => pr.id === product.id); 73 | if (item === undefined) { 74 | item = new Product(); 75 | item.id = product.id; 76 | item.name = product.name; 77 | item.slug = product.slug; 78 | item.price = product.price; 79 | item.quantity = 0; 80 | item.isInCart = true; 81 | } 82 | 83 | this.cartModel.cartItems.push(item); 84 | item.quantity += quantity; 85 | this.commitCartTransaction(); 86 | return item; 87 | } 88 | 89 | private commitCartTransaction() { 90 | this.saveCartToStorage(); 91 | this.notifyDataSetChanged(); 92 | } 93 | 94 | private saveCartToStorage(cart: ShoppingCart = this.cartModel) { 95 | this.storageService.set(this.cartKey, JSON.stringify(cart)); 96 | } 97 | 98 | private notifyDataSetChanged() { 99 | this.cartBehaviorSubject.next(this.cartModel); 100 | } 101 | 102 | updateCartLocal(cartState: Object = {}) { 103 | 104 | } 105 | 106 | deleteCart(): Observable { 107 | alert('delete Cart'); 108 | return this.http.delete(this.BASE_URL).pipe(catchError(this.formatErrors)); 109 | } 110 | 111 | checkout(): Observable { 112 | const body = this.getCartFromStorage(); 113 | return this.http.post(`${environment.urls.orders}`, body); 114 | } 115 | 116 | removeFromCart(product: Product) { 117 | if (this.cartModel === undefined) { 118 | this.cartModel = this.getCartFromStorage(); 119 | } 120 | this.cartModel.cartItems = this.cartModel.cartItems 121 | .filter((cartItem) => cartItem.id !== product.id); 122 | this.commitCartTransaction(); 123 | } 124 | 125 | getFavoritedProductsCount() { 126 | return 2; 127 | } 128 | 129 | updateQuantity(cartItem: Product, quantity: number): Product { 130 | if (cartItem == null || !cartItem.isInCart) { 131 | console.error('product not in cart'); 132 | return null; 133 | } 134 | if (quantity <= 0) { 135 | this.removeFromCart(cartItem); 136 | return cartItem; 137 | } 138 | if (cartItem.quantity !== quantity) { 139 | cartItem.quantity = 0; 140 | const item = this.cartModel.cartItems.find((pr) => pr.id === cartItem.id); 141 | if (item == null) { 142 | debugger; 143 | } 144 | item.quantity = quantity; 145 | cartItem.quantity = quantity; 146 | this.commitCartTransaction(); 147 | return item; 148 | } 149 | 150 | return null; 151 | } 152 | 153 | getCartSnapshot() { 154 | return this.cartModel; 155 | } 156 | 157 | clearCart() { 158 | this.cartModel = new ShoppingCart(); 159 | this.saveCartToStorage(this.cartModel); 160 | this.notifyDataSetChanged(); 161 | } 162 | 163 | } 164 | 165 | -------------------------------------------------------------------------------- /src/app/orders/order-create/checkout.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnDestroy, OnInit} from '@angular/core'; 2 | import {User} from '../../shared/models/user'; 3 | import {UsersService} from '../../shared/services/users.service'; 4 | import {FormBuilder, FormGroup, NgForm, Validators} from '@angular/forms'; 5 | import {OrdersService} from '../../shared/services/orders.service'; 6 | import {Product} from '../../shared/models/product'; 7 | import {ShoppingCartService} from '../../shared/services/shopping-cart.service'; 8 | import {Subscription} from 'rxjs'; 9 | import {NotificationService} from '../../shared/services/notification.service'; 10 | import {ContactInfo} from '../../shared/models/contact_info.model'; 11 | import {Router} from '@angular/router'; 12 | import {AddressesService} from '../../shared/services/addresses.service'; 13 | import {AddressDto, AddressListResponseDto} from '../../shared/dtos/responses/addresses/addresses.dto'; 14 | 15 | @Component({ 16 | selector: 'app-checkout', 17 | templateUrl: './checkout.component.html', 18 | styleUrls: ['./checkout.component.css'] 19 | }) 20 | export class CheckoutComponent implements OnInit, OnDestroy { 21 | user: User; 22 | isLoggedIn: boolean; 23 | cartItems: Product[] = []; 24 | private subscriptions: Subscription[] = []; 25 | private checkoutForm: FormGroup; 26 | private addresses: AddressDto[]; 27 | private selectedAddress: AddressDto; 28 | 29 | constructor(private usersService: UsersService, private ordersService: OrdersService, private shoppingCartService: ShoppingCartService, 30 | private fb: FormBuilder, private notificationService: NotificationService, private addressesService: AddressesService, 31 | private router: Router) { 32 | this.user = new User({}); 33 | this.usersService.getUser().subscribe(user => { 34 | this.user = user; 35 | if (user !== null) { 36 | this.isLoggedIn = true; 37 | this.addressesService.fetchAll().subscribe(res => { 38 | if (res.success) { 39 | this.addresses = (res as AddressListResponseDto).addresses; 40 | } 41 | }, err => { 42 | debugger; 43 | }); 44 | } else { 45 | this.isLoggedIn = false; 46 | } 47 | 48 | }); 49 | 50 | this.subscriptions.push(this.shoppingCartService.getCart().subscribe(cart => { 51 | if (cart !== null) { 52 | 53 | this.cartItems = cart.cartItems; 54 | console.log(this.cartItems); 55 | } 56 | })); 57 | 58 | this.createForm(); 59 | } 60 | 61 | ngOnInit() { 62 | } 63 | 64 | updateUserDetails(form: NgForm) { 65 | const data = form.value; 66 | 67 | data['email'] = this.user.email; 68 | data['username'] = this.user.username; 69 | 70 | console.log('Data: ', data); 71 | } 72 | 73 | ngOnDestroy(): void { 74 | this.subscriptions.forEach(sub => sub.unsubscribe()); 75 | } 76 | 77 | private createForm() { 78 | this.checkoutForm = this.fb.group({ 79 | firstName: [this.user.first_name, [Validators.required, Validators.minLength(2)]], 80 | lastName: [this.user.last_name, [Validators.required, Validators.minLength(2)]], 81 | username: [this.user.username, [Validators.required, Validators.minLength(2)]], 82 | email: [this.user.email, [Validators.required, Validators.email]], 83 | streetAddress: [this.user.address, [Validators.required, Validators.minLength(2)]], 84 | city: [this.user.city, [Validators.required, Validators.minLength(2)]], 85 | country: [this.user.country, [Validators.required, Validators.minLength(2)]], 86 | zipCode: [this.user.zipCode, [Validators.required, Validators.minLength(2)]], 87 | cardNumber: ['', []], 88 | }); 89 | } 90 | 91 | public submitCheckout(): void { 92 | if (this.checkoutForm.valid) { 93 | let checkoutObservable; 94 | if (this.selectedAddressUnchanged()) { 95 | checkoutObservable = this.ordersService.createOrderwithNewAddress(this.cartItems, new ContactInfo(this.checkoutForm.value)); 96 | } else { 97 | checkoutObservable = this.ordersService.createOrderReusingAddress(this.cartItems, this.selectedAddress.id); 98 | } 99 | checkoutObservable.subscribe(res => { 100 | 101 | if (res.success) { 102 | this.router.navigate(['/']); 103 | } 104 | }, err => { 105 | debugger; 106 | }); 107 | } 108 | } 109 | 110 | selectedAddressUnchanged() { 111 | return this.selectedAddress 112 | && this.selectedAddress.first_name === this.checkoutForm.value.firstName 113 | && this.selectedAddress.last_name === this.checkoutForm.value.lastName 114 | && this.selectedAddress.street_address === this.checkoutForm.value.streetAddress 115 | && this.selectedAddress.city === this.checkoutForm.value.city 116 | && this.selectedAddress.country === this.checkoutForm.value.country 117 | && this.selectedAddress.zip_code === this.checkoutForm.value.zipCode; 118 | } 119 | 120 | onAddressChanged($event) { 121 | const address = this.addresses.find(a => String(a.id) === $event.target.value); 122 | this.selectedAddress = address; 123 | this.checkoutForm.patchValue({ 124 | firstName: address.first_name, 125 | lastName: address.last_name, 126 | username: this.isLoggedIn ? this.user.username : '', 127 | email: this.isLoggedIn ? this.user.email : '', 128 | streetAddress: address.street_address, 129 | city: address.city, 130 | country: address.country, 131 | zipCode: address.zip_code, 132 | }); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/app/products/product-details/product-details.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnDestroy, OnInit} from '@angular/core'; 2 | import {ActivatedRoute} from '@angular/router'; 3 | 4 | import {FormBuilder, FormGroup, Validators} from '@angular/forms'; 5 | import {Product} from '../../shared/models/product'; 6 | 7 | import {ProductsService} from '../../shared/services/products.service'; 8 | import {ShoppingCartService} from '../../shared/services/shopping-cart.service'; 9 | import {UsersService} from '../../shared/services/users.service'; 10 | import {Subscription} from 'rxjs'; 11 | import {NotificationService} from '../../shared/services/notification.service'; 12 | import {Comment} from '../../shared/models/comment.model'; 13 | import {CommentSubmittedResponse} from '../../shared/dtos/responses/comments/comment-submitted.response'; 14 | import {ShoppingCart} from '../../shared/models/shopping-cart.model'; 15 | import {User} from '../../shared/models/user'; 16 | 17 | 18 | @Component({ 19 | selector: 'app-product-details', 20 | templateUrl: './product-details.component.html', 21 | styleUrls: ['./product-details.component.css'] 22 | }) 23 | export class ProductDetailsComponent implements OnInit, OnDestroy { 24 | 25 | product: Product; 26 | 27 | private isLoggedIn: boolean; 28 | private subscriptions: Subscription[] = []; 29 | public commentForm: FormGroup; 30 | private quantity = 0; 31 | private cart: ShoppingCart; 32 | private currentUser: User; 33 | 34 | constructor( 35 | private route: ActivatedRoute, 36 | private productService: ProductsService, 37 | private shoppingCartService: ShoppingCartService, 38 | private usersService: UsersService, 39 | private notificationService: NotificationService, 40 | private fb: FormBuilder, 41 | ) { 42 | this.product = new Product(); 43 | this.subscriptions.push(this.usersService.getUser().subscribe(user => { 44 | this.currentUser = user; 45 | this.isLoggedIn = !!user; 46 | })); 47 | 48 | this.commentForm = this.fb.group({ 49 | content: ['', [Validators.required, Validators.minLength(2)]] 50 | }); 51 | 52 | } 53 | 54 | ngOnInit() { 55 | this.subscriptions.push(this.route.params.subscribe(params => { 56 | const slug = params['slug']; // (+) converts string 'id' to a number 57 | this.getProductDetail(slug); 58 | })); 59 | this.subscriptions.push(this.shoppingCartService.getCart().subscribe(cart => { 60 | this.cart = cart; 61 | this.updateQuantityValue(); 62 | // we could actually make the check here with : 63 | // const cartItem = this.cart.cartItems.find(ci => ci.slug === this.route.snapshot.params.slug); 64 | // which does not require the product to be fetched from the remote server to know if it is in cart and 65 | // the quantity. 66 | })); 67 | } 68 | 69 | updateQuantityValue() { 70 | const cartItem = this.cart.cartItems.find(ci => ci.id === this.product.id); 71 | if (cartItem) { 72 | this.quantity = cartItem.quantity; 73 | } else { 74 | this.quantity = 1; 75 | } 76 | } 77 | 78 | getProductDetail(slug: string) { 79 | 80 | this.productService.getBySlug(slug).subscribe(res => { 81 | // this.spinnerService.hide(); 82 | 83 | if (res.success) { 84 | this.product = new Product(res); 85 | this.updateQuantityValue(); 86 | } 87 | 88 | }); 89 | } 90 | 91 | ngOnDestroy() { 92 | this.subscriptions.forEach(sub => sub.unsubscribe()); 93 | } 94 | 95 | addOrUpdateCart(count: string | number) { 96 | if (this.product == null) { 97 | return; 98 | } 99 | // to avoid the type checking error, set as string, it would work anyways though 100 | const quantity = parseInt(count as string, 10); 101 | if (quantity <= 0 && this.product.isInCart) { 102 | this.shoppingCartService.removeFromCart(this.product); 103 | this.product.isInCart = false; 104 | return; 105 | } else if (this.product.isInCart) { 106 | this.product = this.shoppingCartService.updateQuantity(this.product, quantity); 107 | } else { 108 | this.product = this.shoppingCartService.addToCart(this.product, quantity); 109 | } 110 | 111 | this.product.isInCart = true; 112 | } 113 | 114 | 115 | submitComment() { 116 | if (this.commentForm.valid) { 117 | this.notificationService.dispatchSuccessMessage('Submitting Login Form'); 118 | const {content} = this.commentForm.value; 119 | const comment = new Comment({productId: this.product.id, content: content, id: null}); 120 | console.log(this.product.slug); 121 | this.productService.submitComment(comment, this.product.slug).subscribe(res => { 122 | if (res.success) { 123 | // Clear the text area 124 | this.commentForm.patchValue({content: ''}); 125 | 126 | if (this.product.comments == null) { 127 | this.product.comments = []; 128 | } 129 | const response = res as CommentSubmittedResponse; 130 | console.log(response); 131 | this.product.comments.push(new Comment({ 132 | id: response.id, 133 | username: response.username, 134 | content: response.content, 135 | createdAt: response.created_at, 136 | })); 137 | } 138 | }); 139 | } 140 | } 141 | 142 | deleteComment(comment: Comment) { 143 | this.productService.deleteComment(comment.id).subscribe(res => { 144 | if (res.success) { 145 | this.product.comments = this.product.comments.filter(cachedComments => cachedComments.id !== comment.id); 146 | } 147 | }); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/app/orders/order-create/checkout.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

Shipping Address

5 | 6 |
7 | 15 |
16 |
17 |
    18 |
  • 21 | 22 | firstName name is required. 23 | 24 | 25 | firstName must be at least 4 characters long. 26 | 27 |
  • 28 |
  • 31 | 32 | lastName name is required. 33 | 34 | 35 | lastName must be at least 4 characters long. 36 | 37 |
  • 38 |
  • 41 | 42 | address name is required. 43 | 44 | 45 | address must be at least 4 characters long. 46 | 47 |
  • 48 |
  • 51 | 52 | zipCode name is required. 53 | 54 | 55 | zipCode must be at least 4 characters long. 56 | 57 |
  • 58 |
  • 61 | 62 | country name is required. 63 | 64 | 65 | country must be at least 4 characters long. 66 | 67 |
  • 68 |
  • 71 | 72 | City is required. 73 | 74 | 75 | City must be at least 4 characters long. 76 | 77 |
  • 78 |
79 | 80 |
81 |
82 |
83 |
84 | 85 | 87 |
88 |
89 | 90 | 92 |
93 | Valid last name is required. 94 |
95 |
96 |
97 | 98 |
99 | 100 |
101 |
102 | @ 103 |
104 | 106 |
107 | Your username is required. 108 |
109 |
110 |
111 | 112 |
113 | 116 | 118 |
119 | Please enter a valid email address for shipping updates. 120 |
121 |
122 | 123 |
124 | 125 | 127 |
128 | Please enter your shipping address. 129 |
130 |
131 | 132 |
133 |
134 | 135 | 136 | 138 | 139 |
140 | Please select a valid country. 141 |
142 |
143 | 144 |
145 | 146 | 147 | 149 |
150 | 151 | 152 |
153 | 154 | 155 | 157 |
158 | Zip code required. 159 |
160 |
161 |
162 |
163 |
164 | 165 | 166 | 168 | 169 |
170 | Please select a valid country. 171 |
172 |
173 |
174 |
175 | 176 |
177 |
178 | 179 |
180 | 181 | Continue Shopping 182 |
183 |
184 |
185 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | @import "~bootstrap/dist/css/bootstrap.css"; 2 | @import "~font-awesome/css/font-awesome.css"; /* You can add global styles to this file, and also import other style files */ 3 | 4 | 5 | 6 | /* Sticky footer styles 7 | -------------------------------------------------- */ 8 | html { 9 | position: relative; 10 | min-height: 100%; 11 | padding-bottom: 50px; 12 | } 13 | 14 | body { 15 | /* Margin bottom by footer height */ 16 | margin-bottom: 60px; 17 | } 18 | 19 | .footer { 20 | position: absolute; 21 | bottom: 0; 22 | width: 100%; 23 | /* Set the fixed height of the footer here */ 24 | height: 60px; 25 | line-height: 60px; /* Vertically center the text there */ 26 | background-color: #f5f5f5; 27 | } 28 | 29 | 30 | .footer > .container { 31 | padding-right: 15px; 32 | padding-left: 15px; 33 | } 34 | 35 | code { 36 | font-size: 80%; 37 | } 38 | 39 | .btn-social { 40 | position: relative; 41 | padding-left: 44px; 42 | text-align: left; 43 | white-space: nowrap; 44 | overflow: hidden; 45 | text-overflow: ellipsis 46 | } 47 | 48 | .btn-social :first-child { 49 | position: absolute; 50 | left: 0; 51 | top: 0; 52 | bottom: 0; 53 | width: 32px; 54 | line-height: 34px; 55 | font-size: 1.6em; 56 | text-align: center; 57 | border-right: 1px solid rgba(0, 0, 0, 0.2) 58 | } 59 | 60 | .btn-social.btn-lg { 61 | padding-left: 61px 62 | } 63 | 64 | .btn-social.btn-lg :first-child { 65 | line-height: 45px; 66 | width: 45px; 67 | font-size: 1.8em 68 | } 69 | 70 | .btn-social.btn-sm { 71 | padding-left: 38px 72 | } 73 | 74 | .btn-social.btn-sm :first-child { 75 | line-height: 28px; 76 | width: 28px; 77 | font-size: 1.4em 78 | } 79 | 80 | .btn-social.btn-xs { 81 | padding-left: 30px 82 | } 83 | 84 | .btn-social.btn-xs :first-child { 85 | line-height: 20px; 86 | width: 20px; 87 | font-size: 1.2em 88 | } 89 | 90 | .btn-social-icon { 91 | position: relative; 92 | padding-left: 44px; 93 | text-align: left; 94 | white-space: nowrap; 95 | overflow: hidden; 96 | text-overflow: ellipsis; 97 | height: 34px; 98 | width: 34px; 99 | padding: 0 100 | } 101 | 102 | .btn-social-icon :first-child { 103 | position: absolute; 104 | left: 0; 105 | top: 0; 106 | bottom: 0; 107 | width: 32px; 108 | line-height: 34px; 109 | font-size: 1.6em; 110 | text-align: center; 111 | border-right: 1px solid rgba(0, 0, 0, 0.2) 112 | } 113 | 114 | .btn-social-icon.btn-lg { 115 | padding-left: 61px 116 | } 117 | 118 | .btn-social-icon.btn-lg :first-child { 119 | line-height: 45px; 120 | width: 45px; 121 | font-size: 1.8em 122 | } 123 | 124 | .btn-social-icon.btn-sm { 125 | padding-left: 38px 126 | } 127 | 128 | .btn-social-icon.btn-sm :first-child { 129 | line-height: 28px; 130 | width: 28px; 131 | font-size: 1.4em 132 | } 133 | 134 | .btn-social-icon.btn-xs { 135 | padding-left: 30px 136 | } 137 | 138 | .btn-social-icon.btn-xs :first-child { 139 | line-height: 20px; 140 | width: 20px; 141 | font-size: 1.2em 142 | } 143 | 144 | .btn-social-icon :first-child { 145 | border: none; 146 | text-align: center; 147 | width: 100% !important 148 | } 149 | 150 | .btn-social-icon.btn-lg { 151 | height: 45px; 152 | width: 45px; 153 | padding-left: 0; 154 | padding-right: 0 155 | } 156 | 157 | .btn-social-icon.btn-sm { 158 | height: 30px; 159 | width: 30px; 160 | padding-left: 0; 161 | padding-right: 0 162 | } 163 | 164 | .btn-social-icon.btn-xs { 165 | height: 22px; 166 | width: 22px; 167 | padding-left: 0; 168 | padding-right: 0 169 | } 170 | 171 | .btn-facebook { 172 | color: #fff; 173 | background-color: #3b5998; 174 | border-color: rgba(0, 0, 0, 0.2) 175 | } 176 | 177 | .btn-facebook:hover, .btn-facebook:focus, .btn-facebook:active, .btn-facebook.active, .open .dropdown-toggle.btn-facebook { 178 | color: #3b5998; 179 | background-color: #fff; 180 | border-color: rgba(0, 0, 0, 0.2) 181 | } 182 | 183 | .btn-facebook:active, .btn-facebook.active, .open .dropdown-toggle.btn-facebook { 184 | background-image: none 185 | } 186 | 187 | .btn-facebook.disabled, .btn-facebook[disabled], fieldset[disabled] .btn-facebook, .btn-facebook.disabled:hover, .btn-facebook[disabled]:hover, fieldset[disabled] .btn-facebook:hover, .btn-facebook.disabled:focus, .btn-facebook[disabled]:focus, fieldset[disabled] .btn-facebook:focus, .btn-facebook.disabled:active, .btn-facebook[disabled]:active, fieldset[disabled] .btn-facebook:active, .btn-facebook.disabled.active, .btn-facebook[disabled].active, fieldset[disabled] .btn-facebook.active { 188 | background-color: #3b5998; 189 | border-color: rgba(0, 0, 0, 0.2) 190 | } 191 | 192 | .btn-github { 193 | color: #141414; 194 | background-color: #ffffff; 195 | border-color: rgba(0, 0, 0, 0.2) 196 | } 197 | 198 | .btn-github:hover, .btn-github:focus, .btn-github:active, .btn-github.active, .open .dropdown-toggle.btn-github { 199 | color: #ffffff; 200 | background-color: #141414; 201 | border-color: rgba(0, 0, 0, 0.2) 202 | } 203 | 204 | .btn-github:active, .btn-github.active, .open .dropdown-toggle.btn-github { 205 | background-image: none 206 | } 207 | 208 | .btn-github.disabled, .btn-github[disabled], fieldset[disabled] .btn-github, .btn-github.disabled:hover, .btn-github[disabled]:hover, fieldset[disabled] .btn-github:hover, .btn-github.disabled:focus, .btn-github[disabled]:focus, fieldset[disabled] .btn-github:focus, .btn-github.disabled:active, .btn-github[disabled]:active, fieldset[disabled] .btn-github:active, .btn-github.disabled.active, .btn-github[disabled].active, fieldset[disabled] .btn-github.active { 209 | background-color: #444; 210 | border-color: rgba(0, 0, 0, 0.2) 211 | } 212 | 213 | .btn-google-plus { 214 | color: #fff; 215 | background-color: #dd4b39; 216 | text-transform: lowercase; 217 | border-color: rgba(0, 0, 0, 0.2) 218 | } 219 | 220 | .btn-google-plus:hover, .btn-google-plus:focus, .btn-google-plus:active, .btn-google-plus.active, .open .dropdown-toggle.btn-google-plus { 221 | color: #dd4b39; 222 | background-color: #fff; 223 | border-color: rgba(0, 0, 0, 0.2) 224 | } 225 | 226 | .btn-google-plus:active, .btn-google-plus.active, .open .dropdown-toggle.btn-google-plus { 227 | background-image: none 228 | } 229 | 230 | .btn-google-plus.disabled, .btn-google-plus[disabled], fieldset[disabled] .btn-google-plus, .btn-google-plus.disabled:hover, .btn-google-plus[disabled]:hover, fieldset[disabled] .btn-google-plus:hover, .btn-google-plus.disabled:focus, .btn-google-plus[disabled]:focus, fieldset[disabled] .btn-google-plus:focus, .btn-google-plus.disabled:active, .btn-google-plus[disabled]:active, fieldset[disabled] .btn-google-plus:active, .btn-google-plus.disabled.active, .btn-google-plus[disabled].active, fieldset[disabled] .btn-google-plus.active { 231 | background-color: #dd4b39; 232 | border-color: rgba(0, 0, 0, 0.2) 233 | } 234 | 235 | .btn-instagram { 236 | color: #fff; 237 | background-color: #3f729b; 238 | border-color: rgba(0, 0, 0, 0.2) 239 | } 240 | 241 | .btn-instagram:hover, .btn-instagram:focus, .btn-instagram:active, .btn-instagram.active, .open .dropdown-toggle.btn-instagram { 242 | color: #3f729b; 243 | background-color: #fff; 244 | border-color: rgba(0, 0, 0, 0.2) 245 | } 246 | 247 | .btn-instagram:active, .btn-instagram.active, .open .dropdown-toggle.btn-instagram { 248 | background-image: none 249 | } 250 | 251 | .btn-instagram.disabled, .btn-instagram[disabled], fieldset[disabled] .btn-instagram, .btn-instagram.disabled:hover, .btn-instagram[disabled]:hover, fieldset[disabled] .btn-instagram:hover, .btn-instagram.disabled:focus, .btn-instagram[disabled]:focus, fieldset[disabled] .btn-instagram:focus, .btn-instagram.disabled:active, .btn-instagram[disabled]:active, fieldset[disabled] .btn-instagram:active, .btn-instagram.disabled.active, .btn-instagram[disabled].active, fieldset[disabled] .btn-instagram.active { 252 | background-color: #3f729b; 253 | border-color: rgba(0, 0, 0, 0.2) 254 | } 255 | 256 | .btn-twitter { 257 | color: #ffffff; 258 | background-color: #55acee; 259 | border-color: rgba(0, 0, 0, 0.2) 260 | } 261 | 262 | .btn-twitter:hover, .btn-twitter:focus, .btn-twitter:active, .btn-twitter.active, .open .dropdown-toggle.btn-twitter { 263 | color: #55acee; 264 | background-color: #ffffff; 265 | border-color: rgba(0, 0, 0, 0.2) 266 | } 267 | 268 | .btn-twitter:active, .btn-twitter.active, .open .dropdown-toggle.btn-twitter { 269 | background-image: none 270 | } 271 | 272 | .btn-twitter.disabled, .btn-twitter[disabled], fieldset[disabled] .btn-twitter, .btn-twitter.disabled:hover, .btn-twitter[disabled]:hover, fieldset[disabled] .btn-twitter:hover, .btn-twitter.disabled:focus, .btn-twitter[disabled]:focus, fieldset[disabled] .btn-twitter:focus, .btn-twitter.disabled:active, .btn-twitter[disabled]:active, fieldset[disabled] .btn-twitter:active, .btn-twitter.disabled.active, .btn-twitter[disabled].active, fieldset[disabled] .btn-twitter.active { 273 | background-color: #55acee; 274 | border-color: rgba(0, 0, 0, 0.2) 275 | } 276 | 277 | 278 | 279 | 280 | .responsive { 281 | width: 100%; 282 | height: auto; 283 | } 284 | -------------------------------------------------------------------------------- /src/app/shared/services/products.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | 3 | import {BehaviorSubject, Observable} from 'rxjs'; 4 | import {environment} from '../../../environments/environment'; 5 | import {HttpClient, HttpHeaders} from '@angular/common/http'; 6 | import {Product} from '../models/product'; 7 | import {catchError, map, retry, tap} from 'rxjs/operators'; 8 | 9 | import {ShoppingCart} from '../models/shopping-cart.model'; 10 | import {ShoppingCartService} from './shopping-cart.service'; 11 | import {Comment} from '../models/comment.model'; 12 | import {NotificationService} from './notification.service'; 13 | import {ProductDto, ProductListResponseDto} from '../dtos/responses/products/products.dto'; 14 | import {BaseAppDtoResponse, ErrorAppDtoResponse} from '../dtos/responses/shared/base.dto'; 15 | import {PaginatedRequestDto} from '../dtos/requests/base.dto'; 16 | import {ProductLocalDto} from '../dtos/local/products.dto'; 17 | import {ErrorResult, SuccessResult} from '../dtos/local/base'; 18 | import {buildErrorObservable} from '../utils/net.utils'; 19 | 20 | 21 | let CREATED = false; 22 | 23 | @Injectable({ 24 | providedIn: 'root' 25 | }) 26 | export class ProductsService { 27 | private productList: Product[]; 28 | products: ProductListResponseDto; 29 | private readonly baseUrl: string; 30 | 31 | productsBehaviourSubject = new BehaviorSubject(this.products); 32 | private lastUpdatedApiResponseForAll: number; 33 | private lastUpdatedApiResponseForEach: Object[]; 34 | 35 | public cartSnapshot: ShoppingCart; 36 | public defaultPagination: PaginatedRequestDto; 37 | private lastPaginatedRequest: PaginatedRequestDto = {page: 0, pageSize: 0}; 38 | 39 | public constructor(private httpClient: HttpClient, private shoppingCartService: ShoppingCartService, 40 | private notificationService: NotificationService) { 41 | if (CREATED) { 42 | alert('Two instances of the same ProductsService'); 43 | return; 44 | } 45 | CREATED = true; 46 | this.defaultPagination = { 47 | page: 1, 48 | pageSize: 6 49 | }; 50 | this.baseUrl = environment.urls.products; 51 | // subscribe to shopping cart 52 | this.shoppingCartService.getCart().subscribe(cart => { 53 | this.cartSnapshot = cart; 54 | }); 55 | } 56 | 57 | 58 | private httpOptions: object = { 59 | headers: new HttpHeaders({ 60 | 'Content-Type': 'application/json', 61 | 'Accept': 'application/json' 62 | }) 63 | }; 64 | 65 | 66 | getAllProducts(query: PaginatedRequestDto = this.defaultPagination): 67 | Observable { 68 | // If products-api null or length is 0 or last time fetched more than 20 seconds then 69 | if (this.products == null || this.lastPaginatedRequest.page !== query.page || this.lastPaginatedRequest.pageSize !== query.pageSize 70 | || this.products.products.length === 0 71 | || ((new Date().getTime() - this.lastUpdatedApiResponseForAll) > 20 * 1000)) { 72 | this.httpClient.get(`${this.baseUrl}?page=${query.page}&page_size=${query.pageSize}`) 73 | .pipe( 74 | retry(2), 75 | tap((response: any) => { 76 | console.log('canceled:false'); 77 | const isCanceled = false; 78 | })).subscribe( 79 | res => { 80 | this.lastPaginatedRequest = query; 81 | console.log('success:' + res.success); 82 | if (res.success && res.products) { 83 | this.productList = res.products; 84 | this.products = res; 85 | this.lastUpdatedApiResponseForAll = new Date().getTime(); 86 | this.notifyDataChanged(); 87 | } 88 | 89 | return res as ProductListResponseDto; 90 | }, err => { 91 | this.notificationService.dispatchErrorMessage(err); 92 | return buildErrorObservable(err); 93 | }); 94 | } else { 95 | console.log('[+] Products not fetched because the condition has not been met(you recently fetched the same page and pageSize)'); 96 | } 97 | 98 | // always return the behaviourSubject, this guy will notify the observers for any update 99 | return this.productsBehaviourSubject.asObservable(); 100 | } 101 | 102 | getById(id: string): Observable { 103 | return this.httpClient.get(`${this.baseUrl}/by_id/${id}`); 104 | } 105 | 106 | getBySlug(slug: string): Observable { 107 | return this.httpClient.get(`${this.baseUrl}/${slug}`).pipe( 108 | map(res => { 109 | // TODO: Fix this 110 | if (this.cartSnapshot == null) { 111 | this.cartSnapshot = this.shoppingCartService.getCartSnapshot(); 112 | debugger; 113 | } 114 | this.notificationService.dispatchSuccessMessage('Retrieved product details'); 115 | const product = res as ProductLocalDto; 116 | const responseSlug = res.slug; 117 | const id = res.id; 118 | const cartItem = this.cartSnapshot.cartItems.find(ci => ci.id === id && ci.slug === responseSlug); 119 | product.isInCart = !!cartItem; 120 | return product; 121 | }), 122 | catchError(err => { 123 | this.notificationService.dispatchErrorMessage(err.message); 124 | return buildErrorObservable(err); 125 | }) 126 | ); 127 | } 128 | 129 | createProduct(product: Product, images: FileList): Observable { 130 | const formData = new FormData(); 131 | 132 | for (let i = 0; i < images.length; i++) { 133 | formData.append('images[]', images[i], images[i]['name']); 134 | } 135 | 136 | formData.append('name', product.name); 137 | formData.append('description', product.description); 138 | formData.append('stock', product.stock.toString()); 139 | formData.append('price', product.price.toString()); 140 | 141 | return this.httpClient.post(this.baseUrl, formData).pipe( 142 | map(res => { 143 | delete res.success; 144 | this.notificationService.dispatchSuccessMessage(res.full_messages.join('
')); 145 | delete res.full_messages; 146 | return res as ProductDto; 147 | }), 148 | catchError(err => { 149 | this.notificationService.dispatchErrorMessage(err.message); 150 | return buildErrorObservable(err); 151 | }) 152 | ); 153 | } 154 | 155 | update(product: Product): Observable { 156 | return this.httpClient.put(this.baseUrl, product /*, this.httpOptions */) 157 | .pipe(retry(5), map(res => { 158 | if (res.success) { 159 | const at = this.products.products.find(t => t.id === res.id); 160 | // Update the products-api array 161 | this.products.products = this.products.products.map(t => t.id === product.id ? product : t); 162 | } 163 | return res; // || {}; 164 | }), catchError(err => { 165 | return err; 166 | })); 167 | } 168 | 169 | deleteAll(): Observable { 170 | return this.httpClient.delete(this.baseUrl).pipe( 171 | map(res => { 172 | return res; 173 | // this.products-api = []; 174 | // return this.productsBehaviourSubject; 175 | }), catchError(err => { 176 | return err; 177 | })); 178 | } 179 | 180 | deleteById(id: number) { 181 | return this.httpClient.delete(`${this.baseUrl}/${id}`, /*this.httpOptions*/); 182 | } 183 | 184 | private notifyDataChanged() { 185 | this.productsBehaviourSubject.next(this.products); 186 | } 187 | 188 | public unusedGetAll(): Observable { 189 | // TODO: Return an observable but before assigns this.cart 190 | return this.httpClient.get(this.baseUrl); 191 | } 192 | 193 | /* 194 | unusedGetCached(): Observable | any { 195 | return new Observable((obs: Observer) => { 196 | obs.next(this.products); 197 | }); 198 | } 199 | 200 | unusedGetProductCached(id: string): Observable { 201 | return new Observable((obs: Observer) => { 202 | this.unusedGetCached().subscribe(products => { 203 | const product = products.find((p) => p.id === id); 204 | obs.next(product); 205 | obs.complete(); 206 | }); 207 | }); 208 | } 209 | */ 210 | submitComment(comment: Comment, slug: string): Observable { 211 | return this.httpClient.post(`${environment.urls.products}/${slug}/comments`, comment).pipe( 212 | map(res => { 213 | this.notificationService.dispatchSuccessMessage('Comment submitted'); 214 | return res; 215 | }), catchError(err => { 216 | console.log(err); 217 | this.notificationService.dispatchErrorMessage(err); 218 | return []; 219 | })); 220 | } 221 | 222 | deleteComment(id: number): Observable { 223 | if (id == null) { 224 | this.notificationService.dispatchErrorMessage('Invalid comment id provided to delete'); 225 | return buildErrorObservable('Invalid comment id provided to delete'); 226 | } 227 | return this.httpClient.delete(`${environment.urls.comments}/${id}`).pipe( 228 | map(res => { 229 | this.notificationService.dispatchSuccessMessage('Comment deleted'); 230 | return res; 231 | }, catchError(err => { 232 | this.notificationService.dispatchErrorMessage(err); 233 | return buildErrorObservable(err); 234 | })) 235 | ); 236 | } 237 | } 238 | 239 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AngularShopApp 2 | # Table of Contents 3 | - [Introduction](#introduction) 4 | - [Full-stack Applications](#full-stack-applications) 5 | * [E-commerce (shopping cart)](#e-commerce-shopping-cart) 6 | + [Server side implementations](#server-side-implementations) 7 | + [Client side implementations](#client-side-implementations) 8 | * [Blog/CMS](#blogcms) 9 | + [Server side implementations](#server-side-implementations-1) 10 | + [Client side](#client-side) 11 | - [The next come are](#the-next-come-are) 12 | * [Simple CRUD(Create, Read, Update, Delete)](#simple-crudcreate-read-update-delete) 13 | + [Server side implementations](#server-side-implementations-2) 14 | + [Client side implementations](#client-side-implementations-1) 15 | - [The next come are](#the-next-come-are-1) 16 | * [CRUD + Pagination](#crud--pagination) 17 | + [Server side implementations](#server-side-implementations-3) 18 | - [The next come are](#the-next-come-are-2) 19 | + [Client side implementations](#client-side-implementations-2) 20 | - [The next come are](#the-next-come-are-3) 21 | - [Follow me](#social-media-links) 22 | 23 | # AngularShopApp 24 | This shopping cart application is built using Angular, it is supposed to work with any of these servers listed below. **Be warned** that 25 | while making this app I am using this [Spring Boot Server](https://github.com/melardev/SBootApiEcomMVCHibernate) so I strongly encourage you 26 | to use that as well, all the other servers worked from postman, but I don't know If they return in the response something called differently that 27 | this app can not read, so if this redux app does not work, that is the reason, just a typo in a response variable, let me know about any typos 28 | in any of the server apps and I will try to fix it. 29 | 30 | # Full-stack Applications 31 | ## E-commerce (shopping cart) 32 | ### Server side implementations 33 | - [Spring Boot + Spring Data Hibernate](https://github.com/melardev/SBootApiEcomMVCHibernate) 34 | - [Spring Boot + JAX-RS Jersey + Spring Data Hibernate](https://github.com/melardev/SpringBootEcommerceApiJersey) 35 | - [Node Js + Sequelize](https://github.com/melardev/ApiEcomSequelizeExpress) 36 | - [Node Js + Bookshelf](https://github.com/melardev/ApiEcomBookshelfExpress) 37 | - [Node Js + Mongoose](https://github.com/melardev/ApiEcomMongooseExpress) 38 | - [Python Django](https://github.com/melardev/DjangoRestShopApy) 39 | - [Flask](https://github.com/melardev/FlaskApiEcommerce) 40 | - [Golang go gonic](https://github.com/melardev/api_shop_gonic) 41 | - [Ruby on Rails](https://github.com/melardev/RailsApiEcommerce) 42 | - [AspNet Core](https://github.com/melardev/ApiAspCoreEcommerce) 43 | - [Laravel](https://github.com/melardev/ApiEcommerceLaravel) 44 | 45 | The next to come are: 46 | - Spring Boot + Spring Data Hibernate + Kotlin 47 | - Spring Boot + Jax-RS Jersey + Hibernate + Kotlin 48 | - Spring Boot + mybatis 49 | - Spring Boot + mybatis + Kotlin 50 | - Asp.Net Web Api v2 51 | - Elixir 52 | - Golang + Beego 53 | - Golang + Iris 54 | - Golang + Echo 55 | - Golang + Mux 56 | - Golang + Revel 57 | - Golang + Kit 58 | - Flask + Flask-Restful 59 | - AspNetCore + NHibernate 60 | - AspNetCore + Dapper 61 | 62 | ### Client side implementations 63 | This client side E-commerce application is also implemented using other client side technologies: 64 | - [React Redux](https://github.com/melardev/ReactReduxEcommerceRestApi) 65 | - [React](https://github.com/melardev/ReactEcommerceRestApi) 66 | - [Vue](https://github.com/melardev/VueEcommerceRestApi) 67 | - [Vue + Vuex](https://github.com/melardev/VueVuexEcommerceRestApi) 68 | - [Angular](https://github.com/melardev/AngularEcommerceRestApi) 69 | 70 | ## Blog/CMS 71 | ### Server side implementations 72 | - [Spring Boot + Spring Data Hibernate](https://github.com/melardev/SpringBootApiBlog) 73 | - [Go + Gin Gonic](https://github.com/melardev/GoGonicBlogApi) 74 | - [NodeJs + Mongoose](https://github.com/melardev/ApiBlogExpressMongoose) 75 | - [Laravel](https://github.com/melardev/LaravelApiBlog) 76 | - [Ruby on Rails + JBuilder](https://github.com/melardev/RailsApiBlog) 77 | - [Django + Rest-Framework](https://github.com/melardev/DjangoApiBlog) 78 | - [Asp.Net Core](https://github.com/melardev/AspCoreApiBlog) 79 | - [Flask + Flask-SQLAlchemy](https://github.com/melardev/FlaskApiBlog) 80 | 81 | The next to come are: 82 | - Spring Boot + Spring Data Hibernate + Kotlin 83 | - Spring Boot + Jax-RS Jersey + Hibernate + Kotlin 84 | - Spring Boot + mybatis 85 | - Spring Boot + mybatis + Kotlin 86 | - Asp.Net Web Api v2 87 | - Elixir 88 | - Golang + Beego 89 | - Golang + Iris 90 | - Golang + Echo 91 | - Golang + Mux 92 | - Golang + Revel 93 | - Golang + Kit 94 | - Flask + Flask-Restful 95 | - AspNetCore + NHibernate 96 | - AspNetCore + Dapper 97 | 98 | ### Client side 99 | - [Vue + Vuex](https://github.com/melardev/VueVuexBlog) 100 | - [Vue](https://github.com/melardev/VueBlog) 101 | - [React + Redux](https://github.com/melardev/ReactReduxBlog) 102 | - [React](https://github.com/melardev/ReactBlog) 103 | - [Angular](https://github.com/melardev/AngularBlog) 104 | 105 | The next come are 106 | - Angular NgRx-Store 107 | - Angular + Material 108 | - React + Material 109 | - React + Redux + Material 110 | - Vue + Material 111 | - Vue + Vuex + Material 112 | - Ember 113 | 114 | ## Simple CRUD(Create, Read, Update, Delete) 115 | ### Server side implementations 116 | - [Spring Boot + Spring Data Hibernate](https://github.com/melardev/SpringBootApiJpaCrud) 117 | - [Spring boot + Spring Data Reactive Mongo](https://github.com/melardev/SpringBootApiReactiveMongoCrud) 118 | - [Spring Boot + Spring Data Hibernate + Jersey](https://github.com/melardev/SpringBootApiJerseySpringDataCrud) 119 | - [NodeJs Express + Mongoose](https://github.com/melardev/ExpressMongooseApiCrud) 120 | - [Nodejs Express + Bookshelf](https://github.com/melardev/ExpressBookshelfApiCrud) 121 | - [Nodejs Express + Sequelize](https://github.com/melardev/ExpressSequelizeApiCrud) 122 | - [Go + Gin-Gonic + Gorm](https://github.com/melardev/GoGinGonicApiGormCrud) 123 | - [Ruby On Rails](https://github.com/melardev/RailsApiCrud) 124 | - [Ruby On Rails + JBuilder](https://github.com/melardev/RailsApiJBuilderCrud) 125 | - [Laravel](https://github.com/melardev/LaravelApiCrud) 126 | - [AspNet Core](https://github.com/melardev/AspNetCoreApiCrud) 127 | - [AspNet Web Api 2](https://github.com/melardev/AspNetWebApiCrud) 128 | - [Python + Flask](https://github.com/melardev/FlaskApiCrud) 129 | - [Python + Django](https://github.com/melardev/DjanogApiCrud) 130 | - [Python + Django + Rest Framework](https://github.com/melardev/DjangoRestFrameworkCrud) 131 | 132 | ### Client side implementations 133 | - [VueJs](https://github.com/melardev/VueAsyncCrud) 134 | 135 | #### The next come are 136 | - Angular NgRx-Store 137 | - Angular + Material 138 | - React + Material 139 | - React + Redux + Material 140 | - Vue + Material 141 | - Vue + Vuex + Material 142 | - Ember 143 | - Vanilla javascript 144 | 145 | ## CRUD + Pagination 146 | ### Server side implementations 147 | - [Spring Boot + Spring Data + Jersey](https://github.com/melardev/SpringBootJerseyApiPaginatedCrud) 148 | - [Spring Boot + Spring Data](https://github.com/melardev/SpringBootApiJpaPaginatedCrud) 149 | - [Spring Boot Reactive + Spring Data Reactive](https://github.com/melardev/ApiCrudReactiveMongo) 150 | - [Go with Gin Gonic](https://github.com/melardev/GoGinGonicApiPaginatedCrud) 151 | - [Laravel](https://github.com/melardev/LaravelApiPaginatedCrud) 152 | - [Rails + JBuilder](https://github.com/melardev/RailsJBuilderApiPaginatedCrud) 153 | - [Rails](https://github.com/melardev/RailsApiPaginatedCrud) 154 | - [NodeJs Express + Sequelize](https://github.com/melardev/ExpressSequelizeApiPaginatedCrud) 155 | - [NodeJs Express + Bookshelf](https://github.com/melardev/ExpressBookshelfApiPaginatedCrud) 156 | - [NodeJs Express + Mongoose](https://github.com/melardev/ExpressApiMongoosePaginatedCrud) 157 | - [Python Django](https://github.com/melardev/DjangoApiCrudPaginated) 158 | - [Python Django + Rest Framework](https://github.com/melardev/DjangoRestFrameworkPaginatedCrud) 159 | - [Python Flask](https://github.com/melardev/FlaskApiPaginatedCrud) 160 | - [AspNet Core](https://github.com/melardev/AspNetCoreApiPaginatedCrud) 161 | - [AspNet Web Api 2](https://github.com/melardev/WebApiPaginatedAsyncCrud) 162 | 163 | #### The next come are 164 | - NodeJs Express + Knex 165 | - Flask + Flask-Restful 166 | - Laravel + Fractal 167 | - Laravel + ApiResources 168 | - Go with Mux 169 | - AspNet Web Api 2 170 | - Jersey 171 | - Elixir 172 | 173 | ### Client side implementations 174 | - [Angular](https://github.com/melardev/AngularPaginatedAsyncCrud) 175 | - [React-Redux](https://github.com/melardev/ReactReduxPaginatedAsyncCrud) 176 | - [React](https://github.com/melardev/ReactAsyncPaginatedCrud) 177 | - [Vue + Vuex](https://github.com/melardev/VueVuexPaginatedAsyncCrud) 178 | - [Vue](https://github.com/melardev/VuePaginatedAsyncCrud) 179 | 180 | 181 | #### The next come are 182 | - Angular NgRx-Store 183 | - Angular + Material 184 | - React + Material 185 | - React + Redux + Material 186 | - Vue + Material 187 | - Vue + Vuex + Material 188 | - Ember 189 | - Vanilla javascript 190 | 191 | # Social media links 192 | - [Youtube Channel](https://youtube.com/melardev) I publish videos mainly on programming 193 | - [Blog](http://melardev.com) Sometimes I publish the source code there before Github 194 | - [Twitter](https://twitter.com/@melardev) I share tips on programming 195 | 196 | 197 | # TODO 198 | - Filter by category and tag dropdown 199 | - Wish-list 200 | - Admin Feature 201 | - Add Tags/Categories on Product Create 202 | - Rating on comments 203 | - Country flags 204 | - Credit card UI 205 | - Authorization before route 206 | 207 | 208 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 7.0.2. 209 | 210 | ## Development server 211 | 212 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. 213 | 214 | ## Code scaffolding 215 | 216 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 217 | 218 | ## Build 219 | 220 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build. 221 | 222 | ## Running unit tests 223 | 224 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 225 | 226 | ## Running end-to-end tests 227 | 228 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). 229 | 230 | ## Further help 231 | 232 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). 233 | --------------------------------------------------------------------------------