├── .dockerignore ├── .editorconfig ├── .gitignore ├── .prettierrc.json ├── .stylelintrc.json ├── .vscode └── settings.json ├── Dockerfile ├── README.md ├── angular.json ├── browserslist ├── config ├── aliases │ └── hiredis.js ├── dev.setThisFromExternalApiKeys.js ├── keys.js └── prod.js ├── docker-compose.yml ├── middlewares ├── requireAdmin.js └── requireLogin.js ├── models ├── Cart.js ├── Order.js ├── Product.js ├── Translation.js └── User.js ├── nginx-conf └── nginx.conf ├── ngsw-config.json ├── package-lock.json ├── package.json ├── routes ├── adminRoutes.ts ├── authRoutes.ts ├── billingRoutes.ts ├── cartRoutes.ts ├── index.ts └── productRoutes.ts ├── server.ts ├── services ├── cache.js ├── emailTemplates.js ├── mailer.js ├── paginate.js └── passport.js ├── src ├── app │ ├── app.browser.module.ts │ ├── app.component.html │ ├── app.component.scss │ ├── app.component.ts │ ├── app.module.ts │ ├── app.routes.ts │ ├── app.server.module.ts │ ├── cart │ │ ├── cart.module.ts │ │ └── cart │ │ │ ├── cart.component.html │ │ │ ├── cart.component.scss │ │ │ └── cart.component.ts │ ├── dashboard │ │ ├── dashboard.module.ts │ │ ├── dashboard │ │ │ ├── dashboard.component.html │ │ │ ├── dashboard.component.scss │ │ │ └── dashboard.component.ts │ │ ├── orders-edit │ │ │ ├── order-edit │ │ │ │ ├── order-edit.component.html │ │ │ │ ├── order-edit.component.scss │ │ │ │ └── order-edit.component.ts │ │ │ ├── orders-edit.component.html │ │ │ ├── orders-edit.component.scss │ │ │ └── orders-edit.component.ts │ │ ├── products-edit │ │ │ ├── products-edit.component.html │ │ │ ├── products-edit.component.scss │ │ │ └── products-edit.component.ts │ │ ├── tiny-editor.ts │ │ │ ├── tiny-editor.component.html │ │ │ ├── tiny-editor.component.scss │ │ │ └── tiny-editor.component.ts │ │ └── translations-edit │ │ │ ├── translations-edit.component.html │ │ │ ├── translations-edit.component.scss │ │ │ └── translations-edit.component.ts │ ├── eshop │ │ ├── about │ │ │ ├── about.component.html │ │ │ ├── about.component.scss │ │ │ └── about.component.ts │ │ ├── contact │ │ │ ├── contact.component.html │ │ │ ├── contact.component.scss │ │ │ └── contact.component.ts │ │ ├── eshop.module.ts │ │ ├── gdpr │ │ │ ├── gdpr.component.html │ │ │ ├── gdpr.component.scss │ │ │ └── gdpr.component.ts │ │ └── vop │ │ │ ├── vop.component.html │ │ │ ├── vop.component.scss │ │ │ └── vop.component.ts │ ├── footer │ │ ├── footer.component.html │ │ ├── footer.component.scss │ │ └── footer.component.ts │ ├── header │ │ ├── header.component.html │ │ ├── header.component.scss │ │ └── header.component.ts │ ├── order │ │ ├── order.component.html │ │ ├── order.component.scss │ │ └── order.component.ts │ ├── orders │ │ ├── orders.component.html │ │ ├── orders.component.scss │ │ └── orders.component.ts │ ├── pipes │ │ ├── pipe.module.ts │ │ ├── price.pipe.ts │ │ └── translate.pipe.ts │ ├── product │ │ ├── product.module.ts │ │ └── product │ │ │ ├── product.component.html │ │ │ ├── product.component.scss │ │ │ └── product.component.ts │ ├── products │ │ ├── products.component.html │ │ ├── products.component.scss │ │ └── products.component.ts │ ├── services │ │ ├── api.service.ts │ │ ├── auth-admin.guard.ts │ │ ├── auth.guard.ts │ │ ├── auth.service.ts │ │ ├── browser-http-interceptor.ts │ │ ├── server-http-interceptor.ts │ │ ├── translate.service.ts │ │ └── window.service.ts │ ├── shared │ │ ├── card │ │ │ ├── card.component.html │ │ │ ├── card.component.scss │ │ │ └── card.component.ts │ │ ├── cart-show │ │ │ ├── cart-show.component.html │ │ │ ├── cart-show.component.scss │ │ │ └── cart-show.component.ts │ │ ├── pagination │ │ │ ├── pagination.component.html │ │ │ ├── pagination.component.scss │ │ │ └── pagination.component.ts │ │ ├── products-list │ │ │ ├── products-list.component.html │ │ │ ├── products-list.component.scss │ │ │ └── products-list.component.ts │ │ ├── shared.module.ts │ │ └── sidebar │ │ │ ├── sidebar.component.html │ │ │ ├── sidebar.component.scss │ │ │ └── sidebar.component.ts │ ├── store │ │ ├── actions.ts │ │ ├── effects.ts │ │ └── reducers │ │ │ ├── auth.ts │ │ │ ├── dashboard.ts │ │ │ ├── index.ts │ │ │ └── product.ts │ └── utils │ │ └── lazyLoadImg │ │ ├── lazy-src.directive.ts │ │ ├── lazy-viewport.directive.ts │ │ ├── lazy-viewport.ts │ │ └── lazy.module.ts ├── assets │ ├── .gitkeep │ ├── fonts │ │ └── material-icons │ │ │ ├── MaterialIcons-Regular.eot │ │ │ ├── MaterialIcons-Regular.ijmap │ │ │ ├── MaterialIcons-Regular.svg │ │ │ ├── MaterialIcons-Regular.ttf │ │ │ ├── MaterialIcons-Regular.woff │ │ │ ├── MaterialIcons-Regular.woff2 │ │ │ └── material-icons.css │ ├── icon-128x128.png │ ├── icon-152x152.png │ ├── icon-256x256.png │ └── icon-512x512.png ├── config │ └── keys.ts ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── main.server.ts ├── main.ts ├── manifest.json ├── polyfills.ts ├── styles │ ├── _variables.scss │ ├── index.scss │ └── main.scss ├── tsconfig.app.json ├── tsconfig.server.json └── typings.d.ts ├── tsconfig.json └── tslint.json /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_size = 2 7 | indent_style = space 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 | 8 | # dependencies 9 | /node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | 20 | # IDE - VSCode 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/tasks.json 24 | !.vscode/launch.json 25 | !.vscode/extensions.json 26 | 27 | # misc 28 | /.sass-cache 29 | /connect.lock 30 | /coverage 31 | /libpeerconnection.log 32 | npm-debug.log 33 | testem.log 34 | /typings 35 | /config/dev.js 36 | 37 | # e2e 38 | /e2e/*.js 39 | /e2e/*.map 40 | 41 | # System Files 42 | .DS_Store 43 | Thumbs.db 44 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 999, 3 | "semi": true 4 | } 5 | -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "stylelint-config-recommended", 4 | "stylelint-config-standard", 5 | "stylelint-config-recommended-scss" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[scss]": { 3 | "editor.formatOnSave": true, 4 | "editor.formatOnPaste": true 5 | }, 6 | "css.validate": false, 7 | "editor.formatOnPaste": false, 8 | "editor.formatOnSave": false, 9 | "editor.formatOnType": false, 10 | "editor.tabSize": 2, 11 | "extensions.ignoreRecommendations": false, 12 | "files.encoding": "utf8", 13 | "files.eol": "\r\n", 14 | "html.format.enable": false, 15 | "json.format.enable": false, 16 | "typescript.format.enable": false, 17 | "less.validate": false, 18 | "prettier.stylelintIntegration": true, 19 | "scss.validate": false, 20 | "stylelint.additionalDocumentSelectors": [], 21 | "stylelint.config": null, 22 | "stylelint.configOverrides": null, 23 | "stylelint.enable": true, 24 | "telemetry.enableCrashReporter": false, 25 | "telemetry.enableTelemetry": false, 26 | "tslint.enable": true, 27 | "tslint.run": "onType", 28 | "typescript.tsdk": "node_modules/typescript/lib", 29 | "prettier.jsxSingleQuote": true, 30 | "prettier.singleQuote": true, 31 | "prettier.tslintIntegration": true 32 | } 33 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:11.14.0-alpine as node 2 | 3 | 4 | # Sets the path where the app is going to be installed 5 | ENV NODE_ROOT /usr/app/ 6 | # Creates the directory and all the parents (if they don’t exist) 7 | RUN mkdir -p $NODE_ROOT 8 | # Sets the /usr/app as the active directory 9 | WORKDIR $NODE_ROOT 10 | 11 | COPY ./package.json ./ 12 | 13 | RUN npm install 14 | 15 | COPY . . 16 | 17 | # RUN npm run ssr 18 | 19 | CMD ["npm", "run", "ssr"] 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Angular universal (server-side rendering) with node.js and mongoDB 2 | 3 | Eshop with google login, cart save in session or user, test buy with stripe 4 | 5 | ## PREPARE ENVIROMENT 6 | 7 | Create dev.js in config and add keys as their are prepare in - dev.setThisFromExternalApiKeys
8 |
9 | mongoURI - use link to mongoDB database, e.g. from mlab.com 10 |

11 | googleClientID - create to set login through google - google API 12 |
13 | googleClientSecret - create to set login through google - google API 14 |

15 | stripePublishableKey - set to work with stripe payments 16 |
17 | stripeSecretKey - set to work with stripe payments 18 |
19 | sendGridKey - set to use sendGrid 20 |

