├── .editorconfig
├── .gitignore
├── README.md
├── SECURITY.md
├── angular.json
├── data
├── demo_data.json
└── firebase_security.json
├── e2e
├── app.e2e-spec.ts
├── app.po.ts
├── products.po.ts
└── tsconfig.e2e.json
├── karma.conf.js
├── package-lock.json
├── package.json
├── protractor.conf.js
├── src
├── app
│ ├── account
│ │ ├── account.component.html
│ │ ├── account.component.scss
│ │ ├── account.component.ts
│ │ ├── account.module.ts
│ │ ├── orders
│ │ │ ├── orders.component.html
│ │ │ ├── orders.component.scss
│ │ │ ├── orders.component.ts
│ │ │ └── shared
│ │ │ │ └── order.service.ts
│ │ ├── profile
│ │ │ ├── profile.component.html
│ │ │ ├── profile.component.scss
│ │ │ └── profile.component.ts
│ │ ├── register-login
│ │ │ ├── register-login.component.html
│ │ │ ├── register-login.component.scss
│ │ │ └── register-login.component.ts
│ │ └── shared
│ │ │ ├── auth.service.ts
│ │ │ └── user.guard.ts
│ ├── admin
│ │ ├── add-edit
│ │ │ ├── add-edit.component.html
│ │ │ ├── add-edit.component.scss
│ │ │ ├── add-edit.component.spec.ts
│ │ │ └── add-edit.component.ts
│ │ ├── admin.module.ts
│ │ └── shared
│ │ │ └── admin.guard.ts
│ ├── app-routing.module.ts
│ ├── app.component.html
│ ├── app.component.scss
│ ├── app.component.ts
│ ├── app.module.ts
│ ├── cart
│ │ ├── cart.component.html
│ │ ├── cart.component.scss
│ │ ├── cart.component.ts
│ │ └── shared
│ │ │ ├── cart.service.spec.ts
│ │ │ └── cart.service.ts
│ ├── checkout
│ │ ├── address
│ │ │ ├── address.component.html
│ │ │ ├── address.component.scss
│ │ │ └── address.component.ts
│ │ ├── checkout.component.html
│ │ ├── checkout.component.scss
│ │ ├── checkout.component.ts
│ │ ├── checkout.module.ts
│ │ ├── complete
│ │ │ ├── complete.component.html
│ │ │ ├── complete.component.scss
│ │ │ └── complete.component.ts
│ │ ├── footer
│ │ │ ├── footer.component.html
│ │ │ ├── footer.component.scss
│ │ │ └── footer.component.ts
│ │ ├── payment
│ │ │ ├── payment.component.html
│ │ │ ├── payment.component.scss
│ │ │ └── payment.component.ts
│ │ ├── review
│ │ │ ├── review.component.html
│ │ │ ├── review.component.scss
│ │ │ └── review.component.ts
│ │ ├── shared
│ │ │ ├── checkout.service.spec.ts
│ │ │ └── checkout.service.ts
│ │ ├── shipping
│ │ │ ├── shipping.component.html
│ │ │ ├── shipping.component.scss
│ │ │ └── shipping.component.ts
│ │ └── sidebar
│ │ │ ├── sidebar.component.html
│ │ │ ├── sidebar.component.scss
│ │ │ └── sidebar.component.ts
│ ├── core
│ │ ├── content
│ │ │ ├── content.component.html
│ │ │ ├── content.component.scss
│ │ │ ├── content.component.ts
│ │ │ └── footer
│ │ │ │ ├── footer.component.html
│ │ │ │ ├── footer.component.scss
│ │ │ │ └── footer.component.ts
│ │ ├── core.module.ts
│ │ ├── header
│ │ │ ├── header.component.html
│ │ │ ├── header.component.scss
│ │ │ ├── header.component.ts
│ │ │ ├── navigation-main
│ │ │ │ ├── navigation-main.component.html
│ │ │ │ ├── navigation-main.component.scss
│ │ │ │ └── navigation-main.component.ts
│ │ │ ├── search
│ │ │ │ ├── search.component.html
│ │ │ │ ├── search.component.scss
│ │ │ │ └── search.component.ts
│ │ │ └── toolbar
│ │ │ │ └── cart
│ │ │ │ ├── cart.component.html
│ │ │ │ ├── cart.component.scss
│ │ │ │ └── cart.component.ts
│ │ ├── home
│ │ │ ├── home.component.html
│ │ │ ├── home.component.scss
│ │ │ ├── home.component.ts
│ │ │ ├── main-slider
│ │ │ │ ├── main-slider.component.html
│ │ │ │ ├── main-slider.component.scss
│ │ │ │ └── main-slider.component.ts
│ │ │ ├── product-widget
│ │ │ │ ├── product-widget.component.html
│ │ │ │ ├── product-widget.component.scss
│ │ │ │ └── product-widget.component.ts
│ │ │ └── promo
│ │ │ │ ├── promo.component.html
│ │ │ │ ├── promo.component.scss
│ │ │ │ └── promo.component.ts
│ │ ├── module-import-guard.ts
│ │ ├── navigation-off-canvas
│ │ │ ├── navigation-off-canvas.component.html
│ │ │ ├── navigation-off-canvas.component.scss
│ │ │ └── navigation-off-canvas.component.ts
│ │ ├── page-title
│ │ │ ├── page-title.component.html
│ │ │ ├── page-title.component.scss
│ │ │ └── page-title.component.ts
│ │ ├── shared
│ │ │ ├── offcanvas.service.ts
│ │ │ └── promo.service.ts
│ │ └── top-bar
│ │ │ ├── top-bar.component.html
│ │ │ ├── top-bar.component.scss
│ │ │ └── top-bar.component.ts
│ ├── messages
│ │ ├── message.service.spec.ts
│ │ └── message.service.ts
│ ├── models
│ │ ├── cart-item.model.ts
│ │ ├── customer.model.ts
│ │ ├── order.model.ts
│ │ ├── product.model.ts
│ │ ├── promo.model.ts
│ │ ├── rating.model.ts
│ │ └── user.model.ts
│ ├── page-not-found
│ │ ├── page-not-found.component.html
│ │ ├── page-not-found.component.scss
│ │ └── page-not-found.component.ts
│ ├── pager
│ │ └── pager.service.ts
│ ├── products
│ │ ├── product-detail
│ │ │ ├── product-detail.component.html
│ │ │ ├── product-detail.component.scss
│ │ │ └── product-detail.component.ts
│ │ ├── products-list-item
│ │ │ ├── products-list-item.component.html
│ │ │ ├── products-list-item.component.scss
│ │ │ └── products-list-item.component.ts
│ │ ├── products-list
│ │ │ ├── products-list.component.html
│ │ │ ├── products-list.component.scss
│ │ │ └── products-list.component.ts
│ │ ├── products.module.ts
│ │ └── shared
│ │ │ ├── file-upload.service.ts
│ │ │ ├── product-rating.service.spec.ts
│ │ │ ├── product-rating.service.ts
│ │ │ ├── product.service.ts
│ │ │ ├── products-cache.service.ts
│ │ │ ├── productsUrl.ts
│ │ │ ├── rating-stars
│ │ │ ├── rating-stars.component.html
│ │ │ ├── rating-stars.component.scss
│ │ │ └── rating-stars.component.ts
│ │ │ ├── sort.pipe.ts
│ │ │ └── ui.service.ts
│ └── shared
│ │ ├── price
│ │ ├── price.component.html
│ │ ├── price.component.scss
│ │ └── price.component.ts
│ │ └── shared.module.ts
├── assets
│ ├── .gitkeep
│ ├── fonts
│ │ ├── Pe-icon-7-stroke.eot
│ │ ├── Pe-icon-7-stroke.svg
│ │ ├── Pe-icon-7-stroke.ttf
│ │ ├── Pe-icon-7-stroke.woff
│ │ ├── feather-webfont.eot
│ │ ├── feather-webfont.svg
│ │ ├── feather-webfont.ttf
│ │ ├── feather-webfont.woff
│ │ ├── socicon.eot
│ │ ├── socicon.svg
│ │ ├── socicon.ttf
│ │ └── socicon.woff
│ └── js
│ │ └── modernizr.js
├── environments
│ ├── environment.prod.ts
│ └── environment.ts
├── favicon.ico
├── img
│ ├── 404_art.jpg
│ ├── coming-soon-bg.jpg
│ ├── credit-cards.png
│ ├── default-skin.png
│ ├── default-skin.svg
│ ├── loading.gif
│ ├── main-bg.jpg
│ ├── map-marker.png
│ ├── payment_methods.png
│ ├── paypal.svg
│ ├── preloader.gif
│ ├── services
│ │ ├── 01.png
│ │ ├── 02.png
│ │ ├── 03.png
│ │ └── 04.png
│ ├── user-ava-md.jpg
│ └── user-cover-img.jpg
├── index.html
├── main.ts
├── polyfills.ts
├── scss
│ ├── base
│ │ ├── _scaffolding.scss
│ │ └── _utilities.scss
│ ├── components
│ │ ├── _accordion.scss
│ │ ├── _alert.scss
│ │ ├── _banners.scss
│ │ ├── _buttons.scss
│ │ ├── _card.scss
│ │ ├── _comments.scss
│ │ ├── _countdown.scss
│ │ ├── _dropdown.scss
│ │ ├── _forms.scss
│ │ ├── _gallery.scss
│ │ ├── _icons.scss
│ │ ├── _list-group.scss
│ │ ├── _modal.scss
│ │ ├── _navs.scss
│ │ ├── _pagination.scss
│ │ ├── _progress.scss
│ │ ├── _social-buttons.scss
│ │ ├── _steps.scss
│ │ ├── _tables.scss
│ │ ├── _tooltips.scss
│ │ ├── _typography.scss
│ │ └── _widgets.scss
│ ├── helpers
│ │ ├── _mixins.scss
│ │ ├── _placeholders.scss
│ │ └── _variables.scss
│ ├── layout
│ │ ├── _grid.scss
│ │ ├── _header.scss
│ │ ├── _offcanvas-menu.scss
│ │ └── _section.scss
│ └── styles.scss
├── test.ts
├── tsconfig.app.json
├── tsconfig.spec.json
└── typings.d.ts
├── tsconfig.json
├── tslint.json
└── ux-testing
├── erika.jpg
├── task1_step1.png
├── task1_step2.png
├── task2_step1.png
├── ux-test.md
└── uxTest.jpg
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Editor configuration, see http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | indent_style = space
7 | indent_size = 2
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.md]
12 | max_line_length = off
13 | trim_trailing_whitespace = false
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # compiled output
4 | /dist
5 | /tmp
6 | /out-tsc
7 | /server/compiled
8 |
9 | # dependencies
10 | /node_modules
11 |
12 | #config values
13 | .env
14 |
15 | # IDEs and editors
16 | /.idea
17 | .project
18 | .classpath
19 | .c9/
20 | *.launch
21 | .settings/
22 | *.sublime-workspace
23 |
24 | # IDE - VSCode
25 | .vscode/*
26 | !.vscode/settings.json
27 | !.vscode/tasks.json
28 | !.vscode/launch.json
29 | !.vscode/extensions.json
30 |
31 | # misc
32 | /.angular/cache
33 | /.sass-cache
34 | /connect.lock
35 | /coverage
36 | /libpeerconnection.log
37 | npm-debug.log
38 | testem.log
39 | /typings
40 | .env
41 | server/compiled/
42 | src/img/uploads/
43 | src/assets/uploads
44 |
45 | # e2e
46 | /e2e/*.js
47 | /e2e/*.map
48 |
49 | # System Files
50 | .DS_Store
51 | Thumbs.db
52 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Supported Versions
4 |
5 | Use this section to tell people about which versions of your project are
6 | currently being supported with security updates.
7 |
8 | | Version | Supported |
9 | | ------- | ------------------ |
10 | | 5.1.x | :white_check_mark: |
11 | | 5.0.x | :x: |
12 | | 4.0.x | :white_check_mark: |
13 | | < 4.0 | :x: |
14 |
15 | ## Reporting a Vulnerability
16 |
17 | Use this section to tell people how to report a vulnerability.
18 |
19 | Tell them where to go, how often they can expect to get an update on a
20 | reported vulnerability, what to expect if the vulnerability is accepted or
21 | declined, etc.
22 |
--------------------------------------------------------------------------------
/data/firebase_security.json:
--------------------------------------------------------------------------------
1 | {
2 | "rules": {
3 | ".read": false,
4 | ".write": false,
5 | "products": {
6 | ".read": true,
7 | ".write": "auth != null && root.child('users/' + auth.uid + '/roles/admin').exists() && root.child('users/' + auth.uid + '/roles/admin').val() === true",
8 | ".indexOn": ["date", "sale", "currentRating", "name"],
9 | "ratings": {
10 | ".write": "auth != null"
11 | }
12 | },
13 | "users": {
14 | ".write": "auth != null",
15 | "$uid": {
16 | ".read": "$uid === auth.uid",
17 | ".write": "$uid === auth.uid"
18 | }
19 | },
20 | "promos": {
21 | ".read": true,
22 | ".write": "auth != null && root.child('users/' + auth.uid + '/roles/admin').exists() && root.child('users/' + auth.uid + '/roles/admin').val() === true",
23 | },
24 | "featured": {
25 | ".read": true,
26 | ".write": "auth != null && root.child('users/' + auth.uid + '/roles/admin').exists() && root.child('users/' + auth.uid + '/roles/admin').val() === true"
27 | },
28 | "orders": {
29 | ".read": "auth != null && root.child('users/' + auth.uid + '/roles/admin').exists() && root.child('users/' + auth.uid + '/roles/admin').val() === true",
30 | ".write": true
31 | }
32 | }
33 | }
--------------------------------------------------------------------------------
/e2e/app.e2e-spec.ts:
--------------------------------------------------------------------------------
1 | import { ProductsPage } from './products.po';
2 |
3 | describe('products page', () => {
4 | let page: ProductsPage;
5 |
6 | beforeEach(() => {
7 | page = new ProductsPage();
8 | });
9 |
10 | it('should have the right page title', () => {
11 | page.navigateTo();
12 | expect(page.getTitleText()).toEqual('Products');
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/e2e/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 |
--------------------------------------------------------------------------------
/e2e/products.po.ts:
--------------------------------------------------------------------------------
1 | import { browser, by, element } from 'protractor';
2 |
3 | export class ProductsPage {
4 | navigateTo() {
5 | return browser.get('/products');
6 | }
7 |
8 | getTitleText() {
9 | browser.waitForAngularEnabled(false);
10 | return element(by.css('h1')).getText();
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/e2e/tsconfig.e2e.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../out-tsc/e2e",
5 | "baseUrl": "./",
6 | "module": "commonjs",
7 | "target": "es5",
8 | "types": [
9 | "jasmine",
10 | "jasminewd2",
11 | "node"
12 | ]
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/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'), reports: [ 'html', 'lcovonly' ],
20 | fixWebpackSourcePaths: true
21 | },
22 |
23 | reporters: ['progress', 'kjhtml'],
24 | port: 9999,
25 | colors: true,
26 | logLevel: config.LOG_INFO,
27 | autoWatch: true,
28 | browsers: ['Chrome'],
29 | singleRun: false
30 | });
31 | };
32 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "shop",
3 | "version": "0.0.0",
4 | "license": "MIT",
5 | "scripts": {
6 | "ng": "ng",
7 | "start": "ng serve",
8 | "build": "ng build",
9 | "test": "ng test",
10 | "lint": "ng lint",
11 | "e2e": "ng e2e"
12 | },
13 | "private": true,
14 | "dependencies": {
15 | "@angular/animations": "^14.2.4",
16 | "@angular/common": "^14.2.4",
17 | "@angular/compiler": "^14.2.4",
18 | "@angular/core": "^14.2.4",
19 | "@angular/forms": "^14.2.4",
20 | "@angular/platform-browser": "^14.2.4",
21 | "@angular/platform-browser-dynamic": "^14.2.4",
22 | "@angular/router": "^14.2.4",
23 | "@angular/fire": "^7.4",
24 | "bootstrap": "^4.3.1",
25 | "core-js": "^3.3.2",
26 | "firebase": "^9.0",
27 | "modernizr": "^3.7.1",
28 | "ngx-siema": "^2.0.1",
29 | "ngx-toastr": "13.2.1",
30 | "rxjs": "^6.5.3",
31 | "rxjs-compat": "^6.5.3",
32 | "tslib": "^2.0.0",
33 | "zone.js": "~0.11.4"
34 | },
35 | "devDependencies": {
36 | "@angular-devkit/build-angular": "^14.2.4",
37 | "@angular/cli": "^14.2.4",
38 | "@angular/compiler-cli": "^14.2.4",
39 | "@angular/language-service": "^14.2.4",
40 | "@types/dotenv": "^6.1.1",
41 | "@types/jasmine": "^3.4.4",
42 | "@types/jasminewd2": "^2.0.8",
43 | "@types/node": "^12.11.1",
44 | "@types/uuid": "^3.4.5",
45 | "codelyzer": "^5.1.2",
46 | "jasmine-core": "~3.5.0",
47 | "jasmine-spec-reporter": "~5.0.0",
48 | "karma": "~6.4.1",
49 | "karma-chrome-launcher": "~3.1.0",
50 | "karma-coverage-istanbul-reporter": "~3.0.2",
51 | "karma-jasmine": "~4.0.0",
52 | "karma-jasmine-html-reporter": "^1.5.0",
53 | "nodemon": "^1.19.4",
54 | "protractor": "~7.0.0",
55 | "ts-node": "~7.0.0",
56 | "tslint": "~6.1.0",
57 | "typescript": "~4.6.4"
58 | },
59 | "resolutions": {
60 | "minimist": "^1.2.3"
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/protractor.conf.js:
--------------------------------------------------------------------------------
1 | // Protractor configuration file, see link for more information
2 | // https://github.com/angular/protractor/blob/master/lib/config.ts
3 |
4 | const { SpecReporter } = require('jasmine-spec-reporter');
5 |
6 | exports.config = {
7 | allScriptsTimeout: 11000,
8 | specs: [
9 | './e2e/**/*.e2e-spec.ts'
10 | ],
11 | capabilities: {
12 | 'browserName': 'chrome'
13 | },
14 | directConnect: true,
15 | baseUrl: 'http://localhost:4200/',
16 | framework: 'jasmine',
17 | jasmineNodeOpts: {
18 | showColors: true,
19 | defaultTimeoutInterval: 30000,
20 | print: function() {}
21 | },
22 | onPrepare() {
23 | require('ts-node').register({
24 | project: 'e2e/tsconfig.e2e.json'
25 | });
26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
27 | }
28 | };
29 |
--------------------------------------------------------------------------------
/src/app/account/account.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
36 |
37 |
--------------------------------------------------------------------------------
/src/app/account/account.component.scss:
--------------------------------------------------------------------------------
1 | @import '~scss/helpers/variables';
2 | @import '~scss/helpers/mixins';
3 |
4 | .user-info-wrapper {
5 | position: relative;
6 | width: 100%;
7 | margin-bottom: -1px;
8 | padding: {
9 | top: ($cover-height - 30);
10 | bottom: 30px;
11 | }
12 | border: 1px solid $border-color;
13 | border-top-left-radius: $border-radius-lg;
14 | border-top-right-radius: $border-radius-lg;
15 | overflow: hidden;
16 | .user-cover {
17 | position: absolute;
18 | top: 0;
19 | left: 0;
20 | width: 100%;
21 | height: $cover-height;
22 | background: {
23 | position: center;
24 | color: $gray-lighter;
25 | repeat: no-repeat;
26 | size: cover;
27 | }
28 | .tooltip .tooltip-inner {
29 | width: 230px;
30 | max-width: 100%;
31 | padding: 10px 15px;
32 | }
33 | }
34 | .info-label {
35 | display: block;
36 | position: absolute;
37 | top: 18px;
38 | right: 18px;
39 | height: 26px;
40 | padding: 0 12px;
41 | border-radius: 13px;
42 | background-color: $white-color;
43 | color: $gray-dark;
44 | font-size: floor(($font-size-base / 1.33)); //~12px
45 | line-height: 26px;
46 | box-shadow: 0 1px 5px 0 rgba(0, 0, 0, .18);
47 | cursor: pointer;
48 | > i {
49 | display: inline-block;
50 | margin-right: 3px;
51 | font-size: 1.2em;
52 | vertical-align: middle;
53 | }
54 | }
55 | .user-info {
56 | display: table;
57 | position: relative;
58 | width: 100%;
59 | padding: 0 18px;
60 | z-index: 5;
61 | .user-avatar,
62 | .user-data {
63 | display: table-cell;
64 | vertical-align: top;
65 | }
66 | .user-avatar {
67 | position: relative;
68 | width: $user-ava-size;
69 | > img {
70 | display: block;
71 | width: 100%;
72 | border: 5px solid $white-color;
73 | border-radius: 50%;
74 | }
75 | .edit-avatar {
76 | display: block;
77 | position: absolute;
78 | top: -2px;
79 | right: 2px;
80 | width: $btn-sm-height;
81 | height: $btn-sm-height;
82 | transition: opacity .3s;
83 | border-radius: 50%;
84 | background-color: $white-color;
85 | color: $gray-dark;
86 | line-height: $btn-sm-height - 2;
87 | box-shadow: 0 1px 5px 0 rgba(0, 0, 0, .2);
88 | cursor: pointer;
89 | opacity: 0;
90 | text: {
91 | align: center;
92 | decoration: none;
93 | }
94 | &::before {
95 | font: {
96 | family: feather;
97 | size: $font-size-base + 1;
98 | }
99 | content: '\e058';
100 | }
101 | }
102 | &:hover .edit-avatar { opacity: 1; }
103 | }
104 | .user-data {
105 | padding: {
106 | top: 48px;
107 | left: 12px;
108 | }
109 | h4 { margin-bottom: 2px; }
110 | span {
111 | display: block;
112 | color: $gray;
113 | font-size: $font-size-xs;
114 | }
115 | }
116 | }
117 | & + .list-group .list-group-item:first-child { border-radius: 0; }
118 | }
119 |
--------------------------------------------------------------------------------
/src/app/account/account.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 |
3 | import { AuthService } from './shared/auth.service';
4 | import { Router } from '@angular/router';
5 | import { OrderService } from './orders/shared/order.service';
6 |
7 | import { User } from '../models/user.model';
8 |
9 | @Component({
10 | selector: 'app-account',
11 | templateUrl: './account.component.html',
12 | styleUrls: ['./account.component.scss']
13 | })
14 | export class AccountComponent {
15 | public user: User;
16 |
17 | constructor(
18 | private authService: AuthService,
19 | public router: Router,
20 | public orderService: OrderService
21 | ) {}
22 | }
23 |
--------------------------------------------------------------------------------
/src/app/account/account.module.ts:
--------------------------------------------------------------------------------
1 | import { CommonModule } from '@angular/common';
2 | import { NgModule } from '@angular/core';
3 | import { FormsModule, ReactiveFormsModule } from '@angular/forms';
4 |
5 | import { SharedModule } from '../shared/shared.module';
6 |
7 | import { ProfileComponent } from './profile/profile.component';
8 | import { OrdersComponent } from './orders/orders.component';
9 | import { RegisterLoginComponent } from './register-login/register-login.component';
10 | import { AccountComponent } from './account.component';
11 |
12 | @NgModule({
13 | declarations: [
14 | AccountComponent,
15 | ProfileComponent,
16 | OrdersComponent,
17 | RegisterLoginComponent
18 | ],
19 | imports: [
20 | CommonModule,
21 | SharedModule,
22 | FormsModule,
23 | ReactiveFormsModule
24 | ],
25 | exports: [
26 | SharedModule
27 | ]
28 | })
29 | export class AccountModule {}
30 |
--------------------------------------------------------------------------------
/src/app/account/orders/orders.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Order # |
8 | Date Purchased |
9 | Status |
10 | Total |
11 |
12 |
13 |
14 |
15 |
16 |
17 | {{order.number}}
18 | |
19 | {{order.date | date:'mediumDate'}} |
20 |
21 | {{order.status || 'In Progress'}}
22 | |
23 |
24 | {{order.total | currency}}
25 | |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/src/app/account/orders/orders.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monobasic/Angular-Reactive-Demo-Shop/d25467526b2b627132a750775e7e28145a406aad/src/app/account/orders/orders.component.scss
--------------------------------------------------------------------------------
/src/app/account/orders/orders.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit, OnDestroy } from '@angular/core';
2 | import {
3 | CommonModule,
4 | DatePipe,
5 | DecimalPipe,
6 | CurrencyPipe
7 | } from '@angular/common';
8 |
9 | import { Subscription } from 'rxjs';
10 |
11 | import { OrderService } from './shared/order.service';
12 |
13 | import { Order } from '../../models/order.model';
14 |
15 | @Component({
16 | selector: 'app-orders',
17 | templateUrl: './orders.component.html',
18 | styleUrls: ['./orders.component.scss']
19 | })
20 | export class OrdersComponent implements OnInit, OnDestroy {
21 | public orders: Order[];
22 | private ordersSubscription: Subscription;
23 |
24 | constructor(public orderService: OrderService) {}
25 |
26 | ngOnInit() {
27 | this.ordersSubscription = this.orderService
28 | .getOrders()
29 | .subscribe((orders: Order[]) => {
30 | if (orders) {
31 | this.orders = orders.reverse();
32 | }
33 | });
34 | }
35 |
36 | ngOnDestroy() {
37 | this.ordersSubscription.unsubscribe();
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/app/account/orders/shared/order.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, OnInit } from '@angular/core';
2 | import { Observable , of , from as fromPromise } from 'rxjs';
3 | import { switchMap } from 'rxjs/operators';
4 | import { AngularFireDatabase } from '@angular/fire/compat/database';
5 |
6 | import { Order } from '../../../models/order.model';
7 |
8 | import { MessageService } from '../../../messages/message.service';
9 | import { AuthService } from '../../shared/auth.service';
10 |
11 | @Injectable()
12 | export class OrderService {
13 | constructor(
14 | private messageService: MessageService,
15 | private authService: AuthService,
16 | private store: AngularFireDatabase
17 | ) {}
18 |
19 | public getOrders() {
20 | return this.authService.user
21 | .pipe(
22 | switchMap((user) => {
23 | if (user) {
24 | const remoteUserOrders = `/users/${user.uid}/orders`;
25 | return this.store.list(remoteUserOrders).valueChanges();
26 | } else {
27 | return of(null);
28 | }
29 | })
30 | );
31 | }
32 |
33 | public addUserOrder(order: Order, total: number, user: string) {
34 | const orderWithMetaData = {
35 | ...order,
36 | ...this.constructOrderMetaData(order),
37 | total
38 | };
39 |
40 | const databaseOperation = this.store
41 | .list(`users/${user}/orders`)
42 | .push(orderWithMetaData)
43 | .then((response) => response, (error) => error);
44 |
45 | return fromPromise(databaseOperation);
46 | }
47 |
48 | public addAnonymousOrder(order: Order, total: number) {
49 | const orderWithMetaData = {
50 | ...order,
51 | ...this.constructOrderMetaData(order),
52 | total
53 | };
54 |
55 | const databaseOperation = this.store
56 | .list('orders')
57 | .push(orderWithMetaData)
58 | .then((response) => response, (error) => error);
59 |
60 | return fromPromise(databaseOperation);
61 | }
62 |
63 | private constructOrderMetaData(order: Order) {
64 | return {
65 | number: (Math.random() * 10000000000).toString().split('.')[0],
66 | date: new Date().toString(),
67 | status: 'In Progress'
68 | };
69 | }
70 |
71 | private handleError(operation = 'operation', result?: T) {
72 | return (error: any): Observable => {
73 | // TODO: send the error to remote logging infrastructure
74 | console.error(error); // log to console instead
75 |
76 | // TODO: better job of transforming error for user consumption
77 | this.messageService.addError(`${operation} failed: ${error.message}`);
78 |
79 | // Let the app keep running by returning an empty result.
80 | return of(result as T);
81 | };
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/app/account/profile/profile.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
49 |
50 |
--------------------------------------------------------------------------------
/src/app/account/profile/profile.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monobasic/Angular-Reactive-Demo-Shop/d25467526b2b627132a750775e7e28145a406aad/src/app/account/profile/profile.component.scss
--------------------------------------------------------------------------------
/src/app/account/profile/profile.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit, OnDestroy } from '@angular/core';
2 | import { UntypedFormGroup, Validators, UntypedFormControl } from '@angular/forms';
3 |
4 | import { AuthService } from '../shared/auth.service';
5 |
6 | import { User } from '../../models/user.model';
7 | import { Subscription } from 'rxjs';
8 |
9 | @Component({
10 | selector: 'app-profile',
11 | templateUrl: './profile.component.html',
12 | styleUrls: ['./profile.component.scss']
13 | })
14 | export class ProfileComponent implements OnInit, OnDestroy {
15 | private authSubscription: Subscription;
16 | public formProfile: UntypedFormGroup;
17 | public profileErrors: string;
18 | private user: User;
19 |
20 | constructor(private authService: AuthService) { }
21 |
22 | ngOnInit() {
23 | this.initFormGroup();
24 | this.authSubscription = this.authService.user.subscribe(
25 | user => {
26 | if (user) {
27 | this.formProfile.patchValue({
28 | firstName: user.firstName,
29 | lastName: user.lastName,
30 | email: user.email
31 | });
32 | this.user = user;
33 | }
34 | }
35 | );
36 | }
37 |
38 | private initFormGroup() {
39 | this.formProfile = new UntypedFormGroup({
40 | firstName: new UntypedFormControl(null, Validators.required),
41 | lastName: new UntypedFormControl(null, Validators.required),
42 | email: new UntypedFormControl(null, Validators.email),
43 | password: new UntypedFormControl(null),
44 | confirmPassword: new UntypedFormControl(null),
45 | });
46 | }
47 |
48 | public onSubmit() {
49 |
50 | // Update Email
51 | if (this.user.email !== this.formProfile.value.email) {
52 | this.authService.updateEmail(this.formProfile.value.email)
53 | .catch(
54 | error => {
55 | this.profileErrors = error.message;
56 | this.formProfile.patchValue({ email: this.user.email });
57 | }
58 | );
59 | }
60 |
61 | // Update Profile (Firstname, Lastname)
62 | if (this.user.firstName !== this.formProfile.value.firstName || this.user.lastName !== this.formProfile.value.lastName) {
63 | this.authService.updateProfile(this.formProfile.value);
64 | }
65 |
66 | // Update password
67 | if (this.formProfile.value.password && this.formProfile.value.confirmPassword
68 | && (this.formProfile.value.password === this.formProfile.value.confirmPassword)) {
69 | this.authService.updatePassword(this.formProfile.value.password)
70 | .catch(
71 | error => {
72 | this.profileErrors = error.message;
73 | }
74 | );
75 | }
76 | }
77 |
78 | ngOnDestroy() {
79 | this.authSubscription.unsubscribe();
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/app/account/register-login/register-login.component.scss:
--------------------------------------------------------------------------------
1 | @import '~scss/helpers/variables';
2 | @import '~scss/helpers/mixins';
3 |
--------------------------------------------------------------------------------
/src/app/account/register-login/register-login.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit, ViewChild, OnChanges } from '@angular/core';
2 | import {
3 | UntypedFormGroup,
4 | ReactiveFormsModule,
5 | UntypedFormControl,
6 | Validators
7 | } from '@angular/forms';
8 | import { Router, ActivatedRoute, Params } from '@angular/router';
9 | import { MessageService } from '../../messages/message.service';
10 | import { AuthService } from '../shared/auth.service';
11 |
12 | @Component({
13 | selector: 'app-register-login',
14 | templateUrl: './register-login.component.html',
15 | styleUrls: ['./register-login.component.scss']
16 | })
17 | export class RegisterLoginComponent implements OnInit {
18 | public loginForm: UntypedFormGroup;
19 | public registerForm: UntypedFormGroup;
20 | public registerErrors: string;
21 |
22 | constructor(
23 | private authenticationService: AuthService,
24 | private router: Router,
25 | private messageService: MessageService
26 | ) {}
27 |
28 | ngOnInit() {
29 | this.initLoginForm();
30 | this.initRegisterForm();
31 | }
32 |
33 | private initLoginForm() {
34 | this.loginForm = new UntypedFormGroup({
35 | email: new UntypedFormControl(null, [Validators.required, Validators.email]),
36 | password: new UntypedFormControl(null, Validators.required)
37 | });
38 | }
39 |
40 | private initRegisterForm() {
41 | this.registerForm = new UntypedFormGroup({
42 | email: new UntypedFormControl(null, [Validators.required, Validators.email]),
43 | password: new UntypedFormControl(null, Validators.required),
44 | confirmPassword: new UntypedFormControl(null, Validators.required)
45 | });
46 | }
47 |
48 | public onRegister() {
49 | if (this.registerForm.value.password !== this.registerForm.value.confirmPassword) {
50 | this.registerErrors = 'Passwords don\'t match!';
51 | this.registerForm.controls.password.setErrors({ password: true });
52 | this.registerForm.controls.confirmPassword.setErrors({ confirmPassword: true });
53 | } else {
54 | this.authenticationService.emailSignUp(this.registerForm.value.email, this.registerForm.value.password)
55 | .then(
56 | () => {
57 | this.messageService.add('Account created successfully. Please login with your new credentials!');
58 | this.loginForm.setValue({ email: this.registerForm.value.email, password: ''});
59 | this.initRegisterForm();
60 | },
61 | (error) => {
62 | this.registerErrors = error.message;
63 | if (error.code === 'auth/weak-password') {
64 | this.registerForm.controls.password.setErrors({ password: true });
65 | this.registerForm.controls.confirmPassword.setErrors({ confirmPassword: true });
66 | }
67 | if (error.code === 'auth/email-already-in-use') {
68 | this.registerForm.controls.email.setErrors({ email: true });
69 | }
70 | }
71 | );
72 | }
73 | }
74 |
75 | public onLogin() {
76 | this.authenticationService
77 | .emailLogin(this.loginForm.value.email, this.loginForm.value.password)
78 | .then(
79 | () => {
80 | this.messageService.add('Login successful!');
81 | this.router.navigate(['/home']);
82 | },
83 | (error) => {
84 | if (error.code === 'auth/user-not-found') {
85 | this.loginForm.controls.email.setErrors({ email: true });
86 | }
87 | if (error.code === 'auth/wrong-password') {
88 | this.loginForm.controls.password.setErrors({ password: true });
89 | }
90 | }
91 | );
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/src/app/account/shared/user.guard.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import {
3 | CanActivate,
4 | ActivatedRouteSnapshot,
5 | RouterStateSnapshot,
6 | Router
7 | } from '@angular/router';
8 | import { Observable } from 'rxjs';
9 | import { AuthService } from './auth.service';
10 |
11 | import { take , tap , map } from 'rxjs/operators';
12 |
13 | @Injectable()
14 | export class UserGuard implements CanActivate {
15 | constructor(private authService: AuthService, private router: Router) {}
16 |
17 | public canActivate(
18 | next: ActivatedRouteSnapshot,
19 | state: RouterStateSnapshot
20 | ): Observable | Promise | boolean {
21 | return this.authService.user.pipe(
22 | take(1),
23 | map((user) => (user ? true : false)),
24 | tap((authorized) => {
25 | if (!authorized) {
26 | this.router.navigate(['/register-login']);
27 | }
28 | })
29 | );
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/app/admin/add-edit/add-edit.component.scss:
--------------------------------------------------------------------------------
1 | // Progress bars
2 | @import '../../../scss/helpers/_variables.scss';
3 |
4 | //
5 | // Progress Bars
6 | // --------------------------------------------------
7 |
8 | .progress {
9 | height: auto;
10 | border-radius: ceil($progress-height / 2);
11 | background-color: darken($gray-lighter, 2%);
12 | font: {
13 | size: $font-size-xs;
14 | weight: 500;
15 | }
16 | line-height: $progress-height;
17 | }
18 | .progress-bar {
19 | height: $progress-height;
20 | background-color: $progress-bg;
21 | }
22 |
--------------------------------------------------------------------------------
/src/app/admin/admin.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { AddEditComponent } from './add-edit/add-edit.component';
3 | import { SharedModule } from '../shared/shared.module';
4 | import { FormsModule, ReactiveFormsModule } from '@angular/forms';
5 | import { ProductsModule } from '../products/products.module';
6 |
7 | @NgModule({
8 | declarations: [
9 | AddEditComponent
10 | ],
11 | imports: [
12 | SharedModule,
13 | FormsModule,
14 | ReactiveFormsModule,
15 | ProductsModule
16 | ],
17 | exports: [
18 | SharedModule,
19 | ProductsModule
20 | ]
21 | })
22 | export class AdminModule {}
23 |
--------------------------------------------------------------------------------
/src/app/admin/shared/admin.guard.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import {
3 | CanActivate,
4 | ActivatedRouteSnapshot,
5 | RouterStateSnapshot,
6 | Router
7 | } from '@angular/router';
8 | import { Observable } from 'rxjs';
9 | import { AuthService } from '../../account/shared/auth.service';
10 |
11 | import { take , map , tap } from 'rxjs/operators';
12 |
13 | @Injectable()
14 | export class AdminGuard implements CanActivate {
15 | constructor(private authService: AuthService, private router: Router) {}
16 |
17 | public canActivate(
18 | next: ActivatedRouteSnapshot,
19 | state: RouterStateSnapshot
20 | ): Observable | Promise | boolean {
21 | return this.authService.user.pipe(
22 | take(1),
23 | map((user) => (user && user.roles.admin ? true : false)),
24 | tap((authorized) => {
25 | if (!authorized) {
26 | this.router.navigate(['/register-login']);
27 | }
28 | })
29 | );
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/app/app-routing.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { Routes, RouterModule } from '@angular/router';
3 | import { AppComponent } from './app.component';
4 | import { HomeComponent } from './core/home/home.component';
5 | import { CartComponent } from './cart/cart.component';
6 | import { AddEditComponent } from './admin/add-edit/add-edit.component';
7 | import { PageNotFoundComponent } from './page-not-found/page-not-found.component';
8 | import { AdminGuard } from './admin/shared/admin.guard';
9 | import { CheckoutComponent } from './checkout/checkout.component';
10 | import { RegisterLoginComponent } from './account/register-login/register-login.component';
11 | import { OrdersComponent } from './account/orders/orders.component';
12 | import { ProfileComponent } from './account/profile/profile.component';
13 | import { AccountComponent } from './account/account.component';
14 | import { ProductsListComponent } from './products/products-list/products-list.component';
15 | import { ProductDetailComponent } from './products/product-detail/product-detail.component';
16 | import { CompleteComponent } from './checkout/complete/complete.component';
17 |
18 | const routes: Routes = [
19 | { path: '', redirectTo: '/home', pathMatch: 'full' },
20 | { path: 'home', component: HomeComponent },
21 | { path: 'products', component: ProductsListComponent },
22 | { path: 'products/:id', component: ProductDetailComponent },
23 | { path: 'cart', component: CartComponent },
24 | { path: 'admin/add', component: AddEditComponent, canActivate: [AdminGuard] },
25 | {
26 | path: 'admin/edit/:id',
27 | component: AddEditComponent,
28 | canActivate: [AdminGuard]
29 | },
30 | { path: 'checkout', component: CheckoutComponent },
31 | { path: 'register-login', component: RegisterLoginComponent },
32 | {
33 | path: 'account',
34 | component: AccountComponent,
35 | children: [
36 | { path: '', redirectTo: 'profile', pathMatch: 'full' },
37 | { path: 'orders', component: OrdersComponent },
38 | { path: 'profile', component: ProfileComponent }
39 | ]
40 | },
41 | { path: 'order-complete', component: CompleteComponent },
42 | { path: '**', component: PageNotFoundComponent }
43 | ];
44 |
45 | @NgModule({
46 | imports: [RouterModule.forRoot(routes, { relativeLinkResolution: 'legacy' })],
47 | exports: [RouterModule],
48 | providers: [
49 | AdminGuard,
50 | ]
51 | })
52 | export class AppRoutingModule { }
53 |
--------------------------------------------------------------------------------
/src/app/app.component.html:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/src/app/app.component.scss:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/app/app.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 | import { OffcanvasService } from './core/shared/offcanvas.service';
3 |
4 | @Component({
5 | selector: 'app-root',
6 | templateUrl: './app.component.html',
7 | styleUrls: ['./app.component.scss']
8 | })
9 | export class AppComponent {
10 | public products: any;
11 |
12 | constructor(public offcanvasService: OffcanvasService) {}
13 | }
14 |
--------------------------------------------------------------------------------
/src/app/app.module.ts:
--------------------------------------------------------------------------------
1 | // Modules
2 | import { BrowserModule } from '@angular/platform-browser';
3 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
4 | import { NgModule } from '@angular/core';
5 | import { HttpClientModule } from '@angular/common/http';
6 | import { ToastrModule } from 'ngx-toastr';
7 | import { ProductsModule } from './products/products.module';
8 | import { SharedModule } from './shared/shared.module';
9 | import { CoreModule } from './core/core.module';
10 | import { CheckoutModule } from './checkout/checkout.module';
11 | import { AccountModule } from './account/account.module';
12 | import { AdminModule } from './admin/admin.module';
13 | import { AngularFireModule } from '@angular/fire/compat/';
14 | import { AngularFireDatabaseModule } from '@angular/fire/compat/database';
15 | import { AngularFireStorageModule } from '@angular/fire/compat/storage';
16 | import { AngularFireAuthModule } from '@angular/fire/compat/auth';
17 | import { environment } from '../environments/environment';
18 |
19 | // Components
20 | import { AppComponent } from './app.component';
21 | import { CartComponent } from './cart/cart.component';
22 | import { PageNotFoundComponent } from './page-not-found/page-not-found.component';
23 |
24 | @NgModule({
25 | declarations: [
26 | AppComponent,
27 | CartComponent,
28 | PageNotFoundComponent
29 | ],
30 | imports: [
31 | BrowserModule,
32 | BrowserAnimationsModule,
33 | AngularFireModule.initializeApp(environment.firebase),
34 | AngularFireDatabaseModule,
35 | AngularFireAuthModule, // imports firebase/auth, only needed for auth features,
36 | AngularFireStorageModule, // imports firebase/storage only needed for storage features
37 | HttpClientModule,
38 | SharedModule,
39 | ToastrModule.forRoot(),
40 | CoreModule,
41 | ProductsModule,
42 | CheckoutModule,
43 | AccountModule,
44 | AdminModule
45 | ],
46 | bootstrap: [AppComponent]
47 | })
48 | export class AppModule {}
49 |
--------------------------------------------------------------------------------
/src/app/cart/cart.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit, OnDestroy } from '@angular/core';
2 |
3 | import { Subscription } from 'rxjs';
4 |
5 | import { CartService } from './shared/cart.service';
6 | import { CartItem } from '../models/cart-item.model';
7 |
8 | @Component({
9 | selector: 'app-cart',
10 | templateUrl: './cart.component.html',
11 | styleUrls: ['./cart.component.scss']
12 | })
13 | export class CartComponent implements OnInit, OnDestroy {
14 | private cartSubscription: Subscription;
15 | public items: CartItem[];
16 | public total: number;
17 |
18 | constructor(private cartService: CartService) {}
19 |
20 | ngOnInit() {
21 | this.items = this.cartService.getItems();
22 | this.total = this.cartService.getTotal();
23 | this.cartSubscription = this.cartService.itemsChanged.subscribe(
24 | (items: CartItem[]) => {
25 | this.items = items;
26 | this.total = this.cartService.getTotal();
27 | }
28 | );
29 | }
30 |
31 | public onClearCart(event) {
32 | event.preventDefault();
33 | event.stopPropagation();
34 | this.cartService.clearCart();
35 | }
36 |
37 | public onRemoveItem(event, item: CartItem) {
38 | event.preventDefault();
39 | event.stopPropagation();
40 | this.cartService.removeItem(item);
41 | }
42 |
43 | public increaseAmount(item: CartItem) {
44 | this.cartService.updateItemAmount(item, item.amount + 1);
45 | }
46 |
47 | public decreaseAmount(item: CartItem) {
48 | const newAmount = item.amount === 1 ? 1 : item.amount - 1;
49 | this.cartService.updateItemAmount(item, newAmount);
50 | }
51 |
52 | public checkAmount(item: CartItem) {
53 | this.cartService.updateItemAmount(
54 | item,
55 | item.amount < 1 || !item.amount || isNaN(item.amount) ? 1 : item.amount
56 | );
57 | }
58 |
59 | ngOnDestroy() {
60 | this.cartSubscription.unsubscribe();
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/app/cart/shared/cart.service.ts:
--------------------------------------------------------------------------------
1 | import { EventEmitter, Injectable } from '@angular/core';
2 | import { Product } from '../../models/product.model';
3 | import { CartItem } from '../../models/cart-item.model';
4 | import { MessageService } from '../../messages/message.service';
5 |
6 | @Injectable()
7 | export class CartService {
8 | // Init and generate some fixtures
9 | private cartItems: CartItem[];
10 | public itemsChanged: EventEmitter = new EventEmitter();
11 |
12 | constructor(private messageService: MessageService) {
13 | this.cartItems = [];
14 | }
15 |
16 | public getItems() {
17 | return this.cartItems.slice();
18 | }
19 |
20 | // Get Product ids out of CartItem[] in a new array
21 | private getItemIds() {
22 | return this.getItems().map(cartItem => cartItem.product.id);
23 | }
24 |
25 | public addItem(item: CartItem) {
26 | // If item is already in cart, add to the amount, otherwise push item into cart
27 | if (this.getItemIds().includes(item.product.id)) {
28 | this.cartItems.forEach(function (cartItem) {
29 | if (cartItem.product.id === item.product.id) {
30 | cartItem.amount += item.amount;
31 | }
32 | });
33 | this.messageService.add('Amount in cart changed for: ' + item.product.name);
34 | } else {
35 | this.cartItems.push(item);
36 | this.messageService.add('Added to cart: ' + item.product.name);
37 | }
38 | this.itemsChanged.emit(this.cartItems.slice());
39 | }
40 |
41 | public addItems(items: CartItem[]) {
42 | items.forEach((cartItem) => {
43 | this.addItem(cartItem);
44 | });
45 | }
46 |
47 | public removeItem(item: CartItem) {
48 | const indexToRemove = this.cartItems.findIndex(element => element === item);
49 | this.cartItems.splice(indexToRemove, 1);
50 | this.itemsChanged.emit(this.cartItems.slice());
51 | this.messageService.add('Deleted from cart: ' + item.product.name);
52 | }
53 |
54 | public updateItemAmount(item: CartItem, newAmount: number) {
55 | this.cartItems.forEach((cartItem) => {
56 | if (cartItem.product.id === item.product.id) {
57 | cartItem.amount = newAmount;
58 | }
59 | });
60 | this.itemsChanged.emit(this.cartItems.slice());
61 | this.messageService.add('Updated amount for: ' + item.product.name);
62 | }
63 |
64 | public clearCart() {
65 | this.cartItems = [];
66 | this.itemsChanged.emit(this.cartItems.slice());
67 | this.messageService.add('Cleared cart');
68 | }
69 |
70 | public getTotal() {
71 | let total = 0;
72 | this.cartItems.forEach((cartItem) => {
73 | total += cartItem.amount * cartItem.product.price;
74 | });
75 | return total;
76 | }
77 |
78 | }
79 |
--------------------------------------------------------------------------------
/src/app/checkout/address/address.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monobasic/Angular-Reactive-Demo-Shop/d25467526b2b627132a750775e7e28145a406aad/src/app/checkout/address/address.component.scss
--------------------------------------------------------------------------------
/src/app/checkout/address/address.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit, Input, OnDestroy } from '@angular/core';
2 | import { NgForm, UntypedFormGroup, UntypedFormControl, Validators } from '@angular/forms';
3 |
4 | import { Subscription } from 'rxjs';
5 |
6 | import { AuthService } from '../../account/shared/auth.service';
7 | import { CheckoutService } from '../shared/checkout.service';
8 |
9 | @Component({
10 | selector: 'app-checkout-address',
11 | templateUrl: './address.component.html',
12 | styleUrls: ['./address.component.scss']
13 | })
14 | export class AddressComponent implements OnInit, OnDestroy {
15 | private authSubscription: Subscription;
16 | @Input() public user;
17 | public formAddress: UntypedFormGroup;
18 | public countries: string[];
19 |
20 | constructor(
21 | private checkoutService: CheckoutService,
22 | private authService: AuthService
23 | ) {}
24 |
25 | ngOnInit() {
26 | this.initFormGroup();
27 |
28 | this.authSubscription = this.authService.user.subscribe((user) => {
29 | if (user) {
30 | this.user = user;
31 | this.initFormGroup();
32 | }
33 | });
34 | }
35 |
36 | private initFormGroup() {
37 | this.countries = ['Switzerland'];
38 | this.formAddress = new UntypedFormGroup({
39 | firstname: new UntypedFormControl(
40 | this.user && this.user.firstName,
41 | Validators.required
42 | ),
43 | lastname: new UntypedFormControl(
44 | this.user && this.user.lastName,
45 | Validators.required
46 | ),
47 | address1: new UntypedFormControl(null, Validators.required),
48 | address2: new UntypedFormControl(null),
49 | zip: new UntypedFormControl(null, [
50 | Validators.required,
51 | Validators.pattern(/^\d\d\d\d$/)
52 | ]),
53 | city: new UntypedFormControl(null, Validators.required),
54 | email: new UntypedFormControl(
55 | this.user && this.user.email,
56 | Validators.email
57 | ),
58 | phone: new UntypedFormControl(null),
59 | company: new UntypedFormControl(null),
60 | country: new UntypedFormControl({ value: this.countries[0], disabled: false })
61 | });
62 | }
63 |
64 | public onContinue() {
65 | this.checkoutService.setCustomer(this.formAddress.value);
66 | this.checkoutService.nextStep();
67 | }
68 |
69 | // Debug: Fill Form Helper MEthod
70 | public onFillForm(event: Event) {
71 | event.preventDefault();
72 | this.formAddress.setValue({
73 | firstname: 'Hans',
74 | lastname: 'Muster',
75 | address1: 'Musterstrasse 13',
76 | address2: '',
77 | zip: 1234,
78 | city: 'Musterhausen',
79 | email: 'hans.muster@muster.com',
80 | phone: '+41791234567',
81 | company: '',
82 | country: 'Switzerland'
83 | });
84 | }
85 |
86 | ngOnDestroy() {
87 | this.authSubscription.unsubscribe();
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/app/checkout/checkout.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/app/checkout/checkout.component.scss:
--------------------------------------------------------------------------------
1 | @import '~scss/helpers/variables';
2 | @import '~scss/helpers/mixins';
3 |
4 | .checkout-steps {
5 | margin-bottom: floor($grid-vertical-step * 1.7); // ~40px
6 | @include clearfix;
7 |
8 | > a,
9 | > button {
10 | display: block;
11 | position: relative;
12 | padding-right: 18px;
13 | width: 25%;
14 | height: 55px;
15 | float: left;
16 | transition: color .3s;
17 | border: none;
18 |
19 | border: {
20 | top: 1px solid $border-color;
21 | bottom: 1px solid $border-color;
22 | }
23 |
24 | background-color: $body-bg;
25 | color: $nav-link-color;
26 |
27 | font: {
28 | size: $nav-link-font-size;
29 | weight: $nav-link-font-weight;
30 | }
31 |
32 | line-height: 53px;
33 |
34 | text: {
35 | decoration: none;
36 | align: center;
37 | }
38 |
39 | > .angle {
40 | display: block;
41 | position: absolute;
42 | top: 0;
43 | right: 0px;
44 | width: 27px;
45 | height: 53px;
46 | background-color: $body-bg;
47 |
48 | &::before,
49 | &::after {
50 | position: absolute;
51 | top: 0;
52 | left: 0;
53 | width: 0;
54 | height: 0;
55 | border: solid transparent;
56 | content: '';
57 | pointer-events: none;
58 | }
59 |
60 | &::after {
61 | border-width: 26px;
62 | border-color: transparent;
63 | border-left-color: $body-bg;
64 | }
65 |
66 | &::before {
67 | margin-top: -1px;
68 | border-width: 27px;
69 | border-color: transparent;
70 | border-left-color: darken($border-color, 3%);
71 | }
72 | }
73 |
74 | &.active {
75 | background-color: $nav-link-active-color;
76 | color: $white-color;
77 | cursor: default;
78 | pointer-events: none;
79 |
80 | > .angle::after { border-left-color: $nav-link-active-color; }
81 | }
82 |
83 | &.active-sibling {
84 | > .angle { background-color: $nav-link-active-color; }
85 | }
86 |
87 | &:first-child {
88 | border-left: 1px solid $border-color;
89 | border-top-left-radius: $border-radius-lg;
90 | border-bottom-left-radius: $border-radius-lg;
91 | }
92 |
93 | &:last-child {
94 | border-right: 1px solid $border-color;
95 | border-top-right-radius: $border-radius-lg;
96 | border-bottom-right-radius: $border-radius-lg;
97 | }
98 | }
99 |
100 | @media (max-width: $screen-sm) {
101 |
102 | > a {
103 | width: 100%;
104 | margin-bottom: 10px;
105 | float: none;
106 | border: 1px solid $border-color;
107 | border-radius: $border-radius-lg;
108 | > .angle { display: none; }
109 | }
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/src/app/checkout/checkout.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnDestroy, OnInit } from '@angular/core';
2 |
3 | import { Subscription } from 'rxjs';
4 |
5 | import { CheckoutService } from './shared/checkout.service';
6 |
7 | @Component({
8 | selector: 'app-checkout',
9 | templateUrl: './checkout.component.html',
10 | styleUrls: ['./checkout.component.scss']
11 | })
12 | export class CheckoutComponent implements OnInit, OnDestroy {
13 | checkoutSubscription: Subscription;
14 | steps: string[];
15 | activeStep: number;
16 |
17 | constructor(private checkoutService: CheckoutService) {}
18 |
19 | ngOnInit() {
20 | this.steps = ['1. Address', '2. Shipping', '3. Payment', '4. Review'];
21 | this.activeStep = this.checkoutService.activeStep;
22 | this.checkoutSubscription = this.checkoutService.stepChanged.subscribe((step: number) => {
23 | this.activeStep = step;
24 | });
25 | }
26 |
27 | ngOnDestroy() {
28 | this.checkoutSubscription.unsubscribe();
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/app/checkout/checkout.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { AddressComponent } from './address/address.component';
3 | import { FooterComponent } from './footer/footer.component';
4 | import { PaymentComponent } from './payment/payment.component';
5 | import { ReviewComponent } from './review/review.component';
6 | import { ShippingComponent } from './shipping/shipping.component';
7 | import { SidebarComponent } from './sidebar/sidebar.component';
8 | import { CheckoutComponent } from './checkout.component';
9 | import { SharedModule } from '../shared/shared.module';
10 | import { FormsModule, ReactiveFormsModule } from '@angular/forms';
11 | import { CompleteComponent } from './complete/complete.component';
12 |
13 | @NgModule({
14 | declarations: [
15 | CheckoutComponent,
16 | AddressComponent,
17 | FooterComponent,
18 | PaymentComponent,
19 | ReviewComponent,
20 | ShippingComponent,
21 | SidebarComponent,
22 | CompleteComponent
23 | ],
24 | imports: [
25 | SharedModule,
26 | FormsModule,
27 | ReactiveFormsModule
28 | ],
29 | exports: [
30 | SharedModule,
31 | CheckoutComponent,
32 | AddressComponent,
33 | FooterComponent,
34 | PaymentComponent,
35 | ReviewComponent,
36 | ShippingComponent,
37 | SidebarComponent,
38 | FormsModule,
39 | ReactiveFormsModule
40 | ]
41 | })
42 | export class CheckoutModule {}
43 |
--------------------------------------------------------------------------------
/src/app/checkout/complete/complete.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
Thank you for your order!
8 |
Your order has been placed and will be processed as soon as possible.
9 |
You will be receiving an email shortly with confirmation of your order.
10 |
11 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/app/checkout/complete/complete.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monobasic/Angular-Reactive-Demo-Shop/d25467526b2b627132a750775e7e28145a406aad/src/app/checkout/complete/complete.component.scss
--------------------------------------------------------------------------------
/src/app/checkout/complete/complete.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-complete',
5 | templateUrl: './complete.component.html',
6 | styleUrls: ['./complete.component.scss']
7 | })
8 | export class CompleteComponent {}
9 |
--------------------------------------------------------------------------------
/src/app/checkout/footer/footer.component.html:
--------------------------------------------------------------------------------
1 |
20 |
--------------------------------------------------------------------------------
/src/app/checkout/footer/footer.component.scss:
--------------------------------------------------------------------------------
1 | @import '~scss/helpers/variables';
2 | @import '~scss/helpers/mixins';
3 |
4 | .checkout-footer {
5 | display: table;
6 | width: 100%;
7 | margin-top: floor($grid-vertical-step * 1.2);
8 | padding: {
9 | top: 5px;
10 | bottom: 5px;
11 | }
12 | border: 1px solid $border-color;
13 | border-radius: $border-radius-lg;
14 | table-layout: fixed;
15 |
16 | > .column {
17 | display: table-cell;
18 | padding: 10px 15px;
19 | vertical-align: middle;
20 | &:last-child { text-align: right; }
21 | &:first-child { text-align: left; }
22 | }
23 |
24 | .btn { margin: 0; }
25 | }
26 |
--------------------------------------------------------------------------------
/src/app/checkout/footer/footer.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, EventEmitter, Input, Output } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-checkout-footer',
5 | templateUrl: './footer.component.html',
6 | styleUrls: ['./footer.component.scss']
7 | })
8 | export class FooterComponent {
9 | @Input() buttons: string[];
10 | @Input() continueEnabled: boolean;
11 | @Output() back: EventEmitter = new EventEmitter();
12 | @Output() continue: EventEmitter = new EventEmitter();
13 | @Output() completeOrder: EventEmitter = new EventEmitter();
14 |
15 | onBack(e: Event) {
16 | this.back.emit();
17 | }
18 |
19 | onContinue(e: Event) {
20 | this.continue.emit();
21 | }
22 |
23 | onCompleteOrder(e: Event) {
24 | this.completeOrder.emit();
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/app/checkout/payment/payment.component.html:
--------------------------------------------------------------------------------
1 |
2 | Choose Payment Method
3 |
4 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/src/app/checkout/payment/payment.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monobasic/Angular-Reactive-Demo-Shop/d25467526b2b627132a750775e7e28145a406aad/src/app/checkout/payment/payment.component.scss
--------------------------------------------------------------------------------
/src/app/checkout/payment/payment.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit } from '@angular/core';
2 | import { UntypedFormControl, UntypedFormGroup, Validators } from '@angular/forms';
3 |
4 | import { CheckoutService } from '../shared/checkout.service';
5 |
6 | @Component({
7 | selector: 'app-checkout-payment',
8 | templateUrl: './payment.component.html',
9 | styleUrls: ['./payment.component.scss']
10 | })
11 | export class PaymentComponent implements OnInit {
12 | public formPayment: UntypedFormGroup;
13 | public paypalLoggedIn: boolean;
14 | public paymentMethods: string[];
15 |
16 | constructor(private checkoutService: CheckoutService) { }
17 |
18 | ngOnInit() {
19 | this.paypalLoggedIn = false;
20 | this.paymentMethods = ['Paypal', 'Prepayment'];
21 | this.formPayment = new UntypedFormGroup({
22 | 'paymentMethod': new UntypedFormControl(this.paymentMethods[0], Validators.required)
23 | });
24 | }
25 |
26 | public onPaypalLogin(event: Event) {
27 | this.paypalLoggedIn = true;
28 | }
29 |
30 | public onBack() {
31 | this.checkoutService.previousStep();
32 | }
33 |
34 | public onContinue() {
35 | this.checkoutService.setPaymentMethod(this.formPayment.controls.paymentMethod.value);
36 | this.checkoutService.nextStep();
37 | }
38 |
39 | }
40 |
--------------------------------------------------------------------------------
/src/app/checkout/review/review.component.html:
--------------------------------------------------------------------------------
1 | Review Your Order
2 |
3 |
36 |
42 |
43 |
44 |
Shipping to:
45 |
46 | -
47 | Client:{{customer.firstname}} {{customer.lastname}}
48 | -
49 | Address:{{customer.address1}} {{customer.address2}}
50 | -
51 | Phone:{{customer.phone}}
52 |
53 |
54 |
55 |
Payment method:
56 |
57 | -
58 | {{ paymentMethod }}
59 |
60 |
61 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/src/app/checkout/review/review.component.scss:
--------------------------------------------------------------------------------
1 | @import '~scss/helpers/variables';
2 | @import '~scss/helpers/mixins';
3 |
4 | .shopping-cart {
5 | margin-bottom: floor($grid-vertical-step / 1.2); //~20px
6 | .table { margin-bottom: 0; }
7 | .btn { margin: 0; }
8 | > table > thead > tr,
9 | > table > tbody > tr {
10 | > th,
11 | > td { vertical-align: middle !important; }
12 | }
13 | > table thead th {
14 | padding: {
15 | top: 17px;
16 | bottom: 17px;
17 | }
18 | border-width: 1px;
19 | }
20 | .remove-from-cart {
21 | display: inline-block;
22 | color: $brand-danger;
23 | font-size: $font-size-lead;
24 | line-height: 1;
25 | text-decoration: none;
26 | }
27 | .count-input {
28 | display: inline-block;
29 | width: 100%;
30 | width: 170px;
31 | }
32 | .product-item {
33 | display: table;
34 | width: 100%;
35 | min-width: 150px;
36 | margin: {
37 | top: 5px;
38 | bottom: 3px;
39 | }
40 | .product-thumb,
41 | .product-info {
42 | display: table-cell;
43 | vertical-align: middle;
44 | }
45 | .product-thumb {
46 | width: ($cart-thumb-size + 20);
47 | padding-right: 20px;
48 | > img {
49 | display: block;
50 | width: 100%;
51 | }
52 | @media screen and (max-width: 860px) { display: none; }
53 | }
54 | .product-info span {
55 | display: block;
56 | font-size: $font-size-xs;
57 | > em {
58 | font: {
59 | weight: 500;
60 | style: normal;
61 | }
62 | }
63 | }
64 | .product-title {
65 | margin-bottom: floor($grid-vertical-step / 4);
66 | padding-top: 5px;
67 | font: {
68 | size: $font-size-base;
69 | weight: 500;
70 | }
71 | > a {
72 | transition: color .3s;
73 | color: $product-title-color;
74 | line-height: $line-height-base;
75 | text-decoration: none;
76 | &:hover { color: $nav-link-hover-color; }
77 | }
78 | small {
79 | display: inline;
80 | margin-left: 6px;
81 | font-weight: 500;
82 | }
83 | }
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/app/checkout/shared/checkout.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { CheckoutService } from './checkout.service';
2 | import { Customer } from '../../models/customer.model';
3 | import { Product } from '../../models/product.model';
4 | import { CartItem } from '../../models/cart-item.model';
5 |
6 | describe('CheckoutService', () => {
7 | it('should initialize with active step-number 0', () => {
8 | const comp = new CheckoutService();
9 |
10 | expect(comp.activeStep).toBe(0);
11 | });
12 |
13 | describe('should handle steps and', () => {
14 | let comp;
15 | beforeEach(() => {
16 | comp = new CheckoutService();
17 | spyOn(comp.stepChanged, 'emit');
18 | });
19 |
20 | it('should increment steps on nextStep', () => {
21 | comp.nextStep();
22 |
23 | expect(comp.activeStep).toBe(1);
24 | expect(comp.stepChanged.emit).toHaveBeenCalled();
25 | });
26 |
27 | it('should decrement steps on previousStep', () => {
28 | comp.nextStep();
29 | comp.nextStep();
30 | comp.previousStep();
31 |
32 | expect(comp.activeStep).toBe(1);
33 | expect(comp.stepChanged.emit).toHaveBeenCalled();
34 | });
35 |
36 | it('should reset steps on reset', () => {
37 | comp.nextStep();
38 | comp.nextStep();
39 | comp.resetSteps();
40 |
41 | expect(comp.activeStep).toBe(0);
42 | expect(comp.stepChanged.emit).toHaveBeenCalled();
43 | });
44 | });
45 |
46 | describe('should handle additions and', () => {
47 | let comp;
48 | beforeEach(() => {
49 | comp = new CheckoutService();
50 | spyOn(comp.orderInProgressChanged, 'emit');
51 | });
52 |
53 | it('should handle a shipping method', () => {
54 | comp.setPaymentMethod('pay-pal');
55 |
56 | expect(comp.getOrderInProgress().paymentMethod).toBe('pay-pal');
57 | expect(comp.orderInProgressChanged.emit).toHaveBeenCalled();
58 | });
59 |
60 | it('should handle a payment method', () => {
61 | comp.setShippingMethod('air freight');
62 |
63 | expect(comp.getOrderInProgress().shippingMethod).toBe('air freight');
64 | expect(comp.orderInProgressChanged.emit).toHaveBeenCalled();
65 | });
66 |
67 | it('should handle a customer', () => {
68 | const testCustomer = new Customer(
69 | 'Hans',
70 | 'Meier',
71 | '',
72 | '',
73 | 8000,
74 | '',
75 | '',
76 | '',
77 | ''
78 | );
79 |
80 | comp.setCustomer(testCustomer);
81 |
82 | expect(comp.getOrderInProgress().customer).toEqual(
83 | new Customer('Hans', 'Meier', '', '', 8000, '', '', '', '')
84 | );
85 | expect(comp.orderInProgressChanged.emit).toHaveBeenCalled();
86 | });
87 |
88 | it('should handle a cart item', () => {
89 | const testProduct = new Product();
90 | const testCartItem = new CartItem(testProduct, 10);
91 |
92 | comp.setOrderItems([testCartItem]);
93 |
94 | expect(comp.getOrderInProgress().items).toEqual([
95 | new CartItem(testProduct, 10)
96 | ]);
97 | expect(comp.orderInProgressChanged.emit).toHaveBeenCalled();
98 | });
99 | });
100 | });
101 |
--------------------------------------------------------------------------------
/src/app/checkout/shared/checkout.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, EventEmitter } from '@angular/core';
2 | import { Order } from '../../models/order.model';
3 | import { Customer } from '../../models/customer.model';
4 | import { CartItem } from '../../models/cart-item.model';
5 |
6 | @Injectable()
7 | export class CheckoutService {
8 | private orderInProgress: Order;
9 | public orderInProgressChanged: EventEmitter = new EventEmitter();
10 | public stepChanged: EventEmitter = new EventEmitter();
11 | public activeStep: number;
12 |
13 | constructor() {
14 | this.orderInProgress = new Order(new Customer());
15 | this.activeStep = 0;
16 | }
17 |
18 | public gotoStep(number) {
19 | this.activeStep = number;
20 | this.stepChanged.emit(this.activeStep);
21 | }
22 |
23 | public nextStep() {
24 | this.activeStep++;
25 | this.stepChanged.emit(this.activeStep);
26 | }
27 |
28 | previousStep() {
29 | this.activeStep--;
30 | this.stepChanged.emit(this.activeStep);
31 | }
32 |
33 | public resetSteps() {
34 | this.activeStep = 0;
35 | }
36 |
37 | public setCustomer(customer: Customer) {
38 | this.orderInProgress.customer = customer;
39 | this.orderInProgressChanged.emit(this.orderInProgress);
40 | }
41 |
42 | public setShippingMethod(shippingMethod: string) {
43 | this.orderInProgress.shippingMethod = shippingMethod;
44 | this.orderInProgressChanged.emit(this.orderInProgress);
45 | }
46 |
47 | public setOrderItems(items: CartItem[]) {
48 | this.orderInProgress.items = items;
49 | this.orderInProgressChanged.emit(this.orderInProgress);
50 | }
51 |
52 | public getOrderInProgress() {
53 | return this.orderInProgress;
54 | }
55 |
56 | public setPaymentMethod(paymentMethod: string) {
57 | this.orderInProgress.paymentMethod = paymentMethod;
58 | this.orderInProgressChanged.emit(this.orderInProgress);
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/app/checkout/shipping/shipping.component.html:
--------------------------------------------------------------------------------
1 | Choose Shipping Method
2 |
3 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/src/app/checkout/shipping/shipping.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monobasic/Angular-Reactive-Demo-Shop/d25467526b2b627132a750775e7e28145a406aad/src/app/checkout/shipping/shipping.component.scss
--------------------------------------------------------------------------------
/src/app/checkout/shipping/shipping.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit } from '@angular/core';
2 | import { UntypedFormGroup, UntypedFormControl, Validators } from '@angular/forms';
3 | import { CheckoutService } from '../shared/checkout.service';
4 | import { Customer } from '../../models/customer.model';
5 |
6 | @Component({
7 | selector: 'app-checkout-shipping',
8 | templateUrl: './shipping.component.html',
9 | styleUrls: ['./shipping.component.scss']
10 | })
11 | export class ShippingComponent implements OnInit {
12 | public formShipping: UntypedFormGroup;
13 | public shippingMethods: {method: string, time: string, fee: number, value: string}[];
14 |
15 | constructor(private checkoutService: CheckoutService) { }
16 |
17 | ngOnInit() {
18 | this.shippingMethods = [
19 | {
20 | method: 'Swiss Post Priority',
21 | time: '1 - 2 days',
22 | fee: 11,
23 | value: 'priority'
24 | },
25 | {
26 | method: 'Swiss Post Economy',
27 | time: 'up to one week',
28 | fee: 9,
29 | value: 'economy'
30 | }
31 | ];
32 | this.formShipping = new UntypedFormGroup({
33 | 'shippingMethod': new UntypedFormControl(this.shippingMethods[1].value, Validators.required)
34 | });
35 | }
36 |
37 | public onBack() {
38 | this.checkoutService.previousStep();
39 | }
40 |
41 | public onContinue() {
42 | this.checkoutService.setShippingMethod(this.formShipping.controls.shippingMethod.value);
43 | this.checkoutService.nextStep();
44 | }
45 |
46 | }
47 |
--------------------------------------------------------------------------------
/src/app/checkout/sidebar/sidebar.component.html:
--------------------------------------------------------------------------------
1 |
2 |
23 |
24 |
--------------------------------------------------------------------------------
/src/app/checkout/sidebar/sidebar.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monobasic/Angular-Reactive-Demo-Shop/d25467526b2b627132a750775e7e28145a406aad/src/app/checkout/sidebar/sidebar.component.scss
--------------------------------------------------------------------------------
/src/app/checkout/sidebar/sidebar.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit } from '@angular/core';
2 |
3 | import { CartService } from '../../cart/shared/cart.service';
4 |
5 | @Component({
6 | selector: 'app-checkout-sidebar',
7 | templateUrl: './sidebar.component.html',
8 | styleUrls: ['./sidebar.component.scss']
9 | })
10 | export class SidebarComponent implements OnInit {
11 | public cartSubtotal: number;
12 | public shipping: number;
13 | public orderTotal: number;
14 |
15 | constructor(private cartService: CartService) {}
16 |
17 | ngOnInit() {
18 | this.cartSubtotal = this.cartService.getTotal();
19 | // TODO: shipping, hardcoded for now
20 | this.shipping = 9;
21 | this.orderTotal = this.cartSubtotal + this.shipping;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/app/core/content/content.component.html:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/app/core/content/content.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monobasic/Angular-Reactive-Demo-Shop/d25467526b2b627132a750775e7e28145a406aad/src/app/core/content/content.component.scss
--------------------------------------------------------------------------------
/src/app/core/content/content.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 |
3 | import { OffcanvasService } from '../shared/offcanvas.service';
4 |
5 | @Component({
6 | selector: 'app-content',
7 | templateUrl: './content.component.html',
8 | styleUrls: ['./content.component.scss']
9 | })
10 | export class ContentComponent {
11 | constructor(private offcanvasService: OffcanvasService) {}
12 |
13 | onMenuClose(e: Event) {
14 | this.offcanvasService.closeOffcanvasNavigation();
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/app/core/content/footer/footer.component.scss:
--------------------------------------------------------------------------------
1 | @import '~scss/helpers/variables';
2 | @import '~scss/helpers/mixins';
3 |
4 | .site-footer {
5 | padding-top: floor($grid-vertical-step * 3); //~72px;
6 | background-color: $gray-darker;
7 | @media (max-width: $screen-md) {
8 | padding-top: floor($grid-vertical-step * 2); //~48px;
9 | }
10 | }
11 | .footer-copyright {
12 | margin: 0;
13 | padding: {
14 | top: 10px;
15 | bottom: $grid-vertical-step; // ~24px
16 | }
17 | color: rgba($white-color, .5);
18 | font: {
19 | size: $font-size-sm;
20 | weight: normal;
21 | }
22 | > a {
23 | transition: color .25s;
24 | color: rgba($white-color, .5);
25 | text-decoration: none;
26 | &:hover { color: $link-hover-color; }
27 | }
28 | }
29 |
30 | // Light Footer
31 | .footer-light {
32 | background-color: $gray-lighter;
33 | .footer-copyright {
34 | color: $gray;
35 | > a {
36 | color: $gray;
37 | &:hover { color: $link-hover-color; }
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/app/core/content/footer/footer.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-footer',
5 | templateUrl: './footer.component.html',
6 | styleUrls: ['./footer.component.scss']
7 | })
8 | export class FooterComponent {}
9 |
--------------------------------------------------------------------------------
/src/app/core/core.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule, Optional, SkipSelf } from '@angular/core';
2 | import { CommonModule } from '@angular/common';
3 | import { SharedModule } from '../shared/shared.module';
4 | import { NgxSiemaModule } from 'ngx-siema';
5 |
6 | import { ContentComponent } from './content/content.component';
7 | import { HeaderComponent } from './header/header.component';
8 | import { NavigationOffCanvasComponent } from './navigation-off-canvas/navigation-off-canvas.component';
9 | import { TopBarComponent } from './top-bar/top-bar.component';
10 | import { FooterComponent } from './content/footer/footer.component';
11 | import { NavigationMainComponent } from './header/navigation-main/navigation-main.component';
12 | import { ToolbarCartComponent } from './header/toolbar/cart/cart.component';
13 | import { HomeComponent } from './home/home.component';
14 | import { MainSliderComponent } from './home/main-slider/main-slider.component';
15 | import { ProductWidgetComponent } from './home/product-widget/product-widget.component';
16 | import { PromoComponent } from './home/promo/promo.component';
17 | import { SearchComponent } from './header/search/search.component';
18 |
19 | import { ProductService } from '../products/shared/product.service';
20 | import { MessageService } from '../messages/message.service';
21 | import { CartService } from '../cart/shared/cart.service';
22 | import { PagerService } from '../pager/pager.service';
23 | import { OrderService } from '../account/orders/shared/order.service';
24 | import { CheckoutService } from '../checkout/shared/checkout.service';
25 | import { AuthService } from '../account/shared/auth.service';
26 | import { OffcanvasService } from './shared/offcanvas.service';
27 | import { PromoService } from './shared/promo.service';
28 | import { UiService } from '../products/shared/ui.service';
29 | import { ProductsCacheService } from '../products/shared/products-cache.service';
30 |
31 | import { throwIfAlreadyLoaded } from './module-import-guard';
32 |
33 |
34 | @NgModule({
35 | declarations: [
36 | ContentComponent,
37 | HeaderComponent,
38 | NavigationOffCanvasComponent,
39 | TopBarComponent,
40 | FooterComponent,
41 | NavigationMainComponent,
42 | ToolbarCartComponent,
43 | HomeComponent,
44 | MainSliderComponent,
45 | ProductWidgetComponent,
46 | PromoComponent,
47 | SearchComponent
48 | ],
49 | imports: [
50 | CommonModule,
51 | SharedModule,
52 | NgxSiemaModule.forRoot()
53 | ],
54 | exports: [
55 | CommonModule,
56 | SharedModule,
57 | NavigationOffCanvasComponent,
58 | TopBarComponent,
59 | HeaderComponent,
60 | ContentComponent
61 | ],
62 | providers: [
63 | ProductService,
64 | ProductsCacheService,
65 | MessageService,
66 | CartService,
67 | PagerService,
68 | OrderService,
69 | CheckoutService,
70 | AuthService,
71 | OffcanvasService,
72 | PromoService,
73 | UiService
74 | ]
75 | })
76 | export class CoreModule {
77 | constructor(@Optional() @SkipSelf() parentModule: CoreModule) {
78 | throwIfAlreadyLoaded(parentModule, 'CoreModule');
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/app/core/header/header.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
15 |
16 |
17 |
18 |
19 |
60 |
61 |
--------------------------------------------------------------------------------
/src/app/core/header/header.component.scss:
--------------------------------------------------------------------------------
1 | @import '~scss/helpers/variables';
2 | @import '~scss/helpers/mixins';
3 |
--------------------------------------------------------------------------------
/src/app/core/header/header.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnDestroy, OnInit } from '@angular/core';
2 | import { Router } from '@angular/router';
3 |
4 | import { Subscription } from 'rxjs';
5 |
6 | import { AuthService } from '../../account/shared/auth.service';
7 | import { OffcanvasService } from '../shared/offcanvas.service';
8 |
9 | import { User } from '../../models/user.model';
10 |
11 | @Component({
12 | selector: 'app-header',
13 | templateUrl: './header.component.html',
14 | styleUrls: ['./header.component.scss']
15 | })
16 | export class HeaderComponent implements OnInit, OnDestroy {
17 | private authSubscription: Subscription;
18 | public user: User;
19 | public showSearch;
20 |
21 | constructor(
22 | private authService: AuthService,
23 | private router: Router,
24 | private offcanvasService: OffcanvasService
25 | ) {}
26 |
27 | ngOnInit() {
28 | this.authSubscription = this.authService.user.subscribe((user) => {
29 | this.user = user;
30 | });
31 | }
32 |
33 | public onLogOut(e: Event) {
34 | this.authService.signOut();
35 | this.router.navigate(['/register-login']);
36 | e.preventDefault();
37 | }
38 |
39 | public onMenuToggle(e: Event) {
40 | this.offcanvasService.openOffcanvasNavigation();
41 | e.preventDefault();
42 | }
43 |
44 | ngOnDestroy() {
45 | this.authSubscription.unsubscribe();
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/app/core/header/navigation-main/navigation-main.component.html:
--------------------------------------------------------------------------------
1 |
2 |
26 |
--------------------------------------------------------------------------------
/src/app/core/header/navigation-main/navigation-main.component.scss:
--------------------------------------------------------------------------------
1 | @import '~scss/helpers/variables';
2 | @import '~scss/helpers/mixins';
3 | @import '~scss/helpers/placeholders';
4 |
5 | // Main Navigation
6 | .site-menu {
7 | position: absolute;
8 | top: 0;
9 | left: 0;
10 | width: 100%;
11 | height: 100%;
12 | text-align: center;
13 | z-index: 1;
14 |
15 | ul {
16 | margin: 0 auto;
17 | padding: 0;
18 | list-style: none;
19 | > li {
20 |
21 | > a {
22 | padding: 0 15px;
23 | transition: color .3s;
24 | color: $nav-link-color;
25 |
26 | font: {
27 | size: $nav-link-font-size;
28 | weight: $nav-link-font-weight;
29 | }
30 |
31 | text-decoration: none;
32 | }
33 | &:hover > a { color: $nav-link-hover-color; }
34 | &.active > a { color: $nav-link-active-color; }
35 | }
36 | }
37 | > ul {
38 | display: table;
39 | height: 100%;
40 | min-height: 100%;
41 |
42 | > li {
43 | display: table-cell;
44 | position: relative;
45 | vertical-align: middle;
46 |
47 | > a {
48 | display: table;
49 | height: 100%;
50 | min-height: 100%;
51 | border-top: 1px solid transparent;
52 | letter-spacing: .05em;
53 | text-transform: uppercase;
54 | > span {
55 | display: table-cell;
56 | vertical-align: middle;
57 | }
58 | }
59 |
60 | &.active > a {
61 | border-top-color: $nav-link-active-color;
62 | }
63 | }
64 | }
65 | }
66 |
67 | // Sub Menu
68 | .sub-menu { @extend %sub-menu; }
69 | .site-menu ul > li:hover {
70 |
71 | > .sub-menu {
72 | display: block;
73 | animation: submenu-show .3s cubic-bezier(.68, -.55, .265, 1.55);
74 | }
75 |
76 | > .mega-menu {
77 | display: table;
78 | animation: megamenu-show .45s cubic-bezier(.68, -.55, .265, 1.55);
79 | .sub-menu {
80 | animation: none;
81 | }
82 | }
83 | }
84 |
85 | // Mega Menu
86 | .mega-menu { @extend %mega-menu; }
87 | .site-menu > ul > li.has-megamenu { position: static; }
88 |
--------------------------------------------------------------------------------
/src/app/core/header/navigation-main/navigation-main.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnDestroy, OnInit } from '@angular/core';
2 |
3 | import { Subscription } from 'rxjs';
4 |
5 | import { AuthService } from '../../../account/shared/auth.service';
6 |
7 | import { User } from '../../../models/user.model';
8 |
9 | @Component({
10 | selector: 'app-navigation-main',
11 | templateUrl: './navigation-main.component.html',
12 | styleUrls: ['./navigation-main.component.scss']
13 | })
14 | export class NavigationMainComponent implements OnInit, OnDestroy {
15 | public user: User;
16 | private authSubscription: Subscription;
17 |
18 | constructor(public authService: AuthService) {}
19 |
20 | ngOnInit() {
21 | this.authService.user.subscribe((user) => {
22 | this.user = user;
23 | });
24 | }
25 |
26 | ngOnDestroy() {
27 | this.authSubscription.unsubscribe();
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/app/core/header/search/search.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/src/app/core/header/search/search.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monobasic/Angular-Reactive-Demo-Shop/d25467526b2b627132a750775e7e28145a406aad/src/app/core/header/search/search.component.scss
--------------------------------------------------------------------------------
/src/app/core/header/search/search.component.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Component,
3 | OnInit,
4 | Input,
5 | OnChanges,
6 | EventEmitter,
7 | Output
8 | } from '@angular/core';
9 |
10 | import { Subject } from 'rxjs';
11 | import { debounceTime , distinctUntilChanged , filter , switchMap } from 'rxjs/operators';
12 |
13 | import { ProductService } from '../../../products/shared/product.service';
14 |
15 | @Component({
16 | selector: 'app-search',
17 | templateUrl: './search.component.html',
18 | styleUrls: ['./search.component.scss']
19 | })
20 | export class SearchComponent implements OnInit {
21 | products: any[];
22 | term$ = new Subject();
23 | @Input() showSearch: boolean;
24 | @Output() onHideSearch = new EventEmitter();
25 |
26 | constructor(private productService: ProductService) {}
27 |
28 | ngOnInit() {
29 | this.term$
30 | .pipe(
31 | debounceTime(400),
32 | distinctUntilChanged(),
33 | filter((term) => term.length > 0),
34 | switchMap((term) => this.search(term))
35 | )
36 | .subscribe((results) => {
37 | this.products = results;
38 | });
39 | }
40 |
41 | public search(term: string) {
42 | return this.productService.findProducts(term);
43 | }
44 |
45 | public onSearchInput(event) {
46 | let term = event.target.value;
47 | if (term.length > 0) {
48 | term = term.charAt(0).toUpperCase() + term.slice(1);
49 | this.term$.next(term);
50 | } else {
51 | this.products = [];
52 | this.term$.next('');
53 | }
54 | }
55 |
56 | public onCloseSearch() {
57 | this.showSearch = false;
58 | this.onHideSearch.emit(false);
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/app/core/header/toolbar/cart/cart.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
{{items.length}}
5 |
{{ total | currency }}
6 |
43 |
44 |
--------------------------------------------------------------------------------
/src/app/core/header/toolbar/cart/cart.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monobasic/Angular-Reactive-Demo-Shop/d25467526b2b627132a750775e7e28145a406aad/src/app/core/header/toolbar/cart/cart.component.scss
--------------------------------------------------------------------------------
/src/app/core/header/toolbar/cart/cart.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnDestroy, OnInit } from '@angular/core';
2 |
3 | import { Subscription } from 'rxjs';
4 |
5 | import { CartService } from '../../../../cart/shared/cart.service';
6 |
7 | import { CartItem } from '../../../../models/cart-item.model';
8 |
9 | @Component({
10 | selector: 'app-toolbar-cart',
11 | templateUrl: './cart.component.html',
12 | styleUrls: ['./cart.component.scss']
13 | })
14 | export class ToolbarCartComponent implements OnInit, OnDestroy {
15 | public items: CartItem[];
16 | public total: number;
17 | private cartSubscription: Subscription;
18 |
19 | constructor(private cartService: CartService) {}
20 |
21 | ngOnInit() {
22 | this.items = this.cartService.getItems();
23 | this.total = this.cartService.getTotal();
24 | this.cartSubscription = this.cartService.itemsChanged.subscribe(
25 | (items: CartItem[]) => {
26 | this.items = items;
27 | this.total = this.cartService.getTotal();
28 | }
29 | );
30 | }
31 |
32 | public onRemoveItem(event, item: CartItem) {
33 | event.stopPropagation();
34 | this.cartService.removeItem(item);
35 | }
36 |
37 | ngOnDestroy() {
38 | this.cartSubscription.unsubscribe();
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/app/core/home/home.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |

25 |
Free Worldwide Shipping
26 |
Free shipping for all orders over $100
27 |
28 |
29 |

30 |
Money Back Guarantee
31 |
We return money within 30 days
32 |
33 |
34 |

35 |
24/7 Customer Support
36 |
Friendly 24/7 customer support
37 |
38 |
39 |

40 |
Secure Online Payment
41 |
We posess SSL / Secure Certificate
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/src/app/core/home/home.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monobasic/Angular-Reactive-Demo-Shop/d25467526b2b627132a750775e7e28145a406aad/src/app/core/home/home.component.scss
--------------------------------------------------------------------------------
/src/app/core/home/home.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnDestroy, OnInit } from '@angular/core';
2 |
3 | import { Subject } from 'rxjs';
4 | import { takeUntil } from 'rxjs/operators';
5 |
6 | import { MessageService } from '../../messages/message.service';
7 | import { ProductService } from '../../products/shared/product.service';
8 | import { ProductsCacheService } from '../../products/shared/products-cache.service';
9 | import { PromoService } from '../shared/promo.service';
10 |
11 | import { Product } from '../../models/product.model';
12 | import { Promo } from '../../models/promo.model';
13 |
14 | @Component({
15 | selector: 'app-home',
16 | templateUrl: './home.component.html',
17 | styleUrls: ['./home.component.scss']
18 | })
19 | export class HomeComponent implements OnInit, OnDestroy {
20 | private unsubscribe$ = new Subject();
21 | public products: Product[];
22 | public productsFeatured: any;
23 | public productsNewArrivals: Product[];
24 | public productsOnSale: Product[];
25 | public productsBestRated: Product[];
26 | public promos: Promo[];
27 |
28 | constructor(
29 | private messageService: MessageService,
30 | private productsCache: ProductsCacheService,
31 | private productService: ProductService,
32 | private promoService: PromoService
33 | ) {}
34 |
35 | ngOnInit() {
36 | this.productService
37 | .getProducts()
38 | .pipe(takeUntil(this.unsubscribe$))
39 | .subscribe((products) => {
40 | this.products = products;
41 | });
42 |
43 | this.productService
44 | .getFeaturedProducts()
45 | .pipe(takeUntil(this.unsubscribe$))
46 | .subscribe(
47 | (products) => {
48 | this.productsFeatured = products;
49 | },
50 | (err) => console.error(err)
51 | );
52 |
53 | this.productService
54 | .getProductsByDate(3)
55 | .pipe(takeUntil(this.unsubscribe$))
56 | .subscribe(
57 | (products) => {
58 | this.productsNewArrivals = products;
59 | },
60 | (err) => console.error(err)
61 | );
62 |
63 | this.productService
64 | .getProductsByRating(3)
65 | .pipe(takeUntil(this.unsubscribe$))
66 | .subscribe(
67 | (products) => {
68 | this.productsBestRated = products;
69 | },
70 | (err) => console.error(err)
71 | );
72 |
73 | this.productService
74 | .getProductsQuery('sale', true, 3)
75 | .pipe(takeUntil(this.unsubscribe$))
76 | .subscribe(
77 | (products) => {
78 | this.productsOnSale = products;
79 | },
80 | (err) => console.error(err)
81 | );
82 |
83 | this.promoService
84 | .getPromos()
85 | .pipe(takeUntil(this.unsubscribe$))
86 | .subscribe((promos) => {
87 | this.promos = promos;
88 | });
89 | }
90 |
91 | ngOnDestroy() {
92 | this.unsubscribe$.next();
93 | this.unsubscribe$.complete();
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/src/app/core/home/main-slider/main-slider.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | 0" [options]="options" class="large-controls dots-inside">
4 |
5 |
6 |
7 |
8 |
9 |
10 |
{{ item.name }}
11 |
14 |
15 |
Shop now
16 |
17 |
18 |
19 |

20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/src/app/core/home/main-slider/main-slider.component.scss:
--------------------------------------------------------------------------------
1 | @import '~scss/helpers/variables';
2 | @import '~scss/helpers/mixins';
3 |
4 | @keyframes fadeOut {
5 | 0% { opacity: 1; }
6 | 100% { opacity: 0; }
7 | }
8 |
9 | // Hero Slider
10 | .hero-slider {
11 | width: 100%;
12 | min-height: $hero-slider-min-height;
13 | background: {
14 | position: center;
15 | color: $gray-lighter;
16 | repeat: no-repeat;
17 | size: cover;
18 | }
19 | overflow: hidden;
20 |
21 | @media (max-width: 1100px) {
22 | min-height: $hero-slider-min-height - 150;
23 | }
24 | }
25 |
26 |
27 | .hero-slider {
28 | position: relative;
29 | }
30 |
31 | .hero-slider-prev,
32 | .hero-slider-next {
33 | position: absolute;
34 | top: 50%;
35 | transform: translateY(-50%);
36 | border: none;
37 | background: #ffffff;
38 | border-radius: 50%;
39 | width: 55px;
40 | height: 55px;
41 | border: 1px solid #e1e7ec;
42 | color: #374250;
43 | cursor: pointer;
44 | opacity: 0.7;
45 |
46 | &:hover {
47 | opacity: 1;
48 | }
49 |
50 | @media screen and (max-width: 992px) {
51 | display: none;
52 | }
53 | }
54 |
55 | .hero-slider-prev {
56 | left: 15px;
57 |
58 | &::before {
59 | font: {
60 | family: feather;
61 | size: 19px;
62 | }
63 | color: #374250;
64 | content: '\e094';
65 | }
66 | }
67 |
68 | .hero-slider-next {
69 | right: 15px;
70 |
71 | &::before {
72 | font-family: feather;
73 | font-size: 19px;
74 | color: #374250;
75 | content: '\e095';
76 | }
77 | }
78 |
79 | .hero-slider-dots {
80 | position: absolute;
81 | bottom: 0;
82 | margin: 0;
83 | text-align: center;
84 | transform: translateX(-50%);
85 | border-top-left-radius: 7px;
86 | border-top-right-radius: 7px;
87 | background-color: #606975;
88 | display: inline-block;
89 | left: 50%;
90 | width: auto;
91 | padding: 10px 22px 14px;
92 | }
93 |
94 | .hero-slider-dot {
95 | cursor: pointer;
96 | display: inline-block;
97 | width: 6px;
98 | height: 6px;
99 | margin: 0 6px;
100 | transition: opacity .25s;
101 | border-radius: 50%;
102 | background-color: #ffffff;
103 | opacity: 0.5;
104 |
105 | &.active {
106 | opacity: 1;
107 | }
108 | }
109 |
110 | .image-wrapper {
111 |
112 | &.loading {
113 | min-height: 400px;
114 | background: url('../../../../img/loading.gif') center center no-repeat;
115 | }
116 | }
117 |
118 |
--------------------------------------------------------------------------------
/src/app/core/home/main-slider/main-slider.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit, Input, OnDestroy } from '@angular/core';
2 | import { NgxSiemaOptions, NgxSiemaService } from 'ngx-siema';
3 |
4 | import { Subject } from 'rxjs';
5 | import { takeUntil } from 'rxjs/operators';
6 |
7 | @Component({
8 | selector: 'app-main-slider',
9 | templateUrl: './main-slider.component.html',
10 | styleUrls: ['./main-slider.component.scss']
11 | })
12 | export class MainSliderComponent implements OnInit, OnDestroy {
13 | private unsubscribe$ = new Subject();
14 | @Input() public items: any[];
15 | public currentSlide: number;
16 | public imagesLoaded: string[];
17 |
18 | public options: NgxSiemaOptions = {
19 | selector: '.siema',
20 | duration: 200,
21 | easing: 'ease-out',
22 | perPage: 1,
23 | startIndex: 0,
24 | draggable: true,
25 | threshold: 20,
26 | loop: false,
27 | onInit: () => {
28 | // runs immediately after first initialization
29 | },
30 | onChange: () => {
31 | // runs after slide change
32 | }
33 | };
34 |
35 | constructor(private ngxSiemaService: NgxSiemaService) {}
36 |
37 | ngOnInit() {
38 | this.currentSlide = 0;
39 | this.imagesLoaded = [];
40 | }
41 |
42 | public prev() {
43 | if (this.currentSlide > 0) {
44 | this.ngxSiemaService
45 | .prev(1)
46 | .pipe(takeUntil(this.unsubscribe$))
47 | .subscribe((data: any) => {
48 | this.currentSlide = data.currentSlide;
49 | });
50 | }
51 | }
52 |
53 | public next() {
54 | if (this.currentSlide < this.items.length - 1) {
55 | this.ngxSiemaService
56 | .next(1)
57 | .pipe(takeUntil(this.unsubscribe$))
58 | .subscribe((data: any) => {
59 | this.currentSlide = data.currentSlide;
60 | });
61 | }
62 | }
63 |
64 | public goTo(index: number) {
65 | this.ngxSiemaService
66 | .goTo(index)
67 | .pipe(takeUntil(this.unsubscribe$))
68 | .subscribe((data: any) => {
69 | this.currentSlide = data.currentSlide;
70 | });
71 | }
72 |
73 | public onImageLoad(e: any) {
74 | this.imagesLoaded.push(e.target.src);
75 | }
76 |
77 | ngOnDestroy() {
78 | this.unsubscribe$.next();
79 | this.unsubscribe$.complete();
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/app/core/home/product-widget/product-widget.component.html:
--------------------------------------------------------------------------------
1 |
19 |
--------------------------------------------------------------------------------
/src/app/core/home/product-widget/product-widget.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monobasic/Angular-Reactive-Demo-Shop/d25467526b2b627132a750775e7e28145a406aad/src/app/core/home/product-widget/product-widget.component.scss
--------------------------------------------------------------------------------
/src/app/core/home/product-widget/product-widget.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Input } from '@angular/core';
2 |
3 | import { Product } from '../../../models/product.model';
4 |
5 | @Component({
6 | selector: 'app-product-widget',
7 | templateUrl: './product-widget.component.html',
8 | styleUrls: ['./product-widget.component.scss']
9 | })
10 | export class ProductWidgetComponent {
11 | @Input() public products: Product[];
12 | @Input() public widgetTitle: string;
13 | }
14 |
--------------------------------------------------------------------------------
/src/app/core/home/promo/promo.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
{{ promo.preHeading }}
8 |
{{ promo.heading }}
9 |
{{ promo.afterHeading }}
10 |
11 |
{{ promo.buttonText }}
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/app/core/home/promo/promo.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monobasic/Angular-Reactive-Demo-Shop/d25467526b2b627132a750775e7e28145a406aad/src/app/core/home/promo/promo.component.scss
--------------------------------------------------------------------------------
/src/app/core/home/promo/promo.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Input } from '@angular/core';
2 | import { Promo } from '../../../models/promo.model';
3 |
4 | @Component({
5 | selector: 'app-promo',
6 | templateUrl: './promo.component.html',
7 | styleUrls: ['./promo.component.scss']
8 | })
9 | export class PromoComponent {
10 | @Input() public promo: Promo;
11 | }
12 |
--------------------------------------------------------------------------------
/src/app/core/module-import-guard.ts:
--------------------------------------------------------------------------------
1 | export function throwIfAlreadyLoaded(parentModule: any, moduleName: string) {
2 | if (parentModule) {
3 | throw new Error(`${moduleName} has already been loaded. Import Core modules in the AppModule only.`);
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/app/core/navigation-off-canvas/navigation-off-canvas.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monobasic/Angular-Reactive-Demo-Shop/d25467526b2b627132a750775e7e28145a406aad/src/app/core/navigation-off-canvas/navigation-off-canvas.component.scss
--------------------------------------------------------------------------------
/src/app/core/navigation-off-canvas/navigation-off-canvas.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnDestroy, OnInit } from '@angular/core';
2 | import { Router } from '@angular/router';
3 |
4 | import { Subscription } from 'rxjs';
5 |
6 | import { AuthService } from '../../account/shared/auth.service';
7 | import { OffcanvasService } from '../shared/offcanvas.service';
8 |
9 | import { User } from '../../models/user.model';
10 |
11 | @Component({
12 | selector: 'app-navigation-off-canvas',
13 | templateUrl: './navigation-off-canvas.component.html',
14 | styleUrls: ['./navigation-off-canvas.component.scss']
15 | })
16 | export class NavigationOffCanvasComponent implements OnInit, OnDestroy {
17 | private authSubscription: Subscription;
18 | public user: User;
19 |
20 | constructor(
21 | public offcanvasService: OffcanvasService,
22 | public authService: AuthService,
23 | private router: Router
24 | ) {}
25 |
26 | ngOnInit() {
27 | this.authSubscription = this.authService.user.subscribe((user) => {
28 | this.user = user;
29 | });
30 | }
31 |
32 | public onLogout(e: Event) {
33 | this.offcanvasService.closeOffcanvasNavigation();
34 | this.authService.signOut();
35 | this.router.navigate(['/register-login']);
36 | e.preventDefault();
37 | }
38 |
39 | public onNavigationClick() {
40 | this.offcanvasService.closeOffcanvasNavigation();
41 | }
42 |
43 | ngOnDestroy() {
44 | this.authSubscription.unsubscribe();
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/app/core/page-title/page-title.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
{{ title }}
6 |
7 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/src/app/core/page-title/page-title.component.scss:
--------------------------------------------------------------------------------
1 | @import '~scss/helpers/variables';
2 | @import '~scss/helpers/mixins';
3 |
4 | // Page Title
5 | .page-title {
6 | width: 100%;
7 | margin-bottom: ceil($grid-vertical-step * 2.5); //~60px
8 | padding: floor($grid-vertical-step * 1.5) 0; //~36px 0
9 | border-bottom: 1px solid $border-color;
10 | background-color: $gray-lighter;
11 | > .container,
12 | > .container-fluid { display: table; }
13 | .column {
14 | display: table-cell;
15 | vertical-align: middle;
16 | &:first-child { padding-right: 20px; }
17 | }
18 | h1, h2, h3 {
19 | margin: 0;
20 | font: {
21 | size: $font-size-h3;
22 | weight: normal;
23 | }
24 | line-height: $line-height-h3;
25 | }
26 | @media (max-width: $screen-sm) { margin-bottom: ceil($grid-vertical-step * 2.2); }
27 | }
28 |
29 | // Breadcrumbs
30 | .breadcrumbs {
31 | display: block;
32 | margin: 0;
33 | padding: 0;
34 | list-style: none;
35 | text-align: right;
36 | > li {
37 | display: inline-block;
38 | margin-left: 5px;
39 | padding: 5px 0;
40 | color: $gray;
41 | font-size: $font-size-sm;
42 | cursor: default;
43 | vertical-align: middle;
44 | &.separator {
45 | width: 3px;
46 | height: 3px;
47 | margin-top: 2px;
48 | padding: 0;
49 | border-radius: 50%;
50 | background-color: $gray;;
51 | }
52 | > a {
53 | transition: color .25s;
54 | color: $nav-link-color;
55 | text-decoration: none;
56 | &:hover { color: $nav-link-hover-color; }
57 | }
58 | }
59 | }
60 |
61 | // Media query (max-width: 768px)
62 | @media (max-width: $screen-md) {
63 | .page-title {
64 | > .container,
65 | > .container-fluid { display: block; }
66 | .column {
67 | display: block;
68 | width: 100%;
69 | text-align: center;
70 | &:first-child { padding-right: 0; }
71 | }
72 | }
73 | .breadcrumbs {
74 | padding-top: 10px;
75 | text-align: center;
76 | > li {
77 | margin: {
78 | left: 3px;
79 | margin-right: 3px;
80 | }
81 | }
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/app/core/page-title/page-title.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Input } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-page-title',
5 | templateUrl: './page-title.component.html',
6 | styleUrls: ['./page-title.component.scss']
7 | })
8 | export class PageTitleComponent {
9 | @Input() public title: string;
10 | @Input() public children: {title: string, link: string}[];
11 | }
12 |
--------------------------------------------------------------------------------
/src/app/core/shared/offcanvas.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import { BehaviorSubject } from 'rxjs';
3 |
4 | @Injectable()
5 | export class OffcanvasService {
6 | public offcanvasNavigationOpen: BehaviorSubject = new BehaviorSubject(false);
7 |
8 | public toggleOffcanvasNavigation() {
9 | const state = !this.offcanvasNavigationOpen.getValue();
10 | this.offcanvasNavigationOpen.next(state);
11 | }
12 |
13 | public openOffcanvasNavigation() {
14 | this.offcanvasNavigationOpen.next(true);
15 | }
16 |
17 | public closeOffcanvasNavigation() {
18 | this.offcanvasNavigationOpen.next(false);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/app/core/shared/promo.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import { AngularFireDatabase } from '@angular/fire/compat/database';
3 |
4 | import { Observable } from 'rxjs';
5 |
6 | import { Promo } from '../../models/promo.model';
7 |
8 | @Injectable()
9 | export class PromoService {
10 | constructor(private angularFireDatabase: AngularFireDatabase) {}
11 |
12 | getPromos(): Observable {
13 | return this.angularFireDatabase.list('promos').valueChanges();
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/app/core/top-bar/top-bar.component.html:
--------------------------------------------------------------------------------
1 |
2 |
22 |
--------------------------------------------------------------------------------
/src/app/core/top-bar/top-bar.component.scss:
--------------------------------------------------------------------------------
1 | @import '~scss/helpers/variables';
2 | @import '~scss/helpers/mixins';
3 |
4 | .topbar {
5 | display: table;
6 | position: relative;
7 | width: 100%;
8 | height: 40px;
9 | padding: 0 30px;
10 | border-bottom: 1px solid $border-color;
11 | background-color: $gray-lighter;
12 | z-index: 9010;
13 | .topbar-column {
14 | display: table-cell;
15 | width: 50%;
16 | vertical-align: middle;
17 | &:last-child { text-align: right; }
18 | &:first-child { text-align: left; }
19 | a:not(.social-button), span, p {
20 | color: $nav-link-color;
21 | font-size: $font-size-xs;
22 | }
23 | > a:not(.social-button), > span, > p {
24 | display: inline-block;
25 | margin: {
26 | top: 5px;
27 | bottom: 5px;
28 | }
29 | > i { margin-top: -3px; }
30 | > i.icon-download { margin-top: -4px; }
31 | }
32 | a:not(.social-button) {
33 | transition: color .3s;
34 | text-decoration: none;
35 | &:hover { color: $nav-link-hover-color; }
36 | }
37 | }
38 | .topbar-column:last-child {
39 | > a:not(.social-button), > span, > p {
40 | margin-left: 20px;
41 | }
42 | }
43 | .topbar-column:first-child {
44 | > a:not(.social-button), > span, > p {
45 | margin-right: 20px;
46 | }
47 | }
48 |
49 | // Ghost Version
50 | &.topbar-ghost {
51 | position: absolute;
52 | top: 0;
53 | left: 0;
54 | border-bottom-color: rgba($white-color, .15);
55 | background-color: rgba($white-color, .05);
56 | .topbar-column {
57 | a:not(.social-button):not(.dropdown-item), span, p { color: $white-color; }
58 | a:not(.social-button):not(.dropdown-item):hover {
59 | color: $nav-link-hover-color;
60 | }
61 | }
62 | .lang-currency-switcher-wrap .lang-currency-switcher > .currency {
63 | border-left-color: rgba($white-color, .15);
64 | }
65 | .dropdown-toggle::after { color: $white-color; }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/app/core/top-bar/top-bar.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-top-bar',
5 | templateUrl: './top-bar.component.html',
6 | styleUrls: ['./top-bar.component.scss']
7 | })
8 | export class TopBarComponent {}
9 |
--------------------------------------------------------------------------------
/src/app/messages/message.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed, inject } from '@angular/core/testing';
2 |
3 | import { MessageService } from './message.service';
4 | import { ToastrService } from 'ngx-toastr';
5 |
6 | describe('MessageService', () => {
7 | let messageService: MessageService;
8 | let toastrServiceSpy: jasmine.SpyObj;
9 |
10 | beforeEach(() => {
11 | const spy = jasmine.createSpyObj('ToastrService', ['success', 'error']);
12 |
13 | TestBed.configureTestingModule({
14 | providers: [MessageService, { provide: ToastrService, useValue: spy }]
15 | });
16 |
17 | messageService = TestBed.get(MessageService);
18 | toastrServiceSpy = TestBed.get(ToastrService);
19 | });
20 |
21 | it('should be created', () => {
22 | expect(messageService).toBeTruthy();
23 | });
24 |
25 | it('should init messages array', () => {
26 | expect(messageService['messages']).toEqual([]);
27 | });
28 |
29 | it('should handle adding messages', () => {
30 | messageService.add('hello world');
31 | expect(messageService['messages']).toEqual(['hello world']);
32 | expect(toastrServiceSpy.success).toHaveBeenCalled();
33 | });
34 |
35 | it('should handle adding errors', () => {
36 | messageService.addError('My nasty error!');
37 | expect(messageService['messages']).toEqual([]);
38 | expect(toastrServiceSpy.success).toHaveBeenCalledTimes(0);
39 | expect(toastrServiceSpy.error).toHaveBeenCalled();
40 | });
41 |
42 | it('should handle clearing', () => {
43 | messageService.add('hello world');
44 | expect(messageService['messages']).toEqual(['hello world']);
45 | messageService.clear();
46 | expect(messageService['messages']).toEqual([]);
47 | });
48 | });
49 |
--------------------------------------------------------------------------------
/src/app/messages/message.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import { ToastrService } from 'ngx-toastr';
3 |
4 | @Injectable()
5 | export class MessageService {
6 | private messages: string[] = [];
7 | private toastrConfig: {} = {
8 | disableTimeOut: false,
9 | closeButton: false,
10 | positionClass: 'toast-bottom-right'
11 | };
12 |
13 | constructor(private toastr: ToastrService) {}
14 |
15 | public add(message: string): void {
16 | this.messages.push(message);
17 | this.toastr.success(message, 'Message:', this.toastrConfig);
18 | }
19 |
20 | public addError(message: string): void {
21 | this.toastr.error(message, 'Message:', this.toastrConfig);
22 | }
23 |
24 | public clear(): void {
25 | this.messages = [];
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/app/models/cart-item.model.ts:
--------------------------------------------------------------------------------
1 | import { Product } from './product.model';
2 |
3 | export class CartItem {
4 | constructor(public product: Product, public amount: number) {}
5 | }
6 |
--------------------------------------------------------------------------------
/src/app/models/customer.model.ts:
--------------------------------------------------------------------------------
1 | export class Customer {
2 | constructor(
3 | public firstname: string = '',
4 | public lastname: string = '',
5 | public address1: string = '',
6 | public address2: string = '',
7 | public zip: number = null,
8 | public city: string = '',
9 | public email: string = '',
10 | public phone: string = '',
11 | public company: string = '',
12 | public country: string = ''
13 | ) {}
14 | }
15 |
--------------------------------------------------------------------------------
/src/app/models/order.model.ts:
--------------------------------------------------------------------------------
1 | import { CartItem } from './cart-item.model';
2 | import { Customer } from './customer.model';
3 |
4 | export class Order {
5 | constructor(
6 | public customer: Customer = null,
7 | public items: CartItem[] = null,
8 | public total: number = null,
9 | public status: string = '',
10 | public number: string = '',
11 | public date: string = new Date().toISOString().split('T')[0],
12 | public shippingMethod: string = '',
13 | public paymentMethod: string = ''
14 | ) {}
15 | }
16 |
--------------------------------------------------------------------------------
/src/app/models/product.model.ts:
--------------------------------------------------------------------------------
1 | export class Product {
2 | public imageFeaturedUrl?;
3 |
4 | constructor(
5 | public id: number = 1,
6 | public date: string = new Date().toISOString().split('T')[0],
7 | public name: string = '',
8 | public description: string = '',
9 | public price: number = 0,
10 | public priceNormal: number = 0,
11 | public reduction: number = 0,
12 | public imageURLs: string[] = [],
13 | public imageRefs: string[] = [],
14 | public categories: {} = {},
15 | public ratings: {} = {},
16 | public currentRating: number = 0,
17 | public sale: boolean = false
18 | ) {}
19 | }
20 |
--------------------------------------------------------------------------------
/src/app/models/promo.model.ts:
--------------------------------------------------------------------------------
1 | export class Promo {
2 | public preHeading?: string;
3 | public heading: string;
4 | public afterHeading?: string;
5 | public imageUrl: string;
6 | public buttonText?: string;
7 | public link?: string;
8 | }
9 |
--------------------------------------------------------------------------------
/src/app/models/rating.model.ts:
--------------------------------------------------------------------------------
1 | export class Rating {
2 | public userUid: string;
3 | public rating: number;
4 | }
5 |
--------------------------------------------------------------------------------
/src/app/models/user.model.ts:
--------------------------------------------------------------------------------
1 | import { Order } from './order.model';
2 |
3 | export interface Roles {
4 | admin: boolean;
5 | }
6 |
7 | export class User {
8 | public email: string;
9 | public photoURL?: string;
10 | public roles?: Roles;
11 | public firstName?: string;
12 | public lastName?: string;
13 | public password?: string;
14 | public orders?: object;
15 | public confirmPassword?: string;
16 | public uid?: string;
17 |
18 | constructor(authData) {
19 | this.email = authData.email;
20 | this.firstName = authData.firstName ? authData.firstName : '';
21 | this.lastName = authData.lastName ? authData.lastName : '';
22 | this.roles = {
23 | admin: false
24 | };
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/app/page-not-found/page-not-found.component.html:
--------------------------------------------------------------------------------
1 |
2 |

3 |
4 |
Page Not Found
5 |
It seems we can’t find page you are looking for.
6 | Go back to Homepage
7 |
Or try using search at the top right corner of the page.
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/app/page-not-found/page-not-found.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monobasic/Angular-Reactive-Demo-Shop/d25467526b2b627132a750775e7e28145a406aad/src/app/page-not-found/page-not-found.component.scss
--------------------------------------------------------------------------------
/src/app/page-not-found/page-not-found.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } 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 {}
9 |
--------------------------------------------------------------------------------
/src/app/pager/pager.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from "@angular/core";
2 | @Injectable()
3 | export class PagerService {
4 | public getPager(
5 | totalItems: number,
6 | currentPage: number = 1,
7 | pageSize: number = 10
8 | ) {
9 | const totalPages = Math.ceil(totalItems / pageSize);
10 | let startPage: number, endPage: number;
11 |
12 | if (totalPages <= 10) {
13 | // less than 10 total pages so show all
14 | startPage = 1;
15 | endPage = totalPages;
16 | } else {
17 | // more than 10 total pages so calculate start and end pages
18 | if (currentPage <= 6) {
19 | startPage = 1;
20 | endPage = 10;
21 | } else if (currentPage + 4 >= totalPages) {
22 | startPage = totalPages - 9;
23 | endPage = totalPages;
24 | } else {
25 | startPage = currentPage - 5;
26 | endPage = currentPage + 4;
27 | }
28 | }
29 |
30 | // calculate start and end item indexes
31 | const startIndex = (currentPage - 1) * pageSize;
32 | const endIndex = Math.min(startIndex + pageSize - 1, totalItems - 1);
33 |
34 | // create an array of pages to ng-repeat in the pager control
35 | const pages = Array.from(Array(totalPages), (_, i) => 1 + i);
36 |
37 | // return object with all pager properties required by the view
38 | return {
39 | totalItems: totalItems,
40 | currentPage: currentPage,
41 | pageSize: pageSize,
42 | totalPages: totalPages,
43 | startPage: startPage,
44 | endPage: endPage,
45 | startIndex: startIndex,
46 | endIndex: endIndex,
47 | pages: pages
48 | };
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/app/products/product-detail/product-detail.component.scss:
--------------------------------------------------------------------------------
1 | @import '~scss/helpers/variables';
2 | @import '~scss/helpers/mixins';
3 |
4 | // Buttons
5 | .sp-buttons {
6 | .btn {
7 | margin: 0;
8 | margin-left: 8px;
9 |
10 | &.btn-wishlist {
11 | width: $btn-sm-height;
12 | padding: 0;
13 | padding-left: 1px;
14 | border-radius: 50%;
15 |
16 | > i {
17 | font-size: 1.2em;
18 | }
19 |
20 | &.active {
21 | color: $brand-danger;
22 | }
23 | }
24 | }
25 | }
26 |
27 | // Product Gallery
28 | .product-gallery {
29 | position: relative;
30 |
31 | padding: {
32 | top: 15px;
33 | right: 15px;
34 | bottom: 15px;
35 | left: 15px;
36 | }
37 |
38 | border: 1px solid $border-color;
39 | border-radius: $border-radius-lg;
40 |
41 | .product-badge {
42 | top: 25px;
43 | left: 15px;
44 | }
45 | .product-thumbnails {
46 | display: block;
47 | margin: 0;
48 | margin-top: $grid-vertical-step; //~24px
49 | padding: 0;
50 | list-style: none;
51 | text-align: center;
52 |
53 | > li {
54 | display: inline-block;
55 | margin: 0 3px 10px;
56 |
57 | > a {
58 | display: block;
59 | width: 94px;
60 | transition: border-color .25s;
61 | border: 1px solid $border-color;
62 | border-radius: $border-radius-base;
63 | overflow: hidden;
64 | }
65 |
66 | &.active > a {
67 | border-color: $brand-primary;
68 | cursor: default;
69 | }
70 | }
71 | }
72 |
73 | .product-thumbnail-image {
74 | &.loading {
75 | min-height: 80px;
76 | background: url('../../../img/loading.gif') center center no-repeat;
77 | }
78 | }
79 | }
80 |
81 | .product-gallery-image {
82 |
83 | img {
84 | min-width: 100%;
85 | }
86 |
87 | &.loading {
88 | min-height: 200px;
89 | background: url('../../../img/loading.gif') center center no-repeat;
90 | }
91 | }
92 |
93 | .product-detail {
94 |
95 | &.loading {
96 | min-height: 200px;
97 | background: url('../../../img/loading.gif') center center no-repeat;
98 | }
99 | }
100 |
101 | // IE10+ specific styles
102 | @media screen and (-ms-high-contrast: active), screen and (-ms-high-contrast: none) {
103 | .product-gallery .gallery-item { display: none !important; }
104 | }
105 | // Microsoft Edge specific styles
106 | @supports (-ms-ime-align: auto) {
107 | .product-gallery .gallery-item { display: none !important; }
108 | }
109 |
--------------------------------------------------------------------------------
/src/app/products/products-list-item/products-list-item.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
{{ product.reduction }}% Off
4 |
5 |
8 |
9 |
10 |
11 |
12 |
13 |
16 |
17 |
{{product.description}}
18 |
19 |
22 |
23 |
24 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/src/app/products/products-list-item/products-list-item.component.scss:
--------------------------------------------------------------------------------
1 | @import '~scss/helpers/variables';
2 | @import '~scss/helpers/mixins';
3 |
4 | .product-card {
5 | display: block;
6 | position: relative;
7 | width: 100%;
8 | padding: 18px;
9 | border: 1px solid $border-color;
10 | border-radius: $border-radius-lg;
11 | background-color: $white-color;
12 | margin-bottom: 20px;
13 |
14 | .product-badge {
15 | display: inline-block;
16 | position: absolute;
17 | top: 15px;
18 | left: 20px;
19 | z-index: 1;
20 | }
21 |
22 | .product-thumb {
23 | display: block;
24 | width: 100%;
25 | margin-top: 10px;
26 | margin-bottom: 10px;
27 |
28 | > img {
29 | display: block;
30 | width: 100%;
31 | }
32 |
33 | &.loading {
34 | min-height: 150px;
35 | background: url(../../../img/loading.gif) center center no-repeat;
36 | }
37 | }
38 |
39 | .product-title {
40 | margin-bottom: 10px;
41 |
42 | font: {
43 | size: $product-title-font-size;
44 | weight: normal;
45 | }
46 | text-align: center;
47 |
48 | > a {
49 | transition: color .3s;
50 | color: $product-title-color;
51 | text-decoration: none;
52 |
53 | &:hover {
54 | color: $product-title-hover-color;
55 | }
56 | }
57 | }
58 |
59 | .product-price {
60 | margin-bottom: 10px;
61 | color: $product-price-color;
62 |
63 | font: {
64 | size: $product-price-font-size;
65 | weight: 500;
66 | }
67 | text-align: center;
68 |
69 | > del {
70 | margin-right: 5px;
71 | color: $gray;
72 | }
73 | }
74 |
75 | .product-buttons {
76 | padding: 12px 0 8px;
77 | text-align: center;
78 |
79 | > .btn {
80 | margin: 0 4px;
81 |
82 | &.btn-wishlist {
83 | width: $btn-sm-height;
84 | padding: 0;
85 | padding-left: 1px;
86 | border-radius: 50%;
87 |
88 | > i {
89 | font-size: 1.2em;
90 | }
91 |
92 | &.active { color: $brand-danger; }
93 | }
94 | }
95 | }
96 |
97 | &.product-list {
98 | margin-bottom: 30px;
99 | }
100 |
101 | @media (min-width: $screen-sm) {
102 |
103 | &.product-list {
104 | display: table;
105 | width: 100%;
106 | padding: 0;
107 |
108 | .product-thumb,
109 | .product-info {
110 | display: table-cell;
111 | vertical-align: middle;
112 | }
113 | .product-thumb {
114 | position: relative;
115 | width: 270px;
116 | padding: 20px 18px;
117 | border-right: 1px solid $border-color;
118 | }
119 |
120 | .product-info {
121 | padding: 20px 22px;
122 |
123 | .product-title,
124 | .product-price,
125 | .product-buttons {
126 | text-align: left;
127 | }
128 |
129 | .product-buttons {
130 |
131 | padding: {
132 | top: 20px;
133 | bottom: 0;
134 | }
135 | border-top: 1px solid $border-color;
136 |
137 | > .btn {
138 | margin: 0;
139 | margin-right: 8px;
140 | }
141 | }
142 | }
143 |
144 | .product-title {
145 | font-size: $font-size-lead;
146 | }
147 | }
148 | }
149 | }
150 |
151 |
152 | :host ::ng-deep .rating-stars {
153 | position: absolute;
154 | top: 15px;
155 | right: 18px;
156 | }
157 |
--------------------------------------------------------------------------------
/src/app/products/products-list-item/products-list-item.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Input, OnDestroy, OnInit } from '@angular/core';
2 |
3 | import { Subscription } from 'rxjs';
4 |
5 | import { AuthService } from '../../account/shared/auth.service';
6 | import { CartService } from '../../cart/shared/cart.service';
7 |
8 | import { CartItem } from '../../models/cart-item.model';
9 | import { Product } from '../../models/product.model';
10 | import { User } from '../../models/user.model';
11 |
12 | @Component({
13 | selector: 'app-products-list-item',
14 | templateUrl: './products-list-item.component.html',
15 | styleUrls: ['./products-list-item.component.scss']
16 | })
17 | export class ProductsListItemComponent implements OnInit, OnDestroy {
18 | private userSubscription: Subscription;
19 | @Input() public product: Product;
20 | @Input() public displayMode: string;
21 | public user: User;
22 | public imageLoading: boolean;
23 |
24 | constructor(
25 | private cartService: CartService,
26 | private authService: AuthService
27 | ) {}
28 |
29 | ngOnInit() {
30 | this.imageLoading = true;
31 | this.userSubscription = this.authService.user.subscribe((user) => {
32 | this.user = user;
33 | });
34 | }
35 |
36 | public onAddToCart() {
37 | this.cartService.addItem(new CartItem(this.product, 1));
38 | }
39 |
40 | public onImageLoad() {
41 | this.imageLoading = false;
42 | }
43 |
44 | ngOnDestroy() {
45 | this.userSubscription.unsubscribe();
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/app/products/products-list/products-list.component.scss:
--------------------------------------------------------------------------------
1 | @import '~scss/helpers/variables';
2 | @import '~scss/helpers/mixins';
3 |
4 | .products-list {
5 |
6 | &.loading {
7 | min-height: 200px;
8 | background: url('../../../img/loading.gif') center center no-repeat;
9 | }
10 | }
11 |
12 | .shop-toolbar {
13 | display: table;
14 | width: 100%;
15 | > .column {
16 | display: table-cell;
17 | vertical-align: middle;
18 | &:last-child { text-align: right; }
19 | }
20 | @media (max-width: $screen-sm) {
21 | > .column {
22 | display: block;
23 | width: 100%;
24 | text-align: center;
25 | &:last-child {
26 | padding-top: $grid-vertical-step;
27 | text-align: center;
28 | }
29 | }
30 | }
31 | }
32 | .shop-sorting {
33 | label,
34 | .form-control,
35 | span {
36 | display: inline-block;
37 | vertical-align: middle;
38 | }
39 | span { padding: 8px 0; }
40 | label {
41 | margin: 0;
42 | padding: 8px 5px 8px 0;
43 | color: $gray;
44 | font: {
45 | size: $font-size-sm;
46 | weight: normal;
47 | }
48 | }
49 | .form-control {
50 | width: 100%;
51 | max-width: 186px;
52 | margin-right: 10px;
53 | }
54 | @media (max-width: $screen-sm) {
55 | label, .form-control {
56 | display: block;
57 | width: 100%;
58 | max-width: 100%;
59 | margin: 0;
60 | padding: {
61 | top: 0;
62 | right: 0;
63 | }
64 | }
65 | }
66 | }
67 | .shop-view {
68 | display: inline-block;
69 | @include clearfix;
70 | > a {
71 | display: block;
72 | width: $shop-view-size;
73 | height: $shop-view-size;
74 | margin-left: 10px;
75 | padding: 13px;
76 | float: left;
77 | transition: background-color .35s;
78 | border: 1px solid $border-color;
79 | border-radius: 50%;
80 | background-color: $shop-view-bg-color;
81 | span {
82 | display: block;
83 | position: relative;
84 | width: 3px;
85 | height: 3px;
86 | margin-bottom: 3px;
87 | background-color: $shop-view-color;
88 | &::before,
89 | &::after {
90 | display: block;
91 | position: absolute;
92 | background-color: $shop-view-color;
93 | }
94 | &:last-child { margin-bottom: 0; }
95 | }
96 | &:hover { background-color: $shop-view-hover-bg-color; }
97 | &.active {
98 | border-color: $shop-view-active-bg-color;
99 | background-color: $shop-view-active-bg-color;
100 | cursor: default;
101 | pointer-events: none;
102 | span,
103 | span::before,
104 | span::after { background-color: $white-color; }
105 | }
106 | &.grid-view span {
107 | &::before,
108 | &::after {
109 | top: 0;
110 | width: 3px;
111 | height: 3px;
112 | content: '';
113 | }
114 | &::before { left: 6px; }
115 | &::after { left: 12px; }
116 | }
117 | &.list-view span {
118 | &::before {
119 | top: 1px;
120 | left: 6px;
121 | width: 9px;
122 | height: 1px;
123 | content: '';
124 | }
125 | }
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/src/app/products/products-list/products-list.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnDestroy, OnInit } from '@angular/core';
2 |
3 | import { Subject } from 'rxjs';
4 | import { takeUntil } from 'rxjs/operators';
5 |
6 | import { AuthService } from '../../account/shared/auth.service';
7 | import { PagerService } from '../../pager/pager.service';
8 | import { ProductsCacheService } from '../shared/products-cache.service';
9 | import { ProductService } from '../shared/product.service';
10 | import { UiService } from '../shared/ui.service';
11 | import { SortPipe } from '../shared/sort.pipe';
12 |
13 | import { Product } from '../../models/product.model';
14 | import { User } from '../../models/user.model';
15 |
16 | @Component({
17 | selector: 'app-products',
18 | templateUrl: './products-list.component.html',
19 | styleUrls: ['./products-list.component.scss']
20 | })
21 | export class ProductsListComponent implements OnInit, OnDestroy {
22 | unsubscribe$ = new Subject();
23 | products: Product[];
24 | productsPaged: Product[];
25 | pager: any = {};
26 | user: User;
27 | productsLoading: boolean;
28 | currentPagingPage: number;
29 |
30 | constructor(
31 | private productService: ProductService,
32 | private productsCacheService: ProductsCacheService,
33 | private pagerService: PagerService,
34 | private sortPipe: SortPipe,
35 | private authService: AuthService,
36 | public uiService: UiService
37 | ) {}
38 |
39 | ngOnInit() {
40 | this.authService.user
41 | .pipe(takeUntil(this.unsubscribe$))
42 | .subscribe((user) => {
43 | this.user = user;
44 | });
45 | this.uiService.currentPagingPage$
46 | .pipe(takeUntil(this.unsubscribe$))
47 | .subscribe((page) => {
48 | this.currentPagingPage = page;
49 | });
50 | this.getProducts();
51 | }
52 |
53 | getProducts() {
54 | this.productsLoading = true;
55 | this.productService
56 | .getProducts()
57 | .pipe(takeUntil(this.unsubscribe$))
58 | .subscribe((products) => {
59 | this.products = products;
60 | this.setPage(this.currentPagingPage);
61 | this.productsLoading = false;
62 | });
63 | }
64 |
65 | onDisplayModeChange(mode: string, e: Event) {
66 | this.uiService.displayMode$.next(mode);
67 | e.preventDefault();
68 | }
69 |
70 | setPage(page: number) {
71 | if (page < 1 || page > this.pager.totalPages) {
72 | return;
73 | }
74 | this.pager = this.pagerService.getPager(this.products.length, page, 8);
75 | this.productsPaged = this.products.slice(
76 | this.pager.startIndex,
77 | this.pager.endIndex + 1
78 | );
79 | this.uiService.currentPagingPage$.next(page);
80 | }
81 |
82 | onSort(sortBy: string) {
83 | this.sortPipe.transform(
84 | this.products,
85 | sortBy.replace(':reverse', ''),
86 | sortBy.endsWith(':reverse')
87 | );
88 | this.uiService.sorting$.next(sortBy);
89 | this.setPage(1);
90 | }
91 |
92 | ngOnDestroy() {
93 | this.unsubscribe$.next();
94 | this.unsubscribe$.complete();
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/src/app/products/products.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { CommonModule } from '@angular/common';
3 | import { AngularFireDatabaseModule } from '@angular/fire/compat/database';
4 | import { SharedModule } from '../shared/shared.module';
5 |
6 | import { ProductsListComponent } from './products-list/products-list.component';
7 | import { ProductDetailComponent } from './product-detail/product-detail.component';
8 | import { ProductsListItemComponent } from './products-list-item/products-list-item.component';
9 | import { RatingStarsComponent } from './shared/rating-stars/rating-stars.component';
10 |
11 | import { FileUploadService } from './shared/file-upload.service';
12 | import { ProductsCacheService } from './shared/products-cache.service';
13 | import { ProductRatingService } from './shared/product-rating.service';
14 |
15 | import { SortPipe } from './shared/sort.pipe';
16 |
17 | @NgModule({
18 | declarations: [
19 | ProductDetailComponent,
20 | ProductsListComponent,
21 | ProductsListItemComponent,
22 | SortPipe,
23 | RatingStarsComponent
24 | ],
25 | imports: [SharedModule],
26 | exports: [
27 | ProductDetailComponent,
28 | ProductsListComponent,
29 | ProductsListItemComponent,
30 | SortPipe,
31 | RatingStarsComponent
32 | ],
33 | providers: [SortPipe, FileUploadService, ProductsCacheService, ProductRatingService]
34 | })
35 | export class ProductsModule {}
36 |
--------------------------------------------------------------------------------
/src/app/products/shared/file-upload.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import { AngularFireStorage, AngularFireUploadTask } from '@angular/fire/compat/storage';
3 | import { Observable } from 'rxjs';
4 |
5 | @Injectable()
6 | export class FileUploadService {
7 | public task$: AngularFireUploadTask;
8 |
9 | // Progress monitoring
10 | public percentage$: Observable;
11 |
12 | public snapshot: Observable;
13 |
14 | // Download URL
15 | public downloadURL: Observable;
16 |
17 | constructor(public storage: AngularFireStorage) {}
18 |
19 | public startUpload(data) {
20 | // The File object
21 | const file = data.files.item(0);
22 |
23 | // Client-side validation example
24 | if (file.type.split('/')[0] !== 'image') {
25 | console.error('unsupported file type :( ');
26 | throw new Error('upload failed, unsupported file type');
27 | }
28 |
29 | // The storage path
30 | const path = `product-images/${new Date().getTime()}_${file}`;
31 |
32 | // The main task
33 | this.task$ = this.storage.upload(path, file);
34 |
35 | // the percentage
36 | this.percentage$ = this.task$.percentageChanges();
37 |
38 | return this.task$;
39 | }
40 |
41 | public deleteFile(files: string[]) {
42 | if (files) {
43 | return files.map((filePath) => {
44 | return this.storage.ref(filePath).delete();
45 | });
46 | }
47 | }
48 |
49 | // Determines if the upload task is active
50 | public isActive(snapshot) {
51 | return (
52 | snapshot.state === 'running' &&
53 | snapshot.bytesTransferred < snapshot.totalBytes
54 | );
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/app/products/shared/product-rating.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed } from '@angular/core/testing';
2 | import { AngularFireDatabase } from '@angular/fire/compat/database';
3 |
4 | import { Observable } from 'rxjs/observable';
5 | import { of } from 'rxjs/observable/of';
6 |
7 | import { AuthService } from '../../account/shared/auth.service';
8 | import { MessageService } from '../../messages/message.service';
9 | import { ProductRatingService } from './product-rating.service';
10 |
11 | import { Product } from '../../models/product.model';
12 | import { User, Roles } from '../../models/user.model';
13 |
14 | class MockAuthService {
15 | user: Observable;
16 |
17 | constructor() {
18 | this.user = of({
19 | email: 'foo@bar.com',
20 | firstName: 'foo',
21 | lastName: 'bar',
22 | uid: '123456789'
23 | });
24 | }
25 |
26 | }
27 |
28 | describe('Rating', () => {
29 | let productRatingService: ProductRatingService;
30 | let angularFireDatabase: AngularFireDatabase;
31 | let authService: MockAuthService;
32 |
33 | beforeEach(() => {
34 | TestBed.configureTestingModule({
35 | providers: [
36 | ProductRatingService,
37 | { provide: MessageService },
38 | { provide: AngularFireDatabase },
39 | { provide: AuthService, useClass: MockAuthService }
40 | ]
41 | });
42 |
43 | productRatingService = TestBed.get(ProductRatingService);
44 | angularFireDatabase = TestBed.get(AngularFireDatabase);
45 | authService = TestBed.get(AuthService);
46 | });
47 |
48 | it('should be created', () => {
49 | expect(productRatingService).toBeTruthy();
50 | });
51 |
52 | describe('should handle rating actions and', () => {
53 |
54 | it('should handle a first rating', () => {
55 | const product = new Product();
56 |
57 | const result = productRatingService['constructRating'](product, 5);
58 | expect(result).toEqual({
59 | '/ratings/123456789/': 5,
60 | '/currentRating/': 5
61 | });
62 | });
63 |
64 | it('should handle a new rating from same user', () => {
65 | const product = new Product();
66 | product.currentRating = 5;
67 | product.ratings['123456789'] = 5;
68 |
69 | const result = productRatingService['constructRating'](product, 1);
70 |
71 | expect(result).toEqual({
72 | '/ratings/123456789/': 1,
73 | '/currentRating/': 1
74 | });
75 | });
76 |
77 | it('should handle a rating from a second user', () => {
78 | const product = new Product();
79 | product.currentRating = 5;
80 | product.ratings = {'987654321': 5};
81 |
82 | const result = productRatingService['constructRating'](product, 1);
83 |
84 | expect(result).toEqual({
85 | '/currentRating/': 3,
86 | '/ratings/123456789/': 1
87 | });
88 | });
89 | });
90 | });
91 |
--------------------------------------------------------------------------------
/src/app/products/shared/product-rating.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import { AngularFireDatabase } from '@angular/fire/compat/database';
3 |
4 | import { Observable , from as fromPromise , of } from 'rxjs';
5 |
6 | import { AuthService } from '../../account/shared/auth.service';
7 | import { MessageService } from '../../messages/message.service';
8 | import { FileUploadService } from './file-upload.service';
9 |
10 | import { ProductsUrl } from './productsUrl';
11 | import { Product } from '../../models/product.model';
12 | import { User } from '../../models/user.model';
13 |
14 | @Injectable()
15 | export class ProductRatingService {
16 | private productsUrl = ProductsUrl.productsUrl;
17 | private user: User;
18 |
19 | constructor(
20 | private messageService: MessageService,
21 | private angularFireDatabase: AngularFireDatabase,
22 | public authService: AuthService
23 | ) {
24 | this.authService.user.subscribe(user => this.user = user);
25 | }
26 |
27 | /** Log a ProductService message with the MessageService */
28 | private log(message: string) {
29 | this.messageService.add('ProductService: ' + message);
30 | }
31 |
32 | /**
33 | * Handle Http operation that failed.
34 | * Let the app continue.
35 | * @param operation - name of the operation that failed
36 | * @param result - optional value to return as the observable result
37 | */
38 | private handleError(operation = 'operation', result?: T) {
39 | return (error: any): Observable => {
40 | console.error(error); // log to console instead
41 | this.log(`${operation} failed: ${error.message}`);
42 | // Let the app keep running by returning an empty result.
43 | return of(result as T);
44 | };
45 | }
46 |
47 | public rateProduct(product: Product, rating: number) {
48 | const url = `${this.productsUrl}/${product.id}`;
49 | const updates = this.constructRating(product, rating);
50 |
51 | return fromPromise(
52 | this.angularFireDatabase
53 | .object(url)
54 | .update(updates)
55 | .then(() => this.log(`Rated Product ${product.name} width: ${rating}`))
56 | .catch((error) => {
57 | this.handleError(error);
58 | })
59 | );
60 | }
61 | // pure helper functions start here
62 | private constructRating(product: Product, rating: number) {
63 | // construct container for update content
64 | const updates = {};
65 |
66 | // Add user rating to local version of ratings
67 | if (product.ratings) {
68 | product.ratings[this.user.uid] = rating;
69 | } else {
70 | product['ratings'] = [];
71 | product['ratings'][this.user.uid] = rating;
72 | }
73 |
74 | // Add user rating
75 | updates['/ratings/' + this.user.uid + '/'] = rating;
76 |
77 | // calculate current overall rating
78 | updates['/currentRating/'] = this.calculateOverallRating(product, rating);
79 | return updates;
80 | }
81 |
82 | private calculateOverallRating(product: Product, rating: number): number {
83 | // Calculate and add new overall rating
84 | const currentRating =
85 | Object.values(product.ratings).reduce(
86 | (a: number, b: number) => a + b,
87 | 0
88 | ) / Object.values(product.ratings).length;
89 |
90 | return currentRating;
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/src/app/products/shared/products-cache.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 |
3 | import { Observable , of } from 'rxjs';
4 | import { publishReplay , refCount , switchMap } from 'rxjs/operators';
5 |
6 | import { Product } from '../../models/product.model';
7 |
8 | @Injectable()
9 | export class ProductsCacheService {
10 | private products: Observable;
11 |
12 | public get(key: string | number, fallback: Observable) {
13 | if (typeof key === 'string') {
14 | return this.getProducts(fallback);
15 | } else {
16 | return this.getProduct(key, fallback);
17 | }
18 | }
19 |
20 | private getProducts(fallback: Observable): Observable {
21 | if (!this.products) {
22 | this.products = fallback.pipe(publishReplay(1), refCount());
23 | }
24 | return this.products;
25 | }
26 |
27 | private getProduct(key: any, fallback: Observable) {
28 | return this.getProducts(fallback).pipe(
29 | switchMap((products) => {
30 | const selectedProduct = products.find((product) => {
31 | return product.id === key;
32 | });
33 | return of(selectedProduct);
34 | }),
35 | publishReplay(1),
36 | refCount()
37 | );
38 | }
39 |
40 | public clearCache() {
41 | this.products = null;
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/app/products/shared/productsUrl.ts:
--------------------------------------------------------------------------------
1 | export class ProductsUrl {
2 | static productsUrl = '/products';
3 | }
4 |
--------------------------------------------------------------------------------
/src/app/products/shared/rating-stars/rating-stars.component.html:
--------------------------------------------------------------------------------
1 |
2 | 0}" class="icon-star">
3 | 1}" class="icon-star">
4 | 2}" class="icon-star">
5 | 3}" class="icon-star">
6 | 4}" class="icon-star">
7 |
8 |
--------------------------------------------------------------------------------
/src/app/products/shared/rating-stars/rating-stars.component.scss:
--------------------------------------------------------------------------------
1 | @import '~scss/helpers/variables';
2 | @import '~scss/helpers/mixins';
3 |
4 | .rating-stars {
5 | display: inline-block;
6 |
7 | > i {
8 | display: inline-block;
9 | margin-right: 2px;
10 | color: lighten($gray, 15%);
11 | font-size: $font-size-sm;
12 | &.filled { color: $brand-warning; }
13 | &:last-child { margin-right: 0; }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/app/products/shared/rating-stars/rating-stars.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Input } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-rating-stars',
5 | templateUrl: './rating-stars.component.html',
6 | styleUrls: ['./rating-stars.component.scss']
7 | })
8 | export class RatingStarsComponent {
9 | @Input() public rating: number;
10 | }
11 |
--------------------------------------------------------------------------------
/src/app/products/shared/sort.pipe.ts:
--------------------------------------------------------------------------------
1 | import { Pipe, PipeTransform } from '@angular/core';
2 |
3 | @Pipe({
4 | name: 'sort'
5 | })
6 | export class SortPipe implements PipeTransform {
7 | transform(array: any[], field: string, reverse?: boolean): any[] {
8 | if (!array) {
9 | return;
10 | }
11 | array.sort((a: any, b: any) => {
12 | if (a[field] < b[field]) {
13 | return -1;
14 | } else if (a[field] > b[field]) {
15 | return 1;
16 | } else {
17 | return 0;
18 | }
19 | });
20 | if (reverse) {
21 | return array.reverse();
22 | }
23 | return array;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/app/products/shared/ui.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import { BehaviorSubject } from 'rxjs';
3 |
4 | @Injectable()
5 | export class UiService {
6 | public sorting$: BehaviorSubject;
7 | public displayMode$: BehaviorSubject;
8 | public currentPagingPage$: BehaviorSubject;
9 |
10 | constructor() {
11 | this.sorting$ = new BehaviorSubject('date:reverse');
12 | this.displayMode$ = new BehaviorSubject('grid');
13 | this.currentPagingPage$ = new BehaviorSubject(1);
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/app/shared/price/price.component.html:
--------------------------------------------------------------------------------
1 | {{product.priceNormal | currency }}{{product.price | currency }}
2 |
--------------------------------------------------------------------------------
/src/app/shared/price/price.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monobasic/Angular-Reactive-Demo-Shop/d25467526b2b627132a750775e7e28145a406aad/src/app/shared/price/price.component.scss
--------------------------------------------------------------------------------
/src/app/shared/price/price.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Input } from '@angular/core';
2 | import { Product } from '../../models/product.model';
3 |
4 | @Component({
5 | selector: 'app-price',
6 | templateUrl: './price.component.html',
7 | styleUrls: ['./price.component.scss']
8 | })
9 | export class PriceComponent {
10 | @Input() public product: Product;
11 | }
12 |
--------------------------------------------------------------------------------
/src/app/shared/shared.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { CommonModule } from '@angular/common';
3 | import { RouterModule } from '@angular/router';
4 | import { AppRoutingModule } from '../app-routing.module';
5 | import { FormsModule } from '@angular/forms';
6 |
7 | import { PriceComponent } from './price/price.component';
8 | import { PageTitleComponent } from '../core/page-title/page-title.component';
9 |
10 | @NgModule({
11 | declarations: [
12 | PriceComponent,
13 | PageTitleComponent
14 | ],
15 | imports: [
16 | CommonModule,
17 | AppRoutingModule,
18 | FormsModule
19 | ],
20 | exports: [
21 | PriceComponent,
22 | PageTitleComponent,
23 | CommonModule,
24 | AppRoutingModule,
25 | FormsModule
26 | ]
27 | })
28 | export class SharedModule {}
29 |
--------------------------------------------------------------------------------
/src/assets/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monobasic/Angular-Reactive-Demo-Shop/d25467526b2b627132a750775e7e28145a406aad/src/assets/.gitkeep
--------------------------------------------------------------------------------
/src/assets/fonts/Pe-icon-7-stroke.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monobasic/Angular-Reactive-Demo-Shop/d25467526b2b627132a750775e7e28145a406aad/src/assets/fonts/Pe-icon-7-stroke.eot
--------------------------------------------------------------------------------
/src/assets/fonts/Pe-icon-7-stroke.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monobasic/Angular-Reactive-Demo-Shop/d25467526b2b627132a750775e7e28145a406aad/src/assets/fonts/Pe-icon-7-stroke.ttf
--------------------------------------------------------------------------------
/src/assets/fonts/Pe-icon-7-stroke.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monobasic/Angular-Reactive-Demo-Shop/d25467526b2b627132a750775e7e28145a406aad/src/assets/fonts/Pe-icon-7-stroke.woff
--------------------------------------------------------------------------------
/src/assets/fonts/feather-webfont.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monobasic/Angular-Reactive-Demo-Shop/d25467526b2b627132a750775e7e28145a406aad/src/assets/fonts/feather-webfont.eot
--------------------------------------------------------------------------------
/src/assets/fonts/feather-webfont.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monobasic/Angular-Reactive-Demo-Shop/d25467526b2b627132a750775e7e28145a406aad/src/assets/fonts/feather-webfont.ttf
--------------------------------------------------------------------------------
/src/assets/fonts/feather-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monobasic/Angular-Reactive-Demo-Shop/d25467526b2b627132a750775e7e28145a406aad/src/assets/fonts/feather-webfont.woff
--------------------------------------------------------------------------------
/src/assets/fonts/socicon.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monobasic/Angular-Reactive-Demo-Shop/d25467526b2b627132a750775e7e28145a406aad/src/assets/fonts/socicon.eot
--------------------------------------------------------------------------------
/src/assets/fonts/socicon.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monobasic/Angular-Reactive-Demo-Shop/d25467526b2b627132a750775e7e28145a406aad/src/assets/fonts/socicon.ttf
--------------------------------------------------------------------------------
/src/assets/fonts/socicon.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monobasic/Angular-Reactive-Demo-Shop/d25467526b2b627132a750775e7e28145a406aad/src/assets/fonts/socicon.woff
--------------------------------------------------------------------------------
/src/environments/environment.prod.ts:
--------------------------------------------------------------------------------
1 | export const environment = {
2 | production: true,
3 | firebase: {
4 | apiKey: 'AIzaSyCXVTjTuBUXf-UWlpKfKHjomfPezutmPwI',
5 | authDomain: 'cas-fee-shop.firebaseapp.com',
6 | databaseURL: 'https://cas-fee-shop.firebaseio.com',
7 | projectId: 'cas-fee-shop',
8 | storageBucket: 'cas-fee-shop.appspot.com',
9 | messagingSenderId: '323643286137'
10 | }
11 | };
12 |
--------------------------------------------------------------------------------
/src/environments/environment.ts:
--------------------------------------------------------------------------------
1 | // The file contents for the current environment will overwrite these during build.
2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do
3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead.
4 | // The list of which env maps to which file can be found in `.angular-cli.json`.
5 |
6 | export const environment = {
7 | production: false,
8 | firebase: {
9 | apiKey: 'AIzaSyCXVTjTuBUXf-UWlpKfKHjomfPezutmPwI',
10 | authDomain: 'cas-fee-shop.firebaseapp.com',
11 | databaseURL: 'https://cas-fee-shop.firebaseio.com',
12 | projectId: 'cas-fee-shop',
13 | storageBucket: 'cas-fee-shop.appspot.com',
14 | messagingSenderId: '323643286137'
15 | }
16 | };
17 |
--------------------------------------------------------------------------------
/src/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monobasic/Angular-Reactive-Demo-Shop/d25467526b2b627132a750775e7e28145a406aad/src/favicon.ico
--------------------------------------------------------------------------------
/src/img/404_art.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monobasic/Angular-Reactive-Demo-Shop/d25467526b2b627132a750775e7e28145a406aad/src/img/404_art.jpg
--------------------------------------------------------------------------------
/src/img/coming-soon-bg.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monobasic/Angular-Reactive-Demo-Shop/d25467526b2b627132a750775e7e28145a406aad/src/img/coming-soon-bg.jpg
--------------------------------------------------------------------------------
/src/img/credit-cards.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monobasic/Angular-Reactive-Demo-Shop/d25467526b2b627132a750775e7e28145a406aad/src/img/credit-cards.png
--------------------------------------------------------------------------------
/src/img/default-skin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monobasic/Angular-Reactive-Demo-Shop/d25467526b2b627132a750775e7e28145a406aad/src/img/default-skin.png
--------------------------------------------------------------------------------
/src/img/default-skin.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/img/loading.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monobasic/Angular-Reactive-Demo-Shop/d25467526b2b627132a750775e7e28145a406aad/src/img/loading.gif
--------------------------------------------------------------------------------
/src/img/main-bg.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monobasic/Angular-Reactive-Demo-Shop/d25467526b2b627132a750775e7e28145a406aad/src/img/main-bg.jpg
--------------------------------------------------------------------------------
/src/img/map-marker.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monobasic/Angular-Reactive-Demo-Shop/d25467526b2b627132a750775e7e28145a406aad/src/img/map-marker.png
--------------------------------------------------------------------------------
/src/img/payment_methods.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monobasic/Angular-Reactive-Demo-Shop/d25467526b2b627132a750775e7e28145a406aad/src/img/payment_methods.png
--------------------------------------------------------------------------------
/src/img/preloader.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monobasic/Angular-Reactive-Demo-Shop/d25467526b2b627132a750775e7e28145a406aad/src/img/preloader.gif
--------------------------------------------------------------------------------
/src/img/services/01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monobasic/Angular-Reactive-Demo-Shop/d25467526b2b627132a750775e7e28145a406aad/src/img/services/01.png
--------------------------------------------------------------------------------
/src/img/services/02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monobasic/Angular-Reactive-Demo-Shop/d25467526b2b627132a750775e7e28145a406aad/src/img/services/02.png
--------------------------------------------------------------------------------
/src/img/services/03.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monobasic/Angular-Reactive-Demo-Shop/d25467526b2b627132a750775e7e28145a406aad/src/img/services/03.png
--------------------------------------------------------------------------------
/src/img/services/04.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monobasic/Angular-Reactive-Demo-Shop/d25467526b2b627132a750775e7e28145a406aad/src/img/services/04.png
--------------------------------------------------------------------------------
/src/img/user-ava-md.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monobasic/Angular-Reactive-Demo-Shop/d25467526b2b627132a750775e7e28145a406aad/src/img/user-ava-md.jpg
--------------------------------------------------------------------------------
/src/img/user-cover-img.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monobasic/Angular-Reactive-Demo-Shop/d25467526b2b627132a750775e7e28145a406aad/src/img/user-cover-img.jpg
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Shop
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/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.log(err));
13 |
--------------------------------------------------------------------------------
/src/polyfills.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This file includes polyfills needed by Angular and is loaded before the app.
3 | * You can add your own extra polyfills to this file.
4 | *
5 | * This file is divided into 2 sections:
6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main
8 | * file.
9 | *
10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
13 | *
14 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html
15 | */
16 |
17 | /***************************************************************************************************
18 | * BROWSER POLYFILLS
19 | */
20 |
21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/
22 | // import 'core-js/es6/symbol';
23 | // import 'core-js/es6/object';
24 | // import 'core-js/es6/function';
25 | // import 'core-js/es6/parse-int';
26 | // import 'core-js/es6/parse-float';
27 | // import 'core-js/es6/number';
28 | // import 'core-js/es6/math';
29 | // import 'core-js/es6/string';
30 | // import 'core-js/es6/date';
31 | // import 'core-js/es6/array';
32 | // import 'core-js/es6/regexp';
33 | // import 'core-js/es6/map';
34 | // import 'core-js/es6/weak-map';
35 | // import 'core-js/es6/set';
36 |
37 | /** IE10 and IE11 requires the following for the Reflect API. */
38 | // import 'core-js/es6/reflect';
39 |
40 |
41 | /** Evergreen browsers require these. **/
42 | // Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove.
43 | import 'core-js/es/reflect';
44 |
45 |
46 |
47 | /***************************************************************************************************
48 | * Zone JS is required by Angular itself.
49 | */
50 | import 'zone.js'; // Included with Angular CLI.
51 |
52 |
53 |
54 | /***************************************************************************************************
55 | * APPLICATION IMPORTS
56 | */
57 |
58 | /**
59 | * Date, currency, decimal and percent pipes.
60 | * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10
61 | */
62 | // import 'intl'; // Run `npm install --save intl`.
63 | /**
64 | * Need to import at least one locale-data with intl.
65 | */
66 | // import 'intl/locale-data/jsonp/en';
67 |
--------------------------------------------------------------------------------
/src/scss/base/_scaffolding.scss:
--------------------------------------------------------------------------------
1 | //
2 | // Scaffolding
3 | // --------------------------------------------------
4 |
5 | html * {
6 | text-rendering: optimizeLegibility;
7 | -webkit-font-smoothing: antialiased;
8 | -moz-osx-font-smoothing: grayscale;
9 | }
10 |
11 | body {
12 | background: {
13 | position: center;
14 | color: $body-bg;
15 | repeat: no-repeat;
16 | size: cover;
17 | }
18 | color: $body-color;
19 | font: {
20 | family: $font-family-base;
21 | size: $font-size-sm;
22 | weight: $font-weight-base;
23 | }
24 | text-transform: $text-transform-base;
25 | line-height: $line-height-base;
26 | }
27 |
28 | // Links
29 | a {
30 | color: $link-color;
31 | text-decoration: underline;
32 |
33 | &:hover {
34 | color: $link-hover-color;
35 | text-decoration: none;
36 | }
37 | &:focus { outline: none; }
38 | }
39 |
40 | .small, small { font-size: 85%; }
41 |
42 | // Navigation Links
43 | .navi-link,
44 | .navi-link-light {
45 | transition: color .3s;
46 | color: $nav-link-color;
47 | text-decoration: none;
48 | &:hover { color: $nav-link-hover-color; }
49 | }
50 | .navi-link-light { color: $white-color; }
51 |
52 | // Images
53 | // Responsive images (ensure images don't scale beyond their parents)
54 | img,
55 | figure {
56 | max-width: 100%;
57 | height: auto;
58 | vertical-align: middle;
59 | }
60 | svg { max-width: 100%; }
61 |
62 | /* Responsive iframes */
63 | iframe { width: 100%; }
64 |
65 | /* Box Model */
66 | * { box-sizing: border-box; }
67 | *::before,
68 | *::after { box-sizing: border-box; }
69 |
70 | // Horizontal rules
71 | hr {
72 | margin: 0;
73 | border: 0;
74 | border-top: 1px solid $border-color;
75 | &.hr-light { border-top-color: $border-light-color; }
76 | }
77 |
78 | // Pre tag
79 | pre {
80 | display: block;
81 | padding: 15px;
82 | border: 1px solid $border-color;
83 | border-radius: $border-radius-lg;
84 | background-color: $gray-lighter;
85 | }
86 |
87 | // Text Selection Color
88 | ::selection {
89 | background: $gray-darker;
90 | color: $white-color;
91 | }
92 | ::-moz-selection {
93 | background: $gray-darker;
94 | color: $white-color;
95 | }
96 |
97 |
98 | // Image with caption
99 | // -------------------------------------------------------
100 |
101 | figure {
102 | position: relative;
103 | margin: 0;
104 | figcaption {
105 | display: block;
106 | position: absolute;
107 | bottom: 0;
108 | left: 0;
109 | width: 100%;
110 | margin: 0;
111 | padding: floor($grid-vertical-step / 2); // ~12px
112 | font-size: $font-size-sm;
113 | }
114 | }
115 |
116 |
117 | /* Bootstrap Overrides */
118 | @media (min-width: $screen-xl) {
119 | .container {
120 | width: 1170px;
121 | max-width: 1170px;
122 | }
123 | }
124 | @media (max-width: $screen-xl) {
125 | .container {
126 | width: 100% !important;
127 | max-width: 100% !important;
128 | }
129 | }
130 | .container-fluid {
131 | max-width: 1920px;
132 | margin: {
133 | right: auto;
134 | left: auto;
135 | }
136 | padding: {
137 | right: 30px;
138 | left: 30px;
139 | }
140 | @media (max-width: $screen-xl) { padding: 0 15px; }
141 | }
142 |
--------------------------------------------------------------------------------
/src/scss/components/_accordion.scss:
--------------------------------------------------------------------------------
1 | //
2 | // Accordion
3 | // --------------------------------------------------
4 |
5 | .accordion {
6 | .card {
7 | margin-bottom: floor($grid-vertical-step / 3); //~12px
8 | }
9 | [data-toggle='collapse'] {
10 | display: block;
11 | position: relative;
12 | color: $nav-link-color;
13 | text-decoration: none;
14 | &::after {
15 | position: absolute;
16 | top: 50%;
17 | right: 0;
18 | width: 0;
19 | height: 0;
20 | margin-top: -2px;
21 | transition: transform .25s;
22 | border: {
23 | right: 5px solid transparent;
24 | bottom: 5px dashed;
25 | left: 5px solid transparent;
26 | }
27 | content: '';
28 | }
29 | &.collapsed::after {
30 | transform: rotate(-180deg);
31 | }
32 | > i {
33 | margin: {
34 | top: -4px;
35 | right: 7px;
36 | }
37 | &.socicon-paypal {
38 | display: inline-block;
39 | margin-top: 1px;
40 | font-size: .8em;
41 | vertical-align: middle;
42 | }
43 | &.icon-medal {
44 | width: 16px;
45 | height: 16px;
46 | background-size: 16px;
47 | }
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/scss/components/_banners.scss:
--------------------------------------------------------------------------------
1 | //
2 | // Banners
3 | // --------------------------------------------------
4 |
5 | .promo-box {
6 | position: relative;
7 | padding: {
8 | right: 15px;
9 | left: 15px;
10 | }
11 | background: {
12 | position: center;
13 | color: $gray-lighter;
14 | repeat: no-repeat;
15 | size: cover;
16 | }
17 |
18 | // Overlay
19 | .overlay-dark,
20 | .overlay-light {
21 | @include overlay-block(1, $black-color, .5);
22 | }
23 | .overlay-light { background-color: $white-color; }
24 |
25 | // Content
26 | .promo-box-content {
27 | position: relative;
28 | z-index: 5;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/scss/components/_comments.scss:
--------------------------------------------------------------------------------
1 | //
2 | // Comments
3 | // --------------------------------------------------
4 |
5 | .comment {
6 | display: block;
7 | position: relative;
8 | margin-bottom: 30px;
9 | padding-left: ($comment-author-ava-size + 16);
10 | .comment-author-ava {
11 | display: block;
12 | position: absolute;
13 | top: 0;
14 | left: 0;
15 | width: $comment-author-ava-size;
16 | border-radius: 50%;
17 | overflow: hidden;
18 | > img {
19 | display: block;
20 | width: 100%;
21 | }
22 | }
23 | .comment-body {
24 | position: relative;
25 | padding: $grid-vertical-step;
26 | border: 1px solid $border-color;
27 | border-radius: $border-radius-lg;
28 | background-color: $body-bg;
29 | &::after, &::before {
30 | position: absolute;
31 | top: 12px;
32 | right: 100%;
33 | width: 0;
34 | height: 0;
35 | border: solid transparent;
36 | content: '';
37 | pointer-events: none;
38 | }
39 | &::after {
40 | border-width: 9px;
41 | border-color: transparent;
42 | border-right-color: $body-bg;
43 | }
44 | &::before {
45 | margin-top: -1px;
46 | border-width: 10px;
47 | border-color: transparent;
48 | border-right-color: $border-color;
49 | }
50 | }
51 | .comment-title {
52 | margin-bottom: floor($grid-vertical-step / 3); //~8px
53 | color: $gray-dark;
54 | font: {
55 | size: $comment-title-size;
56 | weight: $comment-title-weight;
57 | }
58 | }
59 | .comment-text {
60 | margin-bottom: floor($grid-vertical-step / 2); //~12px
61 | }
62 | .comment-footer {
63 | display: table;
64 | width: 100%;
65 | > .column {
66 | display: table-cell;
67 | vertical-align: middle;
68 | &:last-child { text-align: right; }
69 | }
70 | }
71 | .comment-meta {
72 | color: $gray;
73 | font-size: $font-size-xs;
74 | }
75 | .reply-link {
76 | transition: color .3s;
77 | color: $nav-link-color;
78 | font: {
79 | size: $nav-link-font-size;
80 | weight: $nav-link-font-weight;
81 | }
82 | letter-spacing: .07em;
83 | text: {
84 | transform: uppercase;
85 | decoration: none;
86 | }
87 | > i {
88 | display: inline-block;
89 | margin: {
90 | top: -3px;
91 | right: 4px;
92 | }
93 | vertical-align: middle;
94 | }
95 | &:hover { color: $nav-link-hover-color; }
96 | }
97 | &.comment-reply {
98 | margin: {
99 | top: 30px;
100 | bottom: 0;
101 | }
102 | }
103 | @media (max-width: $screen-sm) {
104 | padding-left: 0;
105 | .comment-author-ava { display: none; }
106 | .comment-body {
107 | padding: 15px;
108 | &::before, &::after { display: none; }
109 | }
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/src/scss/components/_countdown.scss:
--------------------------------------------------------------------------------
1 | //
2 | // Countdown
3 | // --------------------------------------------------
4 |
5 | .countdown {
6 | display: inline-block;
7 | @include clearfix;
8 | .item {
9 | display: block;
10 | margin: 7px;
11 | float: left;
12 | text-align: center;
13 | .days, .hours,
14 | .minutes, .seconds {
15 | width: $countdown-box-size;
16 | height: $countdown-box-size;
17 | margin-bottom: 5px;
18 | border: 1px solid $border-color;
19 | border-radius: $border-radius-lg;
20 | font-size: $countdown-font-size;
21 | line-height: ($countdown-box-size - 2);
22 | }
23 | .days_ref, .hours_ref,
24 | .minutes_ref, .seconds_ref {
25 | font-size: $countdown-label-size;
26 | text-transform: uppercase;
27 | }
28 | }
29 | &.countdown-inverse {
30 | .item {
31 | .days, .hours,
32 | .minutes, .seconds {
33 | border-color: $border-light-color;
34 | color: $white-color;
35 | }
36 | .days_ref, .hours_ref,
37 | .minutes_ref, .seconds_ref { color: rgba($white-color, .8); }
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/scss/components/_dropdown.scss:
--------------------------------------------------------------------------------
1 | //
2 | // Dropdown
3 | // --------------------------------------------------
4 |
5 | .dropdown-menu {
6 | border: {
7 | color: $border-color;
8 | radius: $border-radius-base;
9 | }
10 | font-size: $nav-link-font-size;
11 | box-shadow: $sub-menu-shadow;
12 | .dropdown-item {
13 | padding: {
14 | right: 20px;
15 | left: 20px;
16 | }
17 | transition: color .3s;
18 | color: $nav-link-color;
19 | text-decoration: none;
20 | &:hover, &.active, &:focus, &:active { background: 0; }
21 | &:hover { color: $nav-link-hover-color; }
22 | &.active { color: $nav-link-active-color; }
23 | }
24 | a.dropdown-item { font-weight: $nav-link-font-weight; }
25 | }
26 | .dropdown-toggle::after {
27 | margin: {
28 | top: 1px;
29 | left: .3em;
30 | }
31 | vertical-align: .2em;
32 | }
33 |
34 | // Inside Button Group
35 | .btn.dropdown-toggle::after { vertical-align: .2em; }
36 |
37 | // Show Animation
38 | .show .dropdown-menu {
39 | animation: dropdown-show .25s;
40 | }
41 | @keyframes dropdown-show {
42 | from { opacity: 0; }
43 | to { opacity: 1; }
44 | }
45 |
--------------------------------------------------------------------------------
/src/scss/components/_gallery.scss:
--------------------------------------------------------------------------------
1 | //
2 | // Gallery (Photoswipe)
3 | // --------------------------------------------------
4 |
5 | // Gallery Item
6 | .gallery-item {
7 | margin-bottom: 30px;
8 | > a {
9 | display: block;
10 | position: relative;
11 | width: 100%;
12 | border-radius: $border-radius-lg;
13 | text-decoration: none;
14 | overflow: hidden;
15 | > img {
16 | display: block;
17 | width: 100%;
18 | }
19 | &::before {
20 | @include overlay-block(1, $black-color, 0);
21 | transition: opacity .3s;
22 | }
23 | &::after {
24 | display: block;
25 | position: absolute;
26 | top: 50%;
27 | left: 0;
28 | width: 100%;
29 | margin-top: -19px;
30 | transform: translateY(15px);
31 | transition: all .35s;
32 | color: $white-color;
33 | font: {
34 | family: feather;
35 | size: 26px;
36 | }
37 | text-align: center;
38 | content: '\e036';
39 | opacity: 0;
40 | z-index: 5;
41 | }
42 | &:hover {
43 | &::before { opacity: .45; }
44 | &::after {
45 | transform: translateY(0);
46 | opacity: 1;
47 | }
48 | }
49 | &[data-type='video'] {
50 | &::after {
51 | left: 50%;
52 | width: 46px;
53 | height: 46px;
54 | margin: {
55 | top: -22px;
56 | left: -22px;
57 | }
58 | padding-left: 5px;
59 | transform: none;
60 | border-radius: 50%;
61 | background-color: $white-color;
62 | color: $gray-dark;
63 | font-size: 27px;
64 | line-height: 42px;
65 | box-shadow: 0px 4px 15px 0px rgba($black-color, .25);
66 | content: '\e052';
67 | opacity: 1;
68 | }
69 | }
70 | }
71 | .caption { display: none; }
72 | &.no-hover-effect > a::before { display: none; }
73 | }
74 | .grid-no-gap .gallery-item {
75 | margin-bottom: 0;
76 | > a { border-radius: 0; }
77 | }
78 | .owl-carousel .gallery-item { margin-bottom: 0; }
79 |
80 | // Photoswipe
81 | .pswp__zoom-wrap {
82 | text-align: center;
83 | &::before {
84 | content: '';
85 | display: inline-block;
86 | height: 100%;
87 | vertical-align: middle;
88 | }
89 | }
90 | .wrapper {
91 | line-height: 0;
92 | width: 100%;
93 | max-width: 900px;
94 | position: relative;
95 | display: inline-block;
96 | vertical-align: middle;
97 | margin: 0 auto;
98 | text-align: left;
99 | z-index: 1045;
100 | }
101 | .video-wrapper {
102 | position: relative;
103 | padding-bottom: 56.25%; /* 16:9 */
104 | padding-top: 25px;
105 | height: 0;
106 | width: 100%;
107 | iframe {
108 | position: absolute;
109 | top: 0;
110 | left: 0;
111 | width: 100%;
112 | height: 100%;
113 | }
114 | }
115 | video {
116 | width: 100% !important;
117 | height: auto !important;
118 | }
119 | .pswp__caption__center {
120 | padding: 20px 10px;
121 | font: {
122 | size: $font-size-sm;
123 | weight: 500;
124 | }
125 | text-align: center;
126 | }
--------------------------------------------------------------------------------
/src/scss/components/_list-group.scss:
--------------------------------------------------------------------------------
1 | //
2 | // List Group
3 | // ----------------------------------------------------
4 |
5 | .list-group-item {
6 | border-color: $border-color;
7 | background-color: $white-color;
8 | text-decoration: none;
9 | &:first-child {
10 | border-top-left-radius: $border-radius-lg;
11 | border-top-right-radius: $border-radius-lg;
12 | }
13 | &:last-child {
14 | border-bottom-left-radius: $border-radius-lg;
15 | border-bottom-right-radius: $border-radius-lg;
16 | }
17 | i {
18 | margin-top: -4px;
19 | margin-right: 8px;
20 | font-size: 1.1em;
21 | }
22 | p, ul, ol, li, span { font-weight: normal !important; }
23 | }
24 | a.list-group-item,
25 | .list-group-item-action {
26 | transition: all .25s;
27 | color: $nav-link-color;
28 | font-weight: 500;
29 | &:hover,
30 | &:focus, &:active {
31 | background-color: $gray-lighter;
32 | color: $nav-link-color;
33 | }
34 | }
35 | a.list-group-item {
36 | padding: {
37 | top: .87rem;
38 | bottom: .87rem;
39 | }
40 | }
41 |
42 | // With badges
43 | .with-badge {
44 | position: relative;
45 | padding-right: 3.3rem;
46 | .badge {
47 | position: absolute;
48 | top: 50%;
49 | right: 1.15rem;
50 | transform: translateY(-50%);
51 | }
52 | }
53 |
54 | // Badges
55 | .badge {
56 | color: $white-color;
57 | font: {
58 | size: 90%;
59 | weight: 500;
60 | }
61 | &.badge-default {
62 | background-color: lighten($gray-light, 3%);
63 | color: $gray-dark;
64 | }
65 | &.badge-primary { background-color: $brand-primary; }
66 | &.badge-info { background-color: $brand-info; }
67 | &.badge-success { background-color: $brand-success; }
68 | &.badge-warning { background-color: $brand-warning; }
69 | &.badge-danger { background-color: $brand-danger; }
70 | }
71 |
72 | // Active state
73 | .list-group-item.active {
74 | border-color: $brand-primary;
75 | background-color: $brand-primary;
76 | color: $white-color;
77 | cursor: default;
78 | pointer-events: none;
79 | h1, .h1, h2, .h2, h3, .h3,
80 | h4, .h4, h5, .h5, h6, .h6 { color: $white-color; }
81 | .badge {
82 | background-color: $white-color !important;
83 | color: $gray-dark !important;
84 | }
85 | }
86 |
87 | // Contextual classes
88 | .list-group-item-info {
89 | @include list-group-variant($brand-info, rgba($brand-info, .12));
90 | }
91 | .list-group-item-success {
92 | @include list-group-variant(darken($brand-success, 3%), rgba($brand-success, .12));
93 | }
94 | .list-group-item-warning {
95 | @include list-group-variant(darken($brand-warning, 3%), rgba($brand-warning, .12));
96 | }
97 | .list-group-item-danger {
98 | @include list-group-variant($brand-danger, rgba($brand-danger, .12));
99 | }
100 | .list-group-item-action:hover,
101 | .list-group-item-action.active {
102 | &.list-group-item-info { background-color: rgba($brand-info, .24); }
103 | &.list-group-item-success { background-color: rgba($brand-success, .24); }
104 | &.list-group-item-warning { background-color: rgba($brand-warning, .24); }
105 | &.list-group-item-danger { background-color: rgba($brand-danger, .24); }
106 | }
107 |
108 | // Next to card
109 | .card:not([class*='mb-']):not([class*='margin-bottom-']) + .list-group {
110 | margin-top: -1px;
111 | .list-group-item:first-child { border-radius: 0; }
112 | }
113 |
--------------------------------------------------------------------------------
/src/scss/components/_modal.scss:
--------------------------------------------------------------------------------
1 | //
2 | // Modal
3 | // --------------------------------------------------
4 |
5 | .modal { z-index: 9200; }
6 | .modal-content {
7 | border-radius: $border-radius-lg;
8 | border-color: $border-color;
9 | }
10 | .modal-header,
11 | .modal-body,
12 | .modal-footer {
13 | padding: {
14 | right: 20px;
15 | left: 20px;
16 | }
17 | }
18 | .modal-footer {
19 | padding: {
20 | top: floor($grid-vertical-step / 2);
21 | bottom: floor($grid-vertical-step / 2);
22 | }
23 | .btn {
24 | margin: {
25 | right: 0;
26 | left: 12px;
27 | }
28 | }
29 | }
30 | .modal-open.hasScrollbar .navbar-stuck {
31 | width: calc(100% - 15px);
32 | }
33 | .modal-backdrop { z-index: 9100; }
34 |
35 | // For demo purpose only. TODO: remove on production
36 | .example-modal .modal {
37 | display: block;
38 | position: relative;
39 | top: auto;
40 | right: auto;
41 | bottom: auto;
42 | left: auto;
43 | z-index: 1;
44 | }
45 |
--------------------------------------------------------------------------------
/src/scss/components/_pagination.scss:
--------------------------------------------------------------------------------
1 | //
2 | // Pagination + Post Navigation
3 | // --------------------------------------------------
4 |
5 | // Pagination
6 | .pagination {
7 | display: table;
8 | width: 100%;
9 | border-top: 1px solid $border-color;
10 |
11 | > .column {
12 | display: table-cell;
13 | padding-top: ceil($grid-vertical-step / 1.5); //~16px
14 | vertical-align: middle;
15 | }
16 |
17 | .pages {
18 | display: block;
19 | margin: 0;
20 | padding: 0;
21 | list-style: none;
22 |
23 | > li {
24 | display: inline-block;
25 | width: $pagination-link-size;
26 | height: $pagination-link-size;
27 | font: {
28 | size: $pagination-link-font-size;
29 | weight: $pagination-link-font-weight;
30 | }
31 | line-height: $pagination-link-size - 2;
32 | text-align: center;
33 |
34 | > a {
35 | display: block;
36 | width: $pagination-link-size;
37 | height: $pagination-link-size;
38 | transition: all .3s;
39 | border: 1px solid transparent;
40 | border-radius: 50%;
41 | color: $pagination-link-color;
42 | line-height: $pagination-link-size - 2;
43 | text-decoration: none;
44 | cursor: pointer;
45 |
46 | &:hover {
47 | border-color: $border-color;
48 | background-color: $pagination-link-hover-bg;
49 | }
50 | }
51 |
52 | &.active > a {
53 | border-color: $pagination-link-active-bg;
54 | background-color: $pagination-link-active-bg;
55 | color: $pagination-link-active-color;
56 | }
57 | }
58 | }
59 |
60 | .btn > i { margin-top: -5px; }
61 | }
62 |
63 | // Entry Navigation
64 | .entry-navigation {
65 | display: table;
66 | width: 100%;
67 | border: {
68 | top: 1px solid $border-color;
69 | bottom: 1px solid $border-color;
70 | }
71 | table-layout: fixed;
72 | > .column {
73 | display: table-cell;
74 | padding: {
75 | top: 15px;
76 | bottom: 15px;
77 | }
78 | text-align: center;
79 | vertical-align: middle;
80 | }
81 | .btn {
82 | margin: 0;
83 | > i { margin-top: -4px; }
84 | &.view-all {
85 | width: $btn-height;
86 | padding: {
87 | right: 0;
88 | left: 1px;
89 | }
90 | > i {
91 | margin-top: -6px;
92 | font-size: 1.4em;
93 | }
94 | }
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/src/scss/components/_progress.scss:
--------------------------------------------------------------------------------
1 | //
2 | // Progress Bars
3 | // --------------------------------------------------
4 |
5 | // .progress {
6 | // height: auto;
7 | // border-radius: ceil($progress-height / 2);
8 | // background-color: darken($gray-lighter, 2%);
9 | // font: {
10 | // size: $font-size-xs;
11 | // weight: 500;
12 | // }
13 | // line-height: $progress-height;
14 | // }
15 | // .progress-bar {
16 | // height: $progress-height;
17 | // background-color: $progress-bg;
18 | // }
19 |
--------------------------------------------------------------------------------
/src/scss/components/_steps.scss:
--------------------------------------------------------------------------------
1 | //
2 | // Steps
3 | // ----------------------------------------------------
4 |
5 | .steps {
6 | .step {
7 | display: block;
8 | width: 100%;
9 | margin-bottom: 35px;
10 | text-align: center;
11 | .step-icon-wrap {
12 | display: block;
13 | position: relative;
14 | width: 100%;
15 | height: $step-circle-size;
16 | text-align: center;
17 | &::before,
18 | &::after {
19 | display: block;
20 | position: absolute;
21 | top: 50%;
22 | width: 50%;
23 | height: $step-connect-height;
24 | margin-top: -(floor($step-connect-height / 2));
25 | background-color: $border-color;
26 | content: '';
27 | z-index: 1;
28 | }
29 | &::before { left: 0; }
30 | &::after { right: 0; }
31 | }
32 | .step-icon {
33 | display: inline-block;
34 | position: relative;
35 | width: $step-circle-size;
36 | height: $step-circle-size;
37 | border: 1px solid $border-color;
38 | border-radius: 50%;
39 | background-color: $step-icon-default-bg;
40 | color: $step-icon-default-color;
41 | font-size: $step-icon-size;
42 | line-height: ($step-circle-size + 1);
43 | z-index: 5;
44 | }
45 | .step-title {
46 | margin: {
47 | top: floor($grid-vertical-step / 1.5); //~16px
48 | bottom: 0;
49 | }
50 | color: $step-title-color;
51 | font: {
52 | size: $step-title-size;
53 | weight: 500;
54 | }
55 | }
56 | &:first-child .step-icon-wrap::before { display: none; }
57 | &:last-child .step-icon-wrap::after { display: none; }
58 | &.completed {
59 | .step-icon-wrap {
60 | &::before,
61 | &::after { background-color: $brand-primary; }
62 | }
63 | .step-icon {
64 | border-color: $brand-primary;
65 | background-color: $brand-primary;
66 | color: $white-color;
67 | }
68 | }
69 | }
70 | }
71 | @media (max-width: $screen-sm) {
72 | .flex-sm-nowrap .step .step-icon-wrap {
73 | &::before, &::after { display: none; }
74 | }
75 | }
76 | @media (max-width: $screen-md) {
77 | .flex-md-nowrap .step .step-icon-wrap {
78 | &::before, &::after { display: none; }
79 | }
80 | }
81 | @media (max-width: $screen-lg) {
82 | .flex-lg-nowrap .step .step-icon-wrap {
83 | &::before, &::after { display: none; }
84 | }
85 | }
86 | @media (max-width: $screen-xl) {
87 | .flex-xl-nowrap .step .step-icon-wrap {
88 | &::before, &::after { display: none; }
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/src/scss/components/_tables.scss:
--------------------------------------------------------------------------------
1 | //
2 | // Tables
3 | // --------------------------------------------------
4 |
5 | .table {
6 | thead th, td, th { border-color: $border-color; }
7 | &.table-inverse {
8 | background-color: $gray-darker;
9 | color: $white-color;
10 | thead th, td, th { border-color: $border-light-color; }
11 | }
12 | }
13 |
14 | // Table inverse
15 | .thead-inverse th {
16 | background-color: $gray-darker;
17 | color: $white-color;
18 | }
19 | .thead-default th {
20 | background-color: $gray-lighter;
21 | color: $body-color;
22 | }
23 |
24 | // Table striped
25 | .table-striped {
26 | tbody tr:nth-of-type(odd) { background-color: $gray-lighter; }
27 | &.table-inverse {
28 | tbody tr:nth-of-type(odd) { background-color: rgba($black-color, .08); }
29 | }
30 | }
31 |
32 | // Table hover
33 | .table-hover {
34 | tbody tr:hover { background-color: $gray-lighter; }
35 | &.table-inverse {
36 | tbody tr:hover { background-color: rgba($black-color, .08); }
37 | }
38 | }
39 |
40 | // Contextual classes
41 | .table-active,
42 | .table-active td,
43 | .table-active th { background-color: rgba($black-color, .05); }
44 | .table-success,
45 | .table-success td,
46 | .table-success th { background-color: rgba($brand-success, .09); }
47 | .table-info,
48 | .table-info td,
49 | .table-info th { background-color: rgba($brand-info, .09); }
50 | .table-warning,
51 | .table-warning td,
52 | .table-warning th { background-color: rgba($brand-warning, .09); }
53 | .table-danger,
54 | .table-danger td,
55 | .table-danger th { background-color: rgba($brand-danger, .09); }
--------------------------------------------------------------------------------
/src/scss/components/_tooltips.scss:
--------------------------------------------------------------------------------
1 | //
2 | // Tooltips and Popovers
3 | // --------------------------------------------------
4 |
5 | // Tooltip
6 | .tooltip {
7 | font-family: $font-family-base;
8 | &.bs-tooltip-top {
9 | .arrow::before { border-top-color: $tooltip-arrow-color; }
10 | }
11 | &.bs-tooltip-right {
12 | .arrow::before { border-right-color: $tooltip-arrow-color; }
13 | }
14 | &.bs-tooltip-bottom {
15 | .arrow::before { border-bottom-color: $tooltip-arrow-color; }
16 | }
17 | &.bs-tooltip-left {
18 | .arrow::before { border-left-color: $tooltip-arrow-color; }
19 | }
20 | &.show { opacity: $tooltip-opacity; }
21 | }
22 | .tooltip-inner {
23 | border-radius: $border-radius-sm;
24 | background-color: $tooltip-bg;
25 | color: $tooltip-color;
26 | font-size: $font-size-xs;
27 | }
28 |
29 | // Popover
30 | .popover {
31 | border-radius: $border-radius-lg;
32 | border-color: $border-color;
33 | font-family: $font-family-base;
34 | &.bs-popover-top .arrow::before { border-top-color: darken($border-color, 4%); }
35 | &.bs-popover-right .arrow::before { border-right-color: darken($border-color, 4%); }
36 | &.bs-popover-bottom .arrow {
37 | &::before { border-bottom-color: darken($border-color, 4%); }
38 | &::after { border-bottom-color: #f7f7f7; }
39 | }
40 | &.bs-popover-left .arrow::before { border-left-color: darken($border-color, 4%); }
41 | }
42 | .popover-header {
43 | color: $headings-color;
44 | font-family: $font-family-headings;
45 | }
46 | .popover-body { color: $body-color; }
47 |
48 | // For demo purpose only. TODO: remove on production
49 | .example-tooltip .tooltip {
50 | display: inline-block;
51 | position: relative;
52 | margin: 10px 20px;
53 | opacity: 1;
54 | }
55 | .example-popover .popover {
56 | display: block;
57 | position: relative;
58 | width: 260px;
59 | margin: 1.25rem;
60 | float: left;
61 | }
62 | .bs-tooltip-bottom-demo,
63 | .bs-tooltip-top-demo {
64 | .arrow {
65 | left: 50%;
66 | margin-left: -2px;
67 | }
68 | }
69 | .bs-tooltip-left-demo,
70 | .bs-tooltip-right-demo {
71 | .arrow {
72 | top: 50%;
73 | margin-top: -2px;
74 | }
75 | }
76 | .bs-popover-bottom-demo,
77 | .bs-popover-top-demo {
78 | .arrow {
79 | left: 50%;
80 | margin-left: -11px;
81 | }
82 | }
83 | .bs-popover-left-demo,
84 | .bs-popover-right-demo {
85 | .arrow {
86 | top: 50%;
87 | margin-top: -8px;
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/scss/layout/_offcanvas-menu.scss:
--------------------------------------------------------------------------------
1 | //
2 | // Off-Canvas Menu
3 | // --------------------------------------------------
4 | .offcanvas-wrapper {
5 | position: relative;
6 | min-height: 100vh;
7 | background-color: $body-bg;
8 | z-index: 10;
9 | }
10 | .offcanvas-container {
11 | position: fixed;
12 | top: 0;
13 | left: 0;
14 | width: $offcanvas-width;
15 | height: 100%;
16 | background-color: $gray-darker;
17 | box-shadow: inset -4px 0 17px 0 rgba(0, 0, 0, .35);
18 | //visibility: hidden;
19 | opacity: 0;
20 | z-index: 1;
21 | overflow-y: auto;
22 | transition: opacity 0.4s ease-out;
23 | &.active {
24 | //visibility: visible;
25 | opacity: 1;
26 | }
27 | }
28 | .offcanvas-header {
29 | padding: 28px 20px;
30 | border-bottom: 1px solid $border-light-color;
31 | .offcanvas-title {
32 | margin-bottom: 0;
33 | color: rgba($white-color, .5);
34 | font: {
35 | size: $font-size-base;
36 | weight: $font-weight-headings;
37 | }
38 | }
39 | }
40 | .account-link {
41 | display: table;
42 | width: 100%;
43 | padding: 20px 18px;
44 | transition: background-color .3s;
45 | border-bottom: 1px solid $border-light-color;
46 | background-color: darken($gray-darker, 3%);
47 | text-decoration: none;
48 | .user-ava,
49 | .user-info {
50 | display: table-cell;
51 | vertical-align: middle;
52 | }
53 | .user-ava {
54 | width: 48px;
55 | > img {
56 | display: block;
57 | width: 48px;
58 | padding: 3px;
59 | border: 1px solid $border-light-color;
60 | border-radius: 50%;
61 | }
62 | }
63 | .user-info {
64 | padding-left: 8px;
65 | > .user-name {
66 | margin-bottom: 2px;
67 | color: $white-color;
68 | }
69 | > span { display: block; }
70 | }
71 | &:hover { background-color: lighten($gray-darker, 1%); }
72 | }
73 | .offcanvas-menu {
74 | @extend %offcanvas-menu;
75 | }
76 |
77 | // Site Backdrop
78 | .site-backdrop {
79 | @include overlay-block(9980, $white-color, 0);
80 | position: fixed;
81 | transition: opacity .35s, visibility .35s;
82 | cursor: pointer;
83 | visibility: hidden;
84 | }
85 |
86 | // Off-Canvas open
87 | .no-csstransforms3d {
88 | .offcanvas-wrapper,
89 | .navbar, .topbar {
90 | transition: left .4s ease-in-out, background-color .2s;
91 | }
92 | .site-backdrop {
93 | transition: left .4s ease-in-out, opacity .35s, visibility .35s;
94 | }
95 | .offcanvas-open {
96 | .offcanvas-wrapper,
97 | .site-backdrop,
98 | .navbar, .topbar { left: 290px; }
99 | .site-backdrop {
100 | opacity: .2;
101 | visibility: visible;
102 | }
103 | }
104 | }
105 | .csstransforms3d {
106 | .offcanvas-wrapper,
107 | .navbar, .topbar {
108 | transition: transform .4s ease-in-out, background-color .2s;
109 | }
110 | .site-backdrop {
111 | transition: transform .4s ease-in-out, opacity .35s, visibility .35s;
112 | }
113 | .offcanvas-open {
114 | .offcanvas-wrapper,
115 | .site-backdrop,
116 | .navbar, .topbar {
117 | transform: translate3d(290px, 0, 0);
118 | }
119 | .site-backdrop {
120 | opacity: .2;
121 | visibility: visible;
122 | }
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/src/scss/layout/_section.scss:
--------------------------------------------------------------------------------
1 | //
2 | // Full Width Section / Full Height Section
3 | // --------------------------------------------------
4 |
5 | .fw-section,
6 | .fh-section {
7 | position: relative;
8 | background: {
9 | position: 50% 50%;
10 | repeat: no-repeat;
11 | size: cover;
12 | }
13 |
14 | // Overlay
15 | > .overlay { @include overlay-block(1, $black-color, .6); }
16 |
17 | // Content
18 | > .container,
19 | > .container-fluid,
20 | > div {
21 | position: relative;
22 | z-index: 5;
23 | }
24 |
25 | // Fixed Background
26 | &.bg-fixed { background-attachment: fixed; }
27 |
28 | // No cover background
29 | &.no-cover-bg { background-size: auto; }
30 | }
31 |
32 | .fw-section { width: 100%; }
33 |
34 | .fh-section { height: 100vh; }
--------------------------------------------------------------------------------
/src/scss/styles.scss:
--------------------------------------------------------------------------------
1 | //Import Google Fonts (Maven Pro)
2 | @import url('https://fonts.googleapis.com/css?family=Maven+Pro:400,500,700,900');
3 |
4 |
5 | // Import Bootstrap Modules
6 | @import "~bootstrap/scss/functions";
7 | @import "~bootstrap/scss/variables";
8 | @import "~bootstrap/scss/mixins";
9 | @import "~bootstrap/scss/root";
10 | @import "~bootstrap/scss/reboot";
11 | @import "~bootstrap/scss/type";
12 | @import "~bootstrap/scss/images";
13 | @import "~bootstrap/scss/code";
14 | @import "~bootstrap/scss/grid";
15 | @import "~bootstrap/scss/tables";
16 | @import "~bootstrap/scss/forms";
17 | @import "~bootstrap/scss/buttons";
18 | @import "~bootstrap/scss/transitions";
19 | @import "~bootstrap/scss/dropdown";
20 | @import "~bootstrap/scss/button-group";
21 | // @import "~bootstrap/scss/input-group";
22 | @import "~bootstrap/scss/custom-forms";
23 | @import "~bootstrap/scss/nav";
24 | @import "~bootstrap/scss/navbar";
25 | @import "~bootstrap/scss/card";
26 | @import "~bootstrap/scss/breadcrumb";
27 | @import "~bootstrap/scss/pagination";
28 | @import "~bootstrap/scss/badge";
29 | @import "~bootstrap/scss/jumbotron";
30 | @import "~bootstrap/scss/alert";
31 | @import "~bootstrap/scss/progress";
32 | @import "~bootstrap/scss/media";
33 | @import "~bootstrap/scss/list-group";
34 | @import "~bootstrap/scss/close";
35 | @import "~bootstrap/scss/modal";
36 | @import "~bootstrap/scss/tooltip";
37 | @import "~bootstrap/scss/popover";
38 | @import "~bootstrap/scss/carousel";
39 | @import "~bootstrap/scss/utilities";
40 | @import "~bootstrap/scss/print";
41 |
42 |
43 | // Helpers: Variables, Mixins and Placeholders
44 | @import 'helpers/variables';
45 | @import 'helpers/mixins';
46 | @import 'helpers/placeholders';
47 |
48 |
49 | // Import third party lib styles
50 | @import "~ngx-toastr/toastr-bs4-alert";
51 |
52 |
53 | // Base
54 | @import 'base/scaffolding';
55 | @import 'base/utilities';
56 |
57 |
58 | // Components
59 | @import 'components/icons';
60 | @import 'components/typography';
61 | @import 'components/forms';
62 | @import 'components/tables';
63 | @import 'components/buttons';
64 | @import 'components/social-buttons';
65 | @import 'components/navs';
66 | @import 'components/card';
67 | @import 'components/accordion';
68 | @import 'components/pagination';
69 | @import 'components/comments';
70 | @import 'components/tooltips';
71 | @import 'components/dropdown';
72 | @import 'components/list-group';
73 | @import 'components/alert';
74 | @import 'components/modal';
75 | @import 'components/progress';
76 | @import 'components/gallery';
77 | @import 'components/countdown';
78 | @import 'components/widgets';
79 | @import 'components/steps';
80 | @import 'components/banners';
81 |
82 |
83 | // Layout
84 | @import 'layout/section';
85 | @import 'layout/header';
86 | @import 'layout/offcanvas-menu';
87 |
--------------------------------------------------------------------------------
/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/long-stack-trace-zone';
4 | import 'zone.js/dist/proxy.js';
5 | import 'zone.js/dist/sync-test';
6 | import 'zone.js/dist/jasmine-patch';
7 | import 'zone.js/dist/async-test';
8 | import 'zone.js/dist/fake-async-test';
9 | import { getTestBed } from '@angular/core/testing';
10 | import {
11 | BrowserDynamicTestingModule,
12 | platformBrowserDynamicTesting
13 | } from '@angular/platform-browser-dynamic/testing';
14 |
15 | // Unfortunately there's no typing for the `__karma__` variable. Just declare it as any.
16 | declare const __karma__: any;
17 | declare const require: any;
18 |
19 | // Prevent Karma from running prematurely.
20 | __karma__.loaded = function () {};
21 |
22 | // First, initialize the Angular testing environment.
23 | getTestBed().initTestEnvironment(
24 | BrowserDynamicTestingModule,
25 | platformBrowserDynamicTesting(), {
26 | teardown: { destroyAfterEach: false }
27 | }
28 | );
29 | // Then we find all the tests.
30 | const context = require.context('./', true, /\.spec\.ts$/);
31 | // And load the modules.
32 | context.keys().map(context);
33 | // Finally, start Karma to run the tests.
34 | __karma__.start();
35 |
--------------------------------------------------------------------------------
/src/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../out-tsc/app",
5 | "baseUrl": "./",
6 | "types": []
7 | },
8 | "files": [
9 | "main.ts",
10 | "polyfills.ts"
11 | ],
12 | "include": [
13 | "src/**/*.d.ts"
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/src/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../out-tsc/spec",
5 | "baseUrl": "./",
6 | "target": "es2020",
7 | "types": [
8 | "jasmine",
9 | "node"
10 | ]
11 | },
12 | "files": [
13 | "test.ts",
14 | "polyfills.ts"
15 | ],
16 | "include": [
17 | "**/*.spec.ts",
18 | "**/*.d.ts"
19 | ]
20 | }
21 |
--------------------------------------------------------------------------------
/src/typings.d.ts:
--------------------------------------------------------------------------------
1 | /* SystemJS module definition */
2 | declare var module: NodeModule;
3 | interface NodeModule {
4 | id: string;
5 | }
6 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "baseUrl": "src",
5 | "module": "es2020",
6 | "outDir": "./dist/out-tsc",
7 | "sourceMap": true,
8 | "declaration": false,
9 | "moduleResolution": "node",
10 | "experimentalDecorators": true,
11 | "target": "es2020",
12 | "typeRoots": [
13 | "node_modules/@types"
14 | ],
15 | "lib": [
16 | "es2017",
17 | "dom"
18 | ]
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "rulesDirectory": [
3 | "node_modules/codelyzer"
4 | ],
5 | "rules": {
6 | "arrow-return-shorthand": true,
7 | "callable-types": true,
8 | "class-name": true,
9 | "comment-format": [
10 | true,
11 | "check-space"
12 | ],
13 | "curly": true,
14 | "eofline": true,
15 | "forin": true,
16 | "deprecation": {
17 | "severity": "warning"
18 | },
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": true,
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-shadowed-variable": true,
69 | "no-string-literal": false,
70 | "no-string-throw": true,
71 | "no-switch-case-fall-through": true,
72 | "no-trailing-whitespace": true,
73 | "no-unnecessary-initializer": true,
74 | "no-unused-expression": true,
75 | "no-var-keyword": true,
76 | "object-literal-sort-keys": false,
77 | "one-line": [
78 | true,
79 | "check-open-brace",
80 | "check-catch",
81 | "check-else",
82 | "check-whitespace"
83 | ],
84 | "prefer-const": true,
85 | "quotemark": [
86 | true,
87 | "single"
88 | ],
89 | "radix": true,
90 | "semicolon": [
91 | true,
92 | "always"
93 | ],
94 | "triple-equals": [
95 | true,
96 | "allow-null-check"
97 | ],
98 | "typedef-whitespace": [
99 | true,
100 | {
101 | "call-signature": "nospace",
102 | "index-signature": "nospace",
103 | "parameter": "nospace",
104 | "property-declaration": "nospace",
105 | "variable-declaration": "nospace"
106 | }
107 | ],
108 | "typeof-compare": true,
109 | "unified-signatures": true,
110 | "variable-name": false,
111 | "whitespace": [
112 | true,
113 | "check-branch",
114 | "check-decl",
115 | "check-operator",
116 | "check-separator",
117 | "check-type"
118 | ],
119 | "directive-selector": [
120 | true,
121 | "attribute",
122 | "app",
123 | "camelCase"
124 | ],
125 | "component-selector": [
126 | true,
127 | "element",
128 | "app",
129 | "kebab-case"
130 | ],
131 | "use-input-property-decorator": true,
132 | "use-output-property-decorator": true,
133 | "use-host-property-decorator": true,
134 | "no-input-rename": true,
135 | "no-output-rename": true,
136 | "use-life-cycle-interface": true,
137 | "use-pipe-transform-interface": true,
138 | "component-class-suffix": true,
139 | "directive-class-suffix": true,
140 | "invoke-injectable": true
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/ux-testing/erika.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monobasic/Angular-Reactive-Demo-Shop/d25467526b2b627132a750775e7e28145a406aad/ux-testing/erika.jpg
--------------------------------------------------------------------------------
/ux-testing/task1_step1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monobasic/Angular-Reactive-Demo-Shop/d25467526b2b627132a750775e7e28145a406aad/ux-testing/task1_step1.png
--------------------------------------------------------------------------------
/ux-testing/task1_step2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monobasic/Angular-Reactive-Demo-Shop/d25467526b2b627132a750775e7e28145a406aad/ux-testing/task1_step2.png
--------------------------------------------------------------------------------
/ux-testing/task2_step1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monobasic/Angular-Reactive-Demo-Shop/d25467526b2b627132a750775e7e28145a406aad/ux-testing/task2_step1.png
--------------------------------------------------------------------------------
/ux-testing/uxTest.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monobasic/Angular-Reactive-Demo-Shop/d25467526b2b627132a750775e7e28145a406aad/ux-testing/uxTest.jpg
--------------------------------------------------------------------------------