Added the ability to have mutliple buy options for product

This commit is contained in:
Zakary Timson 2018-08-19 19:07:17 -04:00
parent 538c585cc1
commit d94df9f206
9 changed files with 268 additions and 197 deletions

View File

@ -6,8 +6,9 @@ import { MatDialog } from '@angular/material';
import { LoginComponent } from './login/login.component'; import { LoginComponent } from './login/login.component';
import { LocalStorage } from 'webstorage-decorators'; import { LocalStorage } from 'webstorage-decorators';
import { AppStore } from './app.store'; import { AppStore } from './app.store';
import { AngularFireAuth } from '../../node_modules/angularfire2/auth'; import { AngularFireAuth } from 'angularfire2/auth';
import { SwUpdate } from '../../node_modules/@angular/service-worker'; import { SwUpdate } from '@angular/service-worker';
import { POption } from './store/products/product';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
@ -15,7 +16,7 @@ import { SwUpdate } from '../../node_modules/@angular/service-worker';
}) })
export class AppComponent implements OnInit { export class AppComponent implements OnInit {
@LocalStorage({ defaultValue: [], encryptionKey: 'HmRoBFUEVWqW5uvy' }) @LocalStorage({ defaultValue: [], encryptionKey: 'HmRoBFUEVWqW5uvy' })
cart: { id: string; item: string; price: number; currency: 'CAD' | 'USD'; quantity: number }[]; cart: { id: string; item: string; option: POption, quantity: number}[];
constructor( constructor(
private router: Router, private router: Router,
@ -30,8 +31,8 @@ export class AppComponent implements OnInit {
}); });
} }
cartAdd(id: string, item: string, price: number, currency: 'CAD' | 'USD', quantity: number) { cartAdd(id: string, name: string, option: POption, quantity: number) {
this.cart = [{ id: id, item: item, price: Number(price), currency: currency, quantity: Number(quantity) }].concat( this.cart = [{ id: id, item: name, option: option, quantity: Number(quantity)}].concat(
this.cart this.cart
); );
} }

View File

@ -1,13 +1,13 @@
import {Injectable} from '@angular/core'; import { Injectable } from '@angular/core';
import {AngularFirestore} from 'angularfire2/firestore'; import { AngularFirestore } from 'angularfire2/firestore';
import {Category} from './store/category'; import { Category } from './store/category';
import {Observable, combineLatest} from 'rxjs'; import { Observable, combineLatest } from 'rxjs';
import {map, shareReplay} from 'rxjs/operators'; import { map, shareReplay } from 'rxjs/operators';
import {DomSanitizer} from '@angular/platform-browser'; import { DomSanitizer } from '@angular/platform-browser';
import {Product} from './store/product'; import { Product } from './store/product';
import {AngularFireAuth} from '../../node_modules/angularfire2/auth'; import { AngularFireAuth } from '../../node_modules/angularfire2/auth';
import {Component} from './formulaManager/component'; import { Component } from './formulaManager/component';
import {Formula} from './formulaManager/formula'; import { Formula } from './formulaManager/formula';
@Injectable() @Injectable()
export class AppStore { export class AppStore {
@ -24,7 +24,7 @@ export class AppStore {
.pipe( .pipe(
map(rows => map(rows =>
rows.map((row: any) => { rows.map((row: any) => {
let temp = Object.assign({id: row.payload.doc.id, ref: row.payload.doc.ref}, row.payload.doc.data()); let temp = Object.assign({ id: row.payload.doc.id, ref: row.payload.doc.ref }, row.payload.doc.data());
temp.image = this.domSanitizer.bypassSecurityTrustUrl(temp.image); temp.image = this.domSanitizer.bypassSecurityTrustUrl(temp.image);
return <Category>temp; return <Category>temp;
}) })
@ -38,7 +38,7 @@ export class AppStore {
.pipe( .pipe(
map(rows => map(rows =>
rows.map((row: any) => { rows.map((row: any) => {
let temp = Object.assign({id: row.payload.doc.id, ref: row.payload.doc.ref}, row.payload.doc.data()); let temp = Object.assign({ id: row.payload.doc.id, ref: row.payload.doc.ref }, row.payload.doc.data());
temp.created = temp.created.toDate(); temp.created = temp.created.toDate();
return <Component>temp; return <Component>temp;
}) })
@ -52,12 +52,12 @@ export class AppStore {
).pipe( ).pipe(
map(data => map(data =>
data[0].map(row => { data[0].map(row => {
let temp = <any>Object.assign({id: row.payload.doc.id, ref: row.payload.doc.ref}, row.payload.doc.data()); let temp = <any>Object.assign({ id: row.payload.doc.id, ref: row.payload.doc.ref }, row.payload.doc.data());
temp.created = temp.created.toDate(); temp.created = temp.created.toDate();
temp.components = temp.components.map(row => { temp.components = temp.components.map(row => {
let component = data[1].filter(c => c.id == row.component.id)[0]; let component = data[1].filter(c => c.id == row.component.id)[0];
return {component: component, quantity: row.quantity}; return { component: component, quantity: row.quantity };
}); });
return <Formula>temp; return <Formula>temp;
@ -72,13 +72,12 @@ export class AppStore {
.pipe( .pipe(
map(rows => map(rows =>
rows.map((row: any) => { rows.map((row: any) => {
let temp = Object.assign({id: row.payload.doc.id, ref: row.payload.doc.ref}, row.payload.doc.data()); let temp = Object.assign({ id: row.payload.doc.id, ref: row.payload.doc.ref }, row.payload.doc.data());
temp.originalImage = temp.image; temp.originalImage = temp.image;
temp.image = this.domSanitizer.bypassSecurityTrustUrl(temp.image); temp.image = this.domSanitizer.bypassSecurityTrustUrl(temp.image);
temp.originalDescription = temp.description; temp.originalDescription = temp.description;
temp.description = this.domSanitizer.bypassSecurityTrustHtml( temp.description = this.domSanitizer.bypassSecurityTrustHtml(temp.description);
temp.description.replace(/(\r\n|\r|\n)/g, '<br>')
);
return <Product>temp; return <Product>temp;
}) })
), ),

View File

@ -1,75 +1,75 @@
import {Component, Inject} from '@angular/core'; import { Component, Inject } from '@angular/core';
import {MatDialogRef, MAT_DIALOG_DATA} from '@angular/material'; import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material';
import {AngularFirestore} from 'angularfire2/firestore'; import { AngularFirestore } from 'angularfire2/firestore';
import {LocalStorage} from '../../../../node_modules/webstorage-decorators'; import { LocalStorage } from 'webstorage-decorators';
import {AppStore} from '../../app.store'; import { AppStore } from '../../app.store';
import {ConvertToGPipe} from '../units.pipe'; import { ConvertToGPipe } from '../units.pipe';
@Component({ @Component({
selector: 'new-formula', selector: 'new-formula',
templateUrl: './newFormula.component.html' templateUrl: './newFormula.component.html'
}) })
export class NewFormulaComponent { export class NewFormulaComponent {
name: string; name: string;
amount: number; amount: number;
approved: boolean = false; approved: boolean = false;
component: string; component: string;
components: {component: string; name: string; quantity: number}[] = []; components: { component: string; name: string; quantity: number }[] = [];
componentsList = []; componentsList = [];
@LocalStorage({defaultValue: 'kg', fieldName: 'newFormulaUnit'}) @LocalStorage({ defaultValue: 'kg', fieldName: 'newFormulaUnit' })
unit; unit;
constructor( constructor(
private dialogRef: MatDialogRef<NewFormulaComponent>, private dialogRef: MatDialogRef<NewFormulaComponent>,
private db: AngularFirestore, private db: AngularFirestore,
private store: AppStore, private store: AppStore,
@Inject(MAT_DIALOG_DATA) public data @Inject(MAT_DIALOG_DATA) public data
) { ) {
this.store.components.subscribe(rows => (this.componentsList = rows)); this.store.components.subscribe(rows => (this.componentsList = rows));
if (this.data) { if (this.data) {
this.name = this.data.name; this.name = this.data.name;
this.approved = this.data.approved; this.approved = this.data.approved;
this.components = this.data.components.map(row => { this.components = this.data.components.map(row => {
return {component: row.component.id, name: row.component.name, quantity: row.quantity}; return { component: row.component.id, name: row.component.name, quantity: row.quantity };
}); });
} }
} }
add() { add() {
let id = this.componentsList.filter(row => row.name == this.component)[0].id; let id = this.componentsList.filter(row => row.name == this.component)[0].id;
console.log(id); console.log(id);
let amount = new ConvertToGPipe().transform(Number(this.amount), this.unit); let amount = new ConvertToGPipe().transform(Number(this.amount), this.unit);
this.components.push({component: id, name: this.component, quantity: amount}); this.components.push({ component: id, name: this.component, quantity: amount });
this.component = null; this.component = null;
this.amount = null; this.amount = null;
} }
remove(i) { remove(i) {
this.components.splice(i, 1); this.components.splice(i, 1);
} }
submit() { submit() {
let newFormula = { let newFormula = {
name: this.name, name: this.name,
approved: this.approved, approved: this.approved,
components: this.components.map((row: any) => { components: this.components.map((row: any) => {
return {component: this.db.collection('components').doc(row.component).ref, quantity: row.quantity}; return { component: this.db.collection('components').doc(row.component).ref, quantity: row.quantity };
}) })
}; };
if (!this.data) { if (!this.data) {
newFormula['created'] = new Date(); newFormula['created'] = new Date();
this.db this.db
.collection('formulas') .collection('formulas')
.add(newFormula) .add(newFormula)
.then(data => this.dialogRef.close()); .then(data => this.dialogRef.close());
} else { } else {
this.data.ref.update(newFormula).then(data => this.dialogRef.close()); this.data.ref.update(newFormula).then(data => this.dialogRef.close());
} }
} }
total() { total() {
return this.components.reduce((acc, row) => acc + row.quantity, 0); return this.components.reduce((acc, row) => acc + row.quantity, 0);
} }
} }

View File

@ -15,6 +15,7 @@ export class CartComponent {
constructor(public app: AppComponent) {} constructor(public app: AppComponent) {}
ngOnInit() { ngOnInit() {
console.log(this.app.cart);
if (this.app.cartCount()) { if (this.app.cartCount()) {
window['paypal'].Button.render( window['paypal'].Button.render(
{ {

View File

@ -13,20 +13,52 @@
<mat-form-field class="w-100"> <mat-form-field class="w-100">
<textarea matInput rows="5" placeholder="Description" name="description" [(ngModel)]="description"></textarea> <textarea matInput rows="5" placeholder="Description" name="description" [(ngModel)]="description"></textarea>
</mat-form-field> </mat-form-field>
<mat-form-field> <div class="mt-3 p-3 border rounded border-muted">
<span matPrefix>$&nbsp;</span> <h5 mat-subheader class="mb-4 pl-0">Buying Options</h5>
<input matInput placeholder="Price" type="number" name="price" [(ngModel)]="price"> <button mat-stroked-button class="mb-4" color="accent" (click)="addOption()">Add</button>
<mat-hint *ngIf="!price" align="start">0 will display "Contact For Price"</mat-hint> <mat-accordion>
</mat-form-field> <mat-expansion-panel *ngFor="let o of options; let i = index">
<mat-radio-group [(ngModel)]="currency" name="currency"> <mat-expansion-panel-header>
<mat-radio-button value="CAD" class="pl-3">CAD</mat-radio-button> <mat-panel-title>
<mat-radio-button value="USD" class="pl-3">USD</mat-radio-button> {{o.name || 'Option'}}
</mat-radio-group> </mat-panel-title>
<input #fileInput type="file" (change)="imageChanged($event)" hidden> <mat-panel-description>
<mat-form-field class="float-right" style="width: 150px"> {{o.currency}} {{o.price | currency}}
<input matInput type="number" placeholder="Weight For Shipping" [(ngModel)]="weight"> </mat-panel-description>
<span matSuffix>lb</span> </mat-expansion-panel-header>
</mat-form-field> <div class="row">
<div class="col-4">
<mat-form-field>
<input matInput placeholder="Name" [(ngModel)]="o.name">
</mat-form-field>
</div>
<div class="col-4 text-center">
<mat-form-field>
<span matPrefix>$&nbsp;</span>
<input matInput placeholder="Price" type="number" name="price" [(ngModel)]="o.price">
<mat-hint *ngIf="!price" align="start">0 will display "Contact For Price"</mat-hint>
</mat-form-field>
<mat-radio-group [(ngModel)]="o.currency" name="currency">
<mat-radio-button value="CAD" class="pl-3">CAD</mat-radio-button>
<mat-radio-button value="USD" class="pl-3">USD</mat-radio-button>
</mat-radio-group>
</div>
<div class="col-4">
<mat-form-field class="float-right" style="width: 150px">
<input matInput type="number" placeholder="Weight For Shipping" [(ngModel)]="o.weight">
<span matSuffix>lb</span>
</mat-form-field>
</div>
</div>
<input #fileInput type="file" (change)="imageChanged($event)" hidden>
<mat-action-row>
<button mat-button color="warn" (click)="options.splice(i, 1)">
Delete
</button>
</mat-action-row>
</mat-expansion-panel>
</mat-accordion>
</div>
<div class="mt-3 p-3 border rounded border-muted"> <div class="mt-3 p-3 border rounded border-muted">
<h5 mat-subheader class="pl-0">Uploads</h5> <h5 mat-subheader class="pl-0">Uploads</h5>
<mat-progress-bar *ngIf="uploading" mode="indeterminate"></mat-progress-bar> <mat-progress-bar *ngIf="uploading" mode="indeterminate"></mat-progress-bar>

View File

@ -1,94 +1,98 @@
import {Component, Inject} from '@angular/core'; import { Component, Inject } from '@angular/core';
import {AngularFirestore} from 'angularfire2/firestore'; import { AngularFirestore } from 'angularfire2/firestore';
import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
import {AppStore} from '../../app.store'; import { AppStore } from '../../app.store';
import {AngularFireStorage} from '../../../../node_modules/angularfire2/storage'; import { AngularFireStorage } from 'angularfire2/storage';
@Component({ @Component({
selector: 'new-item', selector: 'new-item',
templateUrl: 'newProduct.component.html' templateUrl: 'newProduct.component.html'
}) })
export class NewProductComponent { export class NewProductComponent {
category; category;
currency = 'CAD'; description: string;
description: string; files: { name: string; link: string; type: string }[] = [];
files: {name: string; link: string; type: string}[] = []; image: string;
image: string; linkError = false;
linkError = false; name: string;
name: string; options: { name: string, price: number, currency: 'CAD' | 'USD', weight: number }[] = [];
price: number = 0.0; uploading = false;
weight: number = 0;
uploading = false;
constructor( constructor(
private dialogRef: MatDialogRef<NewProductComponent>, private dialogRef: MatDialogRef<NewProductComponent>,
private db: AngularFirestore, private db: AngularFirestore,
private storage: AngularFireStorage, private storage: AngularFireStorage,
@Inject(MAT_DIALOG_DATA) public data, @Inject(MAT_DIALOG_DATA) public data,
public store: AppStore public store: AppStore
) { ) {
if (data.currentCategory) this.category = data.currentCategory; if (data.currentCategory) this.category = data.currentCategory;
if (data.product) { if (data.product) {
this.category = data.product.category; this.category = data.product.category;
this.currency = data.product.currency; this.description = data.product.originalDescription;
this.description = data.product.originalDescription; this.files = data.product.files;
this.files = data.product.files; this.name = data.product.name;
this.name = data.product.name; this.options = data.product.options;
this.price = data.product.price; } else {
this.weight = data.product.weight; this.addOption();
} }
} }
async addFile(event) { async addFile(event) {
this.uploading = true; this.uploading = true;
let file = event.target.files[0]; let file = event.target.files[0];
let type = 'other'; let type = 'other';
if (file.type.indexOf('image') != -1) type = 'preview'; if (file.type.indexOf('image') != -1) type = 'preview';
let upload = await this.storage.ref(`${Math.round(Math.random() * 1000000)}/${file.name}`).put(file); let upload = await this.storage.ref(`${Math.round(Math.random() * 1000000)}/${file.name}`).put(file);
this.uploading = false; this.uploading = false;
if (upload.state == 'success') if (upload.state == 'success')
this.files.push({name: file.name, type: type, link: await upload.ref.getDownloadURL()}); this.files.push({ name: file.name, type: type, link: await upload.ref.getDownloadURL() });
} }
addLink(link: string) { addLink(link: string) {
let valid: any = new RegExp( let valid: any = new RegExp(
'^(?:(?<protocol>https?):\\/\\/)?(?<name>(?:(?<subdomain>[\\d|\\w]+).)?(?:[\\d|\\w]+\\.[\\d|\\w]+))(?:\\:(?<port>\\d+))?.*' '^(?:(?<protocol>https?):\\/\\/)?(?<name>(?:(?<subdomain>[\\d|\\w]+).)?(?:[\\d|\\w]+\\.[\\d|\\w]+))(?:\\:(?<port>\\d+))?.*'
).exec(link); ).exec(link);
if (!!valid) { if (!!valid) {
if (!valid.groups['protocol']) link = `http://${link}`; if (!valid.groups['protocol']) link = `http://${link}`;
this.files.push({name: valid.groups['name'], link: link, type: 'link'}); this.files.push({ name: valid.groups['name'], link: link, type: 'link' });
this.linkError = !valid; this.linkError = !valid;
} }
return !!valid; return !!valid;
} }
imageChanged(event) { addOption() {
let reader = new FileReader(); this.options.push({ name: '', price: 0, currency: 'CAD', weight: 0 });
reader.readAsDataURL(event.target.files[0]); }
reader.onload = (event: any) => (this.image = event.target.result);
}
submit() { imageChanged(event) {
let newProduct = { let reader = new FileReader();
category: this.category, reader.readAsDataURL(event.target.files[0]);
currency: this.currency, reader.onload = (event: any) => (this.image = event.target.result);
description: this.description, }
files: this.files,
name: this.name,
price: Number(this.price),
weight: Number(this.weight) || 0
};
if (this.image) newProduct['image'] = this.image;
if (!this.data.product) { submit() {
this.db let newProduct = {
.collection('products') category: this.category,
.add(newProduct) description: this.description,
.then(data => this.dialogRef.close()); files: this.files,
} else { name: this.name,
this.data.product.ref.update(newProduct).then(data => this.dialogRef.close()); options: this.options.map(row => {
} row.price = <number>row.price;
} row.weight = <number>row.weight;
return row;
})
};
if (this.image) newProduct['image'] = this.image;
if (!this.data.product) {
this.db
.collection('products')
.add(newProduct)
.then(data => this.dialogRef.close());
} else {
this.data.product.ref.update(newProduct).then(data => this.dialogRef.close());
}
}
} }

View File

@ -0,0 +1,25 @@
import { SafeUrl, SafeHtml } from "@angular/platform-browser";
export interface PFile {
link: string;
name: string;
type: 'link' | 'other' | 'preview';
}
export interface POption {
currency: 'CAD' | 'USD';
name: string;
price: number;
weight: number;
}
export interface Product {
category: string;
description: string | SafeHtml;
files: any[]
image: string | SafeUrl;
name: string;
options: POption[];
originalDescription: string;
originalImage: string;
}

View File

@ -16,18 +16,26 @@
</div> </div>
<div class="col-12 col-md-9"> <div class="col-12 col-md-9">
<h2 class="roboto">{{product.name}}</h2> <h2 class="roboto">{{product.name}}</h2>
<h5 *ngIf="product.price" class="text-muted">{{product.currency}} {{product.price | currency}}</h5> <mat-form-field *ngIf="product.options.length > 1">
<h5 *ngIf="!product.price" class="text-muted">Contact For Price</h5> <mat-select placeholder="Buying Options" [(ngModel)]="option">
<mat-divider class="my-3"></mat-divider> <mat-option *ngFor="let o of product.options" [value]="o">
<p style="overflow: hidden;" [innerHtml]="product.description"></p> {{o.name}} <span class="float-right text-muted">{{o.currency}} {{o.price | currency}}</span>
<div class="float-right"> </mat-option>
</mat-select>
</mat-form-field>
<div class="d-inline ml-3">
<mat-form-field class="mr-1" style="width: 40px"> <mat-form-field class="mr-1" style="width: 40px">
<span matPrefix>x</span>
<input #quantity matInput type="number" value="1" min="1"> <input #quantity matInput type="number" value="1" min="1">
</mat-form-field> </mat-form-field>
<button mat-raised-button color="primary" [disabled]="quantity.value < 1" (click)="app.cartAdd(product.id, product.name, product.price, product.currency, quantity.value)"> <button mat-raised-button color="primary" [disabled]="quantity.value < 1" (click)="app.cartAdd(product.id, product.name, option, quantity.value)">
<mat-icon>add_shopping_cart</mat-icon> Buy <mat-icon>add_shopping_cart</mat-icon> Buy
</button> </button>
</div> </div>
<h5 *ngIf="product.options.length == 1 && product.options[0].price > 0" class="text-muted">{{product.options[0].currency}} {{product.options[0].price | currency}}</h5>
<h5 *ngIf="product.options.length == 1 && product.options[0].price == 0" class="text-muted">Contact For Price</h5>
<mat-divider class="my-3"></mat-divider>
<p style="overflow: hidden;" [innerHtml]="product.description"></p>
<mat-divider class="my-3"></mat-divider> <mat-divider class="my-3"></mat-divider>
<div *ngIf="attachments?.length"> <div *ngIf="attachments?.length">
<h5 class="ml-3"> <h5 class="ml-3">

View File

@ -1,10 +1,10 @@
import {Component} from '@angular/core'; import { Component } from '@angular/core';
import {ActivatedRoute} from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import {AppComponent} from '../../app.component'; import { AppComponent } from '../../app.component';
import {map} from 'rxjs/operators'; import { map } from 'rxjs/operators';
import {AppStore} from '../../app.store'; import { AppStore } from '../../app.store';
import {Product} from '../product'; import { Product } from '../product';
import {SafeUrl, DomSanitizer} from '../../../../node_modules/@angular/platform-browser'; import { SafeUrl, DomSanitizer } from '@angular/platform-browser';
@Component({ @Component({
selector: 'products', selector: 'products',
@ -13,15 +13,16 @@ import {SafeUrl, DomSanitizer} from '../../../../node_modules/@angular/platform-
export class ProductsComponent { export class ProductsComponent {
product: Product; product: Product;
preview: SafeUrl[]; preview: SafeUrl[];
links: {name: string; link: string; type: string}[]; links: { name: string; link: string; type: string }[];
attachments: {name: string; link: string; type: string}[]; attachments: { name: string; link: string; type: string }[];
option;
constructor( constructor(
private store: AppStore, private store: AppStore,
private route: ActivatedRoute, private route: ActivatedRoute,
private domSanitizer: DomSanitizer, private domSanitizer: DomSanitizer,
public app: AppComponent public app: AppComponent
) {} ) { }
ngOnInit() { ngOnInit() {
this.route.params.subscribe(params => { this.route.params.subscribe(params => {