21 | cloudinaryName - set to upload images straight from angular to cloudinary 22 |
23 | cloudinaryKey - set to upload images straight from angular to cloudinary 24 |
25 | cloudinarySecret - set to upload images straight from angular to cloudinary 26 |
27 | redisUrl: { host: 'localhost', port: 6379 } 28 | 29 | 30 | ## BUILD AND SERVE 31 | 32 | ## without docker 33 | Serve redis - host:localhost port:6379 34 |
35 | Build and serve 36 |
37 | Run `npm run ssr` 38 | 39 | Serve 40 |
41 | Run `npm run start` 42 | 43 | ## with docker 44 | Run `docker compose up` 45 |
46 | (will start redis and app) 47 |
48 | redisUrl: { host: 'redis-serve', port: 6379 } 49 | 50 | 51 | ## TEST ORDER 52 | 53 | -Use test credit card number for stripe for testing the order - 4242 4242 4242 4242 54 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "eshop": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "architect": { 11 | "build": { 12 | "builder": "@angular-devkit/build-angular:browser", 13 | "options": { 14 | "outputPath": "dist/browser", 15 | "index": "src/index.html", 16 | "main": "src/main.ts", 17 | "tsConfig": "src/tsconfig.app.json", 18 | "polyfills": "src/polyfills.ts", 19 | "aot": true, 20 | "stylePreprocessorOptions": { 21 | "includePaths": [ 22 | "src/styles" 23 | ] 24 | }, 25 | "assets": [ 26 | "src/assets", 27 | "src/favicon.ico", 28 | "src/manifest.json" 29 | ], 30 | "styles": [ 31 | "src/assets/fonts/material-icons/material-icons.css", 32 | "src/styles/index.scss" 33 | ], 34 | "scripts": [] 35 | }, 36 | "configurations": { 37 | "production": { 38 | "budgets": [ 39 | { 40 | "type": "anyComponentStyle", 41 | "maximumWarning": "6kb" 42 | } 43 | ], 44 | "optimization": true, 45 | "outputHashing": "all", 46 | "sourceMap": false, 47 | "extractCss": true, 48 | "namedChunks": false, 49 | "extractLicenses": true, 50 | "vendorChunk": false, 51 | "buildOptimizer": true, 52 | "serviceWorker": true, 53 | "fileReplacements": [ 54 | { 55 | "replace": "src/environments/environment.ts", 56 | "with": "src/environments/environment.prod.ts" 57 | } 58 | ] 59 | } 60 | } 61 | }, 62 | "serve": { 63 | "builder": "@angular-devkit/build-angular:dev-server", 64 | "options": { 65 | "browserTarget": "eshop:build" 66 | }, 67 | "configurations": { 68 | "production": { 69 | "browserTarget": "eshop:build:production" 70 | } 71 | } 72 | }, 73 | "extract-i18n": { 74 | "builder": "@angular-devkit/build-angular:extract-i18n", 75 | "options": { 76 | "browserTarget": "eshop:build" 77 | } 78 | }, 79 | "lint": { 80 | "builder": "@angular-devkit/build-angular:tslint", 81 | "options": { 82 | "tsConfig": [ 83 | "src/tsconfig.app.json" 84 | ], 85 | "exclude": [] 86 | } 87 | }, 88 | "server": { 89 | "builder": "@angular-devkit/build-angular:server", 90 | "options": { 91 | "outputPath": "dist/server", 92 | "main": "server.ts", 93 | "tsConfig": "src/tsconfig.server.json" 94 | } 95 | }, 96 | "serve-ssr": { 97 | "builder": "@nguniversal/builders:ssr-dev-server", 98 | "options": { 99 | "browserTarget": "eshop:build", 100 | "serverTarget": "eshop:server" 101 | }, 102 | "configurations": { 103 | "production": { 104 | "browserTarget": "eshop:build:production", 105 | "serverTarget": "eshop:server:production" 106 | } 107 | } 108 | }, 109 | "prerender": { 110 | "builder": "@nguniversal/builders:prerender", 111 | "options": { 112 | "browserTarget": "eshop:build:production", 113 | "serverTarget": "eshop:server:production", 114 | "routes": [ 115 | "/" 116 | ] 117 | }, 118 | "configurations": { 119 | "production": {} 120 | } 121 | } 122 | } 123 | }, 124 | "eshop-e2e": { 125 | "root": "", 126 | "sourceRoot": "", 127 | "projectType": "application" 128 | } 129 | }, 130 | "defaultProject": "eshop", 131 | "schematics": { 132 | "@schematics/angular:component": { 133 | "prefix": "app", 134 | "style": "scss" 135 | }, 136 | "@schematics/angular:directive": { 137 | "prefix": "app" 138 | } 139 | }, 140 | "cli": { 141 | "analytics": false 142 | } 143 | } -------------------------------------------------------------------------------- /browserslist: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # You can see what browsers were selected by your queries by running: 6 | # npx browserslist 7 | 8 | > 0.5% 9 | last 2 versions 10 | Firefox ESR 11 | not dead 12 | not IE 9-11 # For IE 9-11 support, remove 'not'. -------------------------------------------------------------------------------- /config/aliases/hiredis.js: -------------------------------------------------------------------------------- 1 | export default null; 2 | -------------------------------------------------------------------------------- /config/dev.setThisFromExternalApiKeys.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | googleClientID: '0', 3 | googleClientSecret: '0', 4 | mongoURI: '', 5 | cookieKey: '12345', 6 | stripePublishableKey: '', 7 | stripeSecretKey: '', 8 | sendGridKey: '', 9 | cloudinaryName: '', 10 | cloudinaryKey: '', 11 | cloudinarySecret: '', 12 | redisUrl: '', 13 | adminEmails: [] 14 | } 15 | -------------------------------------------------------------------------------- /config/keys.js: -------------------------------------------------------------------------------- 1 | 2 | if(process.env.NODE_ENV === 'production') { 3 | module.exports = require('./prod'); 4 | } else { 5 | // dev.setThisFromExternalApiKeys - not in git ignore 6 | module.exports = require('./dev'); 7 | 8 | // remove comment line bellow and set keys from external API to dev.js --it will be in git ignore 9 | // module.exports = require('./dev'); 10 | } 11 | -------------------------------------------------------------------------------- /config/prod.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | googleClientID: process.env.GOOGLE_CLIENT_ID, 3 | googleClientSecret: process.env.GOOGLE_CLIENT_SECRET, 4 | mongoURI: process.env.MONGO_URI, 5 | cookieKey: process.env.COOKIE_KEY, 6 | stripePublishableKey: process.env.STRIPE_PUBLISHABLE_KEY, 7 | stripeSecretKey: process.env.STRIPE_SECRET_KEY, 8 | sendGridKey: process.env.SEND_GRID_KEY, 9 | cloudinaryName: process.env.CLOUDINARY_NAME, 10 | cloudinaryKey: process.env.CLOUDINARY_KEY, 11 | cloudinarySecret: process.env.CLOUDINARY_SECRET, 12 | redisUrl: process.env.REDIS_URL, 13 | adminEmails: process.env.ADMIN_EMAILS, 14 | baseUrl: process.env.baseUrl 15 | } 16 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | redis-server: 4 | image: "redis" 5 | ports: 6 | - '6379:6379' 7 | app: 8 | restart: always 9 | build: . 10 | ports: 11 | - '5000:5000' 12 | -------------------------------------------------------------------------------- /middlewares/requireAdmin.js: -------------------------------------------------------------------------------- 1 | module.exports = (req, res, next) => { 2 | if(!req.user.admin) { 3 | return res.status(401).send({error: 'No permision!'}); 4 | } 5 | 6 | next(); 7 | }; 8 | 9 | -------------------------------------------------------------------------------- /middlewares/requireLogin.js: -------------------------------------------------------------------------------- 1 | module.exports = (req, res, next) => { 2 | if(!req.user) { 3 | return res.status(401).send({error: 'You must log in!'}); 4 | } 5 | next(); 6 | }; 7 | -------------------------------------------------------------------------------- /models/Cart.js: -------------------------------------------------------------------------------- 1 | module.exports = function Cart(oldCart) { 2 | this.items = oldCart.items || []; 3 | this.totalQty = oldCart.totalQty || 0; 4 | this.totalPrice = oldCart.totalPrice || 0; 5 | 6 | this.add = function (item, id) { 7 | if (item['$init']) { 8 | delete item['$init']; 9 | } 10 | const itemExist = this.items.filter(cartItem => cartItem.id == id).length ? true : false; 11 | 12 | if(!itemExist) { 13 | this.items.push({item, id, price : item.salePrice, qty: 1}); 14 | this.totalQty++; 15 | this.totalPrice += item.salePrice; 16 | } else { 17 | this.items.forEach(cartItem => { 18 | if(cartItem.id == id) { 19 | cartItem.qty++; 20 | cartItem.price = item.salePrice + cartItem.price; 21 | this.totalQty++; 22 | this.totalPrice += item.salePrice; 23 | } 24 | }) 25 | } 26 | }; 27 | 28 | this.remove = function(item, id) { 29 | if (item['$init']) { 30 | delete item['$init']; 31 | } 32 | this.items = this.items.map(cartItem => { 33 | 34 | if(cartItem.id == id && cartItem.qty > 1) { 35 | cartItem.qty--; 36 | cartItem.price = cartItem.price - item.salePrice; 37 | this.totalQty--; 38 | this.totalPrice -= item.salePrice; 39 | } else if(cartItem.id == id && cartItem.qty == 1) { 40 | cartItem = {}; 41 | this.totalQty--; 42 | this.totalPrice -= item.salePrice; 43 | } 44 | return cartItem; 45 | }).filter(cartItem => cartItem.id); 46 | 47 | 48 | }; 49 | 50 | 51 | } 52 | -------------------------------------------------------------------------------- /models/Order.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const { Schema } = mongoose; 3 | 4 | const orderSchema = new Schema({ 5 | orderId : String, 6 | amount: Number, 7 | amount_refunded: Number, 8 | description: String, 9 | customerEmail: String, 10 | status: String, 11 | cart: {}, 12 | outcome: {}, 13 | source: {}, 14 | _user: {type: Schema.Types.ObjectId, ref: 'user'}, 15 | dateAdded: { type: Date, default: Date.now } 16 | }); 17 | 18 | const Order = mongoose.model('orders', orderSchema); 19 | module.exports = Order; 20 | -------------------------------------------------------------------------------- /models/Product.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const { Schema } = mongoose; 3 | const paginate = require('../services/paginate'); 4 | 5 | 6 | const productSchema = new Schema({ 7 | titleUrl:String, 8 | mainImage: { 9 | url: { type: String, trim: true}, 10 | name: { type: String, trim: true} 11 | }, 12 | onSale: Boolean, 13 | stock: String, 14 | visibility: String, 15 | shipping: String, 16 | images: [], 17 | _user: {type: Schema.Types.ObjectId, ref: 'user'}, 18 | dateAdded: Date, 19 | 20 | title: String, 21 | description: String, 22 | descriptionFull: [], 23 | categories: [], 24 | tags: [], 25 | regularPrice: Number, 26 | salePrice: Number, 27 | 28 | sk : { 29 | title: String, 30 | description: String, 31 | descriptionFull: [], 32 | categories: [], 33 | tags: [], 34 | regularPrice: Number, 35 | salePrice: Number 36 | }, 37 | cs: { 38 | title: String, 39 | description: String, 40 | descriptionFull: [], 41 | categories: [], 42 | tags: [], 43 | regularPrice: Number, 44 | salePrice: Number 45 | }, 46 | en : { 47 | title: String, 48 | description: String, 49 | descriptionFull: [], 50 | categories: [], 51 | tags: [], 52 | regularPrice: Number, 53 | salePrice: Number 54 | }, 55 | }); 56 | 57 | productSchema.plugin(paginate); 58 | 59 | const Product = mongoose.model('products', productSchema); 60 | module.exports = Product; 61 | -------------------------------------------------------------------------------- /models/Translation.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const { Schema } = mongoose; 3 | 4 | const translationSchema = new Schema({ 5 | lang: String, 6 | keys : {type: Schema.Types.Mixed, default: { } } 7 | }); 8 | 9 | const Translation = mongoose.model('translations', translationSchema); 10 | module.exports = Translation; 11 | -------------------------------------------------------------------------------- /models/User.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const { Schema } = mongoose; 3 | 4 | const userSchema = new Schema({ 5 | googleId: String, 6 | email: String, 7 | name: String, 8 | cart: { type: Schema.Types.Mixed, default: {items: [], totalQty: 0, totalPrice: 0} }, 9 | images: [], 10 | admin: Boolean 11 | }); 12 | 13 | const User = mongoose.model('users', userSchema); 14 | module.exports = User; 15 | -------------------------------------------------------------------------------- /nginx-conf/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | listen [::]:80; 4 | 5 | root /var/www/html; 6 | index index.html index.htm index.nginx-debian.html; 7 | 8 | server_name localhost; 9 | 10 | location / { 11 | proxy_pass http://nodejs:5000; 12 | } 13 | 14 | location ~ /.well-known/acme-challenge { 15 | allow all; 16 | root /var/www/html; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /ngsw-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "index": "/index.html", 3 | "dataGroups": 4 | [ 5 | { 6 | "name": "auth", 7 | "urls": ["/auth"], 8 | "cacheConfig": { 9 | "maxSize": 0, 10 | "maxAge": "0u", 11 | "strategy": "freshness" 12 | } 13 | }, 14 | { 15 | "name": "api", 16 | "urls": ["/api"], 17 | "cacheConfig": { 18 | "maxSize": 0, 19 | "maxAge": "0u", 20 | "strategy": "freshness" 21 | } 22 | }, 23 | { 24 | "name": "cartApi", 25 | "urls": ["/cartApi"], 26 | "cacheConfig": { 27 | "maxSize": 0, 28 | "maxAge": "0u", 29 | "strategy": "freshness" 30 | } 31 | } 32 | ], 33 | "assetGroups": [{ 34 | "name": "app", 35 | "installMode": "prefetch", 36 | "resources": { 37 | "files": [ 38 | "/favicon.ico", 39 | "/*.bundle.css", 40 | "/*.bundle.js", 41 | "/*.chunk.js" 42 | ] 43 | } 44 | }, { 45 | "name": "assets", 46 | "installMode": "lazy", 47 | "updateMode": "prefetch", 48 | "resources": { 49 | "files": [ 50 | "/assets/**" 51 | ] 52 | } 53 | }] 54 | } 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bluetooth-eshop", 3 | "version": "0.9.5", 4 | "main": "./dist/server/main.js", 5 | "license": "MIT", 6 | "engines": { 7 | "node": "13.5.0", 8 | "npm": "6.13.7" 9 | }, 10 | "scripts": { 11 | "start": "node dist/server/main.js", 12 | "ssr": "npm run build && npm run start", 13 | "lint": "ng lint", 14 | "heroku-postbuild": "npm run build:client-and-server-bundles && npm run start", 15 | "build:client-and-server-bundles": "ng build --prod --output-hashing none && ng run eshop:server", 16 | "dev:ssr": "ng run eshop:serve-ssr", 17 | "serve:ssr": "node dist/server/main.js", 18 | "build": "ng build --prod && ng run eshop:server", 19 | "prerender": "ng run eshop:prerender" 20 | }, 21 | "private": true, 22 | "dependencies": { 23 | "@angular/animations": "^9.1.0", 24 | "@angular/cli": "^9.1.0", 25 | "@angular/common": "^9.1.0", 26 | "@angular/compiler": "^9.1.0", 27 | "@angular/compiler-cli": "^9.1.0", 28 | "@angular/core": "^9.1.0", 29 | "@angular/forms": "^9.1.0", 30 | "@angular/language-service": "^9.1.0", 31 | "@angular/platform-browser": "^9.1.0", 32 | "@angular/platform-browser-dynamic": "^9.1.0", 33 | "@angular/platform-server": "^9.1.0", 34 | "@angular/router": "^9.1.0", 35 | "@angular/service-worker": "^9.1.0", 36 | "@ngrx/effects": "^9.0.0", 37 | "@ngrx/router-store": "^9.0.0", 38 | "@ngrx/store": "^9.0.0", 39 | "@ngrx/store-devtools": "^9.0.0", 40 | "@nguniversal/common": "^9.1.0", 41 | "@nguniversal/express-engine": "^9.1.0", 42 | "@tinymce/tinymce-angular": "^3.5.0", 43 | "body-parser": "^1.19.0", 44 | "cloudinary": "^1.21.0", 45 | "connect-mongo": "^2.0.3", 46 | "cookie-session": "^2.0.0-beta.3", 47 | "core-js": "^3.6.4", 48 | "express": "^4.17.1", 49 | "express-session": "^1.17.0", 50 | "materialize-css": "^1.0.0", 51 | "mongoose": "5.5.11", 52 | "multer": "^1.4.2", 53 | "ng2-file-upload": "^1.4.0", 54 | "ngx-cookie-service": "^3.0.4", 55 | "passport": "^0.4.1", 56 | "passport-google-oauth20": "^2.0.0", 57 | "passport-local": "^1.0.0", 58 | "redis": "^2.8.0", 59 | "reselect": "^4.0.0", 60 | "rxjs": "^6.5.5", 61 | "sendgrid": "^5.2.3", 62 | "stripe": "^6.22.0", 63 | "tslib": "^1.10.0", 64 | "zone.js": "~0.10.2" 65 | }, 66 | "devDependencies": { 67 | "@angular-devkit/build-angular": "~0.901.0", 68 | "@nguniversal/builders": "^9.1.0", 69 | "@types/express": "^4.17.0", 70 | "@types/node": "^12.11.1", 71 | "codelyzer": "^5.2.1", 72 | "compression": "^1.7.4", 73 | "node-sass": "^4.13.0", 74 | "stylelint": "^11.1.1", 75 | "stylelint-config-recommended": "^3.0.0", 76 | "stylelint-config-recommended-scss": "^4.0.0", 77 | "stylelint-config-standard": "^19.0.0", 78 | "stylelint-scss": "^3.12.1", 79 | "ts-loader": "^6.2.1", 80 | "ts-node": "^8.6.2", 81 | "tslint": "^5.20.1", 82 | "typescript": "3.7.5", 83 | "webpack-cli": "^3.3.11" 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /routes/authRoutes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import * as passport from 'passport'; 3 | 4 | const authRoutes = Router(); 5 | 6 | authRoutes.get('/google', passport.authenticate('google', { scope: ['profile', 'email'] })); 7 | authRoutes.get('/google/callback', passport.authenticate('google'), (req, res) => res.redirect('/')); 8 | authRoutes.get('/logout', (req: any, res) => { 9 | req.logout(); 10 | res.redirect('/'); 11 | }); 12 | authRoutes.get('/current_user', (req: any, res) => (req.user ? res.send(req.user) : res.send({}))); 13 | 14 | export { authRoutes }; 15 | -------------------------------------------------------------------------------- /routes/billingRoutes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | 3 | const keys = require('../config/keys'); 4 | const stripe = require('stripe')(keys.stripeSecretKey); 5 | const Order = require('../models/Order'); 6 | const Cart = require('../models/Cart'); 7 | const Mailer = require('../services/mailer'); 8 | 9 | const billingRoutes = Router(); 10 | 11 | billingRoutes.post('/order/add', (req:any, res, next) => { 12 | const orderId = 'order' + new Date().getTime() + 't' + Math.floor(Math.random() * 1000 + 1); 13 | const date = Date.now(); 14 | 15 | const newOrder = { 16 | orderId, 17 | amount: req.body.amount * 100, 18 | _user: req.user ? req.user.id : '123456789012', 19 | dateAdded: date, 20 | cart: req.session.cart, 21 | status: 'NEW', 22 | description: 'PAY_ON_DELIVERY', 23 | customerEmail: req.body.email, 24 | outcome: { 25 | seller_message: 'Payment on delivery' 26 | }, 27 | source: { 28 | address_city: req.body.city, 29 | address_country: req.body.country, 30 | address_line1: req.body.adress, 31 | address_zip: req.body.zip, 32 | name: req.body.name, 33 | object: 'deliver' 34 | } 35 | }; 36 | 37 | const adress = { 38 | city: req.body.city, 39 | country: req.body.country, 40 | adress: req.body.adress, 41 | zip: req.body.zip 42 | }; 43 | 44 | const emailType = { 45 | subject: 'Order', 46 | cart: req.session.cart, 47 | currency: req.body.currency, 48 | orderId: orderId, 49 | adress, 50 | notes: req.body.notes, 51 | date: new Date() 52 | }; 53 | 54 | const mailer = new Mailer(req.body.email, emailType); 55 | 56 | mailer.send(); 57 | 58 | keys.adminEmails 59 | .split(',') 60 | .filter(Boolean) 61 | .forEach(email => { 62 | var mailerToAdmin = new Mailer(email, emailType); 63 | mailerToAdmin.send(); 64 | }); 65 | 66 | const cart = new Cart({}); 67 | req.session.cart = cart; 68 | if (req.user) { 69 | req.user.cart = cart; 70 | req.user.save(); 71 | } 72 | 73 | const order = new Order(newOrder); 74 | order.save(); 75 | 76 | res.send({ order, cart }); 77 | }); 78 | 79 | billingRoutes.post('/stripe', (req:any, res, next) => { 80 | const charge = stripe.charges 81 | .create({ 82 | amount: req.body.amount * 100, 83 | currency: 'eur', 84 | description: 'Credit Card Payment', 85 | source: req.body.token.id 86 | }) 87 | .then( 88 | result => { 89 | const orderId = 'order' + new Date().getTime() + 't' + Math.floor(Math.random() * 1000 + 1); 90 | const newOrder = Object.assign(result, { 91 | orderId, 92 | customerEmail: req.body.token.email, 93 | status: 'NEW', 94 | cart: req.session.cart, 95 | _user: req.user ? req.user.id : '123456789012', 96 | dateAdded: Date.now() 97 | }); 98 | const order = new Order(newOrder); 99 | order.save(); 100 | 101 | const adress = { 102 | city: req.body.token.card.address_city, 103 | country: req.body.token.card.address_country, 104 | adress: req.body.token.card.address_line1, 105 | zip: req.body.token.card.address_zip 106 | }; 107 | 108 | const emailType = { 109 | subject: 'Order', 110 | cart: req.session.cart, 111 | currency: req.body.currency, 112 | orderId: orderId, 113 | adress, 114 | notes: req.body.notes, 115 | date: new Date() 116 | }; 117 | 118 | const mailer = new Mailer(req.body.token.email, emailType); 119 | mailer.send(); 120 | 121 | keys.adminEmails 122 | .split(',') 123 | .filter(Boolean) 124 | .forEach(email => { 125 | var mailerToAdmin = new Mailer(email, emailType); 126 | mailerToAdmin.send(); 127 | }); 128 | 129 | const cart = new Cart({}); 130 | req.session.cart = cart; 131 | if (req.user) { 132 | req.user.cart = cart; 133 | req.user.save(); 134 | } 135 | 136 | res.send({ order, cart }); 137 | }, 138 | err => { 139 | switch (err.type) { 140 | case 'StripeCardError': 141 | // A declined card error 142 | res.send(err); 143 | break; 144 | case 'RateLimitError': 145 | // Too many requests made to the API too quickly 146 | res.send(err); 147 | break; 148 | case 'StripeInvalidRequestError': 149 | // Invalid parameters were supplied to Stripe's API 150 | res.send(err); 151 | break; 152 | case 'StripeAPIError': 153 | // An error occurred internally with Stripe's API 154 | res.send(err); 155 | break; 156 | case 'StripeConnectionError': 157 | // Some kind of error occurred during the HTTPS communication 158 | res.send(err); 159 | break; 160 | case 'StripeAuthenticationError': 161 | // You probably used an incorrect API key 162 | res.send(err); 163 | break; 164 | default: 165 | // Handle any other types of unexpected errors 166 | res.send(err); 167 | break; 168 | } 169 | } 170 | ); 171 | }); 172 | 173 | billingRoutes.post('/contact', (req:any, res, next) => { 174 | const emailType = { 175 | subject: 'Contact', 176 | cart: req.session.cart, 177 | contact: req.body 178 | }; 179 | 180 | const mailer = new Mailer(req.body.email, emailType); 181 | mailer.send(); 182 | 183 | keys.adminEmails 184 | .split(',') 185 | .filter(Boolean) 186 | .forEach(email => { 187 | var mailerToAdmin = new Mailer(email, { ...emailType, subject: 'Contact-From-Customer' }); 188 | mailerToAdmin.send(); 189 | }); 190 | }); 191 | 192 | export { billingRoutes }; 193 | -------------------------------------------------------------------------------- /routes/cartRoutes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | 3 | const Product = require('../models/Product'); 4 | const Cart = require('../models/Cart'); 5 | 6 | const cartRoutes = Router(); 7 | 8 | cartRoutes.get('/addcart/:id/:lang', (req:any, res) => { 9 | const productId = req.params.id; 10 | const cart = new Cart(req.session.cart ? req.session.cart : {}); 11 | 12 | Product.findById(productId, (err, product) => { 13 | if (err) { 14 | return res.redirect('/'); 15 | } 16 | 17 | const updatedProduct = prepareProduct(product, req.params.lang); 18 | cart.add(updatedProduct, product.id); 19 | req.session.cart = cart; 20 | 21 | if (req.user) { 22 | req.user.cart = cart; 23 | req.user.save(); 24 | } 25 | res.send(cart); 26 | }); 27 | }); 28 | 29 | cartRoutes.get('/removefromcart/:id/:lang', (req:any, res) => { 30 | const productId = req.params.id; 31 | const storeCart = req.session.cart ? req.session.cart : new Cart({}); 32 | const cart = new Cart(storeCart); 33 | 34 | Product.findById(productId, (err, product) => { 35 | if (err) { 36 | return res.redirect('/'); 37 | } 38 | 39 | const updatedProduct = prepareProduct(product, req.params.lang); 40 | 41 | cart.remove(updatedProduct, product.id); 42 | req.session.cart = cart; 43 | if (req.user) { 44 | req.user.cart = cart; 45 | req.user.save(); 46 | } 47 | res.header('Cache-Control', 'no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0').send(cart); 48 | }); 49 | }); 50 | 51 | cartRoutes.get('/cart', (req:any, res) => { 52 | const cart = req.user ? req.user.cart : req.session.cart ? req.session.cart : new Cart({}); 53 | req.session.cart = cart; 54 | res.header('Cache-Control', 'no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0').send(cart); 55 | }); 56 | 57 | const prepareProduct = (product, lang) => { 58 | return { 59 | _id : product._id, 60 | titleUrl : product.titleUrl, 61 | mainImage : product.mainImage, 62 | onSale : product.onSale, 63 | stock : product.stock, 64 | visibility: product.visibility, 65 | shipping : product.shipping, 66 | images : product.images, 67 | _user : product._user, 68 | dateAdded : product.dateAdded, 69 | ...product[lang] 70 | }; 71 | }; 72 | 73 | export { cartRoutes }; 74 | -------------------------------------------------------------------------------- /routes/index.ts: -------------------------------------------------------------------------------- 1 | import { authRoutes } from './authRoutes'; 2 | import { billingRoutes } from './billingRoutes'; 3 | import { productRoutes } from './productRoutes'; 4 | import { cartRoutes } from './cartRoutes'; 5 | import { adminRoutes } from './adminRoutes'; 6 | 7 | export {authRoutes, billingRoutes, productRoutes, cartRoutes, adminRoutes}; 8 | -------------------------------------------------------------------------------- /routes/productRoutes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | 3 | const Product = require('../models/Product'); 4 | const Order = require('../models/Order'); 5 | const requireLogin = require('../middlewares/requireLogin'); 6 | 7 | const productRoutes = Router(); 8 | 9 | productRoutes.get('/products/:lang/:page/:sort', (req, res) => { 10 | var query = {}; 11 | var lang = req.params.lang; 12 | var options = { 13 | page: parseFloat(req.params.page), 14 | limit: 10, 15 | sort: prepareSort(req.params.sort, lang) 16 | }; 17 | Product.paginate(query, options) 18 | .then(response => { 19 | const updatedResponse = { 20 | ...response, 21 | docs: response.docs.map(product => prepareProduct(product, lang)) 22 | }; 23 | res.status(200).send(updatedResponse); 24 | }); 25 | }); 26 | 27 | productRoutes.get('/categoryProducts/:lang/:category/:page/:sort', (req, res) => { 28 | const categoriesLang = `${req.params.lang}.categories`; 29 | var query = {[categoriesLang] : new RegExp(req.params.category, 'i' )}; 30 | var lang = req.params.lang; 31 | var options = { 32 | page: parseFloat(req.params.page), 33 | limit: 100, 34 | sort: prepareSort(req.params.sort, lang) 35 | }; 36 | Product.paginate(query, options) 37 | .then(response => { 38 | const updatedResponse = { 39 | ...response, 40 | docs: response.docs.map(product => prepareProduct(product, lang)) 41 | }; 42 | res.status(200).send(updatedResponse); 43 | }); 44 | }); 45 | 46 | productRoutes.get('/productId/:name/:lang*?', async (req, res) => { 47 | const lang = req.params.lang; 48 | Product.findOne({ titleUrl: req.params.name }) 49 | .then(productFind => { 50 | const updatedProduct = req.params.lang 51 | ? prepareProduct(productFind, lang) 52 | : productFind; 53 | 54 | res.send(updatedProduct); 55 | }); 56 | }); 57 | 58 | productRoutes.get('/productQuery/:query', (req, res) => { 59 | Product.find( 60 | { 61 | titleUrl: new RegExp(req.params.query, 'i') 62 | }, 63 | function(err, products) { 64 | const updatedProducts = products 65 | .map(product => product.titleUrl); 66 | res.status(200).send(updatedProducts); 67 | } 68 | ); 69 | }); 70 | 71 | 72 | productRoutes.get('/categories/:lang', (req, res) => { 73 | const categoriesLang = `${req.params.lang}.categories`; 74 | const lang = req.params.lang; 75 | Product.find({[categoriesLang]: { $gt: [] }}, function(err, products) { 76 | const categories = products 77 | .reduce((catSet, product) => catSet.concat(product[lang].categories) , []) 78 | .filter((cat, i, arr) => arr.indexOf(cat) === i) 79 | .map(category => ({title: category , titleUrl: category.replace(/ /g, '_').toLowerCase() })); 80 | 81 | res.status(200).send(categories); 82 | }); 83 | }); 84 | 85 | 86 | productRoutes.post('/orders', requireLogin, (req, res) => { 87 | const token = req.body.token; 88 | Order.find( 89 | { 90 | _user: token 91 | }, 92 | function(err, orders) { 93 | res.status(200).send(orders); 94 | } 95 | ); 96 | }); 97 | 98 | 99 | // help functions 100 | const prepareSort = (sortParams, lang) => { 101 | switch (sortParams) { 102 | case 'newest': 103 | return `${lang}.dateAdded` 104 | case 'oldest': 105 | return `-${lang}.dateAdded`; 106 | case 'priceasc': 107 | return `${lang}.salePrice` 108 | case 'pricedesc': 109 | return `-${lang}.salePrice` 110 | default: 111 | return `-${lang}.dateAdded`; 112 | } 113 | }; 114 | 115 | const prepareProduct = (product, lang) => { 116 | return { 117 | _id : product._id, 118 | titleUrl : product.titleUrl, 119 | mainImage : product.mainImage, 120 | onSale : product.onSale, 121 | stock : product.stock, 122 | visibility: product.visibility, 123 | shipping : product.shipping, 124 | images : product.images, 125 | _user : product._user, 126 | dateAdded : product.dateAdded, 127 | ...product[lang] 128 | } 129 | } 130 | 131 | 132 | export {productRoutes}; 133 | -------------------------------------------------------------------------------- /server.ts: -------------------------------------------------------------------------------- 1 | import 'zone.js/dist/zone-node'; 2 | require('reflect-metadata'); 3 | 4 | import { enableProdMode } from '@angular/core'; 5 | import { ngExpressEngine } from '@nguniversal/express-engine'; 6 | import { existsSync } from 'fs'; 7 | import { APP_BASE_HREF } from '@angular/common'; 8 | 9 | 10 | const express = require('express'); 11 | const session = require('express-session'); 12 | const path = require('path'); 13 | const mongoose = require('mongoose'); 14 | const passport = require('passport'); 15 | const bodyParser = require('body-parser'); 16 | const keys = require('./config/keys'); 17 | const compression = require('compression'); 18 | const MongoStore = require('connect-mongo')(session); 19 | import { AppServerModule } from './src/main.server'; 20 | 21 | const DIST_FOLDER = path.join(process.cwd(), 'dist'); 22 | 23 | 24 | // mongoose models 25 | require('./models/User'); 26 | require('./models/Product'); 27 | require('./models/Order'); 28 | require('./models/Translation'); 29 | 30 | // services 31 | require('./services/passport'); 32 | require('./services/cache'); 33 | 34 | // connect mongoDB 35 | if (keys.mongoURI) { 36 | mongoose.connect(keys.mongoURI, { useNewUrlParser: true }); 37 | mongoose.set('useFindAndModify', false); 38 | } 39 | 40 | import { authRoutes, billingRoutes, productRoutes, cartRoutes, adminRoutes } from './routes'; 41 | 42 | 43 | enableProdMode(); 44 | 45 | 46 | // The Express app is exported so that it can be used by serverless Functions. 47 | export function app() { 48 | const server = express(); 49 | // compress files 50 | server.use(compression()); 51 | 52 | // set CORS headers 53 | server.all('*', (req, res, next) => { 54 | res.header('Access-Control-Allow-Origin', '*'); 55 | res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS'); 56 | res.header('Access-Control-Allow-Credentials', 'true'); 57 | res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Key, Authorization'); 58 | next(); 59 | }); 60 | const distFolder = path.join(process.cwd(), 'dist/browser'); 61 | const indexHtml = existsSync(path.join(distFolder, 'index.html')) ? 'index.html' : 'index'; 62 | 63 | // Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine) 64 | server.engine('html', ngExpressEngine({ 65 | bootstrap: AppServerModule, 66 | })); 67 | 68 | server.set('view engine', 'html'); 69 | server.set('views', distFolder); 70 | 71 | // Example Express Rest API endpoints 72 | // app.get('/api/**', (req, res) => { }); 73 | 74 | server.use(bodyParser.urlencoded({extended: false})); 75 | server.use(bodyParser.json()); 76 | 77 | server.use( 78 | session({ 79 | cookie: { 80 | maxAge: 30 * 24 * 60 * 60 * 1000 81 | }, 82 | secret: keys.cookieKey, 83 | resave: false, 84 | saveUninitialized: false, 85 | store: new MongoStore({ 86 | mongooseConnection: mongoose.connection, 87 | collection: 'session' 88 | }) 89 | }) 90 | ); 91 | 92 | // middlewares 93 | server.use(passport.initialize()); 94 | server.use(passport.session()); 95 | 96 | server.use((req, res, next) => { 97 | res.locals.login = req.isAuthenticated(); 98 | res.locals.session = req.session; 99 | next(); 100 | }); 101 | 102 | server.use('/auth', authRoutes); 103 | server.use('/api', billingRoutes); 104 | server.use('/prod', productRoutes); 105 | server.use('/cartApi', cartRoutes); 106 | server.use('/admin', adminRoutes); 107 | 108 | server.get('*.*', express.static(path.join(DIST_FOLDER, 'browser'))); 109 | 110 | 111 | server.get('*', (req, res) => { 112 | res.render(path.join(DIST_FOLDER, 'browser', 'index.html'), { 113 | req, 114 | res, 115 | providers: [ 116 | { 117 | provide : 'REQUEST', 118 | useValue : (req) 119 | }, 120 | { 121 | provide : 'RESPONSE', 122 | useValue : (res) 123 | } 124 | ] 125 | }); 126 | }); 127 | 128 | 129 | 130 | // Serve static files from /browser 131 | server.get('*.*', express.static(distFolder, { 132 | maxAge: '1y' 133 | })); 134 | 135 | // All regular routes use the Universal engine 136 | server.get('*', (req, res) => { 137 | res.render(indexHtml, { req, providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }] }); 138 | }); 139 | 140 | return server; 141 | } 142 | 143 | 144 | function run() { 145 | const port = process.env.PORT || 4000; 146 | // Start up the Node server 147 | const server = app(); 148 | server.listen(port, () => { 149 | console.log(`Node Express server listening on http://localhost:${port}`); 150 | }); 151 | } 152 | 153 | 154 | // Webpack will replace 'require' with '__webpack_require__' 155 | // '__non_webpack_require__' is a proxy to Node 'require' 156 | // The below code is to ensure that the server is run only when not requiring the bundle. 157 | declare const __non_webpack_require__: NodeRequire; 158 | const mainModule = __non_webpack_require__.main; 159 | const moduleFilename = mainModule && mainModule.filename || ''; 160 | if (moduleFilename === __filename || moduleFilename.includes('iisnode')) { 161 | run(); 162 | } 163 | 164 | 165 | export * from './src/main.server'; -------------------------------------------------------------------------------- /services/cache.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const redis = require('redis'); 3 | const util = require('util'); 4 | const keys = require('../config/keys'); 5 | 6 | const client = redis.createClient(keys.redisUrl); 7 | client.get = util.promisify(client.get); 8 | const exec = mongoose.Query.prototype.exec; 9 | 10 | mongoose.Query.prototype.cache = function(options = {}) { 11 | this.useCache = true; 12 | this.hashKey = JSON.stringify(options.key || ''); 13 | 14 | return this; 15 | }; 16 | 17 | mongoose.Query.prototype.exec = async function() { 18 | if (!this.useCache) { 19 | return exec.apply(this, arguments); 20 | } 21 | 22 | const key = JSON.stringify( 23 | Object.assign({}, this.getQuery(), { 24 | collection: this.mongooseCollection.name 25 | }) 26 | ); 27 | 28 | // See if we have a value for 'key' in redis 29 | const cacheValue = await client.get(key); 30 | 31 | // If we do, return that 32 | if (cacheValue) { 33 | const doc = JSON.parse(cacheValue); 34 | 35 | return Array.isArray(doc) 36 | ? doc.map(d => new this.model(d)) 37 | : new this.model(doc); 38 | } 39 | 40 | // Otherwise, issue the query and store the result in redis 41 | const result = await exec.apply(this, arguments); 42 | 43 | client.set(key, JSON.stringify(result), 'EX', 10000); 44 | 45 | return result; 46 | }; 47 | 48 | module.exports = { 49 | clearHash(hashKey) { 50 | client.del(JSON.stringify(hashKey)); 51 | } 52 | }; 53 | -------------------------------------------------------------------------------- /services/mailer.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | const sendgrid = require("sendgrid"); 4 | const helper = sendgrid.mail; 5 | const keys = require("../config/keys"); 6 | 7 | const prepareOrderEmailTemplate = require('./emailTemplates'); 8 | 9 | 10 | class Mailer extends helper.Mail { 11 | constructor(reqEmail, emailType) { 12 | super(); 13 | 14 | this.sgApi = sendgrid(keys.sendGridKey); 15 | 16 | this.from_email = new helper.Email("no-reply@bluetooh-eshop.com"); 17 | this.subject = emailType.subject; 18 | 19 | this.body = new helper.Content("text/html", getContent(emailType)); 20 | 21 | this.email = new helper.Email(reqEmail); 22 | const personalize = new helper.Personalization(); 23 | personalize.addTo(this.email); 24 | 25 | this.addContent(this.body); 26 | this.addPersonalization(personalize); 27 | } 28 | 29 | async send() { 30 | const request = this.sgApi.emptyRequest({ 31 | method: "POST", 32 | path: "/v3/mail/send", 33 | body: this.toJSON() 34 | }); 35 | 36 | const response = await this.sgApi.API(request); 37 | return response; 38 | } 39 | } 40 | 41 | module.exports = Mailer; 42 | 43 | function getContent(emailType) { 44 | if (emailType.subject === "Order") { 45 | const cart = emailType.cart; 46 | 47 | return prepareOrderEmailTemplate(cart, emailType); 48 | 49 | } else if (emailType.subject === "Contact") { 50 | return ` 51 | 52 |
53 |

Thank you for contact us!

54 |

We will let you know soon about your requirement

55 |

Your requirement:

56 |

Name: ${emailType.contact.name}

57 |

Email: ${emailType.contact.email}

58 |

Notes: ${emailType.contact.notes}

59 |
60 |
61 |
62 |
63 | 70 | 71 |
72 |

Contact from customer

73 |

Name: ${emailType.contact.name}

74 |

Email: ${emailType.contact.email}

75 |

Notes: ${emailType.contact.notes}

76 |
77 |
78 |
79 |
80 |
{ 9 | done(null, user.id); 10 | }); 11 | 12 | passport.deserializeUser((id, done) => { 13 | User.findById(id).then(user => { 14 | done(null, user); 15 | }); 16 | }); 17 | 18 | /** 19 | * Sign in using Email and Password. 20 | */ 21 | passport.use( 22 | new LocalStrategy({ usernameField: "email" }, (email, password, done) => { 23 | User.findOne({ email: email.toLowerCase() }, (err, user) => { 24 | if (err) { 25 | return done(err); 26 | } 27 | if (!user) { 28 | return done(null, false, { msg: `Email ${email} not found.` }); 29 | } 30 | user.comparePassword(password, (err, isMatch) => { 31 | if (err) { 32 | return done(err); 33 | } 34 | if (isMatch) { 35 | return done(null, user); 36 | } 37 | return done(null, false, { msg: "Invalid email or password." }); 38 | }); 39 | }); 40 | }) 41 | ); 42 | 43 | /** 44 | * Sign in using Google 45 | */ 46 | passport.use( 47 | new GoogleStrategy( 48 | { 49 | clientID: keys.googleClientID, 50 | clientSecret: keys.googleClientSecret, 51 | callbackURL: "/auth/google/callback", 52 | proxy: true 53 | }, 54 | async (accessToken, refreshToken, profile, done) => { 55 | const existingUser = await User.findOne({ 56 | googleId: profile.id 57 | }); 58 | 59 | if (existingUser) { 60 | return done(null, existingUser); 61 | } 62 | 63 | const user = await new User({ googleId: profile.id, email: profile.emails && profile.emails.length ? profile.emails[0].value : "", name: profile.displayName }).save(); 64 | 65 | done(null, user); 66 | } 67 | ) 68 | ); 69 | -------------------------------------------------------------------------------- /src/app/app.browser.module.ts: -------------------------------------------------------------------------------- 1 | import { AppModule } from './app.module'; 2 | import { NgModule } from '@angular/core'; 3 | import { HTTP_INTERCEPTORS } from '@angular/common/http'; 4 | 5 | import { AppComponent } from './app.component'; 6 | import { WindowService } from './services/window.service'; 7 | import { BrowserHttpInterceptor } from './services/browser-http-interceptor'; 8 | 9 | 10 | export function WindowFactory() { 11 | return typeof window !== 'undefined' ? window : {}; 12 | } 13 | 14 | @NgModule({ 15 | imports: [ 16 | AppModule 17 | ], 18 | providers: [ 19 | { 20 | provide: HTTP_INTERCEPTORS, 21 | useClass: BrowserHttpInterceptor, 22 | multi: true, 23 | }, 24 | { 25 | provide : WindowService, 26 | useFactory : (WindowFactory) 27 | } 28 | ], 29 | bootstrap: [AppComponent] 30 | }) 31 | export class AppBrowserModule { } 32 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 6 | 7 | 8 |
9 | -------------------------------------------------------------------------------- /src/app/app.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pararell/eshop-angular-node/b9b5c7fffaef797f84a7c79d7f658a30e1bed9cc/src/app/app.component.scss -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { TranslateService } from './services/translate.service'; 2 | import { filter, take } from 'rxjs/operators'; 3 | import { Component, ElementRef, Renderer2 } from '@angular/core'; 4 | import { Store, select } from '@ngrx/store'; 5 | import * as fromRoot from './store/reducers'; 6 | import * as actions from './store/actions'; 7 | 8 | @Component({ 9 | selector : 'app-root', 10 | templateUrl : './app.component.html', 11 | styleUrls : ['./app.component.scss'] 12 | }) 13 | export class AppComponent { 14 | 15 | rememberScroll : any = {}; 16 | position = 0; 17 | 18 | constructor( 19 | private elRef : ElementRef, 20 | private renderer : Renderer2, 21 | private store : Store, 22 | private translate : TranslateService) { 23 | 24 | this.translate.languageSub$ 25 | .pipe(filter(Boolean), take(1)) 26 | .subscribe((lang: string) => { 27 | const langUpdate = { 28 | lang : lang, 29 | currency : lang === 'cs' ? 'CZK' : '€' 30 | }; 31 | this.store.dispatch(new actions.ChangeLang(langUpdate)); 32 | }); 33 | 34 | this.store.pipe(select(fromRoot.getPosition)) 35 | .pipe(filter(Boolean)) 36 | .subscribe((componentPosition: any) => { 37 | this.rememberScroll = {...this.rememberScroll, componentPosition}; 38 | this.renderer.setProperty(this.elRef.nativeElement.querySelector('.main-scroll-wrapp'), 'scrollTop', 0); 39 | }); 40 | } 41 | 42 | onScrolling(event: any) { 43 | this.position = event['target']['scrollTop']; 44 | } 45 | 46 | onActivate(component: string) { 47 | const currentComponent = component['component']; 48 | const position = (currentComponent && this.rememberScroll[currentComponent]) 49 | ? this.rememberScroll[currentComponent] 50 | : 0; 51 | 52 | setTimeout(() => 53 | this.renderer.setProperty(this.elRef.nativeElement.querySelector('.main-scroll-wrapp'), 'scrollTop', position) 54 | , 0) 55 | } 56 | 57 | onDeactivate(component: string) { 58 | if (Object.keys(component).includes('component')) { 59 | const currentComponent = component['component']; 60 | this.rememberScroll = {...this.rememberScroll, [currentComponent]: this.position}; 61 | } 62 | } 63 | 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { OrderComponent } from './order/order.component'; 2 | import { routesAll } from './app.routes'; 3 | import { Routes } from '@angular/router'; 4 | // angular 5 | import { BrowserModule, BrowserTransferStateModule } from '@angular/platform-browser'; 6 | import { NgModule, APP_INITIALIZER } from '@angular/core'; 7 | import { RouterModule } from '@angular/router'; 8 | import { ReactiveFormsModule, FormsModule } from '@angular/forms'; 9 | import { HttpClientModule } from '@angular/common/http'; 10 | import { ServiceWorkerModule } from '@angular/service-worker'; 11 | 12 | // universal 13 | import { TransferHttpCacheModule } from '@nguniversal/common'; 14 | 15 | // app imports 16 | import { AppComponent } from './app.component'; 17 | import { ProductsComponent } from './products/products.component'; 18 | import { OrdersComponent } from './orders/orders.component'; 19 | import { SharedModule } from './shared/shared.module'; 20 | import { PipeModule } from './pipes/pipe.module'; 21 | import { LazyModule } from './utils/lazyLoadImg/lazy.module'; 22 | import { reducers } from './store/reducers/index'; 23 | import { AppEffects } from './store/effects'; 24 | import { HeaderComponent } from './header/header.component'; 25 | import { FooterComponent } from './footer/footer.component'; 26 | 27 | 28 | // external 29 | import { StoreModule } from '@ngrx/store'; 30 | import { EffectsModule } from '@ngrx/effects'; 31 | import { StoreDevtoolsModule } from '@ngrx/store-devtools'; 32 | import { environment } from '../environments/environment'; 33 | import { TranslateService } from './services/translate.service'; 34 | import { CookieService } from 'ngx-cookie-service'; 35 | 36 | export function WindowFactory() { 37 | return typeof window !== 'undefined' ? window : {}; 38 | } 39 | 40 | export function setupTranslateFactory( 41 | translateService: TranslateService) { 42 | return () => translateService.use(''); 43 | } 44 | 45 | const routes: Routes = routesAll; 46 | 47 | @NgModule({ 48 | declarations: [ 49 | AppComponent, 50 | ProductsComponent, 51 | OrderComponent, 52 | OrdersComponent, 53 | HeaderComponent, 54 | FooterComponent 55 | ], 56 | imports: [ 57 | BrowserTransferStateModule, 58 | BrowserModule.withServerTransition({appId: 'eshop'}), 59 | StoreModule.forRoot( reducers ), 60 | HttpClientModule, 61 | SharedModule, 62 | PipeModule, 63 | LazyModule, 64 | ReactiveFormsModule, 65 | FormsModule, 66 | TransferHttpCacheModule, 67 | EffectsModule.forRoot([ AppEffects ]), 68 | RouterModule.forRoot(routes, { initialNavigation: 'enabled' }), 69 | environment.production ? ServiceWorkerModule.register('ngsw-worker.js') : [], 70 | StoreDevtoolsModule.instrument() 71 | ], 72 | providers: [ 73 | CookieService, 74 | { 75 | provide: APP_INITIALIZER, 76 | useFactory: setupTranslateFactory, 77 | deps: [ TranslateService ], 78 | multi: true 79 | } 80 | ] 81 | }) 82 | export class AppModule { } 83 | -------------------------------------------------------------------------------- /src/app/app.routes.ts: -------------------------------------------------------------------------------- 1 | import { OrderComponent } from './order/order.component'; 2 | import { AuthGuardAdmin } from './services/auth-admin.guard'; 3 | import { AuthGuard } from './services/auth.guard'; 4 | import { OrdersComponent } from './orders/orders.component'; 5 | import { ProductsComponent } from './products/products.component'; 6 | 7 | export const routesAll = [ 8 | { path: '', component: ProductsComponent, pathMatch: 'full' }, 9 | { path: 'en/products', component: ProductsComponent }, 10 | { path: 'en/product', loadChildren: () => import('./product/product.module').then(m => m.ProductModule) }, 11 | { path: 'en/cart', loadChildren: () => import('./cart/cart.module').then(m => m.CartModule) }, 12 | { path: 'en/category/:category', component: ProductsComponent }, 13 | { path: 'en/dashboard', loadChildren: () => import('./dashboard/dashboard.module').then(m => m.DashboardModule), canLoad: [AuthGuardAdmin] }, 14 | { path: 'en/orders', component: OrdersComponent, canActivate: [AuthGuard], pathMatch: 'full' }, 15 | { path: 'en/order/:id', component: OrderComponent, pathMatch: 'full' }, 16 | { path: 'en/eshop', loadChildren: () => import('./eshop/eshop.module').then(m => m.EshopModule) }, 17 | 18 | { path: 'sk/produkty', component: ProductsComponent }, 19 | { path: 'sk/produkt', loadChildren: () => import('./product/product.module').then(m => m.ProductModule) }, 20 | { path: 'sk/kosik', loadChildren: () => import('./cart/cart.module').then(m => m.CartModule) }, 21 | { path: 'sk/kategoria/:category', component: ProductsComponent }, 22 | { path: 'sk/dashboard', loadChildren: () => import('./dashboard/dashboard.module').then(m => m.DashboardModule), canLoad: [AuthGuardAdmin] }, 23 | { path: 'sk/objednavky', component: OrdersComponent, canActivate: [AuthGuard], pathMatch: 'full' }, 24 | { path: 'sk/objednavka/:id', component: OrderComponent, pathMatch: 'full' }, 25 | { path: 'sk/eshop', loadChildren: () => import('./eshop/eshop.module').then(m => m.EshopModule) }, 26 | 27 | { path: 'cs/produkty', component: ProductsComponent }, 28 | { path: 'cs/produkt', loadChildren: () => import('./product/product.module').then(m => m.ProductModule) }, 29 | { path: 'cs/kosik', loadChildren: () => import('./cart/cart.module').then(m => m.CartModule) }, 30 | { path: 'cs/kategorie/:category', component: ProductsComponent }, 31 | { path: 'cs/dashboard', loadChildren: () => import('./dashboard/dashboard.module').then(m => m.DashboardModule), canLoad: [AuthGuardAdmin] }, 32 | { path: 'cs/objednavky', component: OrdersComponent, canActivate: [AuthGuard], pathMatch: 'full' }, 33 | { path: 'cs/objednavka/:id', component: OrderComponent, pathMatch: 'full' }, 34 | { path: 'cs/eshop', loadChildren: () => import('./eshop/eshop.module').then(m => m.EshopModule) }, 35 | 36 | { path: '**', redirectTo: '' } 37 | ]; 38 | -------------------------------------------------------------------------------- /src/app/app.server.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {ServerModule, ServerTransferStateModule} from '@angular/platform-server'; 3 | import { HTTP_INTERCEPTORS } from '@angular/common/http'; 4 | import {AppModule} from './app.module'; 5 | import {AppComponent} from './app.component'; 6 | import { ServerHttpInterceptor } from './services/server-http-interceptor'; 7 | 8 | 9 | @NgModule({ 10 | imports: [ 11 | // The AppServerModule should import your AppModule followed 12 | // by the ServerModule from @angular/platform-server. 13 | AppModule, 14 | ServerModule, 15 | ServerTransferStateModule 16 | ], 17 | providers: [ 18 | { 19 | provide: HTTP_INTERCEPTORS, 20 | useClass: ServerHttpInterceptor, 21 | multi: true 22 | } 23 | ], 24 | // Since the bootstrapped component is not inherited from your 25 | // imported AppModule, it needs to be repeated here. 26 | bootstrap: [AppComponent], 27 | }) 28 | export class AppServerModule { } 29 | -------------------------------------------------------------------------------- /src/app/cart/cart.module.ts: -------------------------------------------------------------------------------- 1 | 2 | // angular 3 | import { CommonModule } from '@angular/common'; 4 | import { NgModule } from '@angular/core'; 5 | import { RouterModule } from '@angular/router'; 6 | import { ReactiveFormsModule, FormsModule } from '@angular/forms'; 7 | 8 | // app imports 9 | import { CartComponent } from './cart/cart.component'; 10 | import { SharedModule } from './../shared/shared.module'; 11 | import { PipeModule } from './../pipes/pipe.module'; 12 | 13 | @NgModule({ 14 | declarations: [ 15 | CartComponent 16 | ], 17 | imports: [ 18 | CommonModule, 19 | SharedModule, 20 | FormsModule, 21 | PipeModule, 22 | ReactiveFormsModule, 23 | RouterModule.forChild([ 24 | { path: '', component: CartComponent } 25 | ]), 26 | ], 27 | providers: [] 28 | }) 29 | export class CartModule { } 30 | -------------------------------------------------------------------------------- /src/app/cart/cart/cart.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 |
6 | 7 |
8 | 9 | 10 |
11 | arrow_back{{ 'Back' | translate | async }} 12 |
13 | 14 |
    15 |
  • 16 |
    17 | 18 | 19 | 20 |
    21 | {{cartItem?.item.title}} 22 |
    23 |
    24 |
    25 | {{cartItem.qty}} ks 26 |
    27 |
    28 | {{ (cartItem.price * ((convertVal$ | async) || 1)) | priceFormat }} {{ currency$ | async }} 29 |
    30 |
    31 | remove 32 |
    33 |
  • 34 |
  • 35 |
    36 | {{ 'Summary' | translate | async }} 37 |
    38 |
    39 | {{cart?.totalQty}} ks 40 |
    41 |
    42 | {{ (cart?.totalPrice * ((convertVal$ | async) || 1)) | priceFormat }} {{ currency$ | async }} 43 |
    44 |
  • 45 |
46 | 47 | 56 | 57 |
58 |
59 |
60 | 61 | 62 |
63 |
64 | 65 | 66 |
67 |
68 | 69 | 70 |
71 |
72 | 73 | 74 |
75 |
76 | 77 | 78 |
79 |
80 | 81 | 82 |
83 |
84 | 86 | 87 |
88 | 90 |
91 |
92 | 93 | 94 | 95 |
96 | {{ 'SuccessPayment' | translate | async }} 97 |
98 |
99 | {{ 'SuccessOrder' | translate | async }} 100 |
101 |
102 | 103 |
104 | 105 |
106 | -------------------------------------------------------------------------------- /src/app/cart/cart/cart.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: flex; 3 | min-height: 100vh; 4 | flex-direction: column; 5 | 6 | .pay-wrapp { 7 | padding: 10px; 8 | display: flex; 9 | justify-content: flex-end; 10 | } 11 | 12 | .delivery-button { 13 | margin-right: 15px; 14 | } 15 | 16 | .cart-items { 17 | display: flex; 18 | justify-content: space-between; 19 | align-items: center; 20 | 21 | .cart-title { 22 | padding: 0 10px; 23 | } 24 | 25 | .cart-image { 26 | display: flex; 27 | align-items: center; 28 | width: 50%; 29 | 30 | img { 31 | height: 70px; 32 | } 33 | } 34 | } 35 | 36 | .collection { 37 | box-shadow: 0 0 0 1px #000460 !important; 38 | background: rgba(255, 255, 255, 0.9) !important; 39 | border: none !important; 40 | } 41 | 42 | .remove-from-cart { 43 | color: red; 44 | cursor: pointer; 45 | } 46 | 47 | .pay-success { 48 | color: #76ff03; 49 | } 50 | 51 | .order-form-wrap { 52 | background: #fff; 53 | margin-top: 25px; 54 | padding: 25px; 55 | box-shadow: 0 0 0 1px #000460 !important; 56 | width: 50%; 57 | margin-left: auto; 58 | margin-right: auto; 59 | 60 | @media (max-width: 650px) { 61 | width: 100%; 62 | } 63 | } 64 | 65 | .submit-order-button { 66 | margin-top: 25px; 67 | width: 100%; 68 | } 69 | 70 | input:focus + label { 71 | -webkit-transform: translateY(-14px) scale(0.8); 72 | transform: translateY(-14px) scale(0.8); 73 | -webkit-transform-origin: 0 0; 74 | transform-origin: 0 0; 75 | } 76 | 77 | input { 78 | &.ng-valid + label { 79 | -webkit-transform: translateY(-14px) scale(0.8); 80 | transform: translateY(-14px) scale(0.8); 81 | -webkit-transform-origin: 0 0; 82 | transform-origin: 0 0; 83 | } 84 | } 85 | 86 | textarea:focus + label { 87 | -webkit-transform: translateY(-14px) scale(0.8); 88 | transform: translateY(-14px) scale(0.8); 89 | -webkit-transform-origin: 0 0; 90 | transform-origin: 0 0; 91 | } 92 | 93 | textarea { 94 | &.ng-valid + label { 95 | -webkit-transform: translateY(-14px) scale(0.8); 96 | transform: translateY(-14px) scale(0.8); 97 | -webkit-transform-origin: 0 0; 98 | transform-origin: 0 0; 99 | } 100 | } 101 | 102 | .textarea-wrapp { 103 | margin: 20px 0; 104 | 105 | textarea { 106 | min-height: 100px; 107 | } 108 | } 109 | 110 | .textarea-label { 111 | font-size: 0.8rem; 112 | color: #9e9e9e; 113 | text-align: left; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/app/cart/cart/cart.component.ts: -------------------------------------------------------------------------------- 1 | import { TranslateService } from './../../services/translate.service'; 2 | import { map, filter, take } from 'rxjs/operators'; 3 | import { Component } from '@angular/core'; 4 | import { Location } from '@angular/common'; 5 | import { FormBuilder, FormGroup, Validators } from '@angular/forms'; 6 | 7 | import { Observable, combineLatest } from 'rxjs'; 8 | 9 | import { Store } from '@ngrx/store'; 10 | 11 | import * as actions from './../../store/actions'; 12 | import * as fromRoot from '../../store/reducers'; 13 | 14 | @Component({ 15 | selector: 'app-cart', 16 | templateUrl: './cart.component.html', 17 | styleUrls: ['./cart.component.scss'] 18 | }) 19 | export class CartComponent { 20 | cart$ : Observable; 21 | lang$ : Observable; 22 | order$ : Observable; 23 | user$ : Observable; 24 | orderForm : FormGroup; 25 | convertVal$ : Observable; 26 | currency$ : Observable; 27 | toggleOrderForm = false; 28 | productUrl : string; 29 | 30 | constructor( 31 | private store: Store, 32 | private _fb: FormBuilder, 33 | private location: Location, 34 | private translate: TranslateService) { 35 | 36 | this.translate.translationsSub$.pipe(filter(Boolean)).subscribe(translations => { 37 | this.productUrl = '/' + this.translate.lang + '/' + (translations['product'] || 'product'); 38 | }); 39 | 40 | this.lang$ = this.store.select(fromRoot.getLang).pipe(filter(Boolean)); 41 | 42 | this.cart$ = this.store.select(fromRoot.getCart); 43 | this.order$ = this.store.select(fromRoot.getOrder).pipe( 44 | filter(Boolean), 45 | map((order:any) => order.outcome) 46 | ); 47 | 48 | this.user$ = this.store.select(fromRoot.getUser); 49 | 50 | this.orderForm = this._fb.group({ 51 | name: ['', Validators.required], 52 | email: ['', Validators.required], 53 | adress: ['', Validators.required], 54 | city: ['', Validators.required], 55 | country: ['', Validators.required], 56 | zip: ['', Validators.required], 57 | notes: [''] 58 | }); 59 | 60 | this.convertVal$ = this.store.select(fromRoot.getConvertVal); 61 | this.currency$ = this.store.select(fromRoot.getCurrency); 62 | } 63 | 64 | goBack() { 65 | this.location.back(); 66 | } 67 | 68 | removeFromCart(id) { 69 | this.lang$.pipe(take(1)).subscribe(lang => { 70 | this.store.dispatch(new actions.RemoveFromCart(id + '/' + lang)); 71 | }); 72 | } 73 | 74 | onToggleForm() { 75 | this.toggleOrderForm = !this.toggleOrderForm; 76 | } 77 | 78 | closeToggleForm() { 79 | this.toggleOrderForm = false; 80 | } 81 | 82 | submit() { 83 | combineLatest(this.cart$.pipe(take(1)), this.currency$.pipe(take(1)), (cart, currency) => ({ cart, currency })).subscribe(({ cart, currency }) => { 84 | const order = { ...this.orderForm.value, amount: cart.totalPrice, currency }; 85 | 86 | this.store.dispatch(new actions.MakeOrder(order)); 87 | this.toggleOrderForm = false; 88 | }); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/app/dashboard/dashboard.module.ts: -------------------------------------------------------------------------------- 1 | import { PipeModule } from './../pipes/pipe.module'; 2 | import { TranslationsEditComponent } from './translations-edit/translations-edit.component'; 3 | import { NgModule } from '@angular/core'; 4 | import { Routes , RouterModule } from '@angular/router'; 5 | import { CommonModule } from '@angular/common'; 6 | import { ProductsEditComponent } from './products-edit/products-edit.component'; 7 | import { OrdersEditComponent } from './orders-edit/orders-edit.component'; 8 | import { OrderEditComponent } from './orders-edit/order-edit/order-edit.component'; 9 | import { DashboardComponent } from './dashboard/dashboard.component'; 10 | import { ReactiveFormsModule, FormsModule } from '@angular/forms'; 11 | import { SharedModule } from './../shared/shared.module'; 12 | import { TinyEditorComponent } from './tiny-editor.ts/tiny-editor.component'; 13 | import { FileUploadModule } from 'ng2-file-upload'; 14 | import { EditorModule } from '@tinymce/tinymce-angular'; 15 | 16 | const DASHBOARD_ROUTER: Routes = [ 17 | { 18 | path: '', 19 | component: DashboardComponent 20 | }, 21 | { 22 | path: 'orders', 23 | component: OrdersEditComponent 24 | }, 25 | { 26 | path: 'translations', 27 | component: TranslationsEditComponent 28 | }, 29 | { 30 | path: 'orders/:id', 31 | component: OrderEditComponent 32 | } 33 | ] 34 | 35 | @NgModule({ 36 | imports: [ 37 | CommonModule, 38 | SharedModule, 39 | FormsModule, 40 | ReactiveFormsModule, 41 | FileUploadModule, 42 | PipeModule, 43 | RouterModule.forChild(DASHBOARD_ROUTER), 44 | EditorModule 45 | ], 46 | declarations: [ProductsEditComponent, OrdersEditComponent, OrderEditComponent, DashboardComponent, TinyEditorComponent, TranslationsEditComponent] 47 | }) 48 | export class DashboardModule { } 49 | -------------------------------------------------------------------------------- /src/app/dashboard/dashboard/dashboard.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |

Dashboard

5 | ORDERS 6 | Translations 7 |
8 |
9 | add 10 | Add product 11 |
12 | 13 |
14 | mode_edit 15 | Edit product 16 |
17 | 18 |
19 | clear 20 | Remove product 21 |
22 |
23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 |
31 | -------------------------------------------------------------------------------- /src/app/dashboard/dashboard/dashboard.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: flex; 3 | min-height: 100vh; 4 | flex-direction: column; 5 | 6 | .orders-button { 7 | margin: 15px; 8 | } 9 | 10 | .product-actions-wrap { 11 | display: flex; 12 | align-items: center; 13 | justify-content: space-around; 14 | 15 | .product-actions { 16 | display: flex; 17 | align-items: center; 18 | flex-flow: column; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/app/dashboard/dashboard/dashboard.component.ts: -------------------------------------------------------------------------------- 1 | import { filter } from 'rxjs/operators'; 2 | import { Component } from '@angular/core'; 3 | import { TranslateService } from '../../services/translate.service'; 4 | 5 | @Component({ 6 | selector: 'app-dashboard', 7 | templateUrl: './dashboard.component.html', 8 | styleUrls: ['./dashboard.component.scss'] 9 | }) 10 | export class DashboardComponent { 11 | 12 | productAction: String = ''; 13 | lang: string; 14 | 15 | constructor(private translate: TranslateService) { 16 | this.translate.translationsSub$ 17 | .pipe(filter(Boolean)) 18 | .subscribe(() => { 19 | this.lang = translate.lang; 20 | }); 21 | } 22 | 23 | changeAction(action: string) { 24 | this.productAction = this.productAction === action ? '' : action; 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/app/dashboard/orders-edit/order-edit/order-edit.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 7 | 8 |
9 |
10 |
11 | 12 | 13 |
14 |
15 |
16 |

{{ order?.orderId }}

17 |
18 |
Status: {{ order?.status }}
EDIT
19 |
Amount: {{ order?.amount }}
20 |
21 |
22 |
23 | 24 | 26 |
27 |
28 |
29 |
30 | Customer: {{ order?.customerEmail }} 31 |
32 |
33 |
34 |
35 |

Desc: {{ order?.description }}

36 |

Customer: {{ order?.customerEmail }}

37 |

Created: {{ order.dateAdded | date:'dd-MM-yy' }}

38 |

Paid: {{ order?.outcome?.seller_message }}

39 |

Total Price: {{ order?.cart?.totalPrice }}

40 |

Total Quantity: {{ order?.cart?.totalQty }}

41 |
42 |
43 |
44 |
45 | 46 |

Customer data:

47 |

CITY: {{ order?.source?.address_city }}

48 |

COUNTRY: {{ order?.source?.address_country }}

49 |

ADRESS: {{ order?.source?.address_line1 }}

50 |

ZIP: {{ order?.source?.address_zip }}

51 |

CardType: {{ order?.source?.brand }}

52 |

Customer Name : {{ order?.source?.name }}

53 |
54 | 55 |

Cart:

56 |
    57 |
  • 58 |
    59 | 60 | 61 | 62 |
    63 | {{cartItem?.item.title}} 64 |
    65 |
    66 |
    67 | {{cartItem.qty}} ks 68 |
    69 |
    70 | {{cartItem.price}} € 71 |
    72 |
  • 73 |
74 |
75 |
76 |
77 | 78 |
79 |
80 | -------------------------------------------------------------------------------- /src/app/dashboard/orders-edit/order-edit/order-edit.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: flex; 3 | min-height: 100vh; 4 | flex-direction: column; 5 | 6 | .container { 7 | @media screen and (max-width: 600px) { 8 | padding: 0 !important; 9 | } 10 | } 11 | 12 | .card-content { 13 | &.top-content { 14 | display: flex; 15 | justify-content: space-between; 16 | } 17 | 18 | .cart-img { 19 | max-width: 100px; 20 | } 21 | 22 | h4 { 23 | font-size: 20px; 24 | line-height: 22px; 25 | padding-right: 10px; 26 | } 27 | 28 | .product-info { 29 | display: flex; 30 | align-items: center; 31 | justify-content: space-between; 32 | margin: 20px 0; 33 | 34 | .stock { 35 | font-size: 16px; 36 | color: #1de9b6; 37 | } 38 | 39 | .price { 40 | padding: 0 100px; 41 | font-size: 25px; 42 | font-weight: 500; 43 | } 44 | } 45 | 46 | .tags { 47 | display: flex; 48 | justify-content: flex-start; 49 | padding-right: 10px; 50 | margin: 20px 0; 51 | font-size: 16px; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/app/dashboard/orders-edit/order-edit/order-edit.component.ts: -------------------------------------------------------------------------------- 1 | import { map } from 'rxjs/operators'; 2 | import { FormGroup, Validators, FormBuilder } from '@angular/forms'; 3 | import { ActivatedRoute } from '@angular/router'; 4 | import { Component } from '@angular/core'; 5 | import { Location } from '@angular/common'; 6 | import { Observable } from 'rxjs'; 7 | 8 | import * as fromRoot from '../../../store/reducers'; 9 | import { Store } from '@ngrx/store'; 10 | import * as actions from './../../../store/actions' 11 | import { TranslateService } from '../../../services/translate.service'; 12 | 13 | @Component({ 14 | selector: 'app-order-edit', 15 | templateUrl: './order-edit.component.html', 16 | styleUrls: ['./order-edit.component.scss'] 17 | }) 18 | export class OrderEditComponent { 19 | 20 | order$: Observable; 21 | statusForm: FormGroup; 22 | orderId: string; 23 | 24 | showForm = false; 25 | 26 | constructor( 27 | private store: Store, 28 | private _route: ActivatedRoute, 29 | private _fb: FormBuilder, 30 | private location: Location , 31 | private translate: TranslateService) { 32 | 33 | this.statusForm = this._fb.group({ 34 | status: ['', Validators.required ] 35 | }); 36 | 37 | this._route.params.pipe(map(params => params['id'])) 38 | .subscribe(params => { 39 | this.store.dispatch(new actions.LoadOrder(params)); 40 | this.orderId = params; 41 | }); 42 | 43 | this.order$ = this.store.select(fromRoot.getOrderId); 44 | } 45 | 46 | toggleForm() { 47 | this.showForm = !this.showForm; 48 | } 49 | 50 | submit() { 51 | this.showForm = false; 52 | const status = this.statusForm.get('status').value; 53 | this.store.dispatch(new actions.UpdateOrder({ 54 | orderId: this.orderId, 55 | status 56 | })); 57 | } 58 | 59 | goBack() { 60 | this.location.back(); 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/app/dashboard/orders-edit/orders-edit.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |
6 |
7 |
8 |

{{ order?.orderId }}

9 |
10 | Detail 11 |

Status: {{ order?.status }}

12 |
13 |
Amount: {{ order?.amount }}
14 |

Desc: {{ order?.description }}

15 |

Customer: {{ order?.customerEmail }}

16 |

Created: {{ order.dateAdded | date:'dd-MM-yy' }}

17 |

Paid: {{ order?.outcome?.seller_message }}

18 |
19 |
20 |
21 | 22 |
23 |
24 | -------------------------------------------------------------------------------- /src/app/dashboard/orders-edit/orders-edit.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: flex; 3 | min-height: 100vh; 4 | flex-direction: column; 5 | 6 | .orders-edit-wrapper { 7 | display: flex; 8 | flex-wrap: wrap; 9 | 10 | .card { 11 | flex-basis: 30%; 12 | margin-right: 3%; 13 | overflow: hidden; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/app/dashboard/orders-edit/orders-edit.component.ts: -------------------------------------------------------------------------------- 1 | import { TranslateService } from './../../services/translate.service'; 2 | import { Component } from '@angular/core'; 3 | 4 | import { Observable } from 'rxjs'; 5 | 6 | import * as fromRoot from '../../store/reducers'; 7 | import { Store } from '@ngrx/store'; 8 | import * as actions from './../../store/actions' 9 | 10 | @Component({ 11 | selector: 'app-orders-edit', 12 | templateUrl: './orders-edit.component.html', 13 | styleUrls: ['./orders-edit.component.scss'] 14 | }) 15 | export class OrdersEditComponent { 16 | 17 | orders$: Observable; 18 | 19 | constructor(private store: Store, private translate: TranslateService) { 20 | 21 | this.store.dispatch(new actions.LoadOrders()); 22 | 23 | this.orders$ = this.store.select(fromRoot.getOrders); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/app/dashboard/products-edit/products-edit.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |

{{action | uppercase}} PRODUCT

4 |
5 | 9 |
10 |
11 |
12 | 13 |
14 | 15 | 16 | Find 17 |
18 | 19 | 20 |
21 |
22 | 23 | 24 |
25 | 26 |
27 | 28 | 29 |
30 | 31 |
32 | 33 | 34 |
35 | 36 |
37 | 38 | 39 |
40 | 41 |
42 | 43 | 44 |
45 | 46 |
47 | 48 | 49 |
50 |
51 |
52 | 53 | 54 |
55 | 56 |
57 | 58 | 59 | 63 | 64 | 68 |
69 |
Visibility
70 | 71 |
72 | 73 | 77 | 78 | 82 | 83 | 87 | 88 |
89 |
On Stock
90 | 91 |
92 | 93 | 97 | 98 | 102 | 103 |
104 |
On Sale
105 | 106 |
107 | 108 | 112 | 113 |
114 |
Shiping
115 | 116 |
117 |
118 | 119 |
X
120 |
121 |
122 | 123 |
124 |
125 | 126 |
X
127 |
128 |
129 | 130 |
131 |
132 |
133 | File 134 | 135 |
136 |
137 |
Images
138 |
139 | 140 |
141 | 142 | 143 | Add Image Url 144 |
145 | 146 | 149 | 150 |
DescriptionFull
151 | 152 |
153 | 154 | 155 | 156 |
157 |
158 | 159 |
Request was send. Again
160 | -------------------------------------------------------------------------------- /src/app/dashboard/products-edit/products-edit.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | .full-input { 3 | display: flex; 4 | align-items: center; 5 | margin-bottom: 10px; 6 | flex-flow: wrap; 7 | } 8 | 9 | .full-check { 10 | margin-bottom: 5px; 11 | margin-top: 20px; 12 | display: flex; 13 | border-bottom: 1px solid #ccc; 14 | flex-flow: column; 15 | padding: 10px 20px; 16 | justify-content: space-between; 17 | } 18 | 19 | .check-title { 20 | color: #9e9e9e; 21 | font-size: 0.8rem; 22 | padding: 5px 0; 23 | } 24 | 25 | .file-field .btn { 26 | float: none !important; 27 | } 28 | 29 | .add-images { 30 | margin: 25px 0; 31 | } 32 | 33 | .image-dash-wrap { 34 | img { 35 | max-width: 100px; 36 | max-height: 100px; 37 | } 38 | } 39 | 40 | .images-wrapp { 41 | display: flex; 42 | } 43 | 44 | .remove-image { 45 | cursor: pointer; 46 | } 47 | 48 | .after-send { 49 | display: flex; 50 | align-items: center; 51 | padding: 50px; 52 | } 53 | 54 | .edit-find-button { 55 | margin: 0 20px; 56 | } 57 | 58 | .products-title-wrapp { 59 | display: flex; 60 | justify-content: space-between; 61 | } 62 | 63 | select.lang-select { 64 | background: #004e92; 65 | border: none; 66 | color: #fff; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/app/dashboard/tiny-editor.ts/tiny-editor.component.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | -------------------------------------------------------------------------------- /src/app/dashboard/tiny-editor.ts/tiny-editor.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pararell/eshop-angular-node/b9b5c7fffaef797f84a7c79d7f658a30e1bed9cc/src/app/dashboard/tiny-editor.ts/tiny-editor.component.scss -------------------------------------------------------------------------------- /src/app/dashboard/tiny-editor.ts/tiny-editor.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Output, Input, EventEmitter } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-tiny-editor', 5 | templateUrl: './tiny-editor.component.html', 6 | styleUrls: ['./tiny-editor.component.scss'] 7 | }) 8 | export class TinyEditorComponent { 9 | 10 | @Input() description = ''; 11 | @Output() onEditorContentChange = new EventEmitter(); 12 | 13 | constructor() { } 14 | 15 | onEditorChange(value) { 16 | this.onEditorContentChange.emit(value); 17 | } 18 | 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/app/dashboard/translations-edit/translations-edit.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |
6 | 7 |
8 |
9 | 12 |
13 |
14 | 15 |
16 |
17 | 18 | 21 |
22 |
23 | 24 |
25 |
26 |
27 | -------------------------------------------------------------------------------- /src/app/dashboard/translations-edit/translations-edit.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: flex; 3 | min-height: 100vh; 4 | flex-direction: column; 5 | 6 | .translation-textarea { 7 | min-height: 400px; 8 | } 9 | 10 | .translations-edit-wrapper { 11 | display: flex; 12 | flex-wrap: wrap; 13 | 14 | .card { 15 | flex-basis: 30%; 16 | margin-right: 3%; 17 | overflow: hidden; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/app/dashboard/translations-edit/translations-edit.component.ts: -------------------------------------------------------------------------------- 1 | import { first } from 'rxjs/operators'; 2 | import { FormBuilder, FormGroup, Validators } from '@angular/forms'; 3 | import { Component } from '@angular/core'; 4 | 5 | import { Observable } from 'rxjs'; 6 | 7 | import * as fromRoot from '../../store/reducers'; 8 | import { Store } from '@ngrx/store'; 9 | import * as actions from './../../store/actions' 10 | 11 | @Component({ 12 | selector: 'app-translations-edit', 13 | templateUrl: './translations-edit.component.html', 14 | styleUrls: ['./translations-edit.component.scss'] 15 | }) 16 | export class TranslationsEditComponent { 17 | 18 | translations$: Observable; 19 | languageForm: FormGroup; 20 | 21 | editLang = ''; 22 | 23 | constructor(private store: Store, private _fb: FormBuilder) { 24 | 25 | this.store.dispatch(new actions.GetAllTranslations()); 26 | 27 | this.languageForm = this._fb.group({ 28 | lang: ['', Validators.required ] 29 | }); 30 | 31 | this.translations$ = this.store.select(fromRoot.getAllTranslations); 32 | } 33 | 34 | showLanguageEdit(lang) { 35 | this.translations$.pipe(first()) 36 | .subscribe(translations => { 37 | const keys = translations 38 | .filter(translation => translation.lang === lang)[0].keys; 39 | this.languageForm.get('lang').setValue(JSON.stringify(keys)); 40 | }) 41 | this.editLang = lang; 42 | } 43 | 44 | submit() { 45 | const keys = this.languageForm.get('lang').value; 46 | this.store.dispatch(new actions.EditTranslation( 47 | {lang: this.editLang, keys: JSON.parse(keys) }) 48 | ); 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/app/eshop/about/about.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | 5 | 8 | 9 |
10 | About Us 11 |
12 | 13 |
14 |
15 | -------------------------------------------------------------------------------- /src/app/eshop/about/about.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pararell/eshop-angular-node/b9b5c7fffaef797f84a7c79d7f658a30e1bed9cc/src/app/eshop/about/about.component.scss -------------------------------------------------------------------------------- /src/app/eshop/about/about.component.ts: -------------------------------------------------------------------------------- 1 | import { Location } from '@angular/common'; 2 | import { TranslateService } from './../../services/translate.service'; 3 | import { Component } from '@angular/core'; 4 | 5 | @Component({ 6 | selector: 'app-about', 7 | templateUrl: './about.component.html', 8 | styleUrls: ['./about.component.scss'] 9 | }) 10 | export class AboutComponent { 11 | 12 | lang: string; 13 | 14 | constructor(translate: TranslateService, private location: Location) { 15 | translate.languageSub$ 16 | .subscribe(lang => { 17 | this.lang = lang; 18 | }); 19 | } 20 | 21 | goBack() { 22 | this.location.back(); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/app/eshop/contact/contact.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
5 |

Contact

6 |
7 |
8 | 13 | 14 |
15 |
16 | 17 | 18 |
19 |
20 | 22 | 23 |
24 | 25 | 27 |
28 |
29 |
30 |
31 | -------------------------------------------------------------------------------- /src/app/eshop/contact/contact.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: block; 3 | 4 | .contact-form-wrap { 5 | margin-top: 25px; 6 | 7 | h1 { 8 | margin-bottom: 40px; 9 | } 10 | } 11 | 12 | .submit-order-button { 13 | margin-top: 25px; 14 | width: 100%; 15 | } 16 | 17 | input:focus + label { 18 | -webkit-transform: translateY(-14px) scale(0.8); 19 | transform: translateY(-14px) scale(0.8); 20 | -webkit-transform-origin: 0 0; 21 | transform-origin: 0 0; 22 | } 23 | 24 | input { 25 | &.ng-valid + label { 26 | -webkit-transform: translateY(-14px) scale(0.8); 27 | transform: translateY(-14px) scale(0.8); 28 | -webkit-transform-origin: 0 0; 29 | transform-origin: 0 0; 30 | } 31 | } 32 | 33 | textarea:focus + label { 34 | -webkit-transform: translateY(-14px) scale(0.8); 35 | transform: translateY(-14px) scale(0.8); 36 | -webkit-transform-origin: 0 0; 37 | transform-origin: 0 0; 38 | } 39 | 40 | textarea { 41 | &.ng-valid + label { 42 | -webkit-transform: translateY(-14px) scale(0.8); 43 | transform: translateY(-14px) scale(0.8); 44 | -webkit-transform-origin: 0 0; 45 | transform-origin: 0 0; 46 | } 47 | } 48 | 49 | .textarea-wrapp { 50 | margin: 20px 0; 51 | 52 | textarea { 53 | min-height: 100px; 54 | } 55 | } 56 | 57 | .textarea-label { 58 | font-size: 0.8rem; 59 | color: #9e9e9e; 60 | text-align: left; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/app/eshop/contact/contact.component.ts: -------------------------------------------------------------------------------- 1 | import { FormGroup, FormBuilder, Validators } from '@angular/forms'; 2 | import { Component } from '@angular/core'; 3 | 4 | import * as actions from './../../store/actions' 5 | import * as fromRoot from '../../store/reducers'; 6 | import { Store } from '@ngrx/store'; 7 | 8 | @Component({ 9 | selector: 'app-contact', 10 | templateUrl: './contact.component.html', 11 | styleUrls: ['./contact.component.scss'] 12 | }) 13 | export class ContactComponent { 14 | 15 | contactForm: FormGroup; 16 | 17 | constructor( private _fb: FormBuilder, private store: Store) { 18 | 19 | this.contactForm = this._fb.group({ 20 | name: ['', Validators.required ], 21 | email: ['', Validators.required ], 22 | notes: ['', Validators.required ] 23 | }); 24 | } 25 | submit() { 26 | this.store.dispatch(new actions.SendContact(this.contactForm.value)); 27 | this.contactForm.reset(); 28 | } 29 | 30 | 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/app/eshop/eshop.module.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | // angular 4 | import { CommonModule } from '@angular/common'; 5 | import { NgModule } from '@angular/core'; 6 | import { RouterModule } from '@angular/router'; 7 | import { ReactiveFormsModule, FormsModule } from '@angular/forms'; 8 | 9 | // app imports 10 | 11 | import { SharedModule } from './../shared/shared.module'; 12 | import { PipeModule } from './../pipes/pipe.module'; 13 | import { VopComponent } from './vop/vop.component'; 14 | import { GdprComponent } from './gdpr/gdpr.component'; 15 | import { ContactComponent } from './contact/contact.component'; 16 | import { AboutComponent } from './about/about.component'; 17 | 18 | @NgModule({ 19 | declarations: [ 20 | VopComponent, 21 | GdprComponent, 22 | ContactComponent, 23 | AboutComponent 24 | ], 25 | imports: [ 26 | CommonModule, 27 | SharedModule, 28 | FormsModule, 29 | PipeModule, 30 | ReactiveFormsModule, 31 | RouterModule.forChild([ 32 | { path: 'vop', component: VopComponent }, 33 | { path: 'gdpr', component: GdprComponent }, 34 | { path: 'contact', component: ContactComponent }, 35 | { path: 'info', component: AboutComponent } 36 | ]), 37 | ], 38 | providers: [] 39 | }) 40 | export class EshopModule { } 41 | -------------------------------------------------------------------------------- /src/app/eshop/gdpr/gdpr.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pararell/eshop-angular-node/b9b5c7fffaef797f84a7c79d7f658a30e1bed9cc/src/app/eshop/gdpr/gdpr.component.scss -------------------------------------------------------------------------------- /src/app/eshop/gdpr/gdpr.component.ts: -------------------------------------------------------------------------------- 1 | import { Location } from '@angular/common'; 2 | import { TranslateService } from './../../services/translate.service'; 3 | import { Component } from '@angular/core'; 4 | 5 | @Component({ 6 | selector: 'app-gdpr', 7 | templateUrl: './gdpr.component.html', 8 | styleUrls: ['./gdpr.component.scss'] 9 | }) 10 | export class GdprComponent { 11 | 12 | lang: string; 13 | 14 | constructor(translate: TranslateService, private location: Location) { 15 | translate.languageSub$ 16 | .subscribe(lang => { 17 | this.lang = lang; 18 | }); 19 | } 20 | 21 | goBack() { 22 | this.location.back(); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/app/eshop/vop/vop.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pararell/eshop-angular-node/b9b5c7fffaef797f84a7c79d7f658a30e1bed9cc/src/app/eshop/vop/vop.component.scss -------------------------------------------------------------------------------- /src/app/eshop/vop/vop.component.ts: -------------------------------------------------------------------------------- 1 | import { Location } from '@angular/common'; 2 | import { Component } from '@angular/core'; 3 | import { TranslateService } from '../../services/translate.service'; 4 | 5 | @Component({ 6 | selector: 'app-vop', 7 | templateUrl: './vop.component.html', 8 | styleUrls: ['./vop.component.scss'] 9 | }) 10 | export class VopComponent { 11 | 12 | lang: string; 13 | 14 | constructor(translate: TranslateService, private location: Location) { 15 | translate.languageSub$ 16 | .subscribe(lang => { 17 | this.lang = lang; 18 | }); 19 | } 20 | 21 | goBack() { 22 | this.location.back(); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/app/footer/footer.component.html: -------------------------------------------------------------------------------- 1 | 27 | -------------------------------------------------------------------------------- /src/app/footer/footer.component.scss: -------------------------------------------------------------------------------- 1 | .page-footer { 2 | background: linear-gradient(to right, #000428, #004e92); 3 | } 4 | 5 | .footer-copyright { 6 | background: linear-gradient(to right, #000428, #004e92); 7 | } 8 | -------------------------------------------------------------------------------- /src/app/footer/footer.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { TranslateService } from '../services/translate.service'; 3 | 4 | @Component({ 5 | selector: 'app-footer', 6 | templateUrl: './footer.component.html', 7 | styleUrls: ['./footer.component.scss'] 8 | }) 9 | export class FooterComponent { 10 | currentYear = new Date().getFullYear(); 11 | lang: string; 12 | 13 | constructor(translate: TranslateService) { 14 | translate.languageSub$.subscribe(lang => { 15 | this.lang = lang; 16 | }); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/app/header/header.component.html: -------------------------------------------------------------------------------- 1 | 86 | -------------------------------------------------------------------------------- /src/app/header/header.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: block; 3 | 4 | .main-nav-header { 5 | padding: 0 15px; 6 | height: 50px; 7 | line-height: 50px; 8 | background: linear-gradient(to right, #000460, #004e92); 9 | 10 | select.lang-select { 11 | background: #004e92; 12 | border: none; 13 | color: #fff; 14 | } 15 | 16 | .cart-wrap { 17 | display: flex; 18 | 19 | i { 20 | height: 50px; 21 | line-height: 50px; 22 | } 23 | } 24 | 25 | .brand-logo { 26 | text-overflow: ellipsis; 27 | max-width: 40%; 28 | 29 | @media screen and (max-width: 700px) { 30 | max-width: 30%; 31 | overflow: hidden; 32 | max-height: 55px; 33 | } 34 | 35 | @media screen and (max-width: 600px) { 36 | max-width: 10%; 37 | overflow: hidden; 38 | max-height: 55px; 39 | } 40 | } 41 | 42 | .search-icon-position { 43 | top: -15px; 44 | } 45 | 46 | .fake-form { 47 | height: 100%; 48 | width: 30%; 49 | display: inline-block; 50 | 51 | @media screen and (max-width: 600px) { 52 | width: 50%; 53 | } 54 | 55 | .input-field { 56 | z-index: 20; 57 | 58 | .autocomplete-content { 59 | max-height: 200px; 60 | display: block; 61 | opacity: 1; 62 | top: initial; 63 | } 64 | } 65 | } 66 | } 67 | 68 | .mobile-nav-menu { 69 | display: none; 70 | 71 | &.active { 72 | display: block; 73 | position: fixed; 74 | top: 50px; 75 | width: 100%; 76 | left: 0; 77 | padding: 5px; 78 | background: linear-gradient(to right, #000460, #004e92); 79 | } 80 | } 81 | 82 | .mobile-nav-menu-icon { 83 | display: none; 84 | 85 | @media screen and (max-width: 600px) { 86 | display: block; 87 | } 88 | } 89 | 90 | @media screen and (max-width: 600px) { 91 | padding: 0; 92 | 93 | .nav-menu-right-side { 94 | display: none; 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/app/header/header.component.ts: -------------------------------------------------------------------------------- 1 | import { debounceTime, first, filter, take } from 'rxjs/operators'; 2 | import { Component, OnInit, PLATFORM_ID, Inject } from '@angular/core'; 3 | import { FormControl } from '@angular/forms'; 4 | 5 | import { Observable, BehaviorSubject } from 'rxjs'; 6 | 7 | import * as fromRoot from '../store/reducers'; 8 | import { Store } from '@ngrx/store'; 9 | import * as actions from './../store/actions'; 10 | import { TranslateService } from '../services/translate.service'; 11 | import { isPlatformBrowser } from '@angular/common'; 12 | 13 | 14 | @Component({ 15 | selector: 'app-header', 16 | templateUrl: './header.component.html', 17 | styleUrls: ['./header.component.scss'] 18 | }) 19 | export class HeaderComponent implements OnInit { 20 | user$ : Observable; 21 | cart$ : Observable; 22 | productTitles$ : Observable; 23 | userOrders$ : Observable; 24 | showAutocomplete$ : BehaviorSubject = new BehaviorSubject(false); 25 | languageOptions = ['en', 'sk', 'cs']; 26 | choosenLanguage = 'en'; 27 | showMobileNav = false; 28 | cartUrl : string; 29 | ordersUrl : string; 30 | productUrl : string; 31 | 32 | readonly query: FormControl = new FormControl(); 33 | 34 | 35 | constructor( 36 | @Inject(PLATFORM_ID) 37 | private _platformId : Object, 38 | private store: Store, private translate: TranslateService) { 39 | this.store 40 | .select(fromRoot.getLang) 41 | .pipe(filter(Boolean)) 42 | .subscribe((lang:string) => { 43 | this.choosenLanguage = lang; 44 | translate.use(this.choosenLanguage); 45 | }); 46 | 47 | this.translate.translationsSub$.pipe(filter(Boolean)).subscribe(translations => { 48 | this.cartUrl = '/' + this.translate.lang + '/' + (translations['cart'] || 'cart'); 49 | this.ordersUrl = `/${this.translate.lang}/${translations['orders']}`; 50 | this.productUrl = `/${this.translate.lang}/${translations['product']}`; 51 | }); 52 | } 53 | 54 | ngOnInit() { 55 | this.user$ = this.store.select(fromRoot.getUser); 56 | this.cart$ = this.store.select(fromRoot.getCart); 57 | this.productTitles$ = this.store.select(fromRoot.getProductTitles); 58 | this.userOrders$ = this.store.select(fromRoot.getUserOrders); 59 | 60 | this.query.valueChanges.pipe(debounceTime(200)).subscribe(value => { 61 | const sendQuery = value || 'EMPTY___QUERY'; 62 | this.store.dispatch(new actions.LoadProductsSearch(sendQuery)); 63 | }); 64 | 65 | this.user$.pipe(filter(() => isPlatformBrowser(this._platformId)), take(1)).subscribe(user => { 66 | if (!user) { 67 | this.store.dispatch(new actions.LoadUserAction()); 68 | } 69 | }); 70 | 71 | this.user$ 72 | .pipe( 73 | filter(user => user && user._id), 74 | take(1) 75 | ) 76 | .subscribe(user => { 77 | this.store.dispatch(new actions.LoadUserOrders({ token: user._id })); 78 | }); 79 | 80 | this.cart$.pipe(filter(() => isPlatformBrowser(this._platformId)), take(1)).subscribe(cart => { 81 | if (!cart) { 82 | this.store.dispatch(new actions.GetCart()); 83 | } 84 | }); 85 | } 86 | 87 | onFocus() { 88 | this.showAutocomplete$.next(true); 89 | } 90 | 91 | onBlur() { 92 | setTimeout(() => this.showAutocomplete$.next(false), 300); 93 | } 94 | 95 | onTitleLink(productUrl) { 96 | this.query.setValue(''); 97 | } 98 | 99 | setLang(lang: string) { 100 | const langUpdate = { 101 | lang: lang, 102 | currency: lang === 'cs' ? 'CZK' : '€' 103 | }; 104 | this.store.dispatch(new actions.ChangeLang(langUpdate)); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/app/order/order.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 7 | 8 |
9 |
10 |
11 | 12 | 13 |
14 |
15 |
16 |

{{ order?.orderId }}

17 |
18 |
Status: {{ order?.status }}
19 |
Amount: {{ order?.amount }}
20 |
21 |
22 |
23 | Customer: {{ order?.customerEmail }} 24 |
25 |
26 |
27 |
28 |

Desc: {{ order?.description }}

29 |

Customer: {{ order?.customerEmail }}

30 |

Created: {{ order.dateAdded | date:'dd-MM-yy' }}

31 |

Paid: {{ order?.outcome?.seller_message }}

32 |

Total Price: {{ order?.cart?.totalPrice }}

33 |

Total Quantity: {{ order?.cart?.totalQty }}

34 |
35 |
36 |
37 |
38 | 39 |

Customer data:

40 |

CITY: {{ order?.source?.address_city }}

41 |

COUNTRY: {{ order?.source?.address_country }}

42 |

ADRESS: {{ order?.source?.address_line1 }}

43 |

ZIP: {{ order?.source?.address_zip }}

44 |

CardType: {{ order?.source?.brand }}

45 |

Customer Name : {{ order?.source?.name }}

46 |
47 | 48 |

Cart:

49 |
    50 |
  • 51 |
    52 | 53 | 54 | 55 |
    56 | {{cartItem?.item.title}} 57 |
    58 |
    59 |
    60 | {{cartItem.qty}} ks 61 |
    62 |
    63 | {{cartItem.price}} € 64 |
    65 |
  • 66 |
67 |
68 |
69 |
70 | 71 |
72 |
73 | -------------------------------------------------------------------------------- /src/app/order/order.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: flex; 3 | min-height: 100vh; 4 | flex-direction: column; 5 | 6 | .container { 7 | @media screen and (max-width: 600px) { 8 | padding: 0 !important; 9 | } 10 | } 11 | 12 | .card-content { 13 | &.top-content { 14 | display: flex; 15 | justify-content: space-between; 16 | } 17 | 18 | .cart-img { 19 | max-width: 100px; 20 | } 21 | 22 | h4 { 23 | font-size: 20px; 24 | line-height: 22px; 25 | padding-right: 10px; 26 | } 27 | 28 | .product-info { 29 | display: flex; 30 | align-items: center; 31 | justify-content: space-between; 32 | margin: 20px 0; 33 | 34 | .stock { 35 | font-size: 16px; 36 | color: #1de9b6; 37 | } 38 | 39 | .price { 40 | padding: 0 100px; 41 | font-size: 25px; 42 | font-weight: 500; 43 | } 44 | } 45 | 46 | .tags { 47 | display: flex; 48 | justify-content: flex-start; 49 | padding-right: 10px; 50 | margin: 20px 0; 51 | font-size: 16px; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/app/order/order.component.ts: -------------------------------------------------------------------------------- 1 | import { Location } from '@angular/common'; 2 | import { ActivatedRoute } from '@angular/router'; 3 | import { filter, map } from 'rxjs/operators'; 4 | import { TranslateService } from './../services/translate.service'; 5 | import { Component } from '@angular/core'; 6 | 7 | import { Observable } from 'rxjs'; 8 | 9 | import * as fromRoot from '../store/reducers'; 10 | import { Store } from '@ngrx/store'; 11 | import * as actions from './../store/actions' 12 | 13 | @Component({ 14 | selector: 'app-order', 15 | templateUrl: './order.component.html', 16 | styleUrls: ['./order.component.scss'] 17 | }) 18 | export class OrderComponent { 19 | 20 | order$ : Observable; 21 | ordersUrl : string; 22 | 23 | constructor( 24 | private location : Location, 25 | private store : Store, 26 | private translate : TranslateService, 27 | private _route : ActivatedRoute) { 28 | 29 | this.store.select(fromRoot.getLang) 30 | .pipe(filter(Boolean)) 31 | .subscribe(lang => { 32 | this.ordersUrl = `/${lang}/${this.translate.data['orders']}`; 33 | }); 34 | 35 | this._route.params.pipe(map(params => params['id'])) 36 | .subscribe(params => { 37 | this.store.dispatch(new actions.LoadOrder(params)); 38 | }); 39 | 40 | this.order$ = this.store.select(fromRoot.getOrderId); 41 | 42 | } 43 | 44 | goBack() { 45 | this.location.back(); 46 | } 47 | 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/app/orders/orders.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |
6 |
7 |
8 |

{{ order?.orderId }}

9 |
10 | Detail 11 |

Status: {{ order?.status }}

12 |
13 |
Amount: {{ order?.amount }}
14 |

Desc: {{ order?.description }}

15 |

Customer: {{ order?.customerEmail }}

16 |

Created: {{ order.dateAdded | date:'dd-MM-yy' }}

17 |

Paid: {{ order?.outcome?.seller_message }}

18 |
19 |
20 |
21 | 22 |
23 |
24 | -------------------------------------------------------------------------------- /src/app/orders/orders.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: flex; 3 | min-height: 100vh; 4 | flex-direction: column; 5 | 6 | .orders-edit-wrapper { 7 | display: flex; 8 | flex-wrap: wrap; 9 | 10 | .card { 11 | flex-basis: 30%; 12 | margin-right: 3%; 13 | overflow: hidden; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/app/orders/orders.component.ts: -------------------------------------------------------------------------------- 1 | import { filter } from 'rxjs/operators'; 2 | import { TranslateService } from './../services/translate.service'; 3 | import { Component } from '@angular/core'; 4 | 5 | import { Observable } from 'rxjs'; 6 | 7 | import * as fromRoot from '../store/reducers'; 8 | import { Store } from '@ngrx/store'; 9 | 10 | @Component({ 11 | selector: 'app-orders', 12 | templateUrl: './orders.component.html', 13 | styleUrls: ['./orders.component.scss'] 14 | }) 15 | export class OrdersComponent { 16 | 17 | orders$ : Observable; 18 | ordersUrl : string; 19 | 20 | constructor(private store: Store, private translate: TranslateService) { 21 | this.store.select(fromRoot.getLang) 22 | .pipe(filter(Boolean)) 23 | .subscribe(lang => { 24 | this.ordersUrl = `/${lang}/${translate.data['orders']}`; 25 | }); 26 | 27 | this.orders$ = this.store.select(fromRoot.getUserOrders); 28 | 29 | } 30 | 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/app/pipes/pipe.module.ts: -------------------------------------------------------------------------------- 1 | import { PriceFormatPipe } from './price.pipe'; 2 | import { NgModule } from '@angular/core'; 3 | import {CommonModule} from '@angular/common'; 4 | 5 | import { TranslatePipe } from './translate.pipe'; 6 | 7 | @NgModule({ 8 | declarations: [TranslatePipe, PriceFormatPipe], 9 | imports: [CommonModule], 10 | exports: [TranslatePipe, PriceFormatPipe] 11 | }) 12 | 13 | export class PipeModule {} 14 | -------------------------------------------------------------------------------- /src/app/pipes/price.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | 3 | 4 | 5 | @Pipe({ 6 | name : 'priceFormat', 7 | pure : true 8 | }) 9 | export class PriceFormatPipe implements PipeTransform { 10 | 11 | transform(value: number): string { 12 | const price = value.toFixed(0); 13 | return price.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ' '); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/app/pipes/translate.pipe.ts: -------------------------------------------------------------------------------- 1 | import { take, map } from 'rxjs/operators'; 2 | import { Pipe, PipeTransform } from '@angular/core'; 3 | import { TranslateService } from '../services/translate.service'; 4 | 5 | @Pipe({ 6 | name: 'translate', 7 | pure: false 8 | }) 9 | export class TranslatePipe implements PipeTransform { 10 | 11 | constructor(private translate: TranslateService) {} 12 | transform(key: any): any { 13 | return this.translate.translationsSub$ 14 | .pipe(take(1), map(translations => translations ? translations[key] : key)); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/app/product/product.module.ts: -------------------------------------------------------------------------------- 1 | // angular 2 | import { CommonModule } from '@angular/common'; 3 | import { NgModule } from '@angular/core'; 4 | import { RouterModule } from '@angular/router'; 5 | import { ReactiveFormsModule } from '@angular/forms'; 6 | 7 | // app imports 8 | import { ProductComponent } from './product/product.component'; 9 | import { LazyModule } from './../utils/lazyLoadImg/lazy.module'; 10 | import { SharedModule } from './../shared/shared.module'; 11 | import { PipeModule } from './../pipes/pipe.module'; 12 | 13 | @NgModule({ 14 | declarations: [ 15 | ProductComponent 16 | ], 17 | imports: [ 18 | CommonModule, 19 | SharedModule, 20 | LazyModule, 21 | ReactiveFormsModule, 22 | PipeModule, 23 | RouterModule.forChild([ 24 | { path: ':id', component: ProductComponent }, 25 | { path: '**', redirectTo: 'products' } 26 | ]), 27 | ], 28 | providers: [] 29 | }) 30 | export class ProductModule { } 31 | -------------------------------------------------------------------------------- /src/app/product/product/product.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 7 | 8 |
9 |
10 |
11 | 12 | 13 |
14 |
15 |
16 |

{{ items?.product?.title }}

17 |

{{ items?.product?.description }}

18 |
19 |
{{ items?.product?.stock | translate | async }}
20 |
{{ (items?.product?.salePrice * ((convertVal$ | async) || 1)) | priceFormat }}{{ currency$ | async }}
21 |
22 | 26 | 27 |
28 |
29 |
30 |
31 | {{ tag }} 32 |
33 |
34 |
35 |
36 |
37 | 43 | 60 |
61 |
62 | 63 |
64 |
65 | -------------------------------------------------------------------------------- /src/app/product/product/product.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: flex; 3 | min-height: 100vh; 4 | flex-direction: column; 5 | 6 | .container { 7 | @media screen and (max-width: 600px) { 8 | padding: 5px !important; 9 | } 10 | } 11 | 12 | .card-content { 13 | display: flex; 14 | justify-content: space-between; 15 | 16 | @media screen and (max-width: 600px) { 17 | flex-flow: wrap; 18 | } 19 | 20 | h4 { 21 | font-size: 20px; 22 | line-height: 22px; 23 | padding-right: 10px; 24 | } 25 | 26 | .product-info { 27 | display: flex; 28 | align-items: center; 29 | justify-content: space-between; 30 | margin: 20px 0; 31 | 32 | .stock { 33 | font-size: 16px; 34 | color: #1de9b6; 35 | } 36 | 37 | .price { 38 | padding: 0 5px; 39 | font-size: 25px; 40 | font-weight: 500; 41 | } 42 | } 43 | 44 | .tags { 45 | display: flex; 46 | justify-content: flex-start; 47 | padding-right: 10px; 48 | margin: 20px 0; 49 | font-size: 16px; 50 | } 51 | 52 | .image-wrap { 53 | width: 50%; 54 | background-size: contain !important; 55 | min-height: 300px; 56 | } 57 | } 58 | 59 | .tab { 60 | cursor: pointer; 61 | } 62 | 63 | .cart-wrap { 64 | padding-right: 10px; 65 | } 66 | 67 | .images-wrapp-content { 68 | display: flex; 69 | flex-wrap: wrap; 70 | padding: 0 4px; 71 | 72 | .images-wrapp-content-img { 73 | flex: 25%; 74 | max-width: 25%; 75 | padding: 0 4px; 76 | 77 | &.modal { 78 | display: block; 79 | top: 50px; 80 | min-width: 100%; 81 | min-height: 100%; 82 | z-index: 10; 83 | 84 | .next-btn { 85 | float: right; 86 | margin: 10px; 87 | } 88 | 89 | .prev-btn { 90 | margin: 10px; 91 | } 92 | } 93 | 94 | @media screen and (max-width: 800px) { 95 | flex: 50%; 96 | max-width: 50%; 97 | } 98 | 99 | @media screen and (max-width: 600px) { 100 | flex: 100%; 101 | max-width: 100%; 102 | } 103 | 104 | img { 105 | margin-top: 8px; 106 | vertical-align: middle; 107 | display: block; 108 | max-width: 100%; 109 | } 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/app/product/product/product.component.ts: -------------------------------------------------------------------------------- 1 | import { filter, map, take } from 'rxjs/operators'; 2 | import { Component } from '@angular/core'; 3 | import { ActivatedRoute } from '@angular/router'; 4 | import { Observable, combineLatest } from 'rxjs'; 5 | 6 | import { Store } from '@ngrx/store'; 7 | import * as actions from './../../store/actions'; 8 | import * as fromRoot from '../../store/reducers'; 9 | import { Location } from '@angular/common'; 10 | import { Meta, Title } from '@angular/platform-browser'; 11 | 12 | @Component({ 13 | selector: 'app-product', 14 | templateUrl: './product.component.html', 15 | styleUrls: ['./product.component.scss'] 16 | }) 17 | export class ProductComponent { 18 | 19 | items$ : Observable; 20 | productLoading$ : Observable; 21 | convertVal$ : Observable; 22 | currency$ : Observable; 23 | lang$ : Observable; 24 | activeTab = 'first'; 25 | openImages = {}; 26 | 27 | constructor( 28 | private _route : ActivatedRoute, 29 | private store : Store, 30 | private location: Location, 31 | private _meta : Meta, 32 | private _title : Title) { 33 | this.lang$ = this.store.select(fromRoot.getLang).pipe(filter(Boolean)); 34 | 35 | combineLatest(this.lang$, this._route.params.pipe(map(params => params['id'])), (lang, id) => ({ lang, id })).subscribe(({ lang, id }) => this.store.dispatch(new actions.GetProduct(id + '/' + lang))); 36 | 37 | this.store 38 | .select(fromRoot.getProduct) 39 | .pipe( 40 | filter(product => product && product.title), 41 | take(1) 42 | ) 43 | .subscribe(product => { 44 | this._title.setTitle(product.title); 45 | this._meta.updateTag({ name: 'description', content: product.description }); 46 | }); 47 | 48 | this.productLoading$ = this.store.select(fromRoot.getProductLoading); 49 | 50 | this.items$ = combineLatest( 51 | this.store.select(fromRoot.getProduct), 52 | this.store.select(fromRoot.getCart).pipe( 53 | filter(Boolean), 54 | map((cart:any) => cart.items) 55 | ), 56 | (product, cartItems) => { 57 | return { 58 | product: product, 59 | cartIds: cartItems.reduce((prev, curr) => ({ ...prev, [curr.id]: curr.qty }), {}) 60 | }; 61 | } 62 | ); 63 | 64 | this.convertVal$ = this.store.select(fromRoot.getConvertVal); 65 | this.currency$ = this.store.select(fromRoot.getCurrency); 66 | } 67 | 68 | goBack() { 69 | this.location.back(); 70 | } 71 | 72 | addToCart(id: string) { 73 | this.lang$.pipe(take(1)).subscribe(lang => { 74 | this.store.dispatch(new actions.AddToCart(id + '/' + lang)); 75 | }); 76 | } 77 | 78 | removeFromCart(id: string) { 79 | this.lang$.pipe(take(1)).subscribe(lang => { 80 | this.store.dispatch(new actions.RemoveFromCart(id + '/' + lang)); 81 | }); 82 | } 83 | 84 | toggleModalImg(index: number): void { 85 | this.openImages[index] = this.openImages[index] ? !this.openImages[index] : true; 86 | } 87 | 88 | prevImg(event, i: number) { 89 | event.stopPropagation(); 90 | event.preventDefault(); 91 | this.openImages[i] = false; 92 | this.openImages[i - 1] = true; 93 | } 94 | 95 | nextImg(event, i: number) { 96 | event.stopPropagation(); 97 | event.preventDefault(); 98 | this.openImages[i] = false; 99 | this.openImages[i + 1] = true; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/app/products/products.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 |

6 | Bluetooth {{ 'headphones' | translate | async }} 7 |

8 |

{{category}}

9 |
10 |
11 |
12 | 13 |
16 |
17 |
18 | 19 |
22 | 39 |
40 | 48 | 49 | 50 | 56 | 57 | 58 | 65 | 66 | 67 |
68 |
69 | 70 |
71 |
72 | -------------------------------------------------------------------------------- /src/app/products/products.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: flex; 3 | min-height: 100vh; 4 | flex-direction: column; 5 | 6 | .main { 7 | // background: #efefef; 8 | } 9 | 10 | .container { 11 | padding: 15px; 12 | } 13 | 14 | .top-primary-header { 15 | background: linear-gradient(to left, #000460, #004e92); 16 | 17 | h1 { 18 | font-weight: 600; 19 | margin: 0; 20 | padding: 0; 21 | font-size: 22px; 22 | color: #fff; 23 | text-align: center; 24 | } 25 | } 26 | 27 | .card-button-wrapper { 28 | display: flex; 29 | padding: 20px 0; 30 | justify-content: center; 31 | } 32 | 33 | .products-wrapp { 34 | display: flex; 35 | justify-content: space-between; 36 | flex-wrap: wrap; 37 | } 38 | 39 | .wrapper { 40 | display: grid; 41 | display: -ms-grid; 42 | grid-template-areas: "sidebar main"; 43 | grid-template-columns: 200px 1fr; 44 | -ms-grid-columns: 200px 1fr; 45 | grid-column-gap: 15px; 46 | 47 | .main-wrap { 48 | grid-area: main; 49 | 50 | .category-title { 51 | margin: 0; 52 | } 53 | } 54 | 55 | .sidebar-wrap { 56 | grid-area: sidebar; 57 | } 58 | 59 | @media screen and (max-width: 600px) { 60 | grid-template-areas: "sidebar" "main"; 61 | grid-template-columns: auto; 62 | 63 | .main-wrap { 64 | grid-area: main; 65 | } 66 | 67 | .sidebar-wrap { 68 | grid-area: sidebar; 69 | } 70 | } 71 | 72 | .disabled { 73 | cursor: not-allowed !important; 74 | pointer-events: none !important; 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/app/services/api.service.ts: -------------------------------------------------------------------------------- 1 | import { WindowService } from './window.service'; 2 | import { map } from 'rxjs/operators'; 3 | import { Inject, Injectable, Injector, PLATFORM_ID } from '@angular/core'; 4 | import { HttpClient } from '@angular/common/http'; 5 | import { REQUEST } from '@nguniversal/express-engine/tokens'; 6 | import { isPlatformBrowser, isPlatformServer } from '@angular/common'; 7 | 8 | @Injectable({ 9 | providedIn: 'root' 10 | }) 11 | export class ApiService { 12 | 13 | baseUrl = ''; 14 | 15 | constructor( 16 | private readonly http: HttpClient, 17 | private readonly _injector: Injector, 18 | private readonly _window : WindowService, 19 | @Inject(PLATFORM_ID) 20 | private _platformId : Object 21 | ) { 22 | 23 | if (isPlatformServer(this._platformId)) { 24 | const serverRequest = REQUEST ? this._injector.get(REQUEST) : ''; 25 | this.baseUrl = serverRequest ? `${serverRequest.protocol}://${serverRequest.get('Host')}` : ''; 26 | } 27 | 28 | if (isPlatformBrowser(this._platformId)) { 29 | this.baseUrl = this._window.location.origin || ''; 30 | } 31 | } 32 | 33 | // auth 34 | getUser() { 35 | const userUrl = this.baseUrl + '/auth/current_user'; 36 | return this.http.get(userUrl); 37 | } 38 | 39 | // orders 40 | handleToken(token) { 41 | const tokenUrl = this.baseUrl + '/api/stripe'; 42 | return this.http.post(tokenUrl, token); 43 | }; 44 | 45 | makeOrder(req) { 46 | const addOrder = this.baseUrl + '/api/order/add'; 47 | return this.http.post(addOrder, req); 48 | } 49 | 50 | sendContact(req) { 51 | const sendContact = this.baseUrl + '/api/contact'; 52 | return this.http.post(sendContact, req); 53 | } 54 | 55 | // products 56 | loadProducts(req) { 57 | const productsUrl = this.baseUrl + '/prod/products/' + req.lang + '/' + req.page + '/' + req.sort; 58 | return this.http.get(productsUrl).pipe(map((data: any) => ({ 59 | products : data.docs 60 | .map(product => ({...product, 61 | categories: product.categories.filter(Boolean).map(category => category.toLowerCase()), 62 | tags: product.tags.map(tag => tag ? tag.toLowerCase() : '')})), 63 | pagination: { 64 | limit: data.limit, 65 | page: data.page, 66 | pages: data.pages, 67 | total: data.total, 68 | range: Array(data.pages).fill(0).map((v, i) => i + 1) 69 | }, 70 | }))) 71 | } 72 | 73 | loadCategoryProducts(req) { 74 | const productsUrl = this.baseUrl + '/prod/categoryProducts/' + req.lang + '/' + req.category + '/' + req.page + '/' + req.sort; 75 | return this.http.get(productsUrl).pipe(map((data: any) => ({ 76 | products : data.docs 77 | .map(product => ({...product, 78 | categories: product.categories.filter(Boolean).map(category => category.toLowerCase()), 79 | tags: product.tags.map(tag => tag ? tag.toLowerCase() : '')})), 80 | pagination: { 81 | limit: data.limit, 82 | page: data.page, 83 | pages: data.pages, 84 | total: data.total, 85 | range: Array(data.pages).fill(0).map((v, i) => i + 1) 86 | }, 87 | category: req.category 88 | }))) 89 | } 90 | 91 | 92 | loadCategories(payload) { 93 | const categoriesUrl = this.baseUrl + '/prod/categories/' + payload.lang; 94 | return this.http.get(categoriesUrl) 95 | } 96 | 97 | loadProductsSearch(query: string) { 98 | const productUrl = this.baseUrl + '/prod/productQuery/' + query; 99 | return this.http.get(productUrl); 100 | } 101 | 102 | getProduct(params) { 103 | const productUrl = this.baseUrl + '/prod/productId/' + params; 104 | return this.http.get(productUrl); 105 | } 106 | 107 | getUserOrders(req) { 108 | const userOrderUrl = this.baseUrl + '/prod/orders'; 109 | return this.http.post(userOrderUrl, req); 110 | } 111 | 112 | // cart 113 | getCart() { 114 | const cartUrl = this.baseUrl + '/cartApi/cart/'; 115 | return this.http.get(cartUrl); 116 | } 117 | 118 | addToCart(params: string) { 119 | const addToCartUrl = this.baseUrl + '/cartApi/addcart/' + params; 120 | return this.http.get(addToCartUrl); 121 | } 122 | 123 | removeFromCart(params: string) { 124 | const removeFromCartUrl = this.baseUrl + '/cartApi/removefromcart/' + params; 125 | return this.http.get(removeFromCartUrl); 126 | } 127 | 128 | // dashboard 129 | addProductImagesUrl({imageUrl, titleUrl}) { 130 | const addImageUrl = this.baseUrl + '/admin/addimageurl' + (titleUrl ? '/' + titleUrl : ''); 131 | return this.http.post(addImageUrl, { imageUrl }); 132 | } 133 | 134 | removeImage({image, titleUrl}) { 135 | const removeImage = this.baseUrl + '/admin/removeimage' + (titleUrl ? '/' + titleUrl : ''); 136 | return this.http.post(removeImage, { image }); 137 | } 138 | 139 | addProduct(product) { 140 | const addProduct = this.baseUrl + '/admin/addproduct'; 141 | return this.http.post(addProduct, product); 142 | } 143 | 144 | editProduct(product) { 145 | const eidtProduct = this.baseUrl + '/admin/udpateproduct'; 146 | return this.http.post(eidtProduct, product); 147 | } 148 | 149 | removeProduct(name: string) { 150 | const removeProduct = this.baseUrl + '/admin/removeproduct/' + name; 151 | return this.http.get(removeProduct); 152 | } 153 | 154 | getOrders() { 155 | const ordersUrl = this.baseUrl + '/admin/orders'; 156 | return this.http.get(ordersUrl); 157 | } 158 | 159 | getOrder(id: string) { 160 | const orderUrl = this.baseUrl + '/admin/orderId/' + id; 161 | return this.http.get(orderUrl); 162 | } 163 | 164 | updateOrder(req) { 165 | const orderUpdateUrl = this.baseUrl + '/admin/updateOrder'; 166 | return this.http.post(orderUpdateUrl, req); 167 | } 168 | 169 | getAllTranslations() { 170 | const translationsUrl = this.baseUrl + '/admin/translations'; 171 | return this.http.get(translationsUrl); 172 | } 173 | 174 | getLangTranslations(lang) { 175 | const translationsUrl = this.baseUrl + '/admin/translations/' + lang; 176 | return this.http.get(translationsUrl); 177 | } 178 | 179 | editTranslation({lang, keys}) { 180 | const translationsUpdateUrl = this.baseUrl + '/admin/updateTranslation/' + lang; 181 | return this.http.post(translationsUpdateUrl, { keys : keys }); 182 | } 183 | 184 | changeCurrencyValue(currency) { 185 | const currencyConvertUrl = 'https://free.currencyconverterapi.com/api/v5/convert?q=EUR_' + currency + '&compact=y'; 186 | return this.http.get(currencyConvertUrl).pipe(map(res => res['EUR_' + currency])); 187 | } 188 | 189 | getLocation$() { 190 | const locationFindUrl = 'https://ipinfo.io'; 191 | return this.http.get(locationFindUrl) 192 | .pipe(map((response: any ) => { 193 | const country = response.country ? response.country.toLowerCase() : ''; 194 | if (country === 'sk') { 195 | return country; 196 | } else if (country === 'cz') { 197 | return 'cs'; 198 | } else { 199 | return 'en'; 200 | } 201 | 202 | })) 203 | } 204 | 205 | 206 | } 207 | -------------------------------------------------------------------------------- /src/app/services/auth-admin.guard.ts: -------------------------------------------------------------------------------- 1 | import { map } from 'rxjs/operators'; 2 | import { Injectable } from '@angular/core'; 3 | import { CanLoad, CanActivate, Router } from '@angular/router'; 4 | import { AuthService } from './auth.service'; 5 | import { Observable } from 'rxjs'; 6 | 7 | 8 | 9 | @Injectable({ 10 | providedIn: 'root' 11 | }) 12 | export class AuthGuardAdmin implements CanLoad, CanActivate { 13 | 14 | constructor(private authService: AuthService, private router: Router) {} 15 | 16 | canLoad(): Observable { 17 | return this.authService.isAdmin.pipe(map(user => { 18 | if (user) { 19 | return user; 20 | } else { 21 | this.router.navigate(['']); 22 | return user; 23 | } 24 | })); 25 | } 26 | 27 | canActivate(): Observable { 28 | return this.authService.isAdmin.pipe(map(user => { 29 | if (user) { 30 | return user; 31 | } else { 32 | this.router.navigate(['']); 33 | return user; 34 | } 35 | })); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/app/services/auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { map } from 'rxjs/operators'; 2 | import { Injectable } from '@angular/core'; 3 | import { CanLoad, CanActivate, Router } from '@angular/router'; 4 | import { AuthService } from './auth.service'; 5 | import { Observable } from 'rxjs'; 6 | 7 | 8 | 9 | @Injectable({ 10 | providedIn: 'root' 11 | }) 12 | export class AuthGuard implements CanLoad, CanActivate { 13 | 14 | constructor(private authService: AuthService, private router: Router) {} 15 | 16 | canLoad(): Observable { 17 | return this.authService.isLoggedIn.pipe(map(user => { 18 | if (user) { 19 | return user; 20 | } else { 21 | this.router.navigate(['']); 22 | return user; 23 | } 24 | })); 25 | } 26 | 27 | canActivate(): Observable { 28 | return this.authService.isLoggedIn.pipe(map(user => { 29 | if (user) { 30 | return user; 31 | } else { 32 | this.router.navigate(['']); 33 | return user; 34 | } 35 | })); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/app/services/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ApiService } from './api.service'; 3 | import { Observable } from 'rxjs'; 4 | import { first, map } from 'rxjs/operators'; 5 | 6 | 7 | 8 | @Injectable({ 9 | providedIn: 'root' 10 | }) 11 | export class AuthService { 12 | 13 | constructor(public apiService: ApiService) { } 14 | 15 | get isLoggedIn(): Observable { 16 | return this.apiService.getUser().pipe( 17 | first(), 18 | map((user: any) => { 19 | return (user && user._id) ? true : false; 20 | })); 21 | } 22 | 23 | get isAdmin(): Observable { 24 | return this.apiService.getUser().pipe( 25 | first(), 26 | map((user: any) => { 27 | return (user && user.admin) ? true : false; 28 | })); 29 | 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/app/services/browser-http-interceptor.ts: -------------------------------------------------------------------------------- 1 | import { catchError } from 'rxjs/operators'; 2 | import { Observable, of, throwError } from 'rxjs'; 3 | import { Injectable } from '@angular/core'; 4 | import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest, HttpResponse } from '@angular/common/http'; 5 | import { TransferState, makeStateKey, StateKey } from '@angular/platform-browser'; 6 | 7 | @Injectable() 8 | export class BrowserHttpInterceptor implements HttpInterceptor { 9 | key : StateKey; 10 | 11 | constructor(private _transferState: TransferState) { 12 | } 13 | 14 | intercept(request: HttpRequest, next: HttpHandler): Observable> { 15 | 16 | if (request.method !== 'GET') { 17 | return next.handle(request).pipe( 18 | catchError((error: HttpResponse) => { 19 | this._handleError(error.url, error.status); 20 | return throwError(error); 21 | })); 22 | } 23 | 24 | this.key = makeStateKey>(request.url); 25 | const storedResponse: any = this._transferState.get(this.key, null); 26 | 27 | if (storedResponse) { 28 | const response = new HttpResponse({ body: storedResponse, status: 200 }); 29 | return of(response); 30 | } 31 | 32 | return next.handle(request).pipe( 33 | catchError((error: HttpResponse) => { 34 | this._handleError(error.url, error.status); 35 | return throwError(error); 36 | })); 37 | } 38 | 39 | 40 | private _handleError(url: string, statusCode: number): void { 41 | switch (statusCode) { 42 | case 404: 43 | console.warn('HTTP status code: 404: ', url, statusCode); // tslint:disable-line no-console 44 | break; 45 | case 410: 46 | console.warn('HTTP status code: 410: ', url, statusCode); // tslint:disable-line no-console 47 | break; 48 | case 500: 49 | console.warn('HTTP status code: 500: ', url, statusCode); // tslint:disable-line no-console 50 | break; 51 | case 503: 52 | console.warn('HTTP status code: 503: ', url, statusCode); // tslint:disable-line no-console 53 | break; 54 | default: 55 | console.warn('HTTP status code: Unhandled ', url, statusCode); // tslint:disable-line no-console 56 | break; 57 | } 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/app/services/server-http-interceptor.ts: -------------------------------------------------------------------------------- 1 | import { Observable, of } from 'rxjs'; 2 | import { tap } from 'rxjs/operators'; 3 | import { Injectable } from '@angular/core'; 4 | import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest, HttpResponse } from '@angular/common/http'; 5 | import { TransferState, makeStateKey, StateKey } from '@angular/platform-browser'; 6 | 7 | 8 | @Injectable() 9 | export class ServerHttpInterceptor implements HttpInterceptor { 10 | key : StateKey; 11 | 12 | constructor(private _transferState: TransferState) {} 13 | 14 | intercept(request: HttpRequest, next: HttpHandler): Observable> { 15 | return next.handle(request).pipe(tap(event => { 16 | if (event instanceof HttpResponse && (request.method === 'GET' && !request.url.includes('cartApi') && !request.url.includes('auth'))) { 17 | this.key = makeStateKey>(request.url); 18 | this._transferState.set(this.key, event.body); 19 | } 20 | })); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/app/services/translate.service.ts: -------------------------------------------------------------------------------- 1 | import { filter, take } from 'rxjs/operators'; 2 | import { BehaviorSubject } from 'rxjs'; 3 | import { ApiService } from './api.service'; 4 | import { Injectable, Injector } from '@angular/core'; 5 | import { CookieService } from 'ngx-cookie-service'; 6 | 7 | @Injectable({ 8 | providedIn: 'root' 9 | }) 10 | export class TranslateService { 11 | 12 | translationsSub$ : BehaviorSubject = new BehaviorSubject({}); 13 | languageSub$ : BehaviorSubject = new BehaviorSubject(''); 14 | 15 | data = {}; 16 | lang = ''; 17 | 18 | constructor(private injector: Injector) {} 19 | 20 | private get _apiService(): ApiService { 21 | return this.injector.get(ApiService); 22 | } 23 | 24 | private get _cookie(): CookieService { 25 | return this.injector.get(CookieService); 26 | } 27 | 28 | getLocation$() { 29 | return this._apiService.getLocation$().pipe(filter(Boolean, take(1))); 30 | } 31 | 32 | getTranslationsData(lang: string) { 33 | return this._apiService.getLangTranslations(lang).subscribe( 34 | (translation: any) => { 35 | const translationKeys = translation && translation['keys'] ? translation['keys'] : {}; 36 | this.translationsSub$.next(translationKeys); 37 | return Object.assign({}, translationKeys); 38 | }, 39 | error => { 40 | return {}; 41 | } 42 | ); 43 | } 44 | 45 | use(lang: string): Promise<{}> { 46 | return new Promise<{}>((resolve, reject) => { 47 | const foundLang = lang || this._cookie.get('lang'); 48 | const defaultLang = 'en'; 49 | const useLang = foundLang || defaultLang; 50 | this.data = this.getTranslationsData(useLang); 51 | this.lang = useLang; 52 | 53 | if (lang || !this._cookie.get('lang')) { 54 | this._cookie.set('lang', useLang); 55 | } 56 | 57 | this.languageSub$.next(useLang); 58 | resolve(this.data); 59 | 60 | // this.getLocation$() 61 | // .subscribe(langByCountry => { 62 | // this.data = this.getTranslationsData(langByCountry); 63 | // this.languageSub$.next(langByCountry); 64 | // this.lang = langByCountry; 65 | // resolve(this.data); 66 | // }) 67 | }); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/app/services/window.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | 4 | 5 | @Injectable({ 6 | providedIn: 'root' 7 | }) 8 | export class WindowService { 9 | self : Window; 10 | location : { href: string, origin: string }; 11 | document : Document; 12 | 13 | constructor() { 14 | this.location = { 15 | href: null, 16 | origin : '' 17 | }; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/app/shared/card/card.component.html: -------------------------------------------------------------------------------- 1 | 2 | credit_card{{ 'Pay' | translate | async }} {{price}} {{ currency }} 3 | 4 | -------------------------------------------------------------------------------- /src/app/shared/card/card.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: block; 3 | 4 | .cart-items { 5 | display: flex; 6 | justify-content: space-between; 7 | align-items: center; 8 | 9 | .cart-title { 10 | padding: 0 10px; 11 | } 12 | 13 | .cart-image { 14 | display: flex; 15 | align-items: center; 16 | width: 50%; 17 | 18 | img { 19 | height: 70px; 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/app/shared/card/card.component.ts: -------------------------------------------------------------------------------- 1 | declare const StripeCheckout: any; 2 | 3 | import { Component, Input, HostListener, Inject, PLATFORM_ID } from '@angular/core'; 4 | import { isPlatformServer, DOCUMENT } from '@angular/common'; 5 | import { Store } from '@ngrx/store'; 6 | import * as actions from './../../store/actions' 7 | import * as fromRoot from '../../store/reducers'; 8 | 9 | import { keys } from './../../../config/keys'; 10 | 11 | @Component({ 12 | selector: 'app-card', 13 | templateUrl: './card.component.html', 14 | styleUrls: ['./card.component.scss'] 15 | }) 16 | 17 | export class CardComponent { 18 | 19 | @Input() price: number; 20 | @Input() currency: string; 21 | handler: any; 22 | 23 | constructor(private store: Store, 24 | @Inject(DOCUMENT) 25 | private _document : Document, 26 | @Inject(PLATFORM_ID) 27 | private _platformId : Object) { 28 | 29 | if (!isPlatformServer(this._platformId) && typeof StripeCheckout !== 'object') { 30 | const parentElement : HTMLElement = this._document.querySelector('head') as HTMLElement; 31 | const scriptEl : any = this._document.createElement('script') as HTMLElement; 32 | scriptEl.setAttribute('type', 'text/javascript'); 33 | scriptEl.setAttribute('src', 'https://checkout.stripe.com/checkout.js'); 34 | parentElement.appendChild(scriptEl); 35 | scriptEl.onload = (event: Event) => { 36 | this._setHandler(); 37 | } 38 | } else if (typeof StripeCheckout === 'object' && typeof StripeCheckout.configure === 'function') { 39 | this._setHandler(); 40 | } 41 | } 42 | 43 | onClickBuy() { 44 | this.handler.open({ 45 | name: 'Bluetooth eshop', 46 | description: 'Pay for products', 47 | amount: this.price * 100, 48 | billingAddress: true, 49 | allowRememberMe: false, 50 | locale: 'auto', 51 | currency: 'EUR' 52 | }); 53 | } 54 | 55 | private _setHandler() { 56 | this.handler = StripeCheckout.configure({ 57 | key: keys.stripePublishableKey, 58 | locale: 'auto', 59 | token: token => { 60 | const payment = { token: token, amount: this.price, currency: this.currency}; 61 | this.store.dispatch(new actions.LoadPayment(payment)); 62 | } 63 | }); 64 | } 65 | 66 | @HostListener('window:popstate') 67 | onPopstate() { 68 | this.handler.close() 69 | } 70 | 71 | 72 | 73 | } 74 | -------------------------------------------------------------------------------- /src/app/shared/cart-show/cart-show.component.html: -------------------------------------------------------------------------------- 1 |
2 | add_shopping_cart 3 |
4 | 5 | {{items}} ks 6 | 7 |
8 | remove_shopping_cart 9 |
10 | -------------------------------------------------------------------------------- /src/app/shared/cart-show/cart-show.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: flex; 3 | 4 | .cart-icon { 5 | cursor: pointer; 6 | color: #fff; 7 | font-size: 25px; 8 | } 9 | 10 | .cart-quantity { 11 | color: #fff; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/app/shared/cart-show/cart-show.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-cart-show', 5 | templateUrl: './cart-show.component.html', 6 | styleUrls: ['./cart-show.component.scss'], 7 | changeDetection: ChangeDetectionStrategy.OnPush 8 | }) 9 | export class CartShowComponent { 10 | @Input() items: number; 11 | @Output() add: EventEmitter = new EventEmitter(); 12 | @Output() remove: EventEmitter = new EventEmitter(); 13 | 14 | constructor() { } 15 | 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/app/shared/pagination/pagination.component.html: -------------------------------------------------------------------------------- 1 | 22 | -------------------------------------------------------------------------------- /src/app/shared/pagination/pagination.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: block; 3 | } 4 | -------------------------------------------------------------------------------- /src/app/shared/pagination/pagination.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, Output, EventEmitter } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-pagination', 5 | templateUrl: './pagination.component.html', 6 | styleUrls: ['./pagination.component.scss'] 7 | }) 8 | export class PaginationComponent { 9 | @Input() page: any; 10 | @Input() category: any; 11 | @Input() pagination: any; 12 | 13 | @Output() changePage: EventEmitter = new EventEmitter(); 14 | 15 | 16 | constructor() { } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/app/shared/products-list/products-list.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
5 |

{{product?.title}}

6 | 7 | 8 |
9 |
10 |

{{product?.description}}

11 |
12 |
13 | 15 | 16 | 17 |
18 |
19 |
{{ (product?.salePrice * (convertVal || 1)) | priceFormat }}{{ currency }}
20 |

{{ product?.stock | translate | async }}

21 | 25 | 26 |
27 |
28 |
29 |
30 | -------------------------------------------------------------------------------- /src/app/shared/products-list/products-list.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | // display: grid; 3 | // display: -ms-grid; 4 | // -ms-grid-columns: 1fr 1fr 1fr; 5 | // grid-template-columns: repeat( 3, 1fr ); 6 | // grid-gap: 15px; // edge - ie grid problem 7 | 8 | display: flex; 9 | flex-wrap: wrap; 10 | 11 | // @media(max-width: 1000px) { 12 | // -ms-grid-columns: 1fr 1fr; 13 | // grid-template-columns: repeat( 2, 1fr ); 14 | // } 15 | 16 | // @media(max-width: 650px) { 17 | // -ms-grid-columns: 1fr; 18 | // grid-template-columns: 1fr; 19 | // } 20 | 21 | .card { 22 | flex-basis: 20%; 23 | margin-right: 5%; 24 | overflow: hidden; 25 | transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1) !important; 26 | display: flex; 27 | flex-flow: column; 28 | justify-content: space-between; 29 | 30 | @media (max-width: 1400px) { 31 | flex-basis: 30%; 32 | margin-right: 3%; 33 | } 34 | 35 | @media (max-width: 1000px) { 36 | flex-basis: 48%; 37 | margin-right: 2%; 38 | } 39 | 40 | @media (max-width: 650px) { 41 | flex-basis: 100%; 42 | } 43 | 44 | .card-action { 45 | // background: linear-gradient(to bottom, #ece9e6, #fff); 46 | border-top: none; 47 | padding: 0; 48 | } 49 | 50 | .card-image { 51 | background-repeat: no-repeat; 52 | background-position: center !important; 53 | min-height: 300px; 54 | background-size: cover !important; 55 | cursor: pointer; 56 | overflow: hidden; 57 | display: block; 58 | transition: all 0.2s ease-in-out; 59 | 60 | img { 61 | transition: all 0.2s ease-in-out; 62 | max-height: 300px; 63 | 64 | @media (max-width: 1000px) { 65 | max-height: 250px; 66 | } 67 | 68 | @media (max-width: 650px) { 69 | max-height: 400px; 70 | } 71 | } 72 | } 73 | 74 | &:hover { 75 | box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22); 76 | 77 | .card-image { 78 | transform: scale(1.1); 79 | } 80 | } 81 | } 82 | 83 | .action-wrap { 84 | display: flex; 85 | align-items: center; 86 | justify-content: space-between; 87 | color: #fff; 88 | padding: 5px 20px; 89 | position: relative; 90 | box-shadow: 0 0 40px -9px #000460; 91 | background: linear-gradient(to left, #000460, #004e92); 92 | z-index: 2; 93 | overflow: visible; 94 | 95 | .price-info { 96 | font-weight: 600; 97 | font-size: 18px; 98 | } 99 | } 100 | 101 | .info-wrapp { 102 | padding: 0 15px 0 15px; 103 | margin-bottom: 15px; 104 | 105 | p { 106 | margin: 0; 107 | font-size: 13px; 108 | } 109 | } 110 | 111 | .cart-icon { 112 | cursor: pointer; 113 | color: #c6ff00; 114 | font-size: 25px; 115 | } 116 | 117 | .cart-quantity { 118 | color: #c6ff00; 119 | } 120 | 121 | .price-card { 122 | display: flex; 123 | justify-content: space-between; 124 | font-weight: 600; 125 | font-size: 16px; 126 | padding: 15px 15px 5px 15px; 127 | align-items: flex-start; 128 | } 129 | 130 | .price-card h2 { 131 | font-size: 18px; 132 | line-height: 20px; 133 | margin: 0 5px 0 0; 134 | font-weight: 600; 135 | padding: 0; 136 | } 137 | 138 | .detail-link-wrapp { 139 | font-size: 16px; 140 | margin-top: -5px; 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/app/shared/products-list/products-list.component.ts: -------------------------------------------------------------------------------- 1 | import { filter } from 'rxjs/operators'; 2 | import { TranslateService } from './../../services/translate.service'; 3 | import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core'; 4 | 5 | @Component({ 6 | selector: 'app-products-list', 7 | templateUrl: './products-list.component.html', 8 | styleUrls: ['./products-list.component.scss'], 9 | changeDetection: ChangeDetectionStrategy.OnPush 10 | }) 11 | export class ProductsListComponent { 12 | @Input() products: any; 13 | @Input() cartIds: any; 14 | @Input() convertVal : number; 15 | @Input() currency: string; 16 | @Output() addProduct: EventEmitter = new EventEmitter(); 17 | @Output() removeProduct: EventEmitter = new EventEmitter(); 18 | 19 | productUrl: string; 20 | 21 | constructor(private translate: TranslateService) { 22 | this.translate.translationsSub$ 23 | .pipe(filter(Boolean)) 24 | .subscribe(translations => { 25 | this.productUrl = '/' + this.translate.lang + '/' + (translations['product'] || 'product'); 26 | }); 27 | } 28 | 29 | onAddProduct(id) { 30 | this.addProduct.emit(id); 31 | } 32 | 33 | onRemoveProduct(id) { 34 | this.removeProduct.emit(id); 35 | } 36 | 37 | trackById(index, item) { 38 | return item._id; 39 | 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/app/shared/shared.module.ts: -------------------------------------------------------------------------------- 1 | import { PipeModule } from './../pipes/pipe.module'; 2 | import { LazyModule } from './../utils/lazyLoadImg/lazy.module'; 3 | import { CommonModule } from '@angular/common'; 4 | import { NgModule } from '@angular/core'; 5 | import { ReactiveFormsModule, FormsModule } from '@angular/forms'; 6 | import { RouterModule } from '@angular/router'; 7 | 8 | 9 | import { CardComponent } from './card/card.component'; 10 | import { CartShowComponent } from './cart-show/cart-show.component'; 11 | import { SidebarComponent } from './sidebar/sidebar.component'; 12 | import { ProductsListComponent } from './products-list/products-list.component'; 13 | import { PaginationComponent } from './pagination/pagination.component'; 14 | 15 | @NgModule({ 16 | declarations: [ 17 | CardComponent, 18 | CartShowComponent, 19 | SidebarComponent, 20 | ProductsListComponent, 21 | PaginationComponent 22 | ], 23 | imports: [ 24 | ReactiveFormsModule, 25 | FormsModule, 26 | CommonModule, 27 | RouterModule, 28 | LazyModule, 29 | PipeModule 30 | ], 31 | providers: [], 32 | exports: [ 33 | CardComponent, 34 | CartShowComponent, 35 | SidebarComponent, 36 | ProductsListComponent, 37 | PaginationComponent 38 | ] 39 | }) 40 | export class SharedModule { } 41 | -------------------------------------------------------------------------------- /src/app/shared/sidebar/sidebar.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

{{ 'Categories' | translate | async }}

4 |
5 | 9 | {{ 'All' | translate | async }} {{ 'products' | translate | async }} 10 | 11 | 17 | {{category.title}} 18 | 19 |
20 |
21 |

{{ 'Sorting' | translate | async }}

22 |
23 | 34 |
35 |
36 |

{{ 'PriceRange' | translate | async }}

37 |
38 | 48 | {{ 'Price' | translate | async }} {{ 'to' | translate | async }}: {{ (ref.value * (convertVal || 1)) | priceFormat }}{{ currency }} 49 |
50 | -------------------------------------------------------------------------------- /src/app/shared/sidebar/sidebar.component.scss: -------------------------------------------------------------------------------- 1 | .collection { 2 | padding: 5px; 3 | background: #fff; 4 | 5 | h4 { 6 | font-size: 20px; 7 | font-weight: 600; 8 | } 9 | 10 | .input-field > select { 11 | width: 100%; 12 | pointer-events: initial; 13 | height: initial; 14 | opacity: 1; 15 | cursor: pointer; 16 | } 17 | 18 | .sort-wrap { 19 | height: 120px; 20 | } 21 | 22 | .collection-item { 23 | color: #064887; 24 | 25 | &.active { 26 | background-color: #064887; 27 | color: #fff; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/app/shared/sidebar/sidebar.component.ts: -------------------------------------------------------------------------------- 1 | import { filter } from 'rxjs/operators'; 2 | import { TranslateService } from './../../services/translate.service'; 3 | import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core'; 4 | 5 | @Component({ 6 | selector: 'app-sidebar', 7 | templateUrl: './sidebar.component.html', 8 | styleUrls: ['./sidebar.component.scss'], 9 | changeDetection: ChangeDetectionStrategy.OnPush 10 | }) 11 | export class SidebarComponent { 12 | @Input() categories: any; 13 | @Input() activeCategory?: string; 14 | @Input() minPrice: number; 15 | @Input() maxPrice: number; 16 | @Input() price: number; 17 | @Input() sortOptions: Array; 18 | @Input() choosenSort: string; 19 | @Input() convertVal : number; 20 | @Input() currency: string; 21 | 22 | @Output() changePrice: EventEmitter = new EventEmitter(); 23 | @Output() changeSort: EventEmitter = new EventEmitter(); 24 | @Output() changeCategory: EventEmitter = new EventEmitter(); 25 | 26 | productsUrl: string; 27 | categoryUrl: string; 28 | 29 | constructor(private translate: TranslateService) { 30 | this.translate.translationsSub$ 31 | .pipe(filter(Boolean)) 32 | .subscribe(translations => { 33 | this.productsUrl = '/' + this.translate.lang + '/' + (translations['products'] ? translations['products'].toLowerCase() : 'products'); 34 | this.categoryUrl = '/' + this.translate.lang + '/' + (translations['category']); 35 | }); 36 | } 37 | 38 | onInputChange($event: any): void { 39 | this.changeSort.emit($event); 40 | } 41 | 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/app/store/reducers/auth.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as actions from './../actions'; 3 | 4 | 5 | export interface State { 6 | user: any; 7 | lang: string; 8 | currency: string; 9 | convertVal: number; 10 | } 11 | 12 | export const initialState: State = { 13 | user: null, 14 | lang: '', 15 | currency: '€', 16 | convertVal: 0 17 | }; 18 | 19 | 20 | export function appReducer(state = initialState, action): State { 21 | switch (action.type) { 22 | 23 | case actions.STORE_USER_ACTION: { 24 | return { ...state, user: action.payload }; 25 | } 26 | 27 | case actions.ADD_PRODUCT_IMAGES_URL_SUCCESS: { 28 | return { ...state, user: action.payload.admin ? action.payload : state.user }; 29 | } 30 | 31 | case actions.REMOVE_PRODUCT_IMAGE_SUCCESS: { 32 | return { ...state, user: action.payload.admin ? action.payload : state.user }; 33 | } 34 | 35 | case actions.ADD_PRODUCT_IMAGE: { 36 | return {...state, user: { ...state.user, images: action.payload } }; 37 | } 38 | 39 | case actions.CHANGE_LANG: { 40 | return {...state, lang: action.payload.lang, currency: action.payload.currency, convertVal: 0 }; 41 | } 42 | 43 | case actions.CHANGE_LANG_SUCCESS: { 44 | return {...state, convertVal: action.payload.val }; 45 | } 46 | 47 | 48 | default: { 49 | return state; 50 | } 51 | } 52 | } 53 | 54 | export const user = (state: State) => state.user; 55 | export const lang = (state: State) => state.lang; 56 | export const currency = (state: State) => state.currency; 57 | export const convertVal = (state: State) => state.convertVal; 58 | -------------------------------------------------------------------------------- /src/app/store/reducers/dashboard.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as actions from './../actions'; 3 | 4 | 5 | export interface State { 6 | orders: null; 7 | order: any; 8 | productImages: Array; 9 | translations: Array; 10 | } 11 | 12 | export const initialState: State = { 13 | orders: null, 14 | order: null, 15 | productImages: [], 16 | translations: [] 17 | }; 18 | 19 | 20 | 21 | export function dashboardReducer(state = initialState, action): State { 22 | switch (action.type) { 23 | 24 | case actions.LOAD_ORDERS_SUCCESS: { 25 | return { ...state, orders: action.payload } } 26 | 27 | case actions.LOAD_ORDER_SUCCESS: { 28 | return { ...state, order: action.payload }} 29 | 30 | case actions.ADD_PRODUCT_IMAGE: { 31 | return { ...state, productImages: action.payload } 32 | } 33 | 34 | case actions.GET_ALL_TRANSLATIONS_SUCCESS: { 35 | return { ...state, translations: action.payload } } 36 | 37 | default: { 38 | return state; 39 | } 40 | } 41 | } 42 | 43 | export const orders = (state: State) => state.orders; 44 | export const order = (state: State) => state.order; 45 | export const productImages = (state: State) => state.productImages; 46 | export const translations = (state: State) => state.translations; 47 | -------------------------------------------------------------------------------- /src/app/store/reducers/index.ts: -------------------------------------------------------------------------------- 1 | import { createSelector, createFeatureSelector, combineReducers} from '@ngrx/store'; 2 | import * as fromAuth from './auth'; 3 | import * as fromProducts from './product'; 4 | import * as fromDashboard from './dashboard'; 5 | 6 | export interface State { 7 | auth: fromAuth.State; 8 | products: fromProducts.State; 9 | dashboard: fromDashboard.State; 10 | } 11 | 12 | export const reducers = { 13 | auth: fromAuth.appReducer, 14 | products: fromProducts.productReducer, 15 | dashboard: fromDashboard.dashboardReducer 16 | } 17 | 18 | 19 | export const getAuth = (state: State) => state.auth; 20 | export const Products = (state: State) => state.products; 21 | export const Dashboard = (state: State) => state.dashboard; 22 | 23 | export const getUser = createSelector(getAuth, fromAuth.user); 24 | export const getLang = createSelector(getAuth, fromAuth.lang); 25 | export const getCurrency = createSelector(getAuth, fromAuth.currency); 26 | export const getConvertVal = createSelector(getAuth, fromAuth.convertVal); 27 | 28 | 29 | export const getProducts = createSelector(Products, fromProducts.products); 30 | export const getLoadingProducts = createSelector(Products, fromProducts.loadingProducts); 31 | export const getCategories = createSelector(Products, fromProducts.categories); 32 | export const getPagination = createSelector(Products, fromProducts.pagination); 33 | export const getCategoriesPagination = createSelector(Products, fromProducts.categoriesPagination); 34 | export const getProduct = createSelector(Products, fromProducts.product); 35 | export const getProductLoading = createSelector(Products, fromProducts.productLoading); 36 | export const getCart = createSelector(Products, fromProducts.cart); 37 | export const getOrder = createSelector(Products, fromProducts.order); 38 | export const getUserOrders = createSelector(Products, fromProducts.userOrders); 39 | export const getProductTitles = createSelector(Products, fromProducts.productsTitles); 40 | export const getPriceFilter = createSelector(Products, fromProducts.priceFilter); 41 | export const getPosition = createSelector(Products, fromProducts.position); 42 | 43 | export const getOrderId = createSelector(Dashboard, fromDashboard.order); 44 | export const getOrders = createSelector(Dashboard, fromDashboard.orders); 45 | export const getProductImages = createSelector(Dashboard, fromDashboard.productImages); 46 | export const getAllTranslations = createSelector(Dashboard, fromDashboard.translations); 47 | -------------------------------------------------------------------------------- /src/app/store/reducers/product.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as actions from './../actions'; 3 | 4 | 5 | export interface State { 6 | products: any; 7 | loadingProducts: boolean; 8 | categories: Array; 9 | categoriesPagination: any; 10 | pagination: { 11 | page: number; 12 | pages: number; 13 | limit: number; 14 | total: number; 15 | }; 16 | product: any; 17 | loadingProduct: boolean; 18 | cart: any; 19 | userOrders: null; 20 | order: any; 21 | productsTitles: Array; 22 | priceFilter: number; 23 | position: any; 24 | } 25 | 26 | export const initialState: State = { 27 | products: null, 28 | loadingProducts: false, 29 | categories: [], 30 | categoriesPagination: {}, 31 | pagination: { 32 | page: 1, 33 | pages: 1, 34 | limit: 10, 35 | total: 0 36 | }, 37 | product: null, 38 | loadingProduct: false, 39 | cart: null, 40 | userOrders: null, 41 | order: null, 42 | productsTitles: [], 43 | priceFilter: Infinity, 44 | position: null 45 | }; 46 | 47 | 48 | 49 | export function productReducer(state = initialState, action): State { 50 | switch (action.type) { 51 | 52 | case actions.LOAD_PRODUCTS: { 53 | return {...state, loadingProducts: true }; 54 | } 55 | 56 | case actions.LOAD_CATEGORY_PRODUCTS: { 57 | return {...state, loadingProducts: true }; 58 | } 59 | 60 | 61 | case actions.LOAD_PRODUCTS_SUCESS: { 62 | return { ...state, 63 | products: action.payload.products, 64 | pagination: action.payload.pagination, 65 | loadingProducts: false } 66 | } 67 | 68 | case actions.LOAD_CATEGORY_PRODUCTS_SUCESS: { 69 | return { ...state, 70 | products: action.payload.products, 71 | categoriesPagination : {...state.categoriesPagination, [action.payload.category] : action.payload.pagination}, 72 | loadingProducts: false } 73 | } 74 | 75 | case actions.LOAD_CATEGORIES_SUCESS: { 76 | return { ...state, 77 | categories: action.payload } 78 | } 79 | 80 | 81 | case actions.GET_PRODUCT: { 82 | return { ...state, 83 | loadingProduct: true } 84 | } 85 | 86 | case actions.GET_PRODUCT_SUCESS: { 87 | return { ...state, 88 | product: action.payload, 89 | loadingProduct: false } 90 | } 91 | 92 | case actions.LOAD_PRODUCTS_SEARCH_SUCESS: { 93 | return { ...state, productsTitles: action.payload } 94 | } 95 | 96 | case actions.GET_CART_SUCCESS: 97 | case actions.ADD_TO_CART_SUCCESS: { 98 | return { ...state, 99 | cart: action.payload } 100 | } 101 | 102 | case actions.LOAD_PAYMENT_SUCCESS: 103 | return {...state, 104 | order: action.payload.order, 105 | cart: action.payload.cart 106 | } 107 | 108 | case actions.MAKE_ORDER_SUCCESS: 109 | return {...state, 110 | order: action.payload.order, 111 | cart: action.payload.cart 112 | } 113 | 114 | case actions.FILTER_PRICE: 115 | return {...state, priceFilter: action.payload }; 116 | 117 | case actions.LOAD_USER_ORDERS_SUCCESS: { 118 | return { ...state, userOrders: action.payload } } 119 | 120 | case actions.UPDATE_POSITION: { 121 | return { ...state, position: action.payload } } 122 | 123 | 124 | case actions.ADD_PRODUCT_IMAGE: { 125 | return { ...state, product: state.product 126 | ? {...state.product, images: [...state.product.images, ...action.payload ] 127 | .filter((v, i, a) => i === a.indexOf(v)) } 128 | : null } 129 | } 130 | 131 | case actions.ADD_PRODUCT_IMAGES_URL_SUCCESS: { 132 | return { ...state, product: state.product 133 | ? {...state.product, images: [...state.product.images, ...action.payload ] 134 | .filter((v, i, a) => i === a.indexOf(v)) } 135 | : null } 136 | } 137 | 138 | case actions.REMOVE_PRODUCT_IMAGE_SUCCESS: { 139 | return { ...state, product: action.payload.admin ? state.product : action.payload}; 140 | } 141 | 142 | 143 | 144 | default: { 145 | return state; 146 | } 147 | } 148 | } 149 | 150 | export const products = (state: State) => state.products; 151 | export const loadingProducts = (state: State) => state.loadingProducts; 152 | export const categories = (state: State) => state.categories; 153 | export const pagination = (state: State) => state.pagination; 154 | export const categoriesPagination = (state: State) => state.categoriesPagination; 155 | export const product = (state: State) => state.product; 156 | export const cart = (state: State) => state.cart; 157 | export const productLoading = (state: State) => state.loadingProduct; 158 | export const userOrders = (state: State) => state.userOrders; 159 | export const order = (state: State) => state.order; 160 | export const productsTitles = (state: State) => state.productsTitles; 161 | export const priceFilter = (state: State) => state.priceFilter; 162 | 163 | export const position = (state: State) => state.position; 164 | -------------------------------------------------------------------------------- /src/app/utils/lazyLoadImg/lazy-src.directive.ts: -------------------------------------------------------------------------------- 1 | // Import the core angular services. 2 | import { Directive } from '@angular/core'; 3 | import { ElementRef } from '@angular/core'; 4 | import { OnDestroy } from '@angular/core'; 5 | import { OnInit, Input } from '@angular/core'; 6 | import { Renderer2 } from '@angular/core'; 7 | 8 | // Import the application components and services. 9 | import { LazyTarget } from './lazy-viewport'; 10 | import { LazyViewport } from './lazy-viewport'; 11 | 12 | // ----------------------------------------------------------------------------------- // 13 | // ----------------------------------------------------------------------------------- // 14 | @Directive({ 15 | selector: '[lazySrc]' 16 | }) 17 | export class LazySrcDirective implements OnInit, OnDestroy, LazyTarget { 18 | 19 | @Input('lazySrc') src: string; 20 | @Input('lazyBackground') backgroundUrl: string; 21 | @Input('lazySrcVisible') visibleClass: string; 22 | 23 | public element: Element; 24 | 25 | private lazyViewport: LazyViewport; 26 | private renderer: Renderer2; 27 | 28 | // I initialize the lazy-src directive. 29 | constructor( 30 | elementRef: ElementRef, 31 | lazyViewport: LazyViewport, 32 | renderer: Renderer2 33 | ) { 34 | 35 | this.element = elementRef.nativeElement; 36 | this.lazyViewport = lazyViewport; 37 | this.renderer = renderer; 38 | 39 | this.src = ''; 40 | this.backgroundUrl = ''; 41 | this.visibleClass = ''; 42 | 43 | } 44 | 45 | // --- 46 | // PUBLIC METHODS. 47 | // --- 48 | // I get called once when the directive is being destroyed. 49 | public ngOnDestroy(): void { 50 | 51 | // If we haven't detached from the LazyViewport, do so now. 52 | ( this.lazyViewport ) && this.lazyViewport.removeTarget( this ); 53 | 54 | } 55 | 56 | 57 | 58 | // I get called once after the inputs have been bound for the first time. 59 | public ngOnInit(): void { 60 | 61 | // Attached this directive the LazyViewport so that we can be alerted to changes 62 | // in this element's visibility on the page. 63 | this.lazyViewport.addTarget( this ); 64 | 65 | } 66 | 67 | 68 | // I get called by the LazyViewport service when the element associated with this 69 | // directive has its visibility changed. 70 | public updateVisibility( isVisible: boolean, ratio: number ): void { 71 | 72 | // When this target starts being tracked by the viewport, the initial visibility 73 | // will be reported, even if it is not visible. As such, let's ignore the first 74 | // visibility update. 75 | if ( ! isVisible ) { 76 | 77 | return; 78 | 79 | } 80 | 81 | // Now that the element is visible, load the underlying SRC value. And, since we 82 | // no longer need to worry about loading, we can detach from the LazyViewport. 83 | this.lazyViewport.removeTarget( this ); 84 | this.lazyViewport = null; 85 | if (this.src) { 86 | this.renderer.setProperty( this.element, 'src', this.src ); 87 | } 88 | if (this.backgroundUrl) { 89 | this.renderer.setStyle(this.element, 'background', this.backgroundUrl); 90 | } 91 | 92 | // If an active class has been provided, add it to the element. 93 | ( this.visibleClass ) && this.renderer.addClass( this.element, this.visibleClass ); 94 | 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /src/app/utils/lazyLoadImg/lazy-viewport.directive.ts: -------------------------------------------------------------------------------- 1 | // Import the core angular services. 2 | import { Directive } from '@angular/core'; 3 | import { ElementRef } from '@angular/core'; 4 | import { OnDestroy } from '@angular/core'; 5 | import { OnInit } from '@angular/core'; 6 | 7 | // Import the application components and services. 8 | import { LazyViewport } from './lazy-viewport'; 9 | 10 | // ----------------------------------------------------------------------------------- // 11 | // ----------------------------------------------------------------------------------- // 12 | @Directive({ 13 | selector: '[lazyViewport]', 14 | inputs: [ 'offset: lazyViewportOffset' ], 15 | 16 | // The primary role of this directive is to override the default LazyViewport 17 | // instance at this point in the component tree. This way, any lazy-directives 18 | // that are descendants of this element will receive this instance when using 19 | // dependency-injection. 20 | providers: [ 21 | { 22 | provide: LazyViewport, 23 | useClass: LazyViewport 24 | } 25 | ] 26 | }) 27 | export class LazyViewportDirective implements OnInit, OnDestroy { 28 | 29 | public offset: number; 30 | 31 | private elementRef: ElementRef; 32 | private lazyViewport: LazyViewport; 33 | 34 | // I initialize the lazy-viewport directive. 35 | constructor( 36 | elementRef: ElementRef, 37 | lazyViewport: LazyViewport 38 | ) { 39 | 40 | this.elementRef = elementRef; 41 | this.lazyViewport = lazyViewport; 42 | this.offset = 0; 43 | 44 | } 45 | 46 | // --- 47 | // PUBLIC METHODS. 48 | // --- 49 | // I get called once when the directive is being destroyed. 50 | public ngOnDestroy(): void { 51 | 52 | this.lazyViewport.teardown(); 53 | 54 | } 55 | 56 | 57 | // I get called once after the inputs have been bound for the first time. 58 | public ngOnInit(): void { 59 | 60 | // Ensure that the offset value is numeric when we go to initialize the viewport. 61 | if ( isNaN( +this.offset ) ) { 62 | 63 | console.warn( new Error( `[lazyViewportOffset] must be a number. Currently defined as [${ this.offset }].` ) ); 64 | this.offset = 0; 65 | 66 | } 67 | 68 | // Now that this LazyViewport directive has overridden the instance of 69 | // LazyViewport in the dependency-injection tree, we have to initialize it 70 | // to use the current element as the observer root. 71 | this.lazyViewport.setup( this.elementRef.nativeElement, +this.offset ); 72 | 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /src/app/utils/lazyLoadImg/lazy-viewport.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@angular/core"; 2 | export interface LazyTarget { 3 | element: Element; 4 | updateVisibility: ( isVisible: boolean, ratio: number ) => void; 5 | } 6 | 7 | export declare const window: Window; 8 | 9 | @Injectable() 10 | export class LazyViewport { 11 | 12 | private observer: IntersectionObserver; 13 | private targets: Map; 14 | 15 | // I initialize the lazy-viewport service. 16 | constructor() { 17 | 18 | this.observer = null; 19 | 20 | // The IntersectionObserver watches Elements. However, when an element visibility 21 | // changes, we have to alert an Angular Directive instance. As such, we're going 22 | // to keep a map of Elements-to-Directives. This way, when our observer callback 23 | // is invoked, we'll be able to extract the appropriate Directive from the 24 | // Element-based observer entries collection. 25 | this.targets = new Map(); 26 | 27 | } 28 | 29 | // --- 30 | // PUBLIC METHODS. 31 | // --- 32 | // I add the given LazyTarget implementation to the collection of objects being 33 | // tracked by the IntersectionObserver. 34 | public addTarget( target: LazyTarget ): void { 35 | 36 | if ( this.observer ) { 37 | 38 | this.targets.set( target.element, target ); 39 | this.observer.observe( target.element ); 40 | 41 | // If we don't actually have an observer (lacking browser support), then we're 42 | // going to punt on the feature for now and just immediately tell the target 43 | // that it is visible on the page. 44 | } else { 45 | 46 | target.updateVisibility( true, 1.0 ); 47 | 48 | } 49 | 50 | } 51 | 52 | 53 | // I setup the IntersectionObserver with the given element as the root. 54 | public setup( element: Element = null, offset: number = 0 ): void { 55 | 56 | // While the IntersectionObserver is supported in the modern browsers, it will 57 | // never be added to Internet Explorer (IE) and is not in my version of Safari 58 | // (at the time of this post). As such, we'll only use it if it's available. 59 | // And, if it's not, we'll fall-back to non-lazy behaviors. 60 | if (!window || (window && !window[ 'IntersectionObserver' ] )) { 61 | return; 62 | } 63 | 64 | this.observer = new IntersectionObserver( 65 | this.handleIntersectionUpdate, 66 | { 67 | root: element, 68 | rootMargin: `${ offset }px` 69 | } 70 | ); 71 | 72 | } 73 | 74 | 75 | // I remove the given LazyTarget implementation from the collection of objects being 76 | // tracked by the IntersectionObserver. 77 | public removeTarget( target: LazyTarget ): void { 78 | 79 | // If the IntersectionObserver isn't supported, we never started tracking the 80 | // given target in the first place. 81 | if ( this.observer ) { 82 | 83 | this.targets.delete( target.element ); 84 | this.observer.unobserve( target.element ); 85 | 86 | } 87 | 88 | } 89 | 90 | 91 | // I teardown this service instance. 92 | public teardown(): void { 93 | 94 | if ( this.observer ) { 95 | 96 | this.observer.disconnect(); 97 | this.observer = null; 98 | 99 | } 100 | 101 | this.targets.clear(); 102 | this.targets = null; 103 | 104 | } 105 | 106 | // --- 107 | // PRIVATE METHODS. 108 | // --- 109 | // I handle changes in the visibility for elements being tracked by the intersection 110 | // observer. 111 | // -- 112 | // CAUTION: Using fat-arrow binding for method. 113 | private handleIntersectionUpdate = ( entries: IntersectionObserverEntry[] ): void => { 114 | 115 | for ( var entry of entries ) { 116 | 117 | var lazyTarget = this.targets.get( entry.target ); 118 | 119 | ( lazyTarget ) && lazyTarget.updateVisibility( 120 | entry.isIntersecting, 121 | entry.intersectionRatio 122 | ); 123 | 124 | } 125 | 126 | } 127 | 128 | } 129 | -------------------------------------------------------------------------------- /src/app/utils/lazyLoadImg/lazy.module.ts: -------------------------------------------------------------------------------- 1 | // Import the core angular services. 2 | import { NgModule } from '@angular/core'; 3 | 4 | // Import the application components and services. 5 | import { LazySrcDirective } from './lazy-src.directive'; 6 | import { LazyViewport } from './lazy-viewport'; 7 | import { LazyViewportDirective } from './lazy-viewport.directive'; 8 | 9 | // ----------------------------------------------------------------------------------- // 10 | // ----------------------------------------------------------------------------------- // 11 | @NgModule({ 12 | declarations: [ 13 | LazySrcDirective, 14 | LazyViewportDirective 15 | ], 16 | exports: [ 17 | LazySrcDirective, 18 | LazyViewportDirective 19 | ], 20 | providers: [ 21 | // Setup the default LazyViewport instance without an associated element. This 22 | // will create a IntersectionObserver that uses the browser's viewport as the 23 | // observer root. This way, an instance of LazyViewport is always available for 24 | // injection into other directives and services. 25 | // -- 26 | // NOTE: This service will be overridden at lower-levels in the component tree 27 | // whenever a [lazyViewport] directive is applied. 28 | { 29 | provide: LazyViewport, 30 | useFactory: function() { 31 | 32 | var viewport = new LazyViewport(); 33 | viewport.setup( /* No root. */ ); 34 | 35 | return( viewport ); 36 | 37 | } 38 | } 39 | ] 40 | }) 41 | export class LazyModule { }; 42 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pararell/eshop-angular-node/b9b5c7fffaef797f84a7c79d7f658a30e1bed9cc/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/assets/fonts/material-icons/MaterialIcons-Regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pararell/eshop-angular-node/b9b5c7fffaef797f84a7c79d7f658a30e1bed9cc/src/assets/fonts/material-icons/MaterialIcons-Regular.eot -------------------------------------------------------------------------------- /src/assets/fonts/material-icons/MaterialIcons-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pararell/eshop-angular-node/b9b5c7fffaef797f84a7c79d7f658a30e1bed9cc/src/assets/fonts/material-icons/MaterialIcons-Regular.ttf -------------------------------------------------------------------------------- /src/assets/fonts/material-icons/MaterialIcons-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pararell/eshop-angular-node/b9b5c7fffaef797f84a7c79d7f658a30e1bed9cc/src/assets/fonts/material-icons/MaterialIcons-Regular.woff -------------------------------------------------------------------------------- /src/assets/fonts/material-icons/MaterialIcons-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pararell/eshop-angular-node/b9b5c7fffaef797f84a7c79d7f658a30e1bed9cc/src/assets/fonts/material-icons/MaterialIcons-Regular.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/material-icons/material-icons.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Material Icons'; 3 | font-style: normal; 4 | font-weight: 400; 5 | src: url(MaterialIcons-Regular.eot); /* For IE6-8 */ 6 | src: local('Material Icons'), 7 | local('MaterialIcons-Regular'), 8 | url(MaterialIcons-Regular.woff2) format('woff2'), 9 | url(MaterialIcons-Regular.woff) format('woff'), 10 | url(MaterialIcons-Regular.ttf) format('truetype'); 11 | } 12 | 13 | .material-icons { 14 | font-family: 'Material Icons'; 15 | font-weight: normal; 16 | font-style: normal; 17 | font-size: 24px; /* Preferred icon size */ 18 | display: inline-block; 19 | line-height: 1; 20 | text-transform: none; 21 | letter-spacing: normal; 22 | word-wrap: normal; 23 | white-space: nowrap; 24 | direction: ltr; 25 | 26 | /* Support for all WebKit browsers. */ 27 | -webkit-font-smoothing: antialiased; 28 | /* Support for Safari and Chrome. */ 29 | text-rendering: optimizeLegibility; 30 | 31 | /* Support for Firefox. */ 32 | -moz-osx-font-smoothing: grayscale; 33 | 34 | /* Support for IE. */ 35 | font-feature-settings: 'liga'; 36 | } 37 | -------------------------------------------------------------------------------- /src/assets/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pararell/eshop-angular-node/b9b5c7fffaef797f84a7c79d7f658a30e1bed9cc/src/assets/icon-128x128.png -------------------------------------------------------------------------------- /src/assets/icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pararell/eshop-angular-node/b9b5c7fffaef797f84a7c79d7f658a30e1bed9cc/src/assets/icon-152x152.png -------------------------------------------------------------------------------- /src/assets/icon-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pararell/eshop-angular-node/b9b5c7fffaef797f84a7c79d7f658a30e1bed9cc/src/assets/icon-256x256.png -------------------------------------------------------------------------------- /src/assets/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pararell/eshop-angular-node/b9b5c7fffaef797f84a7c79d7f658a30e1bed9cc/src/assets/icon-512x512.png -------------------------------------------------------------------------------- /src/config/keys.ts: -------------------------------------------------------------------------------- 1 | 2 | export const keys = { 3 | stripePublishableKey: 'pk_test_lYtRYxhbaBtf3kZZy5KxkAIv' 4 | } 5 | -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | apiUrl: '' 4 | }; 5 | -------------------------------------------------------------------------------- /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 | apiUrl: 'http://localhost:4000' 9 | }; 10 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pararell/eshop-angular-node/b9b5c7fffaef797f84a7c79d7f658a30e1bed9cc/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Bluetooth eshop 8 | 9 | 10 | 11 | 12 | 13 | 14 | 30 | 31 | 32 | 33 | 34 |
35 |
36 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/main.server.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | 3 | export { AppServerModule } from './app/app.server.module'; 4 | 5 | enableProdMode(); 6 | 7 | export { renderModule, renderModuleFactory } from '@angular/platform-server'; -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppBrowserModule } from './app/app.browser.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | document.addEventListener('DOMContentLoaded', () => { 12 | platformBrowserDynamic().bootstrapModule(AppBrowserModule); 13 | }); 14 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Bluetooth Eshop", 3 | "short_name": "BtEshop", 4 | "description": "Eshop with bluetoth headphones and widgets", 5 | "icons": [{ 6 | "src": "assets/icon-128x128.png", 7 | "sizes": "128x128", 8 | "type": "image/png" 9 | }, { 10 | "src": "assets/icon-152x152.png", 11 | "sizes": "152x152", 12 | "type": "image/png" 13 | }, { 14 | "src": "assets/icon-256x256.png", 15 | "sizes": "256x256", 16 | "type": "image/png" 17 | }, { 18 | "src": "assets/icon-512x512.png", 19 | "sizes": "512x512", 20 | "type": "image/png" 21 | }], 22 | "start_url": "/", 23 | "display": "standalone", 24 | "background_color": "#ffffff", 25 | "theme_color": "#064887" 26 | } 27 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/ 22 | // import 'core-js/es6/symbol'; 23 | // import 'core-js/es6/object'; 24 | // import 'core-js/es6/function'; 25 | // import 'core-js/es6/parse-int'; 26 | // import 'core-js/es6/parse-float'; 27 | // import 'core-js/es6/number'; 28 | // import 'core-js/es6/math'; 29 | // import 'core-js/es6/string'; 30 | // import 'core-js/es6/date'; 31 | // import 'core-js/es6/array'; 32 | // import 'core-js/es6/regexp'; 33 | // import 'core-js/es6/map'; 34 | // import 'core-js/es6/weak-map'; 35 | // import 'core-js/es6/set'; 36 | 37 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 38 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 39 | 40 | /** IE10 and IE11 requires the following to support `@angular/animation`. */ 41 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 42 | 43 | 44 | /** Evergreen browsers require these. **/ 45 | import 'core-js/proposals/reflect-metadata'; 46 | 47 | 48 | 49 | /** ALL Firefox browsers require the following to support `@angular/animation`. **/ 50 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 51 | 52 | 53 | 54 | /*************************************************************************************************** 55 | * Zone JS is required by Angular itself. 56 | */ 57 | import 'zone.js/dist/zone'; // Included with Angular CLI. 58 | 59 | 60 | 61 | /*************************************************************************************************** 62 | * APPLICATION IMPORTS 63 | */ 64 | 65 | /** 66 | * Date, currency, decimal and percent pipes. 67 | * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10 68 | */ 69 | // import 'intl'; // Run `npm install --save intl`. 70 | /** 71 | * Need to import at least one locale-data with intl. 72 | */ 73 | // import 'intl/locale-data/jsonp/en'; 74 | -------------------------------------------------------------------------------- /src/styles/_variables.scss: -------------------------------------------------------------------------------- 1 | // override fonts path to prevent build errors (or use resolve-url loader) 2 | // $roboto-font-path: "../../node_modules/materialize-css/dist/fonts/roboto/"; 3 | 4 | // optionally override color variables eg: ... 5 | $primary-color: #039be5; 6 | -------------------------------------------------------------------------------- /src/styles/index.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * External 3 | */ 4 | 5 | // @import '~materialize.css'; 6 | // @import '../node_modules/tinymce/skins/lightgray/skin.min.css'; 7 | // @import '../node_modules/tinymce/skins/lightgray/skin.min.css'; 8 | // @import '../node_modules/tinymce/skins/lightgray/content.min.css'; 9 | 10 | /* 11 | * App 12 | */ 13 | 14 | @import "variables"; 15 | @import "main"; 16 | // @import "~materialize-css/sass/materialize"; 17 | // Materialize 18 | // Variables; 19 | @import "~materialize-css/sass/components/_color-variables"; 20 | @import "~materialize-css/sass/components/_variables"; 21 | @import "~materialize-css/sass/components/_color-classes"; 22 | // Reset 23 | @import "~materialize-css/sass/components/_normalize"; 24 | 25 | // components 26 | @import "~materialize-css/sass/components/_global"; 27 | @import "~materialize-css/sass/components/_typography"; 28 | @import "~materialize-css/sass/components/_badges"; 29 | @import "~materialize-css/sass/components/_icons-material-design"; 30 | @import "~materialize-css/sass/components/_navbar"; 31 | @import "~materialize-css/sass/components/_transitions"; 32 | @import "~materialize-css/sass/components/_cards"; 33 | @import "~materialize-css/sass/components/_toast"; 34 | @import "~materialize-css/sass/components/_tabs"; 35 | @import "~materialize-css/sass/components/_tooltip"; 36 | @import "~materialize-css/sass/components/_buttons"; 37 | @import "~materialize-css/sass/components/_dropdown"; 38 | @import "~materialize-css/sass/components/_waves"; 39 | @import "~materialize-css/sass/components/_modal"; 40 | @import "~materialize-css/sass/components/_collapsible"; 41 | @import "~materialize-css/sass/components/_chips"; 42 | @import "~materialize-css/sass/components/_materialbox"; 43 | @import "~materialize-css/sass/components/forms/_forms"; 44 | @import "~materialize-css/sass/components/forms/_checkboxes"; 45 | @import "~materialize-css/sass/components/forms/_radio-buttons"; 46 | @import "~materialize-css/sass/components/_table_of_contents"; 47 | @import "~materialize-css/sass/components/_sidenav"; 48 | @import "~materialize-css/sass/components/_preloader"; 49 | @import "~materialize-css/sass/components/_tapTarget"; 50 | @import "~materialize-css/sass/components/_pulse"; 51 | -------------------------------------------------------------------------------- /src/styles/main.scss: -------------------------------------------------------------------------------- 1 | body { 2 | overflow: hidden; 3 | } 4 | 5 | .full-width { 6 | display: flex; 7 | min-height: 100vh; 8 | flex-direction: column; 9 | } 10 | 11 | .main-scroll-wrapp { 12 | position: absolute; 13 | scroll-behavior: smooth; 14 | top: 50px; 15 | left: 0; 16 | right: 0; 17 | bottom: 0; 18 | overflow: auto; 19 | } 20 | 21 | @mixin stripes($color1: #000460, $color2: transparent, $angle: 0deg, $stripe1-width: 25px, $stripe2-width: null) { 22 | @if ($stripe2-width == null) { 23 | $stripe2-width: $stripe1-width; 24 | } 25 | 26 | $tile-size: ($stripe1-width + $stripe2-width) * 2; 27 | $stripe2-start: ($stripe1-width / $tile-size) * 100%; 28 | $stripe3-start: $stripe2-start + (($stripe2-width / $tile-size) * 100%); 29 | $stripe4-start: $stripe3-start + (($stripe1-width / $tile-size) * 100%); 30 | 31 | background-size: $tile-size $tile-size; 32 | background-image: linear-gradient($angle, $color1, $color1 $stripe2-start, $color2 $stripe2-start, $color2 $stripe3-start, $color1 $stripe3-start, $color1 $stripe4-start, $color2 $stripe4-start, $color2); 33 | background-repeat: repeat; 34 | } 35 | 36 | .main { 37 | flex: 1 0 auto; 38 | 39 | @include stripes(#000460, #fff, 45deg, 1px, 80px); 40 | } 41 | 42 | .container { 43 | width: 100% !important; 44 | padding: 20px; 45 | } 46 | 47 | .clickable { 48 | cursor: pointer; 49 | } 50 | 51 | .disabled { 52 | opacity: 0.5; 53 | cursor: not-allowed !important; 54 | pointer-events: none !important; 55 | } 56 | 57 | .btn { 58 | background-color: #004e92 !important; 59 | } 60 | -------------------------------------------------------------------------------- /src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "baseUrl": "./", 6 | "types": [ "node" ], 7 | "typeRoots": [ "../node_modules/@types" ] 8 | }, 9 | "files": [ 10 | "main.ts", 11 | "polyfills.ts" 12 | ], 13 | "include": [ 14 | "src/**/*.d.ts" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /src/tsconfig.server.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "commonjs", 6 | "baseUrl": "./", 7 | "types": [], 8 | "paths": { 9 | "hiredis" : ["../config/aliases/hiredis"] 10 | } 11 | }, 12 | "angularCompilerOptions": { 13 | "entryModule": "app/app.server.module#AppServerModule" 14 | } 15 | , 16 | "files": [ 17 | "main.server.ts", 18 | "../server.ts" 19 | ], 20 | "include": [ 21 | "src/**/*.d.ts" 22 | ] 23 | } 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | /* SystemJS module definition */ 2 | declare var StripeCheckout:any; 3 | declare var window: Window; 4 | declare var module: NodeModule; 5 | interface NodeModule { 6 | id: string; 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "module": "esnext", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "skipLibCheck": true, 8 | "declaration": false, 9 | "moduleResolution": "node", 10 | "emitDecoratorMetadata": true, 11 | "experimentalDecorators": true, 12 | "target": "es2015", 13 | "typeRoots": [ 14 | "./node_modules/@types" 15 | ], 16 | "lib": [ 17 | "es2017", 18 | "dom" 19 | ], 20 | "mapRoot":"./" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "arrow-return-shorthand": true, 7 | "callable-types": true, 8 | "class-name": true, 9 | "comment-format": [ 10 | true, 11 | "check-space" 12 | ], 13 | "curly": true, 14 | "eofline": true, 15 | "forin": true, 16 | "import-blacklist": [ 17 | true, 18 | "rxjs/Rx" 19 | ], 20 | "import-spacing": true, 21 | "indent": [ 22 | true, 23 | "spaces" 24 | ], 25 | "interface-over-type-literal": true, 26 | "label-position": true, 27 | "max-line-length": [ 28 | true, 29 | 240 30 | ], 31 | "member-access": false, 32 | "no-arg": true, 33 | "no-bitwise": true, 34 | "no-console": [ 35 | true, 36 | "debug", 37 | "info", 38 | "time", 39 | "timeEnd", 40 | "trace" 41 | ], 42 | "no-construct": true, 43 | "no-debugger": true, 44 | "no-duplicate-super": true, 45 | "no-empty": false, 46 | "no-empty-interface": true, 47 | "no-eval": true, 48 | "no-inferrable-types": [ 49 | true, 50 | "ignore-params" 51 | ], 52 | "no-misused-new": true, 53 | "no-non-null-assertion": true, 54 | "no-shadowed-variable": true, 55 | "no-string-literal": false, 56 | "no-string-throw": true, 57 | "no-switch-case-fall-through": true, 58 | "no-trailing-whitespace": true, 59 | "no-unnecessary-initializer": true, 60 | "no-use-before-declare": true, 61 | "object-literal-sort-keys": false, 62 | "one-line": [ 63 | true, 64 | "check-open-brace", 65 | "check-catch", 66 | "check-else", 67 | "check-whitespace" 68 | ], 69 | "quotemark": [ 70 | true, 71 | "single" 72 | ], 73 | "radix": true, 74 | "semicolon": [ 75 | "always" 76 | ], 77 | "triple-equals": [ 78 | true, 79 | "allow-null-check" 80 | ], 81 | "unified-signatures": true, 82 | "variable-name": false, 83 | "whitespace": [ 84 | true, 85 | "check-branch", 86 | "check-decl", 87 | "check-operator", 88 | "check-separator", 89 | "check-type" 90 | ], 91 | "component-selector": [ 92 | true, 93 | "element", 94 | "app", 95 | "kebab-case" 96 | ], 97 | "no-outputs-metadata-property": true, 98 | "no-host-metadata-property": true, 99 | "use-lifecycle-interface": true, 100 | "use-pipe-transform-interface": true, 101 | "component-class-suffix": true, 102 | "directive-class-suffix": true 103 | } 104 | } 105 | --------------------------------------------------------------------------